use super::*;
#[derive(Debug)]
pub(crate) struct PowerLossRoot {
_root: File,
context: FileOpenContext,
identity: SourceDirectoryIdentity,
}
impl PowerLossRoot {
pub(crate) fn open_directory(
&self,
relative: &Path,
context: &FileOpenContext,
) -> io::Result<std::fs::File> {
#[cfg(any(target_os = "linux", target_vendor = "apple", windows))]
{
use std::path::Component;
let mut components = relative.components().peekable();
let mut parent: Option<File> = None;
if components.peek().is_none() {
return self._root.with_native(platform::reopen_directory);
}
while let Some(component) = components.next() {
let Component::Normal(name) = component else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid captured directory path",
));
};
let final_component = components.peek().is_none();
let permit = if final_component {
None
} else {
Some(context.acquire(DescriptorKind::Transient)?)
};
let directory = parent.as_ref().unwrap_or(&self._root);
let inner = directory
.with_native(|root| platform::open_recorded_child(root, name))
.map_err(|error| context.map_open_error(error))?;
if !inner.metadata()?.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
"captured child is not a directory",
));
}
if let Some(permit) = permit {
parent = Some(counted(inner, permit));
} else {
return Ok(inner);
}
}
unreachable!("nonempty normalized path returns its final directory")
}
#[cfg(not(any(target_os = "linux", target_vendor = "apple", windows)))]
{
let _ = (relative, context);
Err(io::Error::new(
io::ErrorKind::Unsupported,
"captured native directory recording requires Linux, Apple or Windows",
))
}
}
pub(crate) fn capture(path: &Path, context: &FileOpenContext) -> io::Result<Self> {
let permit = context.acquire(DescriptorKind::Transient)?;
let inner = platform::open_root(path).map_err(|error| context.map_open_error(error))?;
if !inner.metadata()?.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
"capture root is not a directory",
));
}
let identity = SourceDirectoryIdentity(crate::native_file_identity::of_file(&inner)?);
Ok(Self {
_root: counted(inner, permit),
context: context.without_inventory(),
identity,
})
}
pub(crate) fn require_empty(&self, path: &Path) -> io::Result<()> {
self.require_path(path)?;
{
let _cursor = self.context.acquire(DescriptorKind::Transient)?;
#[cfg(any(target_os = "linux", target_vendor = "apple", windows))]
self._root
.with_native(|root| platform::visit_names(root, &mut |_| Err(nonempty())))?;
#[cfg(not(any(target_os = "linux", target_vendor = "apple", windows)))]
{
let mut entries = std::fs::read_dir(path)?;
if entries.next().transpose()?.is_some() {
return Err(nonempty());
}
}
}
self.require_path(path)
}
#[cfg(any(target_os = "linux", target_vendor = "apple", windows))]
pub(crate) fn visit_tree(
&self,
maximum_entries: usize,
mut visitor: impl FnMut(&Path, &std::fs::File) -> io::Result<()>,
) -> io::Result<()> {
let mut pending = Vec::new();
let mut remaining = maximum_entries;
self._root.with_native(|root| {
visitor(Path::new(""), root)?;
self.visit_directory(
root,
Path::new(""),
&mut remaining,
&mut pending,
&mut visitor,
)
})?;
while let Some((relative, directory)) = pending.pop() {
directory.with_native(|root| {
self.visit_directory(root, &relative, &mut remaining, &mut pending, &mut visitor)
})?;
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple", windows))]
fn visit_directory(
&self,
root: &std::fs::File,
relative: &Path,
remaining: &mut usize,
pending: &mut Vec<(std::path::PathBuf, File)>,
visitor: &mut dyn FnMut(&Path, &std::fs::File) -> io::Result<()>,
) -> io::Result<()> {
let _cursor = self.context.acquire(DescriptorKind::Transient)?;
platform::visit_names(root, &mut |name| {
*remaining = remaining.checked_sub(1).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"native verification entry budget exceeded",
)
})?;
let permit = self.context.acquire(DescriptorKind::Transient)?;
let inner = platform::open_recorded_child(root, name)
.map_err(|error| self.context.map_open_error(error))?;
let metadata = inner.metadata()?;
crate::power_loss::identity::require_direct_entry(&metadata)?;
if !metadata.is_dir() && !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"captured project contains a nonregular file",
));
}
let relative = relative.join(name);
let file = counted(inner, permit);
file.with_native(|inner| visitor(&relative, inner))?;
if metadata.is_dir() {
pending.push((relative, file));
}
Ok(())
})
}
pub(crate) fn require_path(&self, path: &Path) -> io::Result<()> {
self.require_path_with_context(path, &self.context)
}
pub(crate) fn require_path_with_context(
&self,
path: &Path,
context: &FileOpenContext,
) -> io::Result<()> {
#[cfg(unix)]
let _ = context;
let metadata = std::fs::symlink_metadata(path)?;
crate::power_loss::identity::require_direct_entry(&metadata)?;
if !metadata.is_dir() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"captured root pathname is no longer a directory",
));
}
#[cfg(unix)]
let identity = SourceDirectoryIdentity(crate::native_file_identity::of_metadata(&metadata));
#[cfg(not(unix))]
let identity = {
let _permit = context.acquire(DescriptorKind::Transient)?;
let named = platform::open_root(path).map_err(|error| context.map_open_error(error))?;
SourceDirectoryIdentity(crate::native_file_identity::of_file(&named)?)
};
if identity != self.identity {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"power-loss capture root pathname was replaced",
));
}
Ok(())
}
}
fn nonempty() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
"power-loss capture requires an empty project directory",
)
}
#[cfg(all(test, any(target_os = "linux", target_vendor = "apple", windows)))]
mod tests;