use std::io::ErrorKind;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub(crate) enum FileError {
Io {
path: PathBuf,
source: std::io::Error,
},
TooLarge { path: PathBuf },
NotFile,
}
pub(crate) fn os_path(bytes: &[u8]) -> Option<PathBuf> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Some(PathBuf::from(std::ffi::OsStr::from_bytes(bytes)))
}
#[cfg(not(unix))]
{
std::str::from_utf8(bytes).ok().map(PathBuf::from)
}
}
pub(crate) fn native(root: &Path, directory: &[u8]) -> Option<PathBuf> {
let mut path = root.to_owned();
if directory.is_empty() {
return Some(path);
}
for component in directory.split(|byte| *byte == b'/') {
if component.is_empty() || component == b"." || component == b".." {
return None;
}
path.push(os_path(component)?);
}
Some(path)
}
fn io(path: &Path, source: impl Into<std::io::Error>) -> FileError {
FileError::Io {
path: path.to_owned(),
source: source.into(),
}
}
fn read_bounded(
file: impl std::io::Read,
path: &Path,
remaining: usize,
) -> Result<Vec<u8>, FileError> {
use std::io::Read as _;
let mut content = Vec::new();
let limit = u64::try_from(remaining)
.unwrap_or(u64::MAX)
.saturating_add(1);
file.take(limit)
.read_to_end(&mut content)
.map_err(|source| io(path, source))?;
if content.len() > remaining {
return Err(FileError::TooLarge {
path: path.to_owned(),
});
}
Ok(content)
}
pub(crate) fn read_optional(path: &Path, remaining: usize) -> Result<Option<Vec<u8>>, FileError> {
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(rustix::fs::OFlags::NONBLOCK.bits().cast_signed());
}
let file = match options.open(path) {
Ok(file) => file,
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(io(path, source)),
};
if !file
.metadata()
.map_err(|source| io(path, source))?
.is_file()
{
return Err(FileError::NotFile);
}
read_bounded(file, path, remaining).map(Some)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[derive(Debug)]
pub(crate) struct Ancestors {
stack: Vec<rustix::fd::OwnedFd>,
names: Vec<Vec<u8>>,
blocked: bool,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl Ancestors {
pub(crate) fn open(root: &Path) -> Result<Self, FileError> {
let directory = crate::status::worktree::open_root(root).map_err(|_| {
io(
root,
std::io::Error::other("cannot open the working tree without following symlinks"),
)
})?;
Ok(Self {
stack: vec![directory],
names: Vec::new(),
blocked: false,
})
}
pub(crate) fn enter(&mut self, root: &Path, directory: &[u8]) -> Result<bool, FileError> {
use std::os::unix::ffi::OsStrExt;
let components: Vec<&[u8]> = directory
.split(|byte| *byte == b'/')
.filter(|c| !c.is_empty())
.collect();
self.blocked = true;
if components
.iter()
.any(|name| *name == b"." || *name == b"..")
{
return Ok(false);
}
let common = self
.names
.iter()
.zip(&components)
.take_while(|(open, wanted)| open.as_slice() == **wanted)
.count();
self.names.truncate(common);
self.stack.truncate(common + 1);
for name in components.get(common..).unwrap_or_default() {
let Some(current) = self.stack.last() else {
return Ok(false);
};
match rustix::fs::openat(
current,
std::ffi::OsStr::from_bytes(name),
crate::status::worktree::directory_flags(),
rustix::fs::Mode::empty(),
) {
Ok(next) => {
self.stack.push(next);
self.names.push(name.to_vec());
}
Err(
rustix::io::Errno::NOENT | rustix::io::Errno::NOTDIR | rustix::io::Errno::LOOP,
) => return Ok(false),
Err(source) => {
return Err(io(&native(root, directory).unwrap_or_default(), source));
}
}
}
self.blocked = false;
Ok(true)
}
pub(crate) fn read(
&self,
name: &str,
path: &Path,
remaining: usize,
) -> Result<Option<Vec<u8>>, FileError> {
use rustix::fs::{FileType, Mode, OFlags};
let Some(directory) = self.stack.last().filter(|_| !self.blocked) else {
return Ok(None);
};
let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
let file = match rustix::fs::openat(directory, name, flags, Mode::empty()) {
Ok(file) => file,
Err(rustix::io::Errno::NOENT | rustix::io::Errno::LOOP) => return Ok(None),
Err(source) => return Err(io(path, source)),
};
let stat = rustix::fs::fstat(&file).map_err(|source| io(path, source))?;
if FileType::from_raw_mode(stat.st_mode) != FileType::RegularFile {
return Ok(None);
}
read_bounded(std::fs::File::from(file), path, remaining).map(Some)
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
#[derive(Debug)]
pub(crate) struct Ancestors {
blocked: bool,
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
impl Ancestors {
pub(crate) fn open(_root: &Path) -> Result<Self, FileError> {
Ok(Self { blocked: false })
}
pub(crate) fn enter(&mut self, root: &Path, directory: &[u8]) -> Result<bool, FileError> {
let components: Vec<&[u8]> = directory
.split(|byte| *byte == b'/')
.filter(|c| !c.is_empty())
.collect();
self.blocked = !(1..=components.len()).all(|end| {
let prefix = components.get(..end).unwrap_or_default().join(&b'/');
native(root, &prefix)
.and_then(|folder| std::fs::symlink_metadata(folder).ok())
.is_some_and(|metadata| metadata.is_dir())
});
Ok(!self.blocked)
}
pub(crate) fn read(
&self,
_name: &str,
path: &Path,
remaining: usize,
) -> Result<Option<Vec<u8>>, FileError> {
if self.blocked {
return Ok(None);
}
match std::fs::symlink_metadata(path) {
Ok(metadata) if metadata.is_file() => {
let file = std::fs::File::open(path).map_err(|source| io(path, source))?;
read_bounded(file, path, remaining).map(Some)
}
Ok(_) => Ok(None),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(None),
Err(source) => Err(io(path, source)),
}
}
}