返回 CodeWhale
workspace.rs
根目录 / crates / memory / src / workspace.rs
1 //! Host-side freshness collection. A model never supplies a trusted Snapshot.
2 use crate::{Error, Result, Snapshot, policy};
3 use std::{
4 collections::BTreeMap,
5 fs,
6 io::Read,
7 path::{Component, Path},
8 process::Command,
9 };
10
11 pub fn validate_relative_path(path: &str) -> Result<()> {
12 policy::bounded(path, "dependency path", 1024, true)?;
13 // Also reject Windows separators and drive forms on Unix: exports are portable.
14 if path.contains('\\')
15 || path.contains(':')
16 || Path::new(path)
17 .components()
18 .any(|c| !matches!(c, Component::Normal(_)))
19 {
20 return Err(Error::Invalid(
21 "dependency paths must be relative and traversal-free".into(),
22 ));
23 }
24 Ok(())
25 }
26 /// Missing, unreadable, symlink-escaped, or oversized files remain unknown.
27 /// Hashes the working tree, not only HEAD, so uncommitted edits invalidate memory.
28 pub fn snapshot(root: &Path, paths: impl IntoIterator<Item = String>) -> Result<Snapshot> {
29 let root = fs::canonicalize(root)?;
30 if !root.is_dir() {
31 return Err(Error::Invalid("workspace root is not a directory".into()));
32 }
33 let revision = Command::new("git")
34 .arg("-C")
35 .arg(&root)
36 .args(["rev-parse", "--verify", "HEAD"])
37 .output()
38 .ok()
39 .filter(|o| o.status.success())
40 .and_then(|o| String::from_utf8(o.stdout).ok())
41 .map(|s| s.trim().to_owned());
42 let mut files = BTreeMap::new();
43 let mut remaining = 64usize * 1024 * 1024;
44 for (i, path) in paths.into_iter().enumerate() {
45 if i >= 4096 {
46 break;
47 }
48 validate_relative_path(&path)?;
49 let Ok(resolved) = fs::canonicalize(root.join(&path)) else {
50 continue;
51 };
52 if !resolved.starts_with(&root) {
53 continue;
54 }
55 let Ok(meta) = fs::metadata(&resolved) else {
56 continue;
57 };
58 if !meta.is_file() || meta.len() > 16 * 1024 * 1024 {
59 continue;
60 }
61 if meta.len() as usize > remaining {
62 continue;
63 }
64 let Ok(file) = fs::File::open(resolved) else {
65 continue;
66 };
67 let cap = remaining.min(16 * 1024 * 1024);
68 let mut bytes = Vec::new();
69 if file.take(cap as u64 + 1).read_to_end(&mut bytes).is_err() || bytes.len() > cap {
70 continue;
71 }
72 remaining = remaining.saturating_sub(bytes.len());
73 files.insert(path, policy::sha256(&bytes));
74 if remaining == 0 {
75 break;
76 }
77 }
78 Ok(Snapshot { revision, files })
79 }
80
80 lines RUST