Skip to content
Merged
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
26 changes: 25 additions & 1 deletion src/dirext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use cap_tempfile::cap_std::fs::DirEntry;
use rustix::path::Arg;
use std::cmp::Ordering;
use std::ffi::OsStr;
use std::io::Result;
use std::io::{self, Write};
use std::io::{Read, Result};
use std::ops::Deref;
#[cfg(unix)]
use std::os::fd::OwnedFd;
Expand Down Expand Up @@ -150,6 +150,12 @@ pub trait CapStdExtDirExt {
/// Symbolic links are not followed.
fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool>;

/// Read the complete contents of a file, but return `Ok(None)` if the file does not exist.
fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>>;

/// Read the complete contents of a file as a string, but return `Ok(None)` if the file does not exist.
fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>>;

/// Set the access and modification times to the current time. Symbolic links are not followed.
#[cfg(unix)]
fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()>;
Expand Down Expand Up @@ -693,6 +699,24 @@ impl CapStdExtDirExt for Dir {
Ok(true)
}

fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>> {
let mut r = Vec::new();
let Some(mut f) = self.open_optional(path.as_ref())? else {
return Ok(None);
};
f.read_to_end(&mut r)?;
Ok(Some(r))
}

fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>> {
let mut r = String::new();
let Some(mut f) = self.open_optional(path.as_ref())? else {
return Ok(None);
};
f.read_to_string(&mut r)?;
Ok(Some(r))
}

#[cfg(unix)]
fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()> {
use rustix::fd::AsFd;
Expand Down
116 changes: 116 additions & 0 deletions tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,122 @@ fn optionals() -> Result<()> {
Ok(())
}

#[test]
fn read_optional() -> Result<()> {
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;

// Test non-existent file returns None
assert!(td.read_optional("nonexistent")?.is_none());

// Test existing file with string contents
td.write("test.txt", "hello world")?;
let contents = td.read_optional("test.txt")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), b"hello world");

// Test existing file with binary contents
let binary_data = vec![0u8, 1, 2, 254, 255];
td.write("test.bin", &binary_data)?;
let contents = td.read_optional("test.bin")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), binary_data);

// Test empty file
td.write("empty.txt", "")?;
let contents = td.read_optional("empty.txt")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), b"");

// Test file in subdirectory
td.create_dir_all("sub/dir")?;
td.write("sub/dir/file.txt", "nested content")?;
let contents = td.read_optional("sub/dir/file.txt")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), b"nested content");

// Test non-existent file in existing directory
assert!(td.read_optional("sub/dir/missing.txt")?.is_none());

// Test large file
let large_data = vec![b'x'; 10000];
td.write("large.txt", &large_data)?;
let contents = td.read_optional("large.txt")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), large_data);

#[cfg(not(windows))]
{
// Test symlink to existing file
td.symlink("test.txt", "link_to_test")?;
let contents = td.read_optional("link_to_test")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), b"hello world");

// Test broken symlink returns None (NotFound is mapped to None)
td.symlink("nonexistent_target", "broken_link")?;
assert!(td.read_optional("broken_link")?.is_none());
}

Ok(())
}

#[test]
fn read_to_string_optional() -> Result<()> {
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;

// Test non-existent file returns None
assert!(td.read_to_string_optional("nonexistent")?.is_none());

// Test existing file with valid UTF-8 string
td.write("test.txt", "hello world")?;
let contents = td.read_to_string_optional("test.txt")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), "hello world");

// Test file with UTF-8 content including unicode
let unicode_content = "Hello 世界 🌍";
td.write("unicode.txt", unicode_content)?;
let contents = td.read_to_string_optional("unicode.txt")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), unicode_content);

// Test empty file
td.write("empty.txt", "")?;
let contents = td.read_to_string_optional("empty.txt")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), "");

// Test file in subdirectory
td.create_dir_all("sub/dir")?;
td.write("sub/dir/file.txt", "nested content")?;
let contents = td.read_to_string_optional("sub/dir/file.txt")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), "nested content");

// Test non-existent file in existing directory
assert!(td.read_to_string_optional("sub/dir/missing.txt")?.is_none());

// Test file with invalid UTF-8 should return an error
let invalid_utf8 = vec![0xff, 0xfe, 0xfd];
td.write("invalid.bin", invalid_utf8.as_slice())?;
assert!(td.read_to_string_optional("invalid.bin").is_err());

#[cfg(not(windows))]
{
// Test symlink to existing file
td.symlink("test.txt", "link_to_test")?;
let contents = td.read_to_string_optional("link_to_test")?;
assert!(contents.is_some());
assert_eq!(contents.unwrap(), "hello world");

// Test broken symlink returns None (NotFound is mapped to None)
td.symlink("nonexistent_target", "broken_link")?;
assert!(td.read_to_string_optional("broken_link")?.is_none());
}

Ok(())
}

#[test]
fn ensuredir() -> Result<()> {
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;
Expand Down
Loading