spinoso_env/env/
memory.rs

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