Skip to content

Commit 63c131d

Browse files
authored
Merge pull request #78 from cgwalters/readopt
dirext: Add APIs to read whole contents optionally
2 parents 6024c04 + 85aa6d5 commit 63c131d

File tree

2 files changed

+141
-1
lines changed

2 files changed

+141
-1
lines changed

src/dirext.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use cap_tempfile::cap_std::fs::DirEntry;
1616
use rustix::path::Arg;
1717
use std::cmp::Ordering;
1818
use std::ffi::OsStr;
19-
use std::io::Result;
2019
use std::io::{self, Write};
20+
use std::io::{Read, Result};
2121
use std::ops::Deref;
2222
#[cfg(unix)]
2323
use std::os::fd::OwnedFd;
@@ -150,6 +150,12 @@ pub trait CapStdExtDirExt {
150150
/// Symbolic links are not followed.
151151
fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
152152

153+
/// Read the complete contents of a file, but return `Ok(None)` if the file does not exist.
154+
fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>>;
155+
156+
/// Read the complete contents of a file as a string, but return `Ok(None)` if the file does not exist.
157+
fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>>;
158+
153159
/// Set the access and modification times to the current time. Symbolic links are not followed.
154160
#[cfg(unix)]
155161
fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()>;
@@ -693,6 +699,24 @@ impl CapStdExtDirExt for Dir {
693699
Ok(true)
694700
}
695701

702+
fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>> {
703+
let mut r = Vec::new();
704+
let Some(mut f) = self.open_optional(path.as_ref())? else {
705+
return Ok(None);
706+
};
707+
f.read_to_end(&mut r)?;
708+
Ok(Some(r))
709+
}
710+
711+
fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>> {
712+
let mut r = String::new();
713+
let Some(mut f) = self.open_optional(path.as_ref())? else {
714+
return Ok(None);
715+
};
716+
f.read_to_string(&mut r)?;
717+
Ok(Some(r))
718+
}
719+
696720
#[cfg(unix)]
697721
fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()> {
698722
use rustix::fd::AsFd;

tests/it/main.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,122 @@ fn optionals() -> Result<()> {
9090
Ok(())
9191
}
9292

93+
#[test]
94+
fn read_optional() -> Result<()> {
95+
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;
96+
97+
// Test non-existent file returns None
98+
assert!(td.read_optional("nonexistent")?.is_none());
99+
100+
// Test existing file with string contents
101+
td.write("test.txt", "hello world")?;
102+
let contents = td.read_optional("test.txt")?;
103+
assert!(contents.is_some());
104+
assert_eq!(contents.unwrap(), b"hello world");
105+
106+
// Test existing file with binary contents
107+
let binary_data = vec![0u8, 1, 2, 254, 255];
108+
td.write("test.bin", &binary_data)?;
109+
let contents = td.read_optional("test.bin")?;
110+
assert!(contents.is_some());
111+
assert_eq!(contents.unwrap(), binary_data);
112+
113+
// Test empty file
114+
td.write("empty.txt", "")?;
115+
let contents = td.read_optional("empty.txt")?;
116+
assert!(contents.is_some());
117+
assert_eq!(contents.unwrap(), b"");
118+
119+
// Test file in subdirectory
120+
td.create_dir_all("sub/dir")?;
121+
td.write("sub/dir/file.txt", "nested content")?;
122+
let contents = td.read_optional("sub/dir/file.txt")?;
123+
assert!(contents.is_some());
124+
assert_eq!(contents.unwrap(), b"nested content");
125+
126+
// Test non-existent file in existing directory
127+
assert!(td.read_optional("sub/dir/missing.txt")?.is_none());
128+
129+
// Test large file
130+
let large_data = vec![b'x'; 10000];
131+
td.write("large.txt", &large_data)?;
132+
let contents = td.read_optional("large.txt")?;
133+
assert!(contents.is_some());
134+
assert_eq!(contents.unwrap(), large_data);
135+
136+
#[cfg(not(windows))]
137+
{
138+
// Test symlink to existing file
139+
td.symlink("test.txt", "link_to_test")?;
140+
let contents = td.read_optional("link_to_test")?;
141+
assert!(contents.is_some());
142+
assert_eq!(contents.unwrap(), b"hello world");
143+
144+
// Test broken symlink returns None (NotFound is mapped to None)
145+
td.symlink("nonexistent_target", "broken_link")?;
146+
assert!(td.read_optional("broken_link")?.is_none());
147+
}
148+
149+
Ok(())
150+
}
151+
152+
#[test]
153+
fn read_to_string_optional() -> Result<()> {
154+
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;
155+
156+
// Test non-existent file returns None
157+
assert!(td.read_to_string_optional("nonexistent")?.is_none());
158+
159+
// Test existing file with valid UTF-8 string
160+
td.write("test.txt", "hello world")?;
161+
let contents = td.read_to_string_optional("test.txt")?;
162+
assert!(contents.is_some());
163+
assert_eq!(contents.unwrap(), "hello world");
164+
165+
// Test file with UTF-8 content including unicode
166+
let unicode_content = "Hello 世界 🌍";
167+
td.write("unicode.txt", unicode_content)?;
168+
let contents = td.read_to_string_optional("unicode.txt")?;
169+
assert!(contents.is_some());
170+
assert_eq!(contents.unwrap(), unicode_content);
171+
172+
// Test empty file
173+
td.write("empty.txt", "")?;
174+
let contents = td.read_to_string_optional("empty.txt")?;
175+
assert!(contents.is_some());
176+
assert_eq!(contents.unwrap(), "");
177+
178+
// Test file in subdirectory
179+
td.create_dir_all("sub/dir")?;
180+
td.write("sub/dir/file.txt", "nested content")?;
181+
let contents = td.read_to_string_optional("sub/dir/file.txt")?;
182+
assert!(contents.is_some());
183+
assert_eq!(contents.unwrap(), "nested content");
184+
185+
// Test non-existent file in existing directory
186+
assert!(td.read_to_string_optional("sub/dir/missing.txt")?.is_none());
187+
188+
// Test file with invalid UTF-8 should return an error
189+
let invalid_utf8 = vec![0xff, 0xfe, 0xfd];
190+
td.write("invalid.bin", invalid_utf8.as_slice())?;
191+
assert!(td.read_to_string_optional("invalid.bin").is_err());
192+
193+
#[cfg(not(windows))]
194+
{
195+
// Test symlink to existing file
196+
td.symlink("test.txt", "link_to_test")?;
197+
let contents = td.read_to_string_optional("link_to_test")?;
198+
assert!(contents.is_some());
199+
assert_eq!(contents.unwrap(), "hello world");
200+
201+
// Test broken symlink returns None (NotFound is mapped to None)
202+
td.symlink("nonexistent_target", "broken_link")?;
203+
assert!(td.read_to_string_optional("broken_link")?.is_none());
204+
}
205+
206+
Ok(())
207+
}
208+
93209
#[test]
94210
fn ensuredir() -> Result<()> {
95211
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;

0 commit comments

Comments
 (0)