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}