use std::collections::BTreeMap;
use std::env;
use std::ffi::OsString;
use std::fmt;
const MAX_ENVIRONMENT_KEY_LENGTH: usize = 256;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Environment;
impl Environment {
#[must_use]
pub const fn new() -> Self {
Self
}
pub fn get(&self, key: &str) -> Result<Option<String>, EnvironmentError> {
validate_key(key)?;
env::var_os(key)
.map(|value| decode_os_string(value, key))
.transpose()
}
pub fn require(&self, key: &str) -> Result<String, EnvironmentError> {
validate_key(key)?;
match env::var_os(key) {
Some(value) => decode_os_string(value, key),
None => Err(EnvironmentError::MissingKey {
key: key.to_owned(),
}),
}
}
pub fn contains(&self, key: &str) -> Result<bool, EnvironmentError> {
validate_key(key)?;
Ok(env::var_os(key).is_some())
}
pub fn snapshot(&self) -> Result<EnvironmentSnapshot, EnvironmentError> {
EnvironmentSnapshot::from_process()
}
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct EnvironmentSnapshot {
values: BTreeMap<String, String>,
}
impl EnvironmentSnapshot {
fn from_process() -> Result<Self, EnvironmentError> {
let mut values = BTreeMap::new();
for (raw_key, raw_value) in env::vars_os() {
let key = raw_key
.into_string()
.map_err(|_| EnvironmentError::InvalidEncoding {
key: "<non-unicode-environment-key>".to_owned(),
})?;
let value = decode_os_string(raw_value, &key)?;
values.insert(key, value);
}
Ok(Self { values })
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.values.get(key).map(String::as_str)
}
#[must_use]
pub fn contains(&self, key: &str) -> bool {
self.values.contains_key(key)
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.values
.iter()
.map(|(key, value)| (key.as_str(), value.as_str()))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum EnvironmentError {
InvalidKey,
KeyTooLong { max: usize },
InvalidEncoding { key: String },
MissingKey { key: String },
}
impl std::fmt::Display for EnvironmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidKey => {
write!(f, "environment key must not be empty or contain '=' or NUL")
}
Self::KeyTooLong { max } => {
write!(f, "environment key exceeds maximum length of {max}")
}
Self::InvalidEncoding { key } => {
write!(f, "environment value for key {key} is not valid Unicode")
}
Self::MissingKey { key } => {
write!(f, "required environment key is missing: {key}")
}
}
}
}
impl std::error::Error for EnvironmentError {}
fn validate_key(key: &str) -> Result<(), EnvironmentError> {
if key.is_empty() || key.contains('=') || key.contains('\0') {
return Err(EnvironmentError::InvalidKey);
}
if key.len() > MAX_ENVIRONMENT_KEY_LENGTH {
return Err(EnvironmentError::KeyTooLong {
max: MAX_ENVIRONMENT_KEY_LENGTH,
});
}
Ok(())
}
fn decode_os_string(value: OsString, key: &str) -> Result<String, EnvironmentError> {
value
.into_string()
.map_err(|_| EnvironmentError::InvalidEncoding {
key: key.to_owned(),
})
}
impl fmt::Debug for EnvironmentSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EnvironmentSnapshot")
.field("value_count", &self.values.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn environment_can_be_constructed() {
assert_eq!(Environment::new(), Environment);
}
#[test]
fn empty_keys_are_rejected() {
let environment = Environment::new();
assert_eq!(
environment.get("").expect_err("empty key must fail"),
EnvironmentError::InvalidKey
);
assert_eq!(
environment.require("").expect_err("empty key must fail"),
EnvironmentError::InvalidKey
);
assert_eq!(
environment.contains("").expect_err("empty key must fail"),
EnvironmentError::InvalidKey
);
}
#[test]
fn overlong_keys_are_rejected() {
let environment = Environment::new();
let key = "K".repeat(MAX_ENVIRONMENT_KEY_LENGTH + 1);
assert_eq!(
environment.get(&key).expect_err("overlong key must fail"),
EnvironmentError::KeyTooLong {
max: MAX_ENVIRONMENT_KEY_LENGTH,
}
);
}
#[test]
fn missing_required_key_is_reported() {
let environment = Environment::new();
let key = format!("NIZAAM_ENVIRONMENT_TEST_MISSING_{}", std::process::id());
assert_eq!(environment.get(&key).expect("lookup should succeed"), None);
assert_eq!(
environment
.require(&key)
.expect_err("missing key must fail"),
EnvironmentError::MissingKey { key }
);
}
#[test]
fn present_environment_key_is_read_as_a_raw_string() {
let environment = Environment::new();
let key = if cfg!(windows) { "PATH" } else { "HOME" };
let value = environment
.get(key)
.expect("environment lookup should succeed");
if let Some(value) = value {
assert_eq!(
value,
environment
.require(key)
.expect("present environment key should be required successfully")
);
assert!(!value.contains('\0'));
}
}
#[test]
fn contains_matches_get_for_a_known_key() {
let environment = Environment::new();
let key = if cfg!(windows) { "PATH" } else { "HOME" };
assert_eq!(
environment.contains(key).expect("lookup should succeed"),
environment
.get(key)
.expect("lookup should succeed")
.is_some()
);
}
#[test]
fn snapshot_is_deterministically_ordered_and_read_only() {
let environment = Environment::new();
let snapshot = environment
.snapshot()
.expect("environment snapshot should decode successfully");
let entries: Vec<_> = snapshot.iter().collect();
assert_eq!(snapshot.len(), entries.len());
assert_eq!(snapshot.is_empty(), entries.is_empty());
for pair in entries.windows(2) {
assert!(pair[0].0 <= pair[1].0);
}
if let Some((key, value)) = entries.first() {
assert!(snapshot.contains(key));
assert_eq!(snapshot.get(key), Some(*value));
}
}
#[test]
fn keys_containing_equals_or_nul_are_rejected_consistently() {
let environment = Environment::new();
for key in ["BAD=KEY", "BAD\0KEY"] {
assert_eq!(
environment.get(key).expect_err("invalid key must fail"),
EnvironmentError::InvalidKey
);
assert_eq!(
environment.require(key).expect_err("invalid key must fail"),
EnvironmentError::InvalidKey
);
assert_eq!(
environment
.contains(key)
.expect_err("invalid key must fail"),
EnvironmentError::InvalidKey
);
}
}
#[cfg(unix)]
#[test]
fn non_unicode_os_values_are_rejected() {
use std::os::unix::ffi::OsStringExt;
let invalid = OsString::from_vec(vec![0xff]);
let error = decode_os_string(invalid, "SECRET").expect_err("invalid UTF-8 must fail");
assert_eq!(
error,
EnvironmentError::InvalidEncoding {
key: "SECRET".to_owned(),
}
);
}
#[test]
fn environment_snapshot_debug_does_not_render_values() {
let mut values = BTreeMap::new();
values.insert("SECRET".to_owned(), "super-secret-value".to_owned());
let snapshot = EnvironmentSnapshot { values };
let rendered = format!("{snapshot:?}");
assert!(rendered.contains("EnvironmentSnapshot"));
assert!(rendered.contains("value_count"));
assert!(!rendered.contains("super-secret-value"));
}
}