boxology_cli_core/
walk.rs1use boxology_manifest::RelativePath;
2use boxology_workspace::FileEntry;
3use std::{
4 fmt, fs,
5 path::{Path, PathBuf},
6};
7type Rule = (&'static str, &'static str, &'static str);
9const RULE_SOURCE: &str =
10 "boxology-details/02-packages.md discovery walk; S5-T4 #326 PR1 task authority";
11const ROOT_TEXT: &str = "workspace root must be a real directory containing a regular Cargo.toml";
12const IO_TEXT: &str = "filesystem refused a directory, symlink, or manifest read";
13const PATH_TEXT: &str = "walked name/path is not a valid RelativePath";
14const ROOT: Rule = ("BXW0061", ROOT_TEXT, RULE_SOURCE);
15const IO: Rule = ("BXW0062", IO_TEXT, RULE_SOURCE);
16const PATH: Rule = ("BXW0063", PATH_TEXT, RULE_SOURCE);
17const CARGO: &str = "Cargo.toml";
18const MANIFEST: &str = "boxology.toml";
19#[derive(Debug, Eq, PartialEq)]
21pub struct WalkError(&'static str, PathBuf, &'static str);
22impl WalkError {
23 pub fn code(&self) -> &'static str {
25 self.0
26 }
27 pub fn path(&self) -> &Path {
29 &self.1
30 }
31 pub fn detail(&self) -> &'static str {
33 self.2
34 }
35}
36impl fmt::Display for WalkError {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(formatter, "{} {:?}: {}", self.0, self.1, self.2)
39 }
40}
41impl std::error::Error for WalkError {}
42#[derive(Debug, Eq, PartialEq)]
44pub struct WalkedWorkspace(Vec<FileEntry>, Vec<(RelativePath, Vec<u8>)>);
45impl WalkedWorkspace {
46 pub fn files(&self) -> &[FileEntry] {
48 &self.0
49 }
50 pub fn manifests(&self) -> &[(RelativePath, Vec<u8>)] {
52 &self.1
53 }
54}
55pub fn walk(root: &Path) -> Result<WalkedWorkspace, WalkError> {
63 if !fs::symlink_metadata(root).is_ok_and(|metadata| metadata.is_dir()) {
64 return Err(failure(ROOT, root.to_owned()));
65 }
66 let cargo = root.join(CARGO);
67 if !fs::symlink_metadata(&cargo).is_ok_and(|metadata| metadata.is_file()) {
68 return Err(failure(ROOT, cargo));
69 }
70 let mut files = Vec::new();
71 let mut manifests = Vec::new();
72 visit(root, root, &mut files, &mut manifests)?;
73 files.sort_unstable_by(|left, right| left.path().cmp(right.path()));
74 manifests.sort_unstable_by(|left, right| left.0.cmp(&right.0));
75 Ok(WalkedWorkspace(files, manifests))
76}
77fn visit(
78 root: &Path,
79 directory: &Path,
80 files: &mut Vec<FileEntry>,
81 manifests: &mut Vec<(RelativePath, Vec<u8>)>,
82) -> Result<(), WalkError> {
83 let entries = fs::read_dir(directory).map_err(|_| failure(IO, directory.to_owned()))?;
84 for entry in entries {
85 let entry = entry.map_err(|_| failure(IO, directory.to_owned()))?;
86 if entry.file_name() == ".git" {
87 continue;
88 }
89 let physical = entry.path();
90 let logical = logical_path(root, &physical)?;
91 let kind = entry
92 .file_type()
93 .map_err(|_| failure(IO, physical.clone()))?;
94 if kind.is_dir() {
95 if entry.file_name() == "target" {
96 continue;
97 }
98 visit(root, &physical, files, manifests)?;
99 } else if kind.is_symlink() {
100 let target = fs::read_link(&physical).map_err(|_| failure(IO, physical.clone()))?;
101 let target = target
102 .to_str()
103 .ok_or_else(|| failure(PATH, physical.clone()))?;
104 files.push(FileEntry::symlink(logical, target.to_owned()));
105 } else if kind.is_file() {
106 if entry.file_name() == MANIFEST {
107 let bytes = read_manifest(&physical, |path| fs::read(path))?;
108 manifests.push((logical.clone(), bytes));
109 }
110 files.push(FileEntry::file(logical));
111 }
112 }
113 Ok(())
114}
115fn read_manifest(
116 path: &Path,
117 reader: impl FnOnce(&Path) -> std::io::Result<Vec<u8>>,
118) -> Result<Vec<u8>, WalkError> {
119 reader(path).map_err(|_| failure(IO, path.to_owned()))
120}
121fn logical_path(root: &Path, physical: &Path) -> Result<RelativePath, WalkError> {
122 let relative = physical
123 .strip_prefix(root)
124 .map_err(|_| failure(PATH, physical.to_owned()))?;
125 let spelling = relative
126 .components()
127 .map(|component| {
128 component
129 .as_os_str()
130 .to_str()
131 .ok_or_else(|| failure(PATH, physical.to_owned()))
132 })
133 .collect::<Result<Vec<_>, _>>()?
134 .join("/");
135 RelativePath::new(spelling).map_err(|_| failure(PATH, physical.to_owned()))
136}
137fn failure(rule: Rule, path: PathBuf) -> WalkError {
138 WalkError(rule.0, path, rule.1)
139}
140#[cfg(test)]
141mod tests {
142 use super::{IO_TEXT, read_manifest};
143 use std::{io, path::Path};
144 #[test]
145 fn refused_manifest_read_is_stable_and_payload_safe() {
146 let path = Path::new("blocked/boxology.toml");
147 let error = read_manifest(path, |_| {
148 Err(io::Error::other("SECRET operating-system payload"))
149 })
150 .expect_err("injected refusal must map through the production helper");
151 assert_eq!(error.code(), "BXW0062");
152 assert_eq!(error.path(), path);
153 assert_eq!(error.detail(), IO_TEXT);
154 assert_eq!(error.to_string(), format!("BXW0062 {path:?}: {IO_TEXT}"));
155 assert!(!error.to_string().contains("SECRET"));
156 }
157}