1pub mod entry;
29pub mod key;
30pub mod store;
31
32use std::path::Path;
33
34use lanekeep_core::ContentHash;
35
36pub use entry::Entry;
37pub use key::{CacheKey, FORMAT_VERSION, GrammarKey, RunKey};
38pub use store::Store;
39
40#[must_use]
42pub fn hash_bytes(bytes: &[u8]) -> ContentHash {
43 ContentHash::new(*blake3::hash(bytes).as_bytes())
44}
45
46#[must_use]
54pub fn validate(entry: &Entry, root: &Path) -> bool {
55 entry.dependencies.iter().all(|read| {
56 let current = std::fs::read(root.join(read.path.as_str()))
57 .ok()
58 .map(|bytes| hash_bytes(&bytes));
59
60 match (read.hash, current) {
61 (Some(recorded), Some(now)) => recorded == now,
63 (None, None) => true,
66 _ => false,
68 }
69 })
70}
71
72#[cfg(test)]
73mod tests {
74 use std::path::PathBuf;
75
76 use lanekeep_core::FilePath;
77 use lanekeep_core::tracked::TrackedRead;
78
79 use super::*;
80
81 struct Project {
82 dir: PathBuf,
83 }
84
85 impl Project {
86 fn new(name: &str, files: &[(&str, &str)]) -> Self {
87 let dir = std::env::temp_dir()
88 .join(format!("lanekeep-validate-{name}-{}", std::process::id()));
89 let _ = std::fs::remove_dir_all(&dir);
90 std::fs::create_dir_all(&dir).expect("creates dir");
91 let project = Self { dir };
92 for (path, contents) in files {
93 project.write(path, contents);
94 }
95 project
96 }
97
98 fn write(&self, path: &str, contents: &str) {
99 let full = self.dir.join(path);
100 if let Some(parent) = full.parent() {
101 std::fs::create_dir_all(parent).expect("creates parent");
102 }
103 std::fs::write(full, contents).expect("writes");
104 }
105 }
106
107 impl Drop for Project {
108 fn drop(&mut self) {
109 let _ = std::fs::remove_dir_all(&self.dir);
110 }
111 }
112
113 fn entry_depending_on(reads: Vec<TrackedRead>) -> Entry {
114 Entry {
115 dependencies: reads,
116 ..Entry::default()
117 }
118 }
119
120 #[test]
121 fn an_entry_with_no_dependencies_is_always_valid() {
122 let project = Project::new("none", &[]);
123 assert!(validate(&Entry::default(), &project.dir));
124 }
125
126 #[test]
127 fn an_unchanged_dependency_holds() {
128 let project = Project::new("unchanged", &[("package.json", "{}")]);
129 let entry = entry_depending_on(vec![TrackedRead::found(
130 FilePath::new("package.json"),
131 hash_bytes(b"{}"),
132 )]);
133 assert!(validate(&entry, &project.dir));
134 }
135
136 #[test]
137 fn a_changed_dependency_invalidates() {
138 let project = Project::new("changed", &[("package.json", "{\"type\":\"module\"}")]);
139 let entry = entry_depending_on(vec![TrackedRead::found(
140 FilePath::new("package.json"),
141 hash_bytes(b"{}"),
142 )]);
143 assert!(!validate(&entry, &project.dir));
144 }
145
146 #[test]
147 fn a_vanished_dependency_invalidates() {
148 let project = Project::new("vanished", &[]);
149 let entry = entry_depending_on(vec![TrackedRead::found(
150 FilePath::new("package.json"),
151 hash_bytes(b"{}"),
152 )]);
153 assert!(!validate(&entry, &project.dir));
154 }
155
156 #[test]
157 fn an_absent_dependency_that_is_still_absent_holds() {
158 let project = Project::new("still-absent", &[]);
159 let entry = entry_depending_on(vec![TrackedRead::absent(FilePath::new("tsconfig.json"))]);
160 assert!(validate(&entry, &project.dir));
161 }
162
163 #[test]
164 fn a_dependency_that_appeared_invalidates() {
165 let project = Project::new("appeared", &[("tsconfig.json", "{}")]);
169 let entry = entry_depending_on(vec![TrackedRead::absent(FilePath::new("tsconfig.json"))]);
170 assert!(!validate(&entry, &project.dir));
171 }
172
173 #[test]
174 fn one_changed_dependency_among_many_invalidates() {
175 let project = Project::new(
176 "one-of-many",
177 &[("a.json", "{}"), ("b.json", "changed"), ("c.json", "{}")],
178 );
179 let entry = entry_depending_on(vec![
180 TrackedRead::found(FilePath::new("a.json"), hash_bytes(b"{}")),
181 TrackedRead::found(FilePath::new("b.json"), hash_bytes(b"{}")),
182 TrackedRead::found(FilePath::new("c.json"), hash_bytes(b"{}")),
183 ]);
184 assert!(!validate(&entry, &project.dir));
185 }
186
187 #[test]
188 fn a_directory_where_a_file_was_invalidates() {
189 let project = Project::new("directory", &[]);
191 std::fs::create_dir_all(project.dir.join("package.json")).expect("creates dir");
192 let entry = entry_depending_on(vec![TrackedRead::found(
193 FilePath::new("package.json"),
194 hash_bytes(b"{}"),
195 )]);
196 assert!(!validate(&entry, &project.dir));
197 }
198
199 #[test]
200 fn identical_bytes_hash_identically() {
201 assert_eq!(hash_bytes(b"hello"), hash_bytes(b"hello"));
202 assert_ne!(hash_bytes(b"hello"), hash_bytes(b"hellp"));
203 }
204}