use std::path::{Component, Path, PathBuf};
use std::{fs, process};
use crate::error::{StoreError, io_error};
pub const SPEC: &str = "spec";
#[must_use]
pub fn is_under_spec(root: &Path, path: &Path) -> bool {
resolved(&root.join(path)).starts_with(resolved(&root.join(SPEC)))
}
fn resolved(path: &Path) -> PathBuf {
let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
let mut normal = PathBuf::new();
for part in absolute.components() {
match part {
Component::CurDir => {}
Component::ParentDir => {
normal.pop();
}
other => normal.push(other),
}
}
let mut missing = Vec::new();
let mut existing = normal.as_path();
while !existing.exists() {
let (Some(name), Some(parent)) = (existing.file_name(), existing.parent()) else {
break;
};
missing.push(name);
existing = parent;
}
let mut resolved = existing
.canonicalize()
.unwrap_or_else(|_| existing.to_path_buf());
resolved.extend(missing.iter().rev());
resolved
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct WrittenOutputs {
pub written: Vec<PathBuf>,
pub unchanged: Vec<PathBuf>,
}
pub fn write_outputs(
root: &Path,
outputs: &[(PathBuf, Vec<u8>)],
) -> Result<WrittenOutputs, StoreError> {
let mut done = WrittenOutputs::default();
for (path, bytes) in outputs {
let target = root.join(path);
if fs::read(&target).is_ok_and(|current| current == *bytes) {
done.unchanged.push(path.clone());
continue;
}
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(io_error(parent))?;
}
let name = target
.file_name()
.map_or_else(Default::default, |name| name.to_string_lossy());
let temporary = target.with_file_name(format!(".{name}.loft-{}", process::id()));
fs::write(&temporary, bytes).map_err(io_error(&temporary))?;
fs::rename(&temporary, &target).map_err(io_error(&target))?;
done.written.push(path.clone());
}
Ok(done)
}
pub(crate) fn holds_entries(dir: &Path) -> Result<bool, StoreError> {
for entry in fs::read_dir(dir).map_err(io_error(dir))? {
let entry = entry.map_err(io_error(dir))?;
if !entry.file_name().to_str().is_some_and(hidden) {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn hidden(name: &str) -> bool {
name.starts_with('.')
}