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}