Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion libdd-capabilities-impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ bytes = "1"
http = "1"
libdd-capabilities = { path = "../libdd-capabilities", version = "2.1.0" }
libdd-common = { path = "../libdd-common", version = "5.1.0", default-features = false }
tokio = { version = "1", features = ["time"] }
tokio = { version = "1", features = ["fs", "time"] }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
http-body-util = "0.1"

[dev-dependencies]
tempfile = "3"
tokio = { version = "1", features = ["fs", "macros", "rt", "time"] }

[features]
default = ["https"]
https = ["libdd-common/https"]
Expand Down
227 changes: 227 additions & 0 deletions libdd-capabilities-impl/src/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! Native file capability backed by `tokio::fs`.

use core::future::Future;
use std::io;

use bytes::Bytes;
use libdd_capabilities::file::{FileCapability, FileError, FileMetadata};
use libdd_capabilities::maybe_send::MaybeSend;

#[derive(Clone, Debug)]
pub struct NativeFileCapability;

impl FileCapability for NativeFileCapability {
fn new() -> Self {
Self
}

fn read(&self, path: &str) -> impl Future<Output = Result<Bytes, FileError>> + MaybeSend {
let path = path.to_owned();
async move {
match tokio::fs::read(&path).await {
Ok(bytes) => Ok(Bytes::from(bytes)),
Err(e) => Err(map_io_error(e, &path)),
}
}
}

fn write(
&self,
path: &str,
contents: Bytes,
) -> impl Future<Output = Result<(), FileError>> + MaybeSend {
let path = path.to_owned();
async move {
tokio::fs::write(&path, &contents)
.await
.map_err(|e| map_io_error(e, &path))
}
}

fn metadata(
&self,
path: &str,
) -> impl Future<Output = Result<FileMetadata, FileError>> + MaybeSend {
let path = path.to_owned();
async move {
let m = tokio::fs::metadata(&path)
.await
.map_err(|e| map_io_error(e, &path))?;
Ok(FileMetadata {
size: m.len(),
inode: platform_inode(&m),
is_file: m.is_file(),
is_dir: m.is_dir(),
})
}
}

fn exists(&self, path: &str) -> impl Future<Output = Result<bool, FileError>> + MaybeSend {
let path = path.to_owned();
async move {
match tokio::fs::metadata(&path).await {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(map_io_error(e, &path)),
}
}
}
}

fn map_io_error(e: io::Error, path: &str) -> FileError {
match e.kind() {
io::ErrorKind::NotFound => FileError::NotFound(path.to_owned()),
io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.to_owned()),
_ => FileError::Io(anyhow::Error::new(e).context(format!("path: {path}"))),
}
}

#[cfg(unix)]
fn platform_inode(m: &std::fs::Metadata) -> Option<u64> {
use std::os::unix::fs::MetadataExt;
Some(m.ino())
}

#[cfg(not(unix))]
fn platform_inode(_m: &std::fs::Metadata) -> Option<u64> {
None
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;

fn tmp() -> TempDir {
TempDir::new().expect("tempdir")
}

#[tokio::test]
async fn read_write_roundtrip() {
let dir = tmp();
let path = dir.path().join("file.bin");
let path_str = path.to_str().unwrap();
let cap = NativeFileCapability;
cap.write(path_str, Bytes::from_static(b"hello"))
.await
.expect("write ok");
let got = cap.read(path_str).await.expect("read ok");
assert_eq!(&got[..], b"hello");
}

#[tokio::test]
async fn read_missing_yields_not_found() {
let dir = tmp();
let path = dir.path().join("missing.bin");
let cap = NativeFileCapability;
let err = cap.read(path.to_str().unwrap()).await.unwrap_err();
assert!(matches!(err, FileError::NotFound(_)), "got: {err:?}");
}

#[tokio::test]
async fn metadata_reports_size_and_kind() {
let dir = tmp();
let path = dir.path().join("f.bin");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(b"1234567890").unwrap();
drop(f);
let cap = NativeFileCapability;
let m = cap.metadata(path.to_str().unwrap()).await.expect("meta ok");
assert_eq!(m.size, 10);
assert!(m.is_file);
assert!(!m.is_dir);
#[cfg(unix)]
assert!(m.inode.is_some());
#[cfg(not(unix))]
assert!(m.inode.is_none());
}

#[tokio::test]
async fn exists_true_and_false() {
let dir = tmp();
let present = dir.path().join("here");
std::fs::write(&present, b"").unwrap();
let absent = dir.path().join("gone");
let cap = NativeFileCapability;
assert!(cap.exists(present.to_str().unwrap()).await.unwrap());
assert!(!cap.exists(absent.to_str().unwrap()).await.unwrap());
}

// In-memory FileCapability for mocking fs in tests.
#[derive(Clone, Debug, Default)]
struct InMemoryFs {
files: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, Bytes>>>,
}

impl FileCapability for InMemoryFs {
fn new() -> Self {
Self::default()
}

fn read(&self, path: &str) -> impl Future<Output = Result<Bytes, FileError>> + MaybeSend {
let key = path.to_owned();
let files = self.files.clone();
async move {
files
.lock()
.unwrap()
.get(&key)
.cloned()
.ok_or(FileError::NotFound(key))
}
}

fn write(
&self,
path: &str,
contents: Bytes,
) -> impl Future<Output = Result<(), FileError>> + MaybeSend {
let key = path.to_owned();
let files = self.files.clone();
async move {
files.lock().unwrap().insert(key, contents);
Ok(())
}
}

fn metadata(
&self,
path: &str,
) -> impl Future<Output = Result<FileMetadata, FileError>> + MaybeSend {
let key = path.to_owned();
let files = self.files.clone();
async move {
let files = files.lock().unwrap();
let bytes = files.get(&key).ok_or(FileError::NotFound(key))?;
Ok(FileMetadata {
size: bytes.len() as u64,
inode: None,
is_file: true,
is_dir: false,
})
}
}

fn exists(&self, path: &str) -> impl Future<Output = Result<bool, FileError>> + MaybeSend {
let key = path.to_owned();
let files = self.files.clone();
async move { Ok(files.lock().unwrap().contains_key(&key)) }
}
}

#[tokio::test]
async fn in_memory_mock_satisfies_trait() {
let fs = InMemoryFs::default();
fs.write("/proc/self/cgroup", Bytes::from_static(b"0::/"))
.await
.unwrap();
let out = fs.read("/proc/self/cgroup").await.unwrap();
assert_eq!(&out[..], b"0::/");
assert!(fs.exists("/proc/self/cgroup").await.unwrap());
assert!(!fs.exists("/nope").await.unwrap());
}
}
39 changes: 38 additions & 1 deletion libdd-capabilities-impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
//! etc.). Leaf crates (FFI, benchmarks) pin this type as the generic parameter.

pub mod env;
pub mod file;
mod http;
pub mod sleep;

use core::future::Future;
use std::time::Duration;

pub use env::NativeEnvCapability;
pub use file::NativeFileCapability;
pub use http::NativeHttpClient;
use libdd_capabilities::{http::HttpError, MaybeSend};
pub use libdd_capabilities::{
EnvCapability, EnvError, HttpClientCapability, LogWriterCapability, SleepCapability,
EnvCapability, EnvError, FileCapability, FileError, FileMetadata, HttpClientCapability,
LogWriterCapability, SleepCapability,
};
pub use sleep::NativeSleepCapability;

Expand All @@ -36,6 +39,7 @@ pub struct NativeCapabilities {
http: NativeHttpClient,
sleep: NativeSleepCapability,
env: NativeEnvCapability,
file: NativeFileCapability,
}

impl Default for NativeCapabilities {
Expand All @@ -50,6 +54,7 @@ impl NativeCapabilities {
http: NativeHttpClient::new_client(),
sleep: NativeSleepCapability,
env: NativeEnvCapability,
file: NativeFileCapability,
}
}
}
Expand Down Expand Up @@ -98,3 +103,35 @@ impl EnvCapability for NativeCapabilities {
self.env.get(name)
}
}

impl FileCapability for NativeCapabilities {
fn new() -> Self {
Self::new()
}

fn read(
&self,
path: &str,
) -> impl Future<Output = Result<bytes::Bytes, FileError>> + MaybeSend {
self.file.read(path)
}

fn write(
&self,
path: &str,
contents: bytes::Bytes,
) -> impl Future<Output = Result<(), FileError>> + MaybeSend {
self.file.write(path, contents)
}

fn metadata(
&self,
path: &str,
) -> impl Future<Output = Result<FileMetadata, FileError>> + MaybeSend {
self.file.metadata(path)
}

fn exists(&self, path: &str) -> impl Future<Output = Result<bool, FileError>> + MaybeSend {
self.file.exists(path)
}
}
53 changes: 53 additions & 0 deletions libdd-capabilities/src/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! File-system capability trait and error types.
//!
//! Async so a wasm impl can await `fs.promises`. Paths are `&str` because
//! wasm callers hand them across the JS boundary as strings.

use crate::maybe_send::MaybeSend;
use core::future::Future;

#[derive(Debug, thiserror::Error)]
pub enum FileError {
#[error("File not found: {0}")]
NotFound(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("IO error: {0}")]
Io(anyhow::Error),
}

/// Snapshot of a file-system entry's metadata.
///
/// `inode` is `None` when the underlying platform does not expose one (Windows
/// via `std`). Node.js exposes an inode on every platform, so the wasm impl
/// always populates it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileMetadata {
pub size: u64,
pub inode: Option<u64>,
pub is_file: bool,
pub is_dir: bool,
}

pub trait FileCapability: Clone + std::fmt::Debug {
fn new() -> Self;

fn read(&self, path: &str)
-> impl Future<Output = Result<bytes::Bytes, FileError>> + MaybeSend;

fn write(
&self,
path: &str,
contents: bytes::Bytes,
) -> impl Future<Output = Result<(), FileError>> + MaybeSend;

fn metadata(
&self,
path: &str,
) -> impl Future<Output = Result<FileMetadata, FileError>> + MaybeSend;

fn exists(&self, path: &str) -> impl Future<Output = Result<bool, FileError>> + MaybeSend;
}
2 changes: 2 additions & 0 deletions libdd-capabilities/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
//! Portable capability traits for cross-platform libdatadog.

pub mod env;
pub mod file;
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::file::{FileCapability, FileError, FileMetadata};
pub use self::http::{HttpClientCapability, HttpError};
pub use self::log_output::LogWriterCapability;
pub use self::sleep::SleepCapability;
Expand Down
Loading