diff --git a/Cargo.lock b/Cargo.lock index 89335512f3..152bd66b66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2878,6 +2878,7 @@ dependencies = [ name = "libdd-capabilities-impl" version = "3.0.0" dependencies = [ + "anyhow", "bytes", "http", "http-body-util", diff --git a/libdd-capabilities-impl/Cargo.toml b/libdd-capabilities-impl/Cargo.toml index f4da7baa45..a2310f51d8 100644 --- a/libdd-capabilities-impl/Cargo.toml +++ b/libdd-capabilities-impl/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["lib"] bench = false [dependencies] +anyhow = "1.0" bytes = "1" http = "1" libdd-capabilities = { path = "../libdd-capabilities", version = "2.1.0" } diff --git a/libdd-capabilities-impl/src/env.rs b/libdd-capabilities-impl/src/env.rs new file mode 100644 index 0000000000..8bb6963019 --- /dev/null +++ b/libdd-capabilities-impl/src/env.rs @@ -0,0 +1,37 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Native environment-variable capability backed by `std::env`. + +use std::env::VarError; + +use libdd_capabilities::env::{EnvCapability, EnvError}; + +#[derive(Clone, Debug)] +pub struct NativeEnvCapability; + +impl EnvCapability for NativeEnvCapability { + fn new() -> Self { + Self + } + + fn get(&self, name: &str) -> Result, EnvError> { + match std::env::var(name) { + Ok(v) => Ok(Some(v)), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(_)) => Err(EnvError::NotUnicode(name.to_owned())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_absent_returns_none() { + let cap = NativeEnvCapability; + // A name unlikely to be set in any environment. + assert_eq!(cap.get("LIBDD_CAP_TEST_ABSENT_XYZZY").unwrap(), None); + } +} diff --git a/libdd-capabilities-impl/src/lib.rs b/libdd-capabilities-impl/src/lib.rs index e2b48d7d74..68c1854ff6 100644 --- a/libdd-capabilities-impl/src/lib.rs +++ b/libdd-capabilities-impl/src/lib.rs @@ -7,21 +7,25 @@ //! traits using platform-native backends (hyper for HTTP, tokio for sleep, //! etc.). Leaf crates (FFI, benchmarks) pin this type as the generic parameter. +pub mod env; mod http; pub mod sleep; use core::future::Future; use std::time::Duration; +pub use env::NativeEnvCapability; pub use http::NativeHttpClient; use libdd_capabilities::{http::HttpError, MaybeSend}; -pub use libdd_capabilities::{HttpClientCapability, LogWriterCapability, SleepCapability}; +pub use libdd_capabilities::{ + EnvCapability, EnvError, HttpClientCapability, LogWriterCapability, SleepCapability, +}; pub use sleep::NativeSleepCapability; /// Bundle struct for native platform capabilities. /// -/// Delegates to [`NativeHttpClient`] for HTTP and [`NativeSleepCapability`] for -/// sleep. Task spawning is handled internally by `SharedRuntime`. +/// Delegates to [`NativeHttpClient`] for HTTP and to unit-struct capabilities +/// for the rest. Task spawning is handled internally by `SharedRuntime`. /// /// Individual capability traits keep minimal per-function bounds (e.g. /// functions that only need HTTP require just `H: HttpClientCapability`, not the @@ -31,6 +35,7 @@ pub use sleep::NativeSleepCapability; pub struct NativeCapabilities { http: NativeHttpClient, sleep: NativeSleepCapability, + env: NativeEnvCapability, } impl Default for NativeCapabilities { @@ -44,16 +49,14 @@ impl NativeCapabilities { Self { http: NativeHttpClient::new_client(), sleep: NativeSleepCapability, + env: NativeEnvCapability, } } } impl HttpClientCapability for NativeCapabilities { fn new_client() -> Self { - Self { - http: NativeHttpClient::new_client(), - sleep: NativeSleepCapability, - } + Self::new() } fn request( @@ -78,13 +81,20 @@ impl LogWriterCapability for NativeCapabilities { impl SleepCapability for NativeCapabilities { fn new() -> Self { - Self { - http: NativeHttpClient::new_client(), - sleep: NativeSleepCapability, - } + Self::new() } fn sleep(&self, duration: Duration) -> impl Future + MaybeSend { self.sleep.sleep(duration) } } + +impl EnvCapability for NativeCapabilities { + fn new() -> Self { + Self::new() + } + + fn get(&self, name: &str) -> Result, EnvError> { + self.env.get(name) + } +} diff --git a/libdd-capabilities/src/env.rs b/libdd-capabilities/src/env.rs new file mode 100644 index 0000000000..f464f74a2c --- /dev/null +++ b/libdd-capabilities/src/env.rs @@ -0,0 +1,31 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Environment-variable capability trait and error types. +//! +//! Sync: env access is a single map lookup on both native (`std::env`) and +//! wasm (`process.env`). +//! +//! `set` and `unset` are intentionally absent from this trait. libdatadog is +//! embedded in many kinds of runtime, where mutating the process environment +//! very much unsafe. Exposing mutation here would make it trivially easy for +//! callers to corrupt the environment of a multi-threaded host process. + +#[derive(Debug, thiserror::Error)] +pub enum EnvError { + #[error("The value of the environment variable `{0}` is not valid UTF-8")] + NotUnicode(String), + #[error("IO error: {0}")] + Io(anyhow::Error), +} + +pub trait EnvCapability: Clone + std::fmt::Debug { + fn new() -> Self; + + /// Read an env var. + /// + /// `Ok(None)` means the variable is unset; `Err(NotUnicode)` means it is + /// set but its value is not valid UTF-8. Callers that treat "missing" and + /// "invalid" the same should collapse both branches explicitly. + fn get(&self, name: &str) -> Result, EnvError>; +} diff --git a/libdd-capabilities/src/lib.rs b/libdd-capabilities/src/lib.rs index 1798946057..d4e5104b8e 100644 --- a/libdd-capabilities/src/lib.rs +++ b/libdd-capabilities/src/lib.rs @@ -3,12 +3,14 @@ //! Portable capability traits for cross-platform libdatadog. +pub mod env; pub mod http; pub mod log_output; pub mod maybe_send; pub mod sleep; pub mod spawn; +pub use self::env::{EnvCapability, EnvError}; pub use self::http::{HttpClientCapability, HttpError}; pub use self::log_output::LogWriterCapability; pub use self::sleep::SleepCapability;