use boxology_manifest::RelativePath;
use boxology_workspace::FileEntry;
use std::{
fmt, fs,
path::{Path, PathBuf},
};
type Rule = (&'static str, &'static str, &'static str);
const RULE_SOURCE: &str =
"boxology-details/02-packages.md discovery walk; S5-T4 #326 PR1 task authority";
const ROOT_TEXT: &str = "workspace root must be a real directory containing a regular Cargo.toml";
const IO_TEXT: &str = "filesystem refused a directory, symlink, or manifest read";
const PATH_TEXT: &str = "walked name/path is not a valid RelativePath";
const ROOT: Rule = ("BXW0061", ROOT_TEXT, RULE_SOURCE);
const IO: Rule = ("BXW0062", IO_TEXT, RULE_SOURCE);
const PATH: Rule = ("BXW0063", PATH_TEXT, RULE_SOURCE);
const CARGO: &str = "Cargo.toml";
const MANIFEST: &str = "boxology.toml";
#[derive(Debug, Eq, PartialEq)]
pub struct WalkError(&'static str, PathBuf, &'static str);
impl WalkError {
pub fn code(&self) -> &'static str {
self.0
}
pub fn path(&self) -> &Path {
&self.1
}
pub fn detail(&self) -> &'static str {
self.2
}
}
impl fmt::Display for WalkError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{} {:?}: {}", self.0, self.1, self.2)
}
}
impl std::error::Error for WalkError {}
#[derive(Debug, Eq, PartialEq)]
pub struct WalkedWorkspace(Vec<FileEntry>, Vec<(RelativePath, Vec<u8>)>);
impl WalkedWorkspace {
pub fn files(&self) -> &[FileEntry] {
&self.0
}
pub fn manifests(&self) -> &[(RelativePath, Vec<u8>)] {
&self.1
}
}
pub fn walk(root: &Path) -> Result<WalkedWorkspace, WalkError> {
if !fs::symlink_metadata(root).is_ok_and(|metadata| metadata.is_dir()) {
return Err(failure(ROOT, root.to_owned()));
}
let cargo = root.join(CARGO);
if !fs::symlink_metadata(&cargo).is_ok_and(|metadata| metadata.is_file()) {
return Err(failure(ROOT, cargo));
}
let mut files = Vec::new();
let mut manifests = Vec::new();
visit(root, root, &mut files, &mut manifests)?;
files.sort_unstable_by(|left, right| left.path().cmp(right.path()));
manifests.sort_unstable_by(|left, right| left.0.cmp(&right.0));
Ok(WalkedWorkspace(files, manifests))
}
fn visit(
root: &Path,
directory: &Path,
files: &mut Vec<FileEntry>,
manifests: &mut Vec<(RelativePath, Vec<u8>)>,
) -> Result<(), WalkError> {
let entries = fs::read_dir(directory).map_err(|_| failure(IO, directory.to_owned()))?;
for entry in entries {
let entry = entry.map_err(|_| failure(IO, directory.to_owned()))?;
if entry.file_name() == ".git" {
continue;
}
let physical = entry.path();
let logical = logical_path(root, &physical)?;
let kind = entry
.file_type()
.map_err(|_| failure(IO, physical.clone()))?;
if kind.is_dir() {
if entry.file_name() == "target" {
continue;
}
visit(root, &physical, files, manifests)?;
} else if kind.is_symlink() {
let target = fs::read_link(&physical).map_err(|_| failure(IO, physical.clone()))?;
let target = target
.to_str()
.ok_or_else(|| failure(PATH, physical.clone()))?;
files.push(FileEntry::symlink(logical, target.to_owned()));
} else if kind.is_file() {
if entry.file_name() == MANIFEST {
let bytes = read_manifest(&physical, |path| fs::read(path))?;
manifests.push((logical.clone(), bytes));
}
files.push(FileEntry::file(logical));
}
}
Ok(())
}
fn read_manifest(
path: &Path,
reader: impl FnOnce(&Path) -> std::io::Result<Vec<u8>>,
) -> Result<Vec<u8>, WalkError> {
reader(path).map_err(|_| failure(IO, path.to_owned()))
}
fn logical_path(root: &Path, physical: &Path) -> Result<RelativePath, WalkError> {
let relative = physical
.strip_prefix(root)
.map_err(|_| failure(PATH, physical.to_owned()))?;
let spelling = relative
.components()
.map(|component| {
component
.as_os_str()
.to_str()
.ok_or_else(|| failure(PATH, physical.to_owned()))
})
.collect::<Result<Vec<_>, _>>()?
.join("/");
RelativePath::new(spelling).map_err(|_| failure(PATH, physical.to_owned()))
}
fn failure(rule: Rule, path: PathBuf) -> WalkError {
WalkError(rule.0, path, rule.1)
}
#[cfg(test)]
mod tests {
use super::{IO_TEXT, read_manifest};
use std::{io, path::Path};
#[test]
fn refused_manifest_read_is_stable_and_payload_safe() {
let path = Path::new("blocked/boxology.toml");
let error = read_manifest(path, |_| {
Err(io::Error::other("SECRET operating-system payload"))
})
.expect_err("injected refusal must map through the production helper");
assert_eq!(error.code(), "BXW0062");
assert_eq!(error.path(), path);
assert_eq!(error.detail(), IO_TEXT);
assert_eq!(error.to_string(), format!("BXW0062 {path:?}: {IO_TEXT}"));
assert!(!error.to_string().contains("SECRET"));
}
}