diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e2d3405a8..fda79a6a45 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,10 @@ jobs: - name: "[${{ steps.rust-version.outputs.version}}] cargo build --workspace --exclude builder --verbose" shell: bash run: cargo build --workspace --exclude builder --verbose + - name: "[${{ steps.rust-version.outputs.version}}] cargo check no_std (library-config)" + if: runner.os == 'Linux' + shell: bash + run: cargo check -p libdd-library-config --no-default-features && cargo check -p libdd-library-config-ffi --no-default-features - name: "[${{ steps.rust-version.outputs.version}}] cargo nextest run --workspace --features libdd-crashtracker/generate-unit-test-files --exclude builder --profile ci --verbose -E '!test(tracing_integration_tests::)'" shell: bash # Run doc tests with cargo test and run tests with nextest and generate junit.xml diff --git a/Cargo.lock b/Cargo.lock index 6640fe9384..a9190c4065 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1695,6 +1695,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "dlmalloc" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f5b01c17f85ee988d832c40e549a64bd89ab2c9f8d8a613bdf5122ae507e294" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "dunce" version = "1.0.5" @@ -3080,9 +3091,10 @@ dependencies = [ "rmp", "rmp-serde", "serde", - "serde_yaml", "serial_test", "tempfile", + "thiserror 2.0.17", + "yaml_serde", ] [[package]] @@ -3092,9 +3104,11 @@ dependencies = [ "anyhow", "build_common", "constcat", + "dlmalloc", "libdd-common", "libdd-common-ffi", "libdd-library-config", + "rustix-dlmalloc", "tempfile", ] @@ -3412,6 +3426,12 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libyaml-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e126dda6f34391ab7b444f9922055facc83c07a910da3eb16f1e4d9c45dc777" + [[package]] name = "libz-rs-sys" version = "0.5.1" @@ -4767,6 +4787,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-dlmalloc" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011f83250d20ab55d0197e3f89b359a22a5a69e9ac584093593c6e1f3e0b1653" +dependencies = [ + "cfg-if", + "rustix 1.1.3", + "rustix-futex-sync", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix-futex-sync" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba2d42a7bc1a2068a74e31d87f98daf1800df4d7d1cd8736d847f8b70512964" +dependencies = [ + "lock_api", + "rustix 1.1.3", +] + [[package]] name = "rustls" version = "0.23.37" @@ -5138,19 +5180,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.12.1", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "serial_test" version = "3.2.0" @@ -6124,12 +6153,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -6919,6 +6942,18 @@ dependencies = [ "rustix 0.38.39", ] +[[package]] +name = "yaml_serde" +version = "0.10.4" +source = "git+https://github.com/pawelchcki/yaml-serde.git?rev=c7ad78def628d372dca2d9435b3ff0621a464e97#c7ad78def628d372dca2d9435b3ff0621a464e97" +dependencies = [ + "indexmap 2.12.1", + "itoa", + "libyaml-rs", + "ryu", + "serde", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index f5420a1cf0..2d53cee04b 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -122,6 +122,7 @@ diff,https://github.com/utkarshkukreti/diff.rs,MIT OR Apache-2.0,Utkarsh Kukreti digest,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers dispatch2,https://github.com/madsmtm/objc2,Zlib OR Apache-2.0 OR MIT,"Mads Marquart , Mary " displaydoc,https://github.com/yaahc/displaydoc,MIT OR Apache-2.0,Jane Lusby +dlmalloc,https://github.com/alexcrichton/dlmalloc-rs,MIT OR Apache-2.0,Alex Crichton dyn-clone,https://github.com/dtolnay/dyn-clone,MIT OR Apache-2.0,David Tolnay either,https://github.com/rayon-rs/either,MIT OR Apache-2.0,bluss encoding_rs,https://github.com/hsivonen/encoding_rs,(Apache-2.0 OR MIT) AND BSD-3-Clause,Henri Sivonen @@ -221,6 +222,7 @@ lazy_static,https://github.com/rust-lang-nursery/lazy-static.rs,MIT OR Apache-2. libc,https://github.com/rust-lang/libc,MIT OR Apache-2.0,The Rust Project Developers libloading,https://github.com/nagisa/rust_libloading,ISC,Simonas Kazlauskas libredox,https://gitlab.redox-os.org/redox-os/libredox,MIT,4lDO2 <4lDO2@protonmail.com> +libyaml-rs,https://github.com/yaml/libyaml-rs,MIT,"David Tolnay , YAML Organization " libz-rs-sys,https://github.com/trifectatechfoundation/zlib-rs,Zlib,The libz-rs-sys Authors link-cplusplus,https://github.com/dtolnay/link-cplusplus,MIT OR Apache-2.0,David Tolnay linux-raw-sys,https://github.com/sunfishcode/linux-raw-sys,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Dan Gohman @@ -344,6 +346,8 @@ rustc-demangle,https://github.com/rust-lang/rustc-demangle,MIT OR Apache-2.0,Ale rustc-hash,https://github.com/rust-lang-nursery/rustc-hash,Apache-2.0 OR MIT,The Rust Project Developers rustc-hash,https://github.com/rust-lang/rustc-hash,Apache-2.0 OR MIT,The Rust Project Developers rustix,https://github.com/bytecodealliance/rustix,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,"Dan Gohman , Jakub Konka " +rustix-dlmalloc,https://github.com/sunfishcode/rustix-dlmalloc,MIT OR Apache-2.0,"Alex Crichton , Dan Gohman " +rustix-futex-sync,https://github.com/sunfishcode/rustix-futex-sync,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Dan Gohman rustls,https://github.com/rustls/rustls,Apache-2.0 OR ISC OR MIT,The rustls Authors rustls-native-certs,https://github.com/rustls/rustls-native-certs,Apache-2.0 OR ISC OR MIT,The rustls-native-certs Authors rustls-pki-types,https://github.com/rustls/pki-types,MIT OR Apache-2.0,The rustls-pki-types Authors @@ -380,7 +384,6 @@ serde_regex,https://github.com/tailhook/serde-regex,MIT OR Apache-2.0,paul@colom serde_spanned,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The serde_spanned Authors serde_with,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,"Jonas Bushart, Marcin Kaลบmierczak" serde_with_macros,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,Jonas Bushart -serde_yaml,https://github.com/dtolnay/serde-yaml,MIT OR Apache-2.0,David Tolnay serial_test_derive,https://github.com/palfrey/serial_test,MIT,Tom Parker-Shemilt sha1,https://github.com/RustCrypto/hashes,MIT OR Apache-2.0,RustCrypto Developers sha2,https://github.com/RustCrypto/hashes,MIT OR Apache-2.0,RustCrypto Developers @@ -466,7 +469,6 @@ unicase,https://github.com/seanmonstar/unicase,MIT OR Apache-2.0,Sean McArthur < unicode-ident,https://github.com/dtolnay/unicode-ident,(MIT OR Apache-2.0) AND Unicode-DFS-2016,David Tolnay unicode-width,https://github.com/unicode-rs/unicode-width,MIT OR Apache-2.0,"kwantam , Manish Goregaokar " unicode-xid,https://github.com/unicode-rs/unicode-xid,MIT OR Apache-2.0,"erick.tryzelaar , kwantam , Manish Goregaokar " -unsafe-libyaml,https://github.com/dtolnay/unsafe-libyaml,MIT,David Tolnay untrusted,https://github.com/briansmith/untrusted,ISC,Brian Smith url,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers urlencoding,https://github.com/kornelski/rust_urlencoding,MIT,"Kornel , Bertram Truong " @@ -525,6 +527,7 @@ wit-bindgen-rt,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH L write16,https://github.com/hsivonen/write16,Apache-2.0 OR MIT,The write16 Authors writeable,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers xattr,https://github.com/Stebalien/xattr,MIT OR Apache-2.0,Steven Allen +yaml_serde,https://github.com/yaml/yaml-serde,MIT OR Apache-2.0,YAML Organization yansi,https://github.com/SergioBenitez/yansi,MIT OR Apache-2.0,Sergio Benitez yoke,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar yoke-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar diff --git a/datadog-ffe-ffi/Cargo.toml b/datadog-ffe-ffi/Cargo.toml index 7b5d3d2c2e..fb658ab788 100644 --- a/datadog-ffe-ffi/Cargo.toml +++ b/datadog-ffe-ffi/Cargo.toml @@ -23,7 +23,7 @@ build_common = { path = "../build-common" } [dependencies] anyhow = "1.0.93" datadog-ffe = { path = "../datadog-ffe", version = "=1.0.0" } -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, features = ["std"] } function_name = "0.3.0" [dev-dependencies] diff --git a/datadog-live-debugger-ffi/Cargo.toml b/datadog-live-debugger-ffi/Cargo.toml index 466e87a0bd..0b9425957d 100644 --- a/datadog-live-debugger-ffi/Cargo.toml +++ b/datadog-live-debugger-ffi/Cargo.toml @@ -14,7 +14,7 @@ bench = false [dependencies] datadog-live-debugger = { path = "../datadog-live-debugger" } libdd-common = { path = "../libdd-common" } -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, features = ["std"] } percent-encoding = "2.1" uuid = { version = "1.7.0", features = ["v4"] } serde_json = "1.0" diff --git a/datadog-sidecar-ffi/Cargo.toml b/datadog-sidecar-ffi/Cargo.toml index aab9cedd6e..db6d6ae0fe 100644 --- a/datadog-sidecar-ffi/Cargo.toml +++ b/datadog-sidecar-ffi/Cargo.toml @@ -17,7 +17,7 @@ datadog-sidecar = { path = "../datadog-sidecar" } libdd-trace-utils = { path = "../libdd-trace-utils" } datadog-ipc = { path = "../datadog-ipc" } libdd-common = { path = "../libdd-common" } -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, features = ["std"] } libdd-telemetry-ffi = { path = "../libdd-telemetry-ffi", default-features = false } datadog-remote-config = { path = "../datadog-remote-config" } datadog-live-debugger = { path = "../datadog-live-debugger" } diff --git a/datadog-sidecar/Cargo.toml b/datadog-sidecar/Cargo.toml index e0837e8f37..2c44153f38 100644 --- a/datadog-sidecar/Cargo.toml +++ b/datadog-sidecar/Cargo.toml @@ -93,7 +93,7 @@ nix = { version = "0.29", features = ["socket", "mman"] } sendfd = { version = "0.4", features = ["tokio"] } [target.'cfg(windows)'.dependencies] -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, features = ["std"] } libdd-crashtracker-ffi = { path = "../libdd-crashtracker-ffi", default-features = false, features = ["collector", "collector_windows"] } winapi = { version = "0.3.9", features = ["securitybaseapi", "sddl", "winerror", "winbase"] } windows-sys = { version = "0.52.0", features = ["Win32_System_SystemInformation"] } diff --git a/libdd-common-ffi/Cargo.toml b/libdd-common-ffi/Cargo.toml index 0f1443f42d..fe0aa4f756 100644 --- a/libdd-common-ffi/Cargo.toml +++ b/libdd-common-ffi/Cargo.toml @@ -13,19 +13,20 @@ publish = false bench =false [features] -default = ["cbindgen"] -cbindgen = ["build_common/cbindgen"] +default = ["std", "cbindgen"] +std = ["dep:anyhow", "dep:chrono", "dep:crossbeam-queue", "dep:hyper", "dep:serde", "dep:libdd-common"] +cbindgen = ["std", "build_common/cbindgen"] [build-dependencies] build_common = { path = "../build-common" } [dependencies] -anyhow = "1.0" -chrono = { version = "0.4.38", features = ["std"] } -crossbeam-queue = "0.3.11" -libdd-common = { path = "../libdd-common" } -hyper = { workspace = true} -serde = "1.0" +anyhow = { version = "1.0", optional = true } +chrono = { version = "0.4.38", features = ["std"], optional = true } +crossbeam-queue = { version = "0.3.11", optional = true } +libdd-common = { path = "../libdd-common", optional = true } +hyper = { workspace = true, optional = true } +serde = { version = "1.0", optional = true } [dev-dependencies] bolero = "0.13" diff --git a/libdd-common-ffi/src/cstr.rs b/libdd-common-ffi/src/cstr.rs index 0e09516f9e..8df5a8fe50 100644 --- a/libdd-common-ffi/src/cstr.rs +++ b/libdd-common-ffi/src/cstr.rs @@ -1,8 +1,10 @@ // Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +use alloc::ffi; +use alloc::vec::Vec; use core::fmt; -use std::{ +use core::{ ffi::c_char, marker::PhantomData, mem::{self, ManuallyDrop}, @@ -10,28 +12,28 @@ use std::{ }; /// Ffi safe type representing a borrowed null-terminated C array -/// Equivalent to a std::ffi::CStr +/// Equivalent to a core::ffi::CStr #[repr(C)] pub struct CStr<'a> { /// Null terminated char array ptr: ptr::NonNull, /// Length of the array, not counting the null-terminator length: usize, - _lifetime_marker: std::marker::PhantomData<&'a c_char>, + _lifetime_marker: PhantomData<&'a c_char>, } impl<'a> CStr<'a> { - pub fn from_std(s: &'a std::ffi::CStr) -> Self { + pub fn from_std(s: &'a core::ffi::CStr) -> Self { Self { ptr: unsafe { ptr::NonNull::new_unchecked(s.as_ptr().cast_mut()) }, length: s.to_bytes().len(), - _lifetime_marker: std::marker::PhantomData, + _lifetime_marker: PhantomData, } } - pub fn into_std(&self) -> &'a std::ffi::CStr { + pub fn into_std(&self) -> &'a core::ffi::CStr { unsafe { - std::ffi::CStr::from_bytes_with_nul_unchecked(std::slice::from_raw_parts( + core::ffi::CStr::from_bytes_with_nul_unchecked(core::slice::from_raw_parts( self.ptr.as_ptr().cast_const().cast(), self.length + 1, )) @@ -40,7 +42,7 @@ impl<'a> CStr<'a> { } /// Ffi safe type representing an owned null-terminated C array -/// Equivalent to a std::ffi::CString +/// Equivalent to an ffi::CString #[repr(C)] pub struct CString { /// Null terminated char array @@ -56,8 +58,8 @@ impl fmt::Debug for CString { } impl CString { - pub fn new>>(t: T) -> Result { - Ok(Self::from_std(std::ffi::CString::new(t)?)) + pub fn new>>(t: T) -> Result { + Ok(Self::from_std(ffi::CString::new(t)?)) } /// Creates a new `CString` from the given input, or returns an empty `CString` @@ -99,7 +101,7 @@ impl CString { } } - pub fn from_std(s: std::ffi::CString) -> Self { + pub fn from_std(s: ffi::CString) -> Self { let length = s.to_bytes().len(); Self { ptr: unsafe { ptr::NonNull::new_unchecked(s.into_raw()) }, @@ -107,10 +109,10 @@ impl CString { } } - pub fn into_std(self) -> std::ffi::CString { + pub fn into_std(self) -> ffi::CString { let s = ManuallyDrop::new(self); unsafe { - std::ffi::CString::from_vec_with_nul_unchecked(Vec::from_raw_parts( + ffi::CString::from_vec_with_nul_unchecked(Vec::from_raw_parts( s.ptr.as_ptr().cast(), s.length + 1, // +1 for the null terminator s.length + 1, // +1 for the null terminator @@ -123,7 +125,7 @@ impl Drop for CString { fn drop(&mut self) { let ptr = mem::replace(&mut self.ptr, NonNull::dangling()); drop(unsafe { - std::ffi::CString::from_vec_with_nul_unchecked(Vec::from_raw_parts( + ffi::CString::from_vec_with_nul_unchecked(Vec::from_raw_parts( ptr.as_ptr().cast(), self.length + 1, self.length + 1, diff --git a/libdd-common-ffi/src/lib.rs b/libdd-common-ffi/src/lib.rs index 0fdb04b4be..8f67f65878 100644 --- a/libdd-common-ffi/src/lib.rs +++ b/libdd-common-ffi/src/lib.rs @@ -1,34 +1,46 @@ // Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(test), deny(clippy::panic))] #![cfg_attr(not(test), deny(clippy::unwrap_used))] #![cfg_attr(not(test), deny(clippy::expect_used))] #![cfg_attr(not(test), deny(clippy::todo))] #![cfg_attr(not(test), deny(clippy::unimplemented))] -mod error; +extern crate alloc; -pub mod array_queue; +// Always available in both std and no_std builds. pub mod cstr; +pub mod slice; +pub mod vec; + +pub use cstr::*; +pub use slice::{CharSlice, Slice}; +pub use vec::Vec; + +// Modules and re-exports that require std. +#[cfg(feature = "std")] +pub mod array_queue; +#[cfg(feature = "std")] pub mod endpoint; +#[cfg(feature = "std")] +mod error; +#[cfg(feature = "std")] pub mod handle; +#[cfg(feature = "std")] pub mod option; +#[cfg(feature = "std")] pub mod result; -pub mod slice; +#[cfg(feature = "std")] pub mod slice_mut; +#[cfg(feature = "std")] pub mod string; +#[cfg(feature = "std")] pub mod tags; +#[cfg(feature = "std")] pub mod timespec; +#[cfg(feature = "std")] pub mod utils; -pub mod vec; -pub use cstr::*; -pub use error::*; -pub use handle::*; -pub use option::*; -pub use result::*; -pub use slice::{CharSlice, Slice}; -pub use slice_mut::MutSlice; -pub use string::*; -pub use timespec::*; -pub use vec::Vec; +#[cfg(feature = "std")] +pub use {error::*, handle::*, option::*, result::*, slice_mut::MutSlice, string::*, timespec::*}; diff --git a/libdd-common-ffi/src/slice.rs b/libdd-common-ffi/src/slice.rs index a703188e1b..7aabac2795 100644 --- a/libdd-common-ffi/src/slice.rs +++ b/libdd-common-ffi/src/slice.rs @@ -1,16 +1,25 @@ // Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +use core::ffi::c_char; +use core::fmt::{Debug, Display, Formatter}; +use core::hash::{Hash, Hasher}; +use core::marker::PhantomData; use core::slice; -use libdd_common::error::FfiSafeErrorMessage; -use serde::ser::Error; -use serde::Serializer; -use std::borrow::Cow; -use std::fmt::{Debug, Display, Formatter}; -use std::hash::{Hash, Hasher}; -use std::marker::PhantomData; -use std::os::raw::c_char; -use std::str::Utf8Error; +use core::str::Utf8Error; + +#[cfg(not(feature = "std"))] +use alloc::{ + borrow::Cow, + string::{String, ToString}, + vec::Vec, +}; + +#[cfg(feature = "std")] +use { + libdd_common::error::FfiSafeErrorMessage, serde::ser::Error, serde::Serializer, + std::borrow::Cow, +}; #[repr(C)] #[derive(Clone, Copy, Debug)] @@ -20,24 +29,11 @@ pub enum SliceConversionError { MisalignedPointer, } -#[repr(C)] -#[derive(Copy, Clone)] -pub struct Slice<'a, T: 'a> { - /// Should be non-null and suitably aligned for the underlying type. It is - /// allowed but not recommended for the pointer to be null when the len is - /// zero. - ptr: *const T, - - /// The number of elements (not bytes) that `.ptr` points to. Must be less - /// than or equal to [isize::MAX]. - len: usize, - _marker: PhantomData<&'a [T]>, -} - +#[cfg(feature = "std")] /// # Safety /// All strings are valid UTF-8 (enforced by using c-str literals in Rust). unsafe impl FfiSafeErrorMessage for SliceConversionError { - fn as_ffi_str(&self) -> &'static std::ffi::CStr { + fn as_ffi_str(&self) -> &'static core::ffi::CStr { match self { SliceConversionError::LargeLength => c"length was too large", SliceConversionError::NullPointer => c"null pointer with non-zero length", @@ -45,14 +41,34 @@ unsafe impl FfiSafeErrorMessage for SliceConversionError { } } } + impl Display for SliceConversionError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Display::fmt(self.as_rust_str(), f) + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let msg = match self { + SliceConversionError::LargeLength => "length was too large", + SliceConversionError::NullPointer => "null pointer with non-zero length", + SliceConversionError::MisalignedPointer => "pointer was not aligned for the type", + }; + f.write_str(msg) } } impl core::error::Error for SliceConversionError {} +#[repr(C)] +#[derive(Copy, Clone)] +pub struct Slice<'a, T: 'a> { + /// Should be non-null and suitably aligned for the underlying type. It is + /// allowed but not recommended for the pointer to be null when the len is + /// zero. + ptr: *const T, + + /// The number of elements (not bytes) that `.ptr` points to. Must be less + /// than or equal to [isize::MAX]. + len: usize, + _marker: PhantomData<&'a [T]>, +} + impl<'a, T: 'a> core::ops::Deref for Slice<'a, T> { type Target = [T]; @@ -62,7 +78,7 @@ impl<'a, T: 'a> core::ops::Deref for Slice<'a, T> { } impl Debug for Slice<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { self.as_slice().fmt(f) } } @@ -106,7 +122,7 @@ pub trait AsBytes<'a> { #[inline] fn try_to_utf8(&self) -> Result<&'a str, Utf8Error> { - std::str::from_utf8(self.as_bytes()) + core::str::from_utf8(self.as_bytes()) } fn try_to_string(&self) -> Result { @@ -127,7 +143,7 @@ pub trait AsBytes<'a> { /// # Safety /// Must only be used when the underlying data was already confirmed to be utf8. unsafe fn assume_utf8(&self) -> &'a str { - std::str::from_utf8_unchecked(self.as_bytes()) + core::str::from_utf8_unchecked(self.as_bytes()) } } @@ -184,7 +200,7 @@ impl<'a, T: 'a> Slice<'a, T> { } /// # Safety - /// Uphold the same safety requirements as [std::str::from_raw_parts]. + /// Uphold the same safety requirements as [`core::slice::from_raw_parts`]. /// However, it is allowed but not recommended to provide a null pointer /// when the len is 0. pub const unsafe fn from_raw_parts(ptr: *const T, len: usize) -> Self { @@ -260,6 +276,7 @@ where } } +#[cfg(feature = "std")] impl<'a, T> serde::Serialize for Slice<'a, T> where Slice<'a, T>: AsBytes<'a>, @@ -276,8 +293,8 @@ impl<'a, T> Display for Slice<'a, T> where Slice<'a, T>: AsBytes<'a>, { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.try_to_utf8().map_err(|_| std::fmt::Error)?) + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.try_to_utf8().map_err(|_| core::fmt::Error)?) } } diff --git a/libdd-common-ffi/src/vec.rs b/libdd-common-ffi/src/vec.rs index c3ee225123..9c246e38e0 100644 --- a/libdd-common-ffi/src/vec.rs +++ b/libdd-common-ffi/src/vec.rs @@ -4,10 +4,10 @@ extern crate alloc; use crate::slice::Slice; +use core::marker::PhantomData; +use core::mem::ManuallyDrop; use core::ops::Deref; -use std::marker::PhantomData; -use std::mem::ManuallyDrop; -use std::ptr::NonNull; +use core::ptr::NonNull; /// Holds the raw parts of a Rust Vec; it should only be created from Rust, /// never from C. @@ -81,6 +81,7 @@ impl From> for Vec { } } +#[cfg(feature = "std")] impl From for Vec { fn from(err: anyhow::Error) -> Self { Self::from(err.to_string().into_bytes()) @@ -97,7 +98,7 @@ impl<'a, T> IntoIterator for &'a Vec { } impl Vec { - fn replace(&mut self, mut vec: ManuallyDrop>) { + fn replace(&mut self, mut vec: ManuallyDrop>) { self.ptr = vec.as_mut_ptr(); self.len = vec.len(); self.capacity = vec.capacity(); diff --git a/libdd-common/src/cstr.rs b/libdd-common/src/cstr.rs index 458dd4cca9..15ea126b5d 100644 --- a/libdd-common/src/cstr.rs +++ b/libdd-common/src/cstr.rs @@ -31,7 +31,7 @@ macro_rules! cstr { } $crate::cstr::validate_cstr_contents(bytes); - unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(bytes) } + unsafe { core::ffi::CStr::from_bytes_with_nul_unchecked(bytes) } }}; } @@ -39,7 +39,7 @@ macro_rules! cstr { macro_rules! cstr_u8 { ($s:literal) => {{ $crate::cstr::validate_cstr_contents($s); - unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked($s as &[u8]) } + unsafe { core::ffi::CStr::from_bytes_with_nul_unchecked($s as &[u8]) } }}; } diff --git a/libdd-crashtracker-ffi/Cargo.toml b/libdd-crashtracker-ffi/Cargo.toml index 2af19d68b1..b85ca9bcb9 100644 --- a/libdd-crashtracker-ffi/Cargo.toml +++ b/libdd-crashtracker-ffi/Cargo.toml @@ -39,7 +39,7 @@ build_common = { path = "../build-common" } anyhow = "1.0" libdd-crashtracker = { path = "../libdd-crashtracker" } libdd-common = { path = "../libdd-common" } -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, features = ["std"] } symbolic-demangle = { version = "12.8.0", default-features = false, features = ["rust", "cpp", "msvc"], optional = true } symbolic-common = { version = "12.8.0", default-features = false, optional = true } function_name = "0.3.0" diff --git a/libdd-data-pipeline-ffi/Cargo.toml b/libdd-data-pipeline-ffi/Cargo.toml index 0f8e081083..b4c7b53186 100644 --- a/libdd-data-pipeline-ffi/Cargo.toml +++ b/libdd-data-pipeline-ffi/Cargo.toml @@ -32,6 +32,6 @@ libdd-trace-utils = { path = "../libdd-trace-utils" } libdd-capabilities-impl = { version = "0.1.0", path = "../libdd-capabilities-impl" } libdd-data-pipeline = { path = "../libdd-data-pipeline" } libdd-shared-runtime = { version = "0.1.0", path = "../libdd-shared-runtime" } -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, features = ["std"] } libdd-tinybytes = { path = "../libdd-tinybytes" } tracing = { version = "0.1", default-features = false } diff --git a/libdd-ddsketch-ffi/Cargo.toml b/libdd-ddsketch-ffi/Cargo.toml index 85b09772d2..9e4f7fec8f 100644 --- a/libdd-ddsketch-ffi/Cargo.toml +++ b/libdd-ddsketch-ffi/Cargo.toml @@ -22,6 +22,6 @@ build_common = { path = "../build-common" } [dependencies] libdd-ddsketch = { path = "../libdd-ddsketch" } -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, features = ["std"] } [dev-dependencies] diff --git a/libdd-library-config-ffi/Cargo.toml b/libdd-library-config-ffi/Cargo.toml index 25801392b5..43884f50bf 100644 --- a/libdd-library-config-ffi/Cargo.toml +++ b/libdd-library-config-ffi/Cargo.toml @@ -11,17 +11,32 @@ publish = false crate-type = ["staticlib", "cdylib", "lib"] bench = false +[features] +default = ["std", "cbindgen", "catch_panic"] +std = [ + "dep:libdd-common", + "dep:anyhow", + "libdd-library-config/std", + "libdd-common-ffi/std", +] +cbindgen = ["std", "build_common/cbindgen", "libdd-common-ffi/cbindgen"] +catch_panic = [] +# Provides #[global_allocator], #[panic_handler], and rust_eh_personality for +# building a standalone no_std staticlib/cdylib. Must be built with panic=abort. +no_std_entry = ["dep:dlmalloc", "dep:rustix-dlmalloc"] + [dependencies] -libdd-common = { path = "../libdd-common" } +libdd-common = { path = "../libdd-common", optional = true } libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } -libdd-library-config = { path = "../libdd-library-config" } -anyhow = "1.0" +libdd-library-config = { path = "../libdd-library-config", default-features = false } +anyhow = { version = "1.0", default-features = false, optional = true } constcat = "0.4.1" -[features] -default = ["cbindgen", "catch_panic"] -cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"] -catch_panic = [] +[target.'cfg(target_os = "linux")'.dependencies] +rustix-dlmalloc = { version = "0.2", default-features = false, features = ["global"], optional = true } + +[target.'cfg(not(target_os = "linux"))'.dependencies] +dlmalloc = { version = "0.2", default-features = false, features = ["global"], optional = true } [build-dependencies] build_common = { path = "../build-common" } diff --git a/libdd-library-config-ffi/src/lib.rs b/libdd-library-config-ffi/src/lib.rs index 9e53349f9d..132abcaadd 100644 --- a/libdd-library-config-ffi/src/lib.rs +++ b/libdd-library-config-ffi/src/lib.rs @@ -1,43 +1,99 @@ // Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +// Only set #![no_std] when `no_std_entry` is active. When built as a `lib` crate-type (not a +// standalone staticlib/cdylib), the consuming crate provides its own allocator and panic handler, +// so #![no_std] should only be set when this crate is the binary entry point. +#![cfg_attr(all(not(feature = "std"), feature = "no_std_entry"), no_std)] +extern crate alloc; + +#[cfg(all(not(feature = "std"), feature = "no_std_entry", not(panic = "abort")))] +compile_error!( + "The `no_std_entry` feature requires `panic = \"abort\"` in the Cargo profile. \ + Building with panic=unwind causes undefined behavior at FFI boundaries." +); + +#[cfg(all(not(feature = "std"), feature = "no_std_entry"))] +mod no_std_support { + #[cfg(target_os = "linux")] + #[global_allocator] + static ALLOC: rustix_dlmalloc::GlobalDlmalloc = rustix_dlmalloc::GlobalDlmalloc; + + #[cfg(not(target_os = "linux"))] + #[global_allocator] + static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; + + #[panic_handler] + fn panic(_info: &core::panic::PanicInfo) -> ! { + // abort() is provided by the C runtime, which is always linked for FFI libs. + // Note: _info is intentionally discarded โ€” in no_std there is no reliable way to + // write diagnostics without std I/O. Panics in no_std mode are silent and fatal. + extern "C" { + fn abort() -> !; + } + // SAFETY: abort() is a C standard library function with no preconditions; it + // unconditionally terminates the process. + unsafe { abort() } + } + + /// Required by the Rust compiler's exception handling ABI. A no-op is safe because + /// unwinding will never occur under `panic = "abort"` (enforced by the compile_error! + /// guard above). WARNING: this symbol is globally visible โ€” this library must not be + /// linked with other Rust code compiled with `panic = "unwind"`. + #[no_mangle] + pub extern "C" fn rust_eh_personality() {} +} + +#[cfg(feature = "std")] pub mod tracer_metadata; -use libdd_common_ffi::{self as ffi, slice::AsBytes, CString, CharSlice, Error}; +use libdd_common_ffi::{self as ffi, slice::AsBytes}; +#[cfg(feature = "std")] +use libdd_common_ffi::{CString, Error}; + +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, string::ToString, vec::Vec}; + +use ffi::CharSlice; use libdd_library_config::{self as lib_config, LibraryConfigSource}; -#[cfg(all(feature = "catch_panic", panic = "unwind"))] +#[cfg(all(feature = "std", feature = "catch_panic", panic = "unwind"))] use std::panic::{catch_unwind, AssertUnwindSafe}; -#[cfg(all(feature = "catch_panic", panic = "unwind"))] +#[cfg(all(feature = "std", feature = "catch_panic", panic = "unwind"))] macro_rules! catch_panic { - ($f:expr) => { + ($f:expr, $err_ctor:expr) => { match catch_unwind(AssertUnwindSafe(|| $f)) { Ok(ret) => ret, Err(info) => { - let panic_msg = if let Some(s) = info.downcast_ref::<&'static str>() { - s.to_string() + let detail = if let Some(s) = info.downcast_ref::<&'static str>() { + format!("FFI function panicked: {s}") } else if let Some(s) = info.downcast_ref::() { - s.clone() + format!("FFI function panicked: {s}") } else { - "Unable to retrieve panic context".to_string() + "FFI function panicked".to_string() }; - LibraryConfigLoggedResult::Err(Error::from(format!( - "FFI function panicked: {}", - panic_msg - ))) + $err_ctor(detail) } } }; } -#[cfg(any(not(feature = "catch_panic"), panic = "abort"))] +#[cfg(all(feature = "std", any(not(feature = "catch_panic"), panic = "abort")))] macro_rules! catch_panic { - ($f:expr) => { + ($f:expr, $err_ctor:expr) => { $f }; } +#[cfg(not(feature = "std"))] +macro_rules! catch_panic { + ($f:expr, $err_ctor:expr) => { + $f + }; +} + +#[cfg(feature = "std")] /// A result type that includes debug/log messages along with the data #[repr(C)] pub struct OkResult { @@ -45,6 +101,7 @@ pub struct OkResult { pub logs: CString, } +#[cfg(feature = "std")] #[repr(C)] pub enum LibraryConfigLoggedResult { Ok(OkResult), @@ -52,15 +109,15 @@ pub enum LibraryConfigLoggedResult { } // TODO: Centos 6 build -// Trust me it works bro ๐Ÿ˜‰๐Ÿ˜‰๐Ÿ˜‰ +// Trust me it works bro // #[cfg(linux)] // std::arch::global_asm!(".symver memcpy,memcpy@GLIBC_2.2.5"); #[repr(C)] pub struct ProcessInfo<'a> { - pub args: ffi::Slice<'a, ffi::CharSlice<'a>>, - pub envp: ffi::Slice<'a, ffi::CharSlice<'a>>, - pub language: ffi::CharSlice<'a>, + pub args: ffi::Slice<'a, CharSlice<'a>>, + pub envp: ffi::Slice<'a, CharSlice<'a>>, + pub language: CharSlice<'a>, } impl<'a> ProcessInfo<'a> { @@ -82,29 +139,30 @@ pub struct LibraryConfig { } impl LibraryConfig { - fn rs_vec_to_ffi(configs: Vec) -> anyhow::Result> { + fn vec_to_ffi( + configs: Vec, + ) -> Result, alloc::ffi::NulError> { let cfg: Vec = configs .into_iter() .map(|c| { Ok(LibraryConfig { - name: ffi::CString::from_std(std::ffi::CString::new(c.name)?), - value: ffi::CString::from_std(std::ffi::CString::new(c.value)?), + name: ffi::CString::new(c.name)?, + value: ffi::CString::new(c.value)?, source: c.source, - config_id: ffi::CString::from_std(std::ffi::CString::new( - c.config_id.unwrap_or_default(), - )?), + config_id: ffi::CString::new(c.config_id.unwrap_or_default())?, }) }) - .collect::, std::ffi::NulError>>()?; + .collect::, alloc::ffi::NulError>>()?; Ok(ffi::Vec::from_std(cfg)) } + #[cfg(feature = "std")] fn logged_result_to_ffi_with_messages( result: libdd_library_config::LoggedResult, anyhow::Error>, ) -> LibraryConfigLoggedResult { match result { libdd_library_config::LoggedResult::Ok(configs, logs) => { - match Self::rs_vec_to_ffi(configs) { + match Self::vec_to_ffi(configs) { Ok(ffi_configs) => { let messages = logs.join("\n"); let cstring_logs = CString::new_or_empty(messages); @@ -113,7 +171,7 @@ impl LibraryConfig { logs: cstring_logs, }) } - Err(err) => LibraryConfigLoggedResult::Err(err.into()), + Err(err) => LibraryConfigLoggedResult::Err(Error::from(err.to_string())), } } libdd_library_config::LoggedResult::Err(err) => { @@ -171,6 +229,7 @@ pub extern "C" fn ddog_library_configurator_with_process_info<'a>( c.process_info = Some(p.ffi_to_rs()); } +#[cfg(feature = "std")] #[no_mangle] pub extern "C" fn ddog_library_configurator_with_detect_process_info(c: &mut Configurator) { c.process_info = Some(lib_config::ProcessInfo::detect_global( @@ -181,42 +240,48 @@ pub extern "C" fn ddog_library_configurator_with_detect_process_info(c: &mut Con #[no_mangle] pub extern "C" fn ddog_library_configurator_drop(_: Box) {} + +#[cfg(feature = "std")] #[no_mangle] pub extern "C" fn ddog_library_configurator_get( configurator: &Configurator, ) -> LibraryConfigLoggedResult { - catch_panic!({ - let local_path = configurator - .local_path - .as_ref() - .and_then(|p| p.into_std().to_str().ok()) - .unwrap_or(lib_config::Configurator::LOCAL_STABLE_CONFIGURATION_PATH); - let fleet_path = configurator - .fleet_path - .as_ref() - .and_then(|p| p.into_std().to_str().ok()) - .unwrap_or(lib_config::Configurator::FLEET_STABLE_CONFIGURATION_PATH); - let detected_process_info; - let process_info = match configurator.process_info { - Some(ref p) => p, - None => { - detected_process_info = lib_config::ProcessInfo::detect_global( - configurator.language.to_utf8_lossy().into_owned(), - ); - &detected_process_info - } - }; + catch_panic!( + { + let local_path = configurator + .local_path + .as_ref() + .and_then(|p| p.into_std().to_str().ok()) + .unwrap_or(lib_config::Configurator::LOCAL_STABLE_CONFIGURATION_PATH); + let fleet_path = configurator + .fleet_path + .as_ref() + .and_then(|p| p.into_std().to_str().ok()) + .unwrap_or(lib_config::Configurator::FLEET_STABLE_CONFIGURATION_PATH); + let detected_process_info; + let process_info = match configurator.process_info { + Some(ref p) => p, + None => { + detected_process_info = lib_config::ProcessInfo::detect_global( + configurator.language.to_utf8_lossy().into_owned(), + ); + &detected_process_info + } + }; - let result = configurator.inner.get_config_from_file( - local_path.as_ref(), - fleet_path.as_ref(), - process_info, - ); + let result = configurator.inner.get_config_from_file( + local_path.as_ref(), + fleet_path.as_ref(), + process_info, + ); - LibraryConfig::logged_result_to_ffi_with_messages(result) - }) + LibraryConfig::logged_result_to_ffi_with_messages(result) + }, + |msg| LibraryConfigLoggedResult::Err(Error::from(msg)) + ) } +#[cfg(feature = "std")] #[no_mangle] /// Returns a static null-terminated string, containing the name of the environment variable /// associated with the library configuration @@ -229,10 +294,13 @@ pub extern "C" fn ddog_library_config_source_to_string( }) } +#[cfg(feature = "std")] #[no_mangle] /// Returns a static null-terminated string with the path to the managed stable config yaml config /// file pub extern "C" fn ddog_library_config_fleet_stable_config_path() -> ffi::CStr<'static> { + // SAFETY: constcat! appends a literal "\0", guaranteeing a single null terminator + // at the end. The path constant contains no interior null bytes. ffi::CStr::from_std(unsafe { let path: &'static str = constcat::concat!( lib_config::Configurator::FLEET_STABLE_CONFIGURATION_PATH, @@ -242,10 +310,13 @@ pub extern "C" fn ddog_library_config_fleet_stable_config_path() -> ffi::CStr<'s }) } +#[cfg(feature = "std")] #[no_mangle] /// Returns a static null-terminated string with the path to the local stable config yaml config /// file pub extern "C" fn ddog_library_config_local_stable_config_path() -> ffi::CStr<'static> { + // SAFETY: constcat! appends a literal "\0", guaranteeing a single null terminator + // at the end. The path constant contains no interior null bytes. ffi::CStr::from_std(unsafe { let path: &'static str = constcat::concat!( lib_config::Configurator::LOCAL_STABLE_CONFIGURATION_PATH, @@ -255,6 +326,7 @@ pub extern "C" fn ddog_library_config_local_stable_config_path() -> ffi::CStr<'s }) } +#[cfg(feature = "std")] #[no_mangle] pub extern "C" fn ddog_library_config_drop(mut config_result: LibraryConfigLoggedResult) { match &mut config_result { diff --git a/libdd-library-config/Cargo.toml b/libdd-library-config/Cargo.toml index 0d379c7645..557f8122ea 100644 --- a/libdd-library-config/Cargo.toml +++ b/libdd-library-config/Cargo.toml @@ -15,24 +15,40 @@ crate-type = ["lib"] bench = false [features] +default = ["std"] +std = [ + "serde/std", + "anyhow/std", + "yaml_serde/std", + "dep:prost", + "dep:rand", + "dep:rmp", + "dep:rmp-serde", + "dep:libdd-trace-protobuf", + "dep:memfd", + "dep:libc", +] otel-thread-ctx = [] [dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_yaml = "0.9.34" -prost = "0.14.1" -anyhow = "1.0" +serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } +# TODO: Switch to official crates.io release once no_std support is merged: +# https://github.com/yaml/yaml-serde/pull/7 +yaml_serde = { git = "https://github.com/pawelchcki/yaml-serde.git", rev = "c7ad78def628d372dca2d9435b3ff0621a464e97", default-features = false } +prost = { version = "0.14.1", optional = true } +anyhow = { version = "1.0", default-features = false } +thiserror = { version = "2", default-features = false } -rand = "0.8.3" -rmp = "0.8.14" -rmp-serde = "1.3.0" +rand = { version = "0.8.3", optional = true } +rmp = { version = "0.8.14", optional = true } +rmp-serde = { version = "1.3.0", optional = true } -libdd-trace-protobuf = { version = "3.0.1", path = "../libdd-trace-protobuf" } +libdd-trace-protobuf = { version = "3.0.1", path = "../libdd-trace-protobuf", optional = true } [dev-dependencies] tempfile = { version = "3.3" } serial_test = "3.2" [target.'cfg(unix)'.dependencies] -memfd = { version = "0.6" } -libc = "0.2" +memfd = { version = "0.6", optional = true } +libc = { version = "0.2", optional = true } diff --git a/libdd-library-config/src/config_read.rs b/libdd-library-config/src/config_read.rs new file mode 100644 index 0000000000..34b72d7e30 --- /dev/null +++ b/libdd-library-config/src/config_read.rs @@ -0,0 +1,75 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use alloc::vec::Vec; + +/// Maximum allowed config file size (100 MB). +pub const MAX_CONFIG_FILE_SIZE: usize = 100 * 1024 * 1024; + +/// Error returned by [`ConfigRead::read`]. +/// +/// This enum classifies all failure modes so the configurator can decide which +/// are fatal (abort) and which are gracefully skipped: +/// +/// - [`NotFound`](Self::NotFound) โ€” the file does not exist; the configuration layer is simply +/// absent and will be treated as empty. +/// - [`TooLarge`](Self::TooLarge) โ€” the file exceeds [`MAX_CONFIG_FILE_SIZE`]; skipped with a debug +/// log. +/// - [`Io`](Self::Io) โ€” any other I/O or access error; aborts config loading. +#[derive(Debug, thiserror::Error)] +pub enum ConfigReadError { + /// File does not exist at the given path. + #[error("file not found")] + NotFound, + /// File exceeds [`MAX_CONFIG_FILE_SIZE`]. + #[error("file is too large (> 100mb)")] + TooLarge, + /// An I/O or platform-specific error. + #[error("{0}")] + Io(E), +} + +/// Trait for reading configuration files from a filesystem or virtual filesystem. +/// +/// Implement this to provide custom file access for environments where `std::fs` +/// is not available (e.g. no_std, sandboxed, or in-memory configurations). +pub trait ConfigRead { + /// The platform-specific error type carried by [`ConfigReadError::Io`]. + type IoError: core::fmt::Display + core::fmt::Debug; + + /// Read the entire contents of the configuration file at `path`. + /// + /// Implementations **should** return [`ConfigReadError::TooLarge`] for files + /// exceeding [`MAX_CONFIG_FILE_SIZE`] to avoid unnecessary allocations. + /// The configurator also checks the returned bytes as a safety net. + fn read(&self, path: &str) -> Result, ConfigReadError>; +} + +/// Standard filesystem implementation of [`ConfigRead`]. +#[cfg(feature = "std")] +pub struct StdConfigRead; + +#[cfg(feature = "std")] +impl ConfigRead for StdConfigRead { + type IoError = std::io::Error; + + fn read(&self, path: &str) -> Result, ConfigReadError> { + use std::{fs, io}; + + let file = match fs::File::open(path) { + Ok(f) => f, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(ConfigReadError::NotFound), + Err(e) => return Err(ConfigReadError::Io(e)), + }; + let len = match file.metadata() { + Ok(m) if m.len() as usize > MAX_CONFIG_FILE_SIZE => { + return Err(ConfigReadError::TooLarge) + } + Ok(m) => m.len() as usize, + Err(e) => return Err(ConfigReadError::Io(e)), + }; + let mut buf = Vec::with_capacity(len); + io::Read::read_to_end(&mut &file, &mut buf).map_err(ConfigReadError::Io)?; + Ok(buf) + } +} diff --git a/libdd-library-config/src/lib.rs b/libdd-library-config/src/lib.rs index f243678c09..ea399f9695 100644 --- a/libdd-library-config/src/lib.rs +++ b/libdd-library-config/src/lib.rs @@ -1,14 +1,31 @@ // Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +#![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + +mod config_read; +pub use config_read::*; + +#[cfg(feature = "std")] pub mod otel_process_ctx; +#[cfg(feature = "std")] pub mod tracer_metadata; -use std::borrow::Cow; -use std::cell::OnceCell; -use std::collections::HashMap; -use std::ops::Deref; +use alloc::borrow::Cow; +use alloc::boxed::Box; +use alloc::collections::BTreeMap; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; +use core::cell::OnceCell; +use core::mem; +use core::ops::Deref; + +#[cfg(feature = "std")] +use std::env; +#[cfg(feature = "std")] use std::path::Path; -use std::{env, fs, io, mem}; /// This struct holds maps used to match and template configurations. /// @@ -20,17 +37,17 @@ use std::{env, fs, io, mem}; /// * envs: Splits env variables with format KEY=VALUE /// * args: Splits args with format key=value. If the arg doesn't contain an '=', skip it struct MatchMaps<'a> { - tags: &'a HashMap, - env_map: OnceCell>, - args_map: OnceCell>, + tags: &'a BTreeMap, + env_map: OnceCell>, + args_map: OnceCell>, } impl<'a> MatchMaps<'a> { - fn env(&self, process_info: &'a ProcessInfo) -> &HashMap<&'a str, &'a str> { + fn env(&self, process_info: &'a ProcessInfo) -> &BTreeMap<&'a str, &'a str> { self.env_map.get_or_init(|| { - let mut map = HashMap::new(); + let mut map = BTreeMap::new(); for e in &process_info.envp { - let Ok(s) = std::str::from_utf8(e.deref()) else { + let Ok(s) = core::str::from_utf8(e.deref()) else { continue; }; let (k, v) = match s.split_once('=') { @@ -43,11 +60,11 @@ impl<'a> MatchMaps<'a> { }) } - fn args(&self, process_info: &'a ProcessInfo) -> &HashMap<&str, &str> { + fn args(&self, process_info: &'a ProcessInfo) -> &BTreeMap<&str, &str> { self.args_map.get_or_init(|| { - let mut map = HashMap::new(); + let mut map = BTreeMap::new(); for arg in &process_info.args { - let Ok(arg) = std::str::from_utf8(arg.deref()) else { + let Ok(arg) = core::str::from_utf8(arg.deref()) else { continue; }; // Split args between key and value on '=' @@ -66,7 +83,7 @@ struct Matcher<'a> { } impl<'a> Matcher<'a> { - fn new(process_info: &'a ProcessInfo, tags: &'a HashMap) -> Self { + fn new(process_info: &'a ProcessInfo, tags: &'a BTreeMap) -> Self { Self { process_info, match_maps: MatchMaps { @@ -121,7 +138,7 @@ impl<'a> Matcher<'a> { /// /// For instance: /// - /// with the following varriable definition, var = "abc" var2 = "def", this transforms \ + /// with the following variable definition, var = "abc" var2 = "def", this transforms \ /// "foo_{{ var }}_bar_{{ var2 }}" -> "foo_abc_bar_def" fn template_config(&'a self, config_val: &str) -> anyhow::Result { let mut rest = config_val; @@ -145,7 +162,7 @@ impl<'a> Matcher<'a> { template_map_key(index, self.match_maps.args(self.process_info)) } "tags" => template_map_key(index, self.match_maps.tags), - _ => std::borrow::Cow::Borrowed("UNDEFINED"), + _ => Cow::Borrowed("UNDEFINED"), }; templated.push_str(&val); rest = tail; @@ -186,6 +203,7 @@ pub struct ProcessInfo { pub language: Vec, } +#[cfg(feature = "std")] fn process_envp() -> Vec> { #[allow(clippy::unnecessary_filter_map)] env::vars_os() @@ -211,6 +229,7 @@ fn process_envp() -> Vec> { .collect() } +#[cfg(feature = "std")] fn process_args() -> Vec> { #[allow(clippy::unnecessary_filter_map)] env::args_os() @@ -229,6 +248,7 @@ fn process_args() -> Vec> { } impl ProcessInfo { + #[cfg(feature = "std")] pub fn detect_global(language: String) -> Self { let envp = process_envp(); let args = process_args(); @@ -244,7 +264,8 @@ impl ProcessInfo { /// /// This type has a custom serde Deserialize implementation from maps: /// * It skips invalid/unknown keys in the map -/// * Since the storage is a Boxed slice and not a Hashmap, it doesn't over-allocate +/// * Since the storage is a Boxed slice and not a map, it doesn't over-allocate +/// * Duplicate keys are preserved; when later inserted into a BTreeMap, the last entry wins #[derive(Debug, Default, PartialEq, Eq)] struct ConfigMap(Box<[(String, String)]>); @@ -257,8 +278,8 @@ impl<'de> serde::Deserialize<'de> for ConfigMap { impl<'de> serde::de::Visitor<'de> for ConfigMapVisitor { type Value = ConfigMap; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("struct ConfigMap(HashMap)") + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("a string-to-string map") } fn visit_map(self, mut map: A) -> Result @@ -353,7 +374,7 @@ struct StableConfig { // Phase 2 #[serde(default)] - tags: HashMap, + tags: BTreeMap, #[serde(default)] rules: Vec, } @@ -395,7 +416,7 @@ pub struct LibraryConfig { } #[derive(Debug)] -/// This struct is used to hold configuration item data in a Hashmap, while the name of +/// This struct is used to hold configuration item data in a BTreeMap, while the name of /// the configuration is the key used for deduplication struct LibraryConfigVal { value: String, @@ -495,13 +516,9 @@ impl Configurator { } fn parse_stable_config_slice(&self, buf: &[u8]) -> LoggedResult { - let stable_config = if buf.is_empty() { - StableConfig::default() - } else { - match serde_yaml::from_slice(buf) { - Ok(config) => config, - Err(e) => return LoggedResult::Err(e.into()), - } + let stable_config = match yaml_serde::from_slice::(buf) { + Ok(config) => config, + Err(e) => return LoggedResult::Err(e.into()), }; let messages = if self.debug_logs { @@ -515,7 +532,8 @@ impl Configurator { LoggedResult::Ok(stable_config, messages) } - fn parse_stable_config_file( + #[cfg(all(feature = "std", test))] + fn parse_stable_config_file( &self, mut f: F, ) -> LoggedResult { @@ -524,92 +542,55 @@ impl Configurator { Ok(_) => {} Err(e) => return LoggedResult::Err(e.into()), } - self.parse_stable_config_slice(utils::trim_bytes(&buffer)) + self.parse_stable_config_slice(&buffer) } + #[cfg(feature = "std")] pub fn get_config_from_file( &self, path_local: &Path, path_managed: &Path, process_info: &ProcessInfo, ) -> LoggedResult, anyhow::Error> { + self.get_config_from_reader( + &StdConfigRead, + path_local.to_string_lossy(), + path_managed.to_string_lossy(), + process_info, + ) + } + + /// Load configuration using a custom [`ConfigRead`] implementation. + /// + /// This is the primary entry point for no_std or virtual-filesystem + /// environments. The reader controls how files are fetched; the + /// configurator handles parsing, layering, and rule evaluation. + pub fn get_config_from_reader( + &self, + reader: &impl ConfigRead, + local_path: impl AsRef, + fleet_path: impl AsRef, + process_info: &ProcessInfo, + ) -> LoggedResult, anyhow::Error> { + let local_path = local_path.as_ref(); + let fleet_path = fleet_path.as_ref(); let mut debug_messages = Vec::new(); if self.debug_logs { debug_messages.push("Reading stable configuration from files:".to_string()); - debug_messages.push(format!("\tlocal: {path_local:?}")); - debug_messages.push(format!("\tfleet: {path_managed:?}")); + debug_messages.push(format!("\tlocal: {local_path:?}")); + debug_messages.push(format!("\tfleet: {fleet_path:?}")); } - let local_config = match fs::File::open(path_local) { - Ok(file) => { - match file.metadata() { - Ok(metadata) => { - // Fail if the file is > 100mb - if metadata.len() > 1024 * 1024 * 100 { - debug_messages.push( - "failed to read local config file: file is too large (> 100mb)" - .to_string(), - ); - StableConfig::default() - } else { - match self.parse_stable_config_file(file) { - LoggedResult::Ok(config, logs) => { - debug_messages.extend(logs); - config - } - LoggedResult::Err(e) => return LoggedResult::Err(e), - } - } - } - Err(e) => { - return LoggedResult::Err( - anyhow::Error::from(e).context("failed to get file metadata"), - ) - } - } - } - Err(e) if e.kind() == io::ErrorKind::NotFound => StableConfig::default(), - Err(e) => { - return LoggedResult::Err( - anyhow::Error::from(e).context("failed to open config file"), - ) - } - }; - let fleet_config = match fs::File::open(path_managed) { - Ok(file) => { - match file.metadata() { - Ok(metadata) => { - // Fail if the file is > 100mb - if metadata.len() > 1024 * 1024 * 100 { - debug_messages.push( - "failed to read fleet config file: file is too large (> 100mb)" - .to_string(), - ); - StableConfig::default() - } else { - match self.parse_stable_config_file(file) { - LoggedResult::Ok(config, logs) => { - debug_messages.extend(logs); - config - } - LoggedResult::Err(e) => return LoggedResult::Err(e), - } - } - } - Err(e) => { - return LoggedResult::Err( - anyhow::Error::from(e).context("failed to get file metadata"), - ) - } - } - } - Err(e) if e.kind() == io::ErrorKind::NotFound => StableConfig::default(), - Err(e) => { - return LoggedResult::Err( - anyhow::Error::from(e).context("failed to open config file"), - ) - } - }; + let local_config = + match self.read_config_source(reader, local_path, "local", &mut debug_messages) { + Ok(config) => config, + Err(e) => return LoggedResult::Err(e), + }; + let fleet_config = + match self.read_config_source(reader, fleet_path, "fleet", &mut debug_messages) { + Ok(config) => config, + Err(e) => return LoggedResult::Err(e), + }; match self.get_config(local_config, fleet_config, process_info) { LoggedResult::Ok(configs, msgs) => { @@ -620,23 +601,32 @@ impl Configurator { } } - pub fn get_config_from_bytes( + fn read_config_source( &self, - s_local: &[u8], - s_managed: &[u8], - process_info: ProcessInfo, - ) -> anyhow::Result> { - let local_config = match self.parse_stable_config_slice(s_local) { - LoggedResult::Ok(config, _) => config, - LoggedResult::Err(e) => return Err(e), - }; - let fleet_config = match self.parse_stable_config_slice(s_managed) { - LoggedResult::Ok(config, _) => config, - LoggedResult::Err(e) => return Err(e), + reader: &impl ConfigRead, + path: &str, + label: &str, + debug_messages: &mut Vec, + ) -> Result { + let bytes = match reader.read(path) { + Ok(bytes) => bytes, + Err(ConfigReadError::NotFound) => return Ok(StableConfig::default()), + Err(ConfigReadError::TooLarge) => { + debug_messages.push(format!( + "failed to read {label} config file: file is too large (> 100mb)" + )); + return Ok(StableConfig::default()); + } + Err(ConfigReadError::Io(e)) => { + anyhow::bail!("failed to read {label} config file: {e}") + } }; - match self.get_config(local_config, fleet_config, &process_info) { - LoggedResult::Ok(configs, _) => Ok(configs), + match self.parse_stable_config_slice(&bytes) { + LoggedResult::Ok(config, logs) => { + debug_messages.extend(logs); + Ok(config) + } LoggedResult::Err(e) => Err(e), } } @@ -662,7 +652,7 @@ impl Configurator { )); } - let mut cfg = HashMap::new(); + let mut cfg = BTreeMap::new(); // First get local configuration match self.get_single_source_config( local_config, @@ -726,7 +716,7 @@ impl Configurator { mut stable_config: StableConfig, source: LibraryConfigSource, process_info: &ProcessInfo, - cfg: &mut HashMap, + cfg: &mut BTreeMap, ) -> LoggedResult<(), anyhow::Error> { // Phase 1: take host default config cfg.extend( @@ -757,7 +747,7 @@ impl Configurator { stable_config: StableConfig, source: LibraryConfigSource, process_info: &ProcessInfo, - library_config: &mut HashMap, + library_config: &mut BTreeMap, ) -> LoggedResult<(), anyhow::Error> { let matcher = Matcher::new(process_info, &stable_config.tags); let Some(configs) = matcher.find_stable_config(&stable_config) else { @@ -794,43 +784,37 @@ impl Configurator { } } +// TODO: Switch yaml_serde to official crates.io release once no_std support +// is merged: https://github.com/yaml/yaml-serde/pull/7 + use utils::Get; mod utils { - use std::collections::HashMap; - - /// Removes leading and trailing ascci whitespaces from a byte slice - pub(crate) fn trim_bytes(mut b: &[u8]) -> &[u8] { - while b.first().map(u8::is_ascii_whitespace).unwrap_or(false) { - b = &b[1..]; - } - while b.last().map(u8::is_ascii_whitespace).unwrap_or(false) { - b = &b[..b.len() - 1]; - } - b - } + use alloc::collections::BTreeMap; + use alloc::string::String; /// Helper trait so we don't have to duplicate code for - /// HashMap<&str, &str> and HashMap + /// BTreeMap<&str, &str> and BTreeMap pub(crate) trait Get { fn get(&self, k: &str) -> Option<&str>; } - impl Get for HashMap<&str, &str> { + impl Get for BTreeMap<&str, &str> { fn get(&self, k: &str) -> Option<&str> { self.get(k).copied() } } - impl Get for HashMap { + impl Get for BTreeMap { fn get(&self, k: &str) -> Option<&str> { self.get(k).map(|v| v.as_str()) } } } -#[cfg(test)] +#[cfg(all(test, feature = "std"))] mod tests { - use std::{collections::HashMap, io::Write, path::Path}; + use alloc::collections::BTreeMap; + use std::{io::Write, path::Path}; use super::{Configurator, LoggedResult, ProcessInfo}; use crate::{ @@ -849,8 +833,17 @@ mod tests { language: b"java".to_vec(), }; let configurator = Configurator::new(true); + let local_config = configurator + .parse_stable_config_slice(local_cfg) + .data() + .unwrap(); + let fleet_config = configurator + .parse_stable_config_slice(fleet_cfg) + .data() + .unwrap(); let mut actual = configurator - .get_config_from_bytes(local_cfg, fleet_cfg, process_info) + .get_config(local_config, fleet_config, &process_info) + .data() .unwrap(); // Sort by name for determinism @@ -913,16 +906,16 @@ mod tests { let temp_local_path = temp_local_file.into_temp_path(); let temp_fleet_path = temp_fleet_file.into_temp_path(); let result = configurator.get_config_from_file( - temp_local_path.to_str().unwrap().as_ref(), - temp_fleet_path.to_str().unwrap().as_ref(), + temp_local_path.as_ref(), + temp_fleet_path.as_ref(), &ProcessInfo { args: vec![b"-jar HelloWorld.jar".to_vec()], envp: vec![b"ENV=VAR".to_vec()], language: b"java".to_vec(), }, ); - let local_path: &Path = temp_local_path.to_str().unwrap().as_ref(); - let fleet_path: &Path = temp_fleet_path.to_str().unwrap().as_ref(); + let local_path: &Path = temp_local_path.as_ref(); + let fleet_path: &Path = temp_fleet_path.as_ref(); match result { LoggedResult::Ok(configs, logs) => { assert_eq!(configs, vec![]); @@ -1206,6 +1199,26 @@ rules: ); } + #[test] + fn test_parse_comment_only_yaml() { + let configurator = Configurator::new(true); + let result = configurator.parse_stable_config_slice(b"# this is a comment\n"); + match result { + LoggedResult::Ok(config, _) => assert_eq!(config, StableConfig::default()), + LoggedResult::Err(e) => panic!("Expected success, got: {e:?}"), + } + } + + #[test] + fn test_parse_empty_yaml() { + let configurator = Configurator::new(true); + let result = configurator.parse_stable_config_slice(b""); + match result { + LoggedResult::Ok(config, _) => assert_eq!(config, StableConfig::default()), + LoggedResult::Err(e) => panic!("Expected success, got: {e:?}"), + } + } + #[test] fn test_parse_static_config() { let mut tmp = tempfile::NamedTempFile::new().unwrap(); @@ -1237,7 +1250,7 @@ rules: StableConfig { config_id: None, apm_configuration_default: ConfigMap::default(), - tags: HashMap::default(), + tags: BTreeMap::default(), rules: vec![Rule { selectors: vec![Selector { origin: Origin::Language, @@ -1266,7 +1279,7 @@ rules: envp: vec![b"ENV=VAR".to_vec()], language: b"java".to_vec(), }; - let tags = HashMap::new(); + let tags = BTreeMap::new(); let matcher = Matcher::new(&process_info, &tags); let test_cases = &[ @@ -1328,12 +1341,12 @@ rules: language: b"java".to_vec(), }; let configurator = Configurator::new(true); - let config = configurator - .get_config_from_bytes( + let local_config = configurator + .parse_stable_config_slice( b" config_id: abc tags: - cluster_name: my_cluster + cluster_name: my_cluster rules: - selectors: - origin: language @@ -1342,6 +1355,11 @@ rules: configuration: DD_SERVICE: local ", + ) + .data() + .unwrap(); + let fleet_config = configurator + .parse_stable_config_slice( b" config_id: def rules: @@ -1351,8 +1369,12 @@ rules: operator: equals configuration: DD_SERVICE: managed", - process_info, ) + .data() + .unwrap(); + let config = configurator + .get_config(local_config, fleet_config, &process_info) + .data() .unwrap(); assert_eq!( config, @@ -1365,3 +1387,98 @@ rules: ); } } + +#[cfg(test)] +mod config_read_tests { + use alloc::collections::BTreeMap; + use alloc::format; + use alloc::string::{String, ToString}; + use alloc::vec; + use alloc::vec::Vec; + + use super::{ConfigRead, ConfigReadError, Configurator, ProcessInfo}; + + /// In-memory reader for testing the `ConfigRead` trait without filesystem access. + struct MemReader { + files: BTreeMap, ConfigReadError>>, + } + + impl MemReader { + fn new() -> Self { + Self { + files: BTreeMap::new(), + } + } + + fn with_file(mut self, path: &str, content: &[u8]) -> Self { + self.files.insert(path.to_string(), Ok(content.to_vec())); + self + } + + fn with_error(mut self, path: &str, err: ConfigReadError) -> Self { + self.files.insert(path.to_string(), Err(err)); + self + } + } + + impl ConfigRead for MemReader { + type IoError = String; + + fn read(&self, path: &str) -> Result, ConfigReadError> { + match self.files.get(path) { + Some(Ok(bytes)) => Ok(bytes.clone()), + Some(Err(ConfigReadError::NotFound)) => Err(ConfigReadError::NotFound), + Some(Err(ConfigReadError::TooLarge)) => Err(ConfigReadError::TooLarge), + Some(Err(ConfigReadError::Io(e))) => Err(ConfigReadError::Io(e.clone())), + None => Err(ConfigReadError::NotFound), + } + } + } + + fn java_process_info() -> ProcessInfo { + ProcessInfo { + args: vec![b"-jar".to_vec(), b"app.jar".to_vec()], + envp: vec![b"DD_ENV=prod".to_vec()], + language: b"java".to_vec(), + } + } + + #[test] + fn reader_too_large_skipped() { + let reader = MemReader::new() + .with_error("/local", ConfigReadError::TooLarge) + .with_file( + "/fleet", + b"apm_configuration_default:\n DD_SERVICE: fleet-svc", + ); + + let configurator = Configurator::new(true); + let result = + configurator.get_config_from_reader(&reader, "/local", "/fleet", &java_process_info()); + + // TooLarge is non-fatal: should still get fleet config + let logs = result.logs().to_vec(); + let configs = result.data().unwrap(); + + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "DD_SERVICE"); + assert_eq!(configs[0].value, "fleet-svc"); + assert!(logs.iter().any(|l| l.contains("too large"))); + } + + #[test] + fn reader_io_error_aborts() { + let reader = MemReader::new().with_error( + "/local", + ConfigReadError::Io("permission denied".to_string()), + ); + + let configurator = Configurator::new(false); + let result = + configurator.get_config_from_reader(&reader, "/local", "/fleet", &java_process_info()); + + let err = result.data().unwrap_err(); + let msg = format!("{err}"); + assert!(msg.contains("permission denied"), "got: {msg}"); + } +} diff --git a/libdd-profiling-ffi/Cargo.toml b/libdd-profiling-ffi/Cargo.toml index cfa1031ebe..08c9c9ac76 100644 --- a/libdd-profiling-ffi/Cargo.toml +++ b/libdd-profiling-ffi/Cargo.toml @@ -29,8 +29,8 @@ crashtracker-collector = ["crashtracker-ffi", "libdd-crashtracker-ffi/collector" # Enables the use of this library to receiver crash-info from a suitable collector crashtracker-receiver = ["crashtracker-ffi", "libdd-crashtracker-ffi/receiver"] demangler = ["crashtracker-ffi", "libdd-crashtracker-ffi/demangler"] -datadog-library-config-ffi = ["dep:libdd-library-config-ffi"] -ddcommon-ffi = ["dep:libdd-common-ffi"] +datadog-library-config-ffi = ["dep:libdd-library-config-ffi", "libdd-library-config-ffi/std"] +ddcommon-ffi = ["dep:libdd-common-ffi", "libdd-common-ffi/std"] ddsketch-ffi = ["dep:libdd-ddsketch-ffi"] datadog-ffe-ffi = ["dep:datadog-ffe-ffi"] diff --git a/libdd-telemetry-ffi/Cargo.toml b/libdd-telemetry-ffi/Cargo.toml index 055ff1db5d..eb1890a80b 100644 --- a/libdd-telemetry-ffi/Cargo.toml +++ b/libdd-telemetry-ffi/Cargo.toml @@ -24,7 +24,7 @@ build_common = { path = "../build-common" } [dependencies] libdd-telemetry = { path = "../libdd-telemetry" } libdd-common = { path = "../libdd-common" } -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, features = ["std"] } function_name = "0.3" libc = "0.2" paste = "1"