spinoso_env/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::undocumented_unsafe_blocks)]
2#![allow(
3    clippy::let_underscore_untyped,
4    reason = "https://github.com/rust-lang/rust-clippy/pull/10442#issuecomment-1516570154"
5)]
6#![allow(
7    clippy::question_mark,
8    reason = "https://github.com/rust-lang/rust-clippy/issues/8281"
9)]
10#![allow(clippy::manual_let_else, reason = "manual_let_else was very buggy on release")]
11#![allow(clippy::missing_errors_doc, reason = "A lot of existing code fails this lint")]
12#![allow(
13    clippy::unnecessary_lazy_evaluations,
14    reason = "https://github.com/rust-lang/rust-clippy/issues/8109"
15)]
16#![cfg_attr(
17    test,
18    allow(clippy::non_ascii_literal, reason = "tests sometimes require UTF-8 string content")
19)]
20#![allow(unknown_lints)]
21#![warn(
22    missing_copy_implementations,
23    missing_debug_implementations,
24    missing_docs,
25    rust_2024_compatibility,
26    trivial_casts,
27    trivial_numeric_casts,
28    unused_qualifications,
29    variant_size_differences
30)]
31// Enable feature callouts in generated documentation:
32// https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html
33//
34// This approach is borrowed from tokio.
35#![cfg_attr(docsrs, feature(doc_cfg))]
36#![cfg_attr(docsrs, feature(doc_alias))]
37
38//! ENV is a hash-like accessor for environment variables.
39//!
40//! This module implements the [`ENV`] singleton object from Ruby Core.
41//!
42//! In Artichoke, the environment variable store is modeled as a hash map of
43//! byte vector keys and values, e.g. `HashMap<Vec<u8>, Vec<u8>>`. Backends are
44//! expected to convert their internals to this representation in their public
45//! APIs. For this reason, all APIs exposed by ENV backends in this crate are
46//! fallible.
47//!
48//! You can use this object in your application by accessing it directly. As a
49//! Core API, it is globally available:
50//!
51//! ```ruby
52//! ENV["PATH"]
53//! ENV["PS1"] = 'artichoke> '
54//! ```
55//!
56//! There are two `ENV` implementations in this crate:
57//!
58//! - [`Memory`], enabled by default, implements an `ENV` store and accessor on
59//!   top of a Rust [`HashMap`]. This backend does not query or modify the host
60//!   system.
61//! - [`System`], enabled when the **system-env** feature is activated, is a
62//!   proxy for the system environment and uses platform-specific APIs defined
63//!   in the [Rust Standard Library].
64//!
65//! # Examples
66//!
67//! Using the in-memory backend allows safely manipulating an emulated environment:
68//!
69//! ```
70//! # use spinoso_env::Memory;
71//! # fn example() -> Result<(), spinoso_env::Error> {
72//! let mut env = Memory::new();
73//! // This does not alter the behavior of the host Rust process.
74//! env.put(b"PATH", None)?;
75//! // `Memory` backends start out empty.
76//! assert_eq!(env.get(b"HOME")?, None);
77//! # Ok(())
78//! # }
79//! # example().unwrap()
80//! ```
81//!
82//! System backends inherit and mutate the environment from the current Rust
83//! process:
84//!
85//! ```
86//! # #[cfg(feature = "system-env")]
87//! # use spinoso_env::System;
88//! # #[cfg(feature = "system-env")]
89//! const ENV: System = System::new();
90//! # #[cfg(feature = "system-env")]
91//! # fn example() -> Result<(), spinoso_env::Error> {
92//! ENV.put(b"RUBY", Some(b"Artichoke"))?;
93//! assert!(ENV.get(b"PATH")?.is_some());
94//! # Ok(())
95//! # }
96//! # #[cfg(feature = "system-env")]
97//! # example().unwrap()
98//! ```
99//!
100//! # Crate features
101//!
102//! This crate requires [`std`], the Rust Standard Library.
103//!
104//! All features are enabled by default:
105//!
106//! - **system-env** - Enable an `ENV` backend that accesses the host system's
107//!   environment variables via the [`std::env`] module.
108//!
109#![cfg_attr(
110    not(feature = "system-env"),
111    doc = "[`System`]: https://artichoke.github.io/artichoke/spinoso_env/struct.System.html"
112)]
113//! [`ENV`]: https://ruby-doc.org/core-3.1.2/ENV.html
114//! [`HashMap`]: std::collections::HashMap
115//! [Rust Standard Library]: std
116//! [`std::env`]: module@std::env
117
118// Ensure code blocks in `README.md` compile
119#[cfg(all(doctest, feature = "system-env"))]
120#[doc = include_str!("../README.md")]
121mod readme {}
122
123use core::fmt;
124use std::borrow::Cow;
125use std::error;
126
127#[cfg(feature = "system-env")]
128use scolapasta_path::ConvertBytesError;
129use scolapasta_string_escape::format_debug_escape_into;
130
131mod env;
132
133pub use env::memory::Memory;
134#[cfg(feature = "system-env")]
135pub use env::system::System;
136
137/// Sum type of all errors possibly returned from [`get`], [`put`], and
138/// [`to_map`].
139///
140/// These APIs can return errors under several conditions:
141///
142/// - An environment variable name is not convertible to a [platform string].
143/// - An environment variable value is not convertible to a [platform string].
144/// - An environment variable name contains a NUL byte.
145/// - An environment variable name contains an `=` byte.
146/// - An environment variable value contains a NUL byte.
147///
148/// Ruby represents these error conditions with different exception types.
149///
150/// [`get`]: Memory::get
151/// [`put`]: Memory::put
152/// [`to_map`]: Memory::to_map
153/// [platform string]: std::ffi::OsString
154#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
155pub enum Error {
156    /// Error that indicates an argument parsing or value logic error occurred.
157    ///
158    /// See [`ArgumentError`].
159    Argument(ArgumentError),
160    /// Error that indicates the access to the underlying platform APIs failed.
161    ///
162    /// This error type corresponds to the `EINVAL` syscall error.
163    ///
164    /// See [`InvalidError`].
165    Invalid(InvalidError),
166}
167
168#[cfg(feature = "system-env")]
169impl From<ConvertBytesError> for Error {
170    fn from(err: ConvertBytesError) -> Self {
171        Self::Argument(err.into())
172    }
173}
174
175impl From<ArgumentError> for Error {
176    #[inline]
177    fn from(err: ArgumentError) -> Self {
178        Self::Argument(err)
179    }
180}
181
182impl From<InvalidError> for Error {
183    #[inline]
184    fn from(err: InvalidError) -> Self {
185        Self::Invalid(err)
186    }
187}
188
189impl fmt::Display for Error {
190    #[inline]
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.write_str("ENV error")
193    }
194}
195
196impl error::Error for Error {
197    #[inline]
198    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
199        match self {
200            Self::Argument(err) => Some(err),
201            Self::Invalid(err) => Some(err),
202        }
203    }
204}
205
206/// Error that indicates an argument parsing or value logic error occurred.
207///
208/// Argument errors have an associated message.
209///
210/// This error corresponds to the [Ruby `ArgumentError` Exception class].
211///
212/// # Examples
213///
214/// ```
215/// # use spinoso_env::ArgumentError;
216/// let err = ArgumentError::new();
217/// assert_eq!(err.message(), "ArgumentError");
218///
219/// let err = ArgumentError::with_message("bad environment variable name: contains null byte");
220/// assert_eq!(
221///     err.message(),
222///     "bad environment variable name: contains null byte"
223/// );
224/// ```
225///
226/// [Ruby `ArgumentError` Exception class]: https://ruby-doc.org/core-3.1.2/ArgumentError.html
227#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
228pub struct ArgumentError(&'static str);
229
230impl From<&'static str> for ArgumentError {
231    #[inline]
232    fn from(message: &'static str) -> Self {
233        Self::with_message(message)
234    }
235}
236
237#[cfg(feature = "system-env")]
238impl From<ConvertBytesError> for ArgumentError {
239    fn from(_err: ConvertBytesError) -> Self {
240        Self::with_message("bytes could not be converted to a platform string")
241    }
242}
243
244impl Default for ArgumentError {
245    #[inline]
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251impl fmt::Display for ArgumentError {
252    #[inline]
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        f.write_str(self.message())
255    }
256}
257
258impl error::Error for ArgumentError {}
259
260impl ArgumentError {
261    /// Construct a new, default argument error.
262    ///
263    /// # Examples
264    ///
265    /// ```
266    /// # use spinoso_env::ArgumentError;
267    /// const ERR: ArgumentError = ArgumentError::new();
268    /// assert_eq!(ERR.message(), "ArgumentError");
269    /// ```
270    #[inline]
271    #[must_use]
272    pub const fn new() -> Self {
273        Self("ArgumentError")
274    }
275
276    /// Construct a new, argument error with a message.
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// # use spinoso_env::ArgumentError;
282    /// const ERR: ArgumentError =
283    ///     ArgumentError::with_message("bad environment variable name: contains null byte");
284    /// assert_eq!(
285    ///     ERR.message(),
286    ///     "bad environment variable name: contains null byte"
287    /// );
288    /// ```
289    #[inline]
290    #[must_use]
291    pub const fn with_message(message: &'static str) -> Self {
292        Self(message)
293    }
294
295    /// Retrieve the exception message associated with this argument error.
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// # use spinoso_env::ArgumentError;
301    /// let err = ArgumentError::new();
302    /// assert_eq!(err.message(), "ArgumentError");
303    ///
304    /// let err = ArgumentError::with_message("bad environment variable name: contains null byte");
305    /// assert_eq!(
306    ///     err.message(),
307    ///     "bad environment variable name: contains null byte"
308    /// );
309    /// ```
310    #[inline]
311    #[must_use]
312    pub const fn message(self) -> &'static str {
313        self.0
314    }
315}
316
317/// Error that indicates the underlying platform API returned an error.
318///
319/// This error is typically returned by the operating system and corresponds to
320/// `EINVAL`.
321///
322/// # Examples
323///
324/// ```
325/// # use spinoso_env::InvalidError;
326/// let err = InvalidError::new();
327/// assert_eq!(err.message(), b"Errno::EINVAL");
328///
329/// let err = InvalidError::with_message("Invalid argument - setenv()");
330/// assert_eq!(err.message(), b"Invalid argument - setenv()");
331/// ```
332#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
333pub struct InvalidError(Cow<'static, [u8]>);
334
335impl fmt::Display for InvalidError {
336    #[inline]
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        format_debug_escape_into(f, self.message())
339    }
340}
341
342impl error::Error for InvalidError {}
343
344impl From<&'static str> for InvalidError {
345    #[inline]
346    fn from(message: &'static str) -> Self {
347        Self::with_message(message)
348    }
349}
350
351impl From<&'static [u8]> for InvalidError {
352    #[inline]
353    fn from(message: &'static [u8]) -> Self {
354        Self(Cow::Borrowed(message))
355    }
356}
357
358impl From<Vec<u8>> for InvalidError {
359    #[inline]
360    fn from(message: Vec<u8>) -> Self {
361        Self(Cow::Owned(message))
362    }
363}
364
365impl Default for InvalidError {
366    fn default() -> Self {
367        Self::new()
368    }
369}
370
371impl InvalidError {
372    /// Construct a new, default invalid error.
373    ///
374    /// # Examples
375    ///
376    /// ```
377    /// # use spinoso_env::InvalidError;
378    /// const ERR: InvalidError = InvalidError::new();
379    /// assert_eq!(ERR.message(), b"Errno::EINVAL");
380    /// ```
381    #[inline]
382    #[must_use]
383    pub const fn new() -> Self {
384        const MESSAGE: &[u8] = b"Errno::EINVAL";
385
386        Self(Cow::Borrowed(MESSAGE))
387    }
388
389    /// Construct a new, invalid error with a message.
390    ///
391    /// # Examples
392    ///
393    /// ```
394    /// # use spinoso_env::InvalidError;
395    /// const ERR: InvalidError = InvalidError::with_message("Invalid argument - setenv()");
396    /// assert_eq!(ERR.message(), b"Invalid argument - setenv()");
397    /// ```
398    #[inline]
399    #[must_use]
400    pub const fn with_message(message: &'static str) -> Self {
401        Self(Cow::Borrowed(message.as_bytes()))
402    }
403
404    /// Retrieve the exception message associated with this invalid error.
405    ///
406    /// # Examples
407    ///
408    /// ```
409    /// # use spinoso_env::InvalidError;
410    /// let err = InvalidError::new();
411    /// assert_eq!(err.message(), b"Errno::EINVAL");
412    /// ```
413    #[inline]
414    #[must_use]
415    pub fn message(&self) -> &[u8] {
416        &self.0
417    }
418
419    /// Consume this error and return the inner message.
420    ///
421    /// This method allows taking ownership of this error's message without an
422    /// allocation.
423    ///
424    /// # Examples
425    ///
426    /// ```
427    /// # use spinoso_env::InvalidError;
428    /// # use std::borrow::Cow;
429    /// let err = InvalidError::new();
430    /// assert_eq!(err.into_message(), Cow::Borrowed(b"Errno::EINVAL"));
431    /// ```
432    #[inline]
433    #[must_use]
434    pub fn into_message(self) -> Cow<'static, [u8]> {
435        self.0
436    }
437}