use std::io;
use std::path::Path;
use std::sync::Arc;
use std::io::Write;
use cap_std::ambient_authority;
use cap_std::fs::Dir;
use cap_tempfile::TempFile;
use crate::paths::ProjectPath;
use crate::tree::ReadTree;
pub struct WorkingDir {
dir: Dir,
}
pub struct Writer<'a> {
dir: &'a Dir,
}
impl WorkingDir {
pub fn open(root: &Path) -> io::Result<Self> {
let dir = Dir::open_ambient_dir(root, ambient_authority())?;
Ok(Self { dir })
}
#[must_use]
pub fn writer(&self) -> Writer<'_> {
Writer { dir: &self.dir }
}
fn walk_into(&self, directory: &ProjectPath, found: &mut Vec<ProjectPath>) -> io::Result<()> {
let mut entries = Vec::new();
for entry in self.dir.read_dir(directory.to_native())? {
let entry = entry?;
let name = entry.file_name();
let Some(name) = name.to_str() else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"`{directory}` contains an entry whose name is not valid UTF-8: {}",
name.to_string_lossy()
),
));
};
let file_type = entry.file_type()?;
entries.push((name.to_owned(), file_type));
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
for (name, file_type) in entries {
let segment = ProjectPath::parse(&name).map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("`{directory}` contains an entry that is not a portable path segment: {error}"),
)
})?;
let path = directory.join(&segment);
if file_type.is_symlink() {
continue;
}
if file_type.is_dir() {
self.walk_into(&path, found)?;
} else if file_type.is_file() {
found.push(path);
}
}
Ok(())
}
}
impl ReadTree for WorkingDir {
fn read(&self, path: &ProjectPath) -> io::Result<Vec<u8>> {
self.dir.read(path.to_native())
}
fn read_bounded(&self, path: &ProjectPath, limit: u64) -> io::Result<(Vec<u8>, bool)> {
use std::io::Read;
let file = self.dir.open(path.to_native())?;
let mut bytes = Vec::new();
file.take(limit.saturating_add(1)).read_to_end(&mut bytes)?;
let over = u64::try_from(bytes.len()).is_ok_and(|len| len > limit);
Ok((bytes, over))
}
fn file_len(&self, path: &ProjectPath) -> io::Result<u64> {
Ok(self.dir.metadata(path.to_native())?.len())
}
fn is_file(&self, path: &ProjectPath) -> bool {
self.dir.is_file(path.to_native())
}
fn is_dir(&self, path: &ProjectPath) -> bool {
path.as_str().is_empty() || self.dir.is_dir(path.to_native())
}
fn exists(&self, path: &ProjectPath) -> bool {
self.dir.exists(path.to_native())
}
fn symlink_component(&self, path: &ProjectPath) -> io::Result<Option<ProjectPath>> {
for ancestor in path.ancestors() {
match self.dir.symlink_metadata(ancestor.to_native()) {
Ok(metadata) if metadata.is_symlink() => return Ok(Some(ancestor)),
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
}
}
Ok(None)
}
fn walk(&self, directory: &ProjectPath) -> io::Result<Vec<ProjectPath>> {
if let Some(link) = self.symlink_component(directory)? {
return Err(linked_directory(&link));
}
let mut found = Vec::new();
self.walk_into(directory, &mut found)?;
found.sort();
Ok(found)
}
fn subtree(&self, directory: &ProjectPath) -> io::Result<Arc<dyn ReadTree>> {
let dir = if directory.as_str().is_empty() {
self.dir.try_clone()?
} else {
self.dir.open_dir(directory.to_native())?
};
Ok(Arc::new(Self { dir }))
}
}
pub fn linked_directory(link: &ProjectPath) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!("`{link}` is a symbolic link; directories are never walked through links"),
)
}
impl Writer<'_> {
pub fn write_atomic(&self, path: &ProjectPath, bytes: &[u8]) -> io::Result<()> {
let parent = path.parent();
if !parent.as_str().is_empty() {
self.dir.create_dir_all(parent.to_native())?;
}
let directory = if parent.as_str().is_empty() {
self.dir.try_clone()?
} else {
self.dir.open_dir(parent.to_native())?
};
let mut temp = TempFile::new(&directory)?;
temp.write_all(bytes)?;
temp.as_file_mut().sync_data()?;
temp.replace(path.file_name())
}
pub fn remove_file(&self, path: &ProjectPath) -> io::Result<()> {
self.dir.remove_file(path.to_native())
}
pub fn replace_preserving(&self, path: &ProjectPath, bytes: &[u8]) -> io::Result<()> {
let permissions = self.dir.metadata(path.to_native())?.permissions();
let parent = path.parent();
let directory = if parent.as_str().is_empty() {
self.dir.try_clone()?
} else {
self.dir.open_dir(parent.to_native())?
};
let mut temp = TempFile::new(&directory)?;
temp.write_all(bytes)?;
temp.as_file_mut().set_permissions(permissions)?;
temp.as_file_mut().sync_data()?;
temp.replace(path.file_name())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bounded_reads_never_read_more_than_the_bound_plus_one() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("big.txt"), vec![b'x'; 10_000]).unwrap();
std::fs::write(dir.path().join("small.txt"), b"tiny").unwrap();
let working = WorkingDir::open(dir.path()).unwrap();
let big = ProjectPath::parse("big.txt").unwrap();
let (bytes, over) = working.read_bounded(&big, 100).unwrap();
assert!(over);
assert_eq!(
bytes.len(),
101,
"the bound plus one probe byte, nothing more"
);
let (bytes, over) = working.read_bounded(&big, 10_000).unwrap();
assert!(!over);
assert_eq!(bytes.len(), 10_000);
let small = ProjectPath::parse("small.txt").unwrap();
let (bytes, over) = working.read_bounded(&small, 100).unwrap();
assert!(!over);
assert_eq!(bytes, b"tiny");
assert!(
working
.read_bounded(&ProjectPath::parse("missing").unwrap(), 1)
.is_err()
);
}
}