spinoso_env/env/
system.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::env;
4
5use bstr::ByteSlice;
6use scolapasta_path::{bytes_to_os_str, os_string_to_bytes};
7
8use crate::{ArgumentError, Error, InvalidError};
9
10type Bytes = Vec<u8>;
11
12/// A hash-like accessor for environment variables using platform APIs.
13///
14/// `System` is an accessor to the host system's environment variables using the
15/// functions provided by the [Rust Standard Library] in the
16/// [`std::env`] module.
17///
18/// Use of this `ENV` backend allows Ruby code to access and modify the host
19/// system. It is not appropriate to use this backend in embedded or untrusted
20/// contexts.
21///
22/// # Examples
23///
24/// Fetching an environment variable:
25///
26/// ```no_run
27/// # use spinoso_env::System;
28/// # fn example() -> Result<(), spinoso_env::Error> {
29/// const ENV: System = System::new();
30/// assert!(ENV.get(b"PATH")?.is_some());
31/// # Ok(())
32/// # }
33/// # example().unwrap()
34/// ```
35///
36/// Setting an environment variable:
37///
38/// ```no_run
39/// # use spinoso_env::System;
40/// const ENV: System = System::new();
41/// # fn example() -> Result<(), spinoso_env::Error> {
42/// ENV.put(b"ENV_BACKEND", Some(b"spinoso_env::System"))?;
43/// assert_eq!(
44///     std::env::var("ENV_BACKEND").as_deref(),
45///     Ok("spinoso_env::System")
46/// );
47/// # Ok(())
48/// # }
49/// # example().unwrap()
50/// ```
51///
52/// [Rust Standard Library]: std
53/// [`std::env`]: module@env
54#[cfg_attr(docsrs, doc(cfg(feature = "system-env")))]
55#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
56pub struct System {
57    _private: (),
58}
59
60impl System {
61    /// Constructs a new, default ENV `System` backend.
62    ///
63    /// The resulting environment variable accessor has access to the host
64    /// system via platform APIs.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// # use spinoso_env::System;
70    /// const ENV: System = System::new();
71    /// ```
72    #[inline]
73    #[must_use]
74    pub const fn new() -> Self {
75        Self { _private: () }
76    }
77
78    /// Retrieves the value for environment variable `name`.
79    ///
80    /// Returns [`None`] if the named variable does not exist. If the retrieved
81    /// environment variable value cannot be converted from a [platform string]
82    /// to a byte vector, [`None`] is returned.
83    ///
84    /// # Implementation notes
85    ///
86    /// This method accesses the host system's environment using [`env::var_os`].
87    ///
88    /// # Examples
89    ///
90    /// ```no_run
91    /// # use spinoso_env::System;
92    /// # fn example() -> Result<(), spinoso_env::Error> {
93    /// const ENV: System = System::new();
94    /// assert!(ENV.get(b"PATH")?.is_some());
95    /// # Ok(())
96    /// # }
97    /// # example().unwrap()
98    /// ```
99    ///
100    /// # Errors
101    ///
102    /// If `name` contains a NUL byte, e.g. `b'\0'`, an error is returned.
103    ///
104    /// If the environment variable name or value cannot be converted from a
105    /// byte vector to a [platform string], an error is returned.
106    ///
107    /// [platform string]: std::ffi::OsString
108    #[inline]
109    pub fn get(self, name: &[u8]) -> Result<Option<Cow<'static, [u8]>>, ArgumentError> {
110        // Per Rust docs for `std::env::set_var` and `std::env::remove_var`:
111        //
112        // <https://doc.rust-lang.org/std/env/fn.set_var.html>
113        // <https://doc.rust-lang.org/std/env/fn.remove_var.html>
114        //
115        // This function may panic if key is empty, contains an ASCII equals
116        // sign `'='` or the NUL character `'\0'`, or when the value contains
117        // the NUL character.
118        if name.is_empty() {
119            // MRI accepts empty names on get and should always return `nil`
120            // since empty names are invalid at the OS level.
121            Ok(None)
122        } else if name.find_byte(b'\0').is_some() {
123            let message = "bad environment variable name: contains null byte";
124            Err(ArgumentError::with_message(message))
125        } else if name.find_byte(b'=').is_some() {
126            // MRI accepts names containing '=' on get and should always return
127            // `nil` since these names are invalid at the OS level.
128            Ok(None)
129        } else {
130            let name = bytes_to_os_str(name)?;
131            if let Some(value) = env::var_os(name) {
132                let value = os_string_to_bytes(value).map(Cow::Owned);
133                Ok(value.ok())
134            } else {
135                Ok(None)
136            }
137        }
138    }
139
140    /// Sets the environment variable `name` to `value`.
141    ///
142    /// If the value given is [`None`] the environment variable is deleted.
143    ///
144    /// # Implementation notes
145    ///
146    /// This method accesses the host system's environment using [`env::set_var`]
147    /// and [`env::remove_var`].
148    ///
149    /// # Examples
150    ///
151    /// ```no_run
152    /// # use spinoso_env::System;
153    /// # use std::borrow::Cow;
154    /// const ENV: System = System::new();
155    /// # fn example() -> Result<(), spinoso_env::Error> {
156    /// ENV.put(b"RUBY", Some(b"Artichoke"))?;
157    /// assert_eq!(ENV.get(b"RUBY")?.as_deref(), Some(&b"Artichoke"[..]));
158    /// ENV.put(b"RUBY", None)?;
159    /// assert_eq!(ENV.get(b"RUBY")?, None);
160    /// # Ok(())
161    /// # }
162    /// # example().unwrap();
163    /// ```
164    ///
165    /// # Errors
166    ///
167    /// If `name` contains a NUL byte, e.g. `b'\0'`, an argument error is
168    /// returned.
169    ///
170    /// If `name` contains an '=' byte, e.g. `b'='`, an `EINVAL` error is
171    /// returned.
172    ///
173    /// If `value` is [`Some`] and contains a NUL byte, e.g. `b'\0'`, an
174    /// argument error is returned.
175    ///
176    /// If the environment variable name or value cannot be converted from a
177    /// byte vector to a [platform string], an error is returned.
178    ///
179    /// [platform string]: std::ffi::OsString
180    #[inline]
181    pub fn put(self, name: &[u8], value: Option<&[u8]>) -> Result<(), Error> {
182        // Per Rust docs for `std::env::set_var` and `std::env::remove_var`:
183        //
184        // <https://doc.rust-lang.org/std/env/fn.set_var.html>
185        // <https://doc.rust-lang.org/std/env/fn.remove_var.html>
186        //
187        // This function may panic if key is empty, contains an ASCII equals
188        // sign `'='` or the NUL character `'\0'`, or when the value contains
189        // the NUL character.
190        if name.find_byte(b'\0').is_some() {
191            let message = "bad environment variable name: contains null byte";
192            Err(ArgumentError::with_message(message).into())
193        } else if let Some(value) = value {
194            if value.find_byte(b'\0').is_some() {
195                let message = "bad environment variable value: contains null byte";
196                return Err(ArgumentError::with_message(message).into());
197            }
198            if name.find_byte(b'=').is_some() {
199                let mut message = b"Invalid argument - setenv(".to_vec();
200                message.extend_from_slice(name);
201                message.push(b')');
202                return Err(InvalidError::from(message).into());
203            }
204            if name.is_empty() {
205                let message = "Invalid argument - setenv()";
206                return Err(InvalidError::with_message(message).into());
207            }
208            let name = bytes_to_os_str(name)?;
209            let value = bytes_to_os_str(value)?;
210            // SAFETY: MRI Ruby permits this unsafety.
211            unsafe {
212                env::set_var(name, value);
213            }
214            Ok(())
215        } else if name.is_empty() || name.find_byte(b'=').is_some() {
216            Ok(())
217        } else {
218            let name = bytes_to_os_str(name)?;
219            // SAFETY: MRI Ruby permits this unsafety.
220            unsafe {
221                env::remove_var(name);
222            }
223            Ok(())
224        }
225    }
226
227    /// Serialize the environ to a [`HashMap`].
228    ///
229    /// Map keys are environment variable names and map values are environment
230    /// variable values.
231    ///
232    /// # Implementation notes
233    ///
234    /// This method accesses the host system's environment using [`env::vars_os`].
235    ///
236    /// # Examples
237    ///
238    /// ```no_run
239    /// # use spinoso_env::System;
240    /// const ENV: System = System::new();
241    /// # fn example() -> Result<(), spinoso_env::Error> {
242    /// let map = ENV.to_map()?;
243    /// assert!(map.contains_key(&b"PATH"[..]));
244    /// # Ok(())
245    /// # }
246    /// # example().unwrap()
247    /// ```
248    ///
249    /// # Errors
250    ///
251    /// If any environment variable name or value cannot be converted from a
252    /// [platform string] to a byte vector, an error is returned.
253    ///
254    /// [platform string]: std::ffi::OsString
255    #[inline]
256    pub fn to_map(self) -> Result<HashMap<Bytes, Bytes>, ArgumentError> {
257        let mut map = HashMap::new();
258        for (name, value) in env::vars_os() {
259            let name = os_string_to_bytes(name)?;
260            let value = os_string_to_bytes(value)?;
261            map.insert(name, value);
262        }
263        Ok(map)
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::System;
270    use crate::{ArgumentError, Error, InvalidError};
271
272    const ENV: System = System::new();
273
274    // ```console
275    // $ ruby -e 'puts ENV[""].inspect'
276    // nil
277    // ```
278    #[test]
279    fn get_name_empty() {
280        let name: &[u8] = b"";
281        assert_eq!(ENV.get(name), Ok(None));
282    }
283
284    // ```consol
285    // $ ruby -e 'puts ENV["980b1f2f-a155-4cc6-97f3-cafc3cea2b1a-foo\0bar"].inspect'
286    // Traceback (most recent call last):
287    // 	1: from -e:1:in `<main>'
288    // -e:1:in `[]': bad environment variable name: contains null byte (ArgumentError)
289    // ```
290    #[test]
291    fn get_name_nul_byte_err() {
292        let name: &[u8] = b"980b1f2f-a155-4cc6-97f3-cafc3cea2b1a-foo\0bar";
293        assert_eq!(
294            ENV.get(name),
295            Err(ArgumentError::with_message(
296                "bad environment variable name: contains null byte"
297            ))
298        );
299    }
300
301    // ```console
302    // $ ruby -e 'puts ENV["fa7575b4-3224-4fbb-9201-85d54ea95b93-foo=bar"].inspect'
303    // nil
304    // ```
305    #[test]
306    fn get_name_equal_byte_unset() {
307        let name: &[u8] = b"fa7575b4-3224-4fbb-9201-85d54ea95b93-foo=bar";
308        assert_eq!(ENV.get(name), Ok(None));
309    }
310
311    // ```console
312    // $ ruby -e 'ENV["0f87d787-bf18-437a-a205-ed38d81fa4da-foo\0bar"] = "3427d141-700f-494f-bfa6-877147333249-baz"'
313    // Traceback (most recent call last):
314    // 	1: from -e:1:in `<main>'
315    // -e:1:in `[]=': bad environment variable name: contains null byte (ArgumentError)
316    // ```
317    #[test]
318    fn put_name_null_byte_err_set_value() {
319        let name: &[u8] = b"0f87d787-bf18-437a-a205-ed38d81fa4da-foo\0bar";
320        let value: &[u8] = b"3427d141-700f-494f-bfa6-877147333249-baz";
321        assert_eq!(
322            ENV.put(name, Some(value)),
323            Err(Error::Argument(ArgumentError::with_message(
324                "bad environment variable name: contains null byte"
325            )))
326        );
327    }
328
329    // ```console
330    // $ ruby -e 'ENV["1437e58a-b7e3-4c5e-9b1f-a67b78fe1e42-foo\0bar"] = nil'
331    // Traceback (most recent call last):
332    // 	1: from -e:1:in `<main>'
333    // -e:1:in `[]=': bad environment variable name: contains null byte (ArgumentError)
334    // ```
335    #[test]
336    fn put_name_nul_byte_err_unset_value() {
337        let name: &[u8] = b"1437e58a-b7e3-4c5e-9b1f-a67b78fe1e42-foo\0bar";
338        assert_eq!(
339            ENV.put(name, None),
340            Err(Error::Argument(ArgumentError::with_message(
341                "bad environment variable name: contains null byte"
342            )))
343        );
344    }
345
346    // ```console
347    // $ ruby -e 'ENV["75b8c10e-4a1d-4f61-9800-5f5c29087edd-foo\0bar"] = "a19660e3-304d-45b8-8746-297a2065a076-baz\0quux"'
348    // Traceback (most recent call last):
349    // 	1: from -e:1:in `<main>'
350    // -e:1:in `[]=': bad environment variable name: contains null byte (ArgumentError)
351    // ```
352    #[test]
353    fn put_name_null_byte_set_value_nul_byte_err() {
354        let name: &[u8] = b"75b8c10e-4a1d-4f61-9800-5f5c29087edd-foo\0bar";
355        let value: &[u8] = b"a19660e3-304d-45b8-8746-297a2065a076-baz\0quux";
356        assert_eq!(
357            ENV.put(name, Some(value)),
358            Err(Error::Argument(ArgumentError::with_message(
359                "bad environment variable name: contains null byte"
360            )))
361        );
362    }
363
364    // ```console
365    // $ ruby -e 'ENV["044f35c0-f711-4b80-8de5-4579075cd754-foo-bar"] = "52bb4d27-6d8a-4a83-90f8-51940ce1f1a7-baz\0quux"'
366    // Traceback (most recent call last):
367    // 	1: from -e:1:in `<main>'
368    // -e:1:in `[]=': bad environment variable value: contains null byte (ArgumentError)
369    // ```
370    #[test]
371    fn put_name_set_value_nul_byte_err() {
372        let name: &[u8] = b"044f35c0-f711-4b80-8de5-4579075cd754-foo-bar";
373        let value: &[u8] = b"52bb4d27-6d8a-4a83-90f8-51940ce1f1a7-baz\0quux";
374        assert_eq!(
375            ENV.put(name, Some(value)),
376            Err(Error::Argument(ArgumentError::with_message(
377                "bad environment variable value: contains null byte"
378            )))
379        );
380    }
381
382    // ```console
383    // $ ruby -e 'ENV["="] = nil'
384    // ```
385    #[test]
386    fn put_name_eq_unset() {
387        let name: &[u8] = b"=";
388        assert_eq!(ENV.put(name, None), Ok(()));
389    }
390
391    // ```console
392    // $ ruby -e 'ENV["="] = ""'
393    // Traceback (most recent call last):
394    // 	1: from -e:1:in `<main>'
395    // -e:1:in `[]=': Invalid argument - setenv(=) (Errno::EINVAL)
396    // ```
397    #[test]
398    fn put_name_eq_set_value_empty_byte_err() {
399        let name: &[u8] = b"=";
400        let value: &[u8] = b"";
401        assert_eq!(
402            ENV.put(name, Some(value)),
403            Err(Error::Invalid(InvalidError::with_message(
404                "Invalid argument - setenv(=)"
405            )))
406        );
407    }
408
409    // ```console
410    // $ ruby -e 'ENV["="] = "4ac79e15-2b8c-4771-8fc8-ff0b095ce7d0-baz-quux"'
411    // Traceback (most recent call last):
412    // 	1: from -e:1:in `<main>'
413    // -e:1:in `[]=': Invalid argument - setenv(=) (Errno::EINVAL)
414    // ```
415    #[test]
416    fn put_name_eq_set_value_non_empty_err() {
417        let name: &[u8] = b"=";
418        let value: &[u8] = b"4ac79e15-2b8c-4771-8fc8-ff0b095ce7d0-baz-quux";
419        assert_eq!(
420            ENV.put(name, Some(value)),
421            Err(Error::Invalid(InvalidError::with_message(
422                "Invalid argument - setenv(=)"
423            )))
424        );
425    }
426
427    // ```console
428    // $ ruby -e 'ENV["="] = "42db3f11-46f5-4cab-93f4-ee543c1634f9-baz\0quux"'
429    // Traceback (most recent call last):
430    // 	1: from -e:1:in `<main>'
431    // -e:1:in `[]=': bad environment variable value: contains null byte (ArgumentError)
432    // ```
433    #[test]
434    fn put_name_eq_set_value_null_byte_err() {
435        let name: &[u8] = b"=";
436        let value: &[u8] = b"42db3f11-46f5-4cab-93f4-ee543c1634f9-baz\0quux";
437        assert_eq!(
438            ENV.put(name, Some(value)),
439            Err(Error::Argument(ArgumentError::with_message(
440                "bad environment variable value: contains null byte"
441            )))
442        );
443    }
444
445    // ```console
446    // $ ruby -e 'ENV["=71cb1499-3a0d-476a-8334-aa7a334f387e-\0"] = "42db3f11-46f5-4cab-93f4-ee543c1634f9-baz\0quux"'
447    // Traceback (most recent call last):
448    // 	1: from -e:1:in `<main>'
449    // -e:1:in `[]=': bad environment variable name: contains null byte (ArgumentError)
450    // ```
451    #[test]
452    fn put_name_eq_nul_set_value_null_byte_err() {
453        let name: &[u8] = b"=71cb1499-3a0d-476a-8334-aa7a334f387e-\0";
454        let value: &[u8] = b"42db3f11-46f5-4cab-93f4-ee543c1634f9-baz\0quux";
455        assert_eq!(
456            ENV.put(name, Some(value)),
457            Err(Error::Argument(ArgumentError::with_message(
458                "bad environment variable name: contains null byte"
459            )))
460        );
461    }
462
463    // ```console
464    // $ ruby -e 'ENV[""] = nil'
465    // ```
466    #[test]
467    fn put_name_empty_value_unset() {
468        let name: &[u8] = b"";
469        assert_eq!(ENV.put(name, None), Ok(()));
470    }
471
472    // ```console
473    // $ ruby -e 'ENV[""] = ""'
474    // Traceback (most recent call last):
475    // 	1: from -e:1:in `<main>'
476    // -e:1:in `[]=': Invalid argument - setenv() (Errno::EINVAL)
477    // ```
478    #[test]
479    fn put_name_empty_set_value_empty_err() {
480        let name: &[u8] = b"";
481        let value: &[u8] = b"";
482        assert_eq!(
483            ENV.put(name, Some(value)),
484            Err(Error::Invalid(InvalidError::with_message(
485                "Invalid argument - setenv()"
486            )))
487        );
488    }
489
490    // ```console
491    // $ ruby -e 'ENV[""] = "157f6920-04e5-4561-8f06-6f00d09c3610-foo"'
492    // Traceback (most recent call last):
493    // 	1: from -e:1:in `<main>'
494    // -e:1:in `[]=': Invalid argument - setenv() (Errno::EINVAL)
495    // ```
496    #[test]
497    fn put_name_empty_set_value_non_empty_err() {
498        let name: &[u8] = b"";
499        let value: &[u8] = b"157f6920-04e5-4561-8f06-6f00d09c3610-foo";
500        assert_eq!(
501            ENV.put(name, Some(value)),
502            Err(Error::Invalid(InvalidError::with_message(
503                "Invalid argument - setenv()"
504            )))
505        );
506    }
507
508    // ```console
509    // $ ruby -e 'ENV[""] = "1d50869d-e71a-4347-8b28-b274f34e2892-foo\0bar"'
510    // Traceback (most recent call last):
511    // 	1: from -e:1:in `<main>'
512    // -e:1:in `[]=': bad environment variable value: contains null byte (ArgumentError)
513    // ```
514    #[test]
515    fn put_name_empty_set_value_non_empty_nul_byte_err() {
516        let name: &[u8] = b"";
517        let value: &[u8] = b"1d50869d-e71a-4347-8b28-b274f34e2892-foo\0bar";
518        assert_eq!(
519            ENV.put(name, Some(value)),
520            Err(Error::Argument(ArgumentError::with_message(
521                "bad environment variable value: contains null byte"
522            )))
523        );
524    }
525
526    #[test]
527    fn set_get_happy_path() {
528        // given
529        let name: &[u8] = b"308a3d98-2f87-46fd-b996-ae471a76b64e";
530        let value: &[u8] = b"value";
531        assert_eq!(ENV.get(name), Ok(None));
532
533        // when
534        ENV.put(name, Some(value)).unwrap();
535        let retrieved = ENV.get(name);
536
537        // then
538        assert_eq!(retrieved.unwrap().unwrap(), value);
539    }
540
541    #[test]
542    fn set_unset_happy_path() {
543        // given
544        let name: &[u8] = b"7a6885c3-0c17-4310-a5e7-ed971cac69b6";
545        let value: &[u8] = b"value";
546        assert_eq!(ENV.get(name), Ok(None));
547
548        // when
549        ENV.put(name, Some(value)).unwrap();
550        ENV.put(name, None).unwrap();
551        let value = ENV.get(name);
552
553        // then
554        assert!(value.unwrap().is_none());
555    }
556
557    #[test]
558    fn to_h() {
559        // given
560        let name_a: &[u8] = b"3ab42e94-9b7f-4e96-b9c7-ba1738c61f89";
561        let value_a: &[u8] = b"value1";
562        let name_b: &[u8] = b"3e7bf2b3-9517-444b-bda8-7f5dd3b36648";
563        let value_b: &[u8] = b"value2";
564
565        // when
566        ENV.put(name_a, Some(value_a)).unwrap();
567        ENV.put(name_b, Some(value_b)).unwrap();
568        let data = ENV.to_map().unwrap();
569
570        // then
571        let value1 = data.get(name_a);
572        let value2 = data.get(name_b);
573        assert!(value1.is_some());
574        assert!(value2.is_some());
575        assert_eq!(value1.unwrap(), &value_a);
576        assert_eq!(value2.unwrap(), &value_b);
577    }
578}