Skip to content
Merged
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
153 changes: 153 additions & 0 deletions libdd-capabilities-impl/src/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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());
}
}
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