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}