1use std::path::{Path, PathBuf};
23
24use crate::hex;
25use crate::key::CacheKey;
26
27pub const MANIFEST_FILE_NAME: &str = "manifest.json";
30
31pub const STDOUT_FILE_NAME: &str = "stdout";
34
35pub const STDERR_FILE_NAME: &str = "stderr";
38
39pub const OUTPUTS_SUBDIR: &str = "outputs";
42
43#[must_use]
46pub fn cache_root(workspace_root: &Path) -> PathBuf {
47 workspace_root.join(".haz").join("cache")
48}
49
50#[must_use]
53pub fn shard(key: &CacheKey) -> String {
54 let hex = key.to_hex();
55 hex[..2].to_owned()
56}
57
58#[must_use]
60pub fn shard_dir(cache_root: &Path, key: &CacheKey) -> PathBuf {
61 cache_root.join(shard(key))
62}
63
64#[must_use]
66pub fn entry_dir(cache_root: &Path, key: &CacheKey) -> PathBuf {
67 shard_dir(cache_root, key).join(key.to_hex())
68}
69
70#[must_use]
72pub fn manifest_path(cache_root: &Path, key: &CacheKey) -> PathBuf {
73 entry_dir(cache_root, key).join(MANIFEST_FILE_NAME)
74}
75
76#[must_use]
78pub fn stdout_path(cache_root: &Path, key: &CacheKey) -> PathBuf {
79 entry_dir(cache_root, key).join(STDOUT_FILE_NAME)
80}
81
82#[must_use]
84pub fn stderr_path(cache_root: &Path, key: &CacheKey) -> PathBuf {
85 entry_dir(cache_root, key).join(STDERR_FILE_NAME)
86}
87
88#[must_use]
90pub fn outputs_dir(cache_root: &Path, key: &CacheKey) -> PathBuf {
91 entry_dir(cache_root, key).join(OUTPUTS_SUBDIR)
92}
93
94#[must_use]
97pub fn output_blob_path(cache_root: &Path, key: &CacheKey, content_hash: &[u8; 32]) -> PathBuf {
98 outputs_dir(cache_root, key).join(hex::encode_32(content_hash))
99}
100
101#[must_use]
108pub fn tmp_entry_dir(cache_root: &Path, key: &CacheKey, random_suffix: &str) -> PathBuf {
109 let name = format!(".tmp-{}-{}", key.to_hex(), random_suffix);
110 shard_dir(cache_root, key).join(name)
111}
112
113#[must_use]
125pub fn restore_staging_dir(cache_root: &Path, key: &CacheKey, random_suffix: &str) -> PathBuf {
126 let name = format!(".restore-{}-{}", key.to_hex(), random_suffix);
127 cache_root.join(name)
128}
129
130#[cfg(test)]
131mod tests {
132 use std::path::Path;
133
134 use crate::CacheKey;
135 use crate::layout::{
136 cache_root, entry_dir, manifest_path, output_blob_path, outputs_dir, restore_staging_dir,
137 shard, shard_dir, stderr_path, stdout_path, tmp_entry_dir,
138 };
139
140 fn key_with_first_byte(first: u8) -> CacheKey {
143 let mut bytes = [0u8; 32];
144 bytes[0] = first;
145 for (i, b) in bytes.iter_mut().enumerate().skip(1) {
146 *b = u8::try_from(i & 0xFF).unwrap();
147 }
148 CacheKey::from_bytes(bytes)
149 }
150
151 #[test]
152 fn cache_010_cache_root_is_workspace_dot_haz_cache() {
153 let root = cache_root(Path::new("/ws"));
154 assert_eq!(root, Path::new("/ws/.haz/cache"));
155 }
156
157 #[test]
158 fn cache_010_shard_is_first_two_hex_chars_of_key() {
159 let key = key_with_first_byte(0xAB);
160 assert_eq!(shard(&key), "ab");
161
162 let key = key_with_first_byte(0x00);
163 assert_eq!(shard(&key), "00");
164
165 let key = key_with_first_byte(0xFF);
166 assert_eq!(shard(&key), "ff");
167 }
168
169 #[test]
170 fn cache_010_shard_dir_joins_cache_root_and_shard() {
171 let key = key_with_first_byte(0xAB);
172 let root = Path::new("/ws/.haz/cache");
173 assert_eq!(shard_dir(root, &key), Path::new("/ws/.haz/cache/ab"));
174 }
175
176 #[test]
177 fn cache_010_entry_dir_is_shard_dir_joined_with_full_hex_key() {
178 let key = key_with_first_byte(0xAB);
179 let root = Path::new("/ws/.haz/cache");
180 let entry = entry_dir(root, &key);
181 let expected = format!("/ws/.haz/cache/ab/{}", key.to_hex());
182 assert_eq!(entry, Path::new(&expected));
183 }
184
185 #[test]
186 fn cache_011_manifest_path_lives_inside_entry_dir() {
187 let key = key_with_first_byte(0xAB);
188 let root = Path::new("/ws/.haz/cache");
189 let mpath = manifest_path(root, &key);
190 let expected = format!("/ws/.haz/cache/ab/{}/manifest.json", key.to_hex());
191 assert_eq!(mpath, Path::new(&expected));
192 }
193
194 #[test]
195 fn cache_012_stdout_and_stderr_paths_use_canonical_names() {
196 let key = key_with_first_byte(0xAB);
197 let root = Path::new("/ws/.haz/cache");
198 assert!(stdout_path(root, &key).ends_with("stdout"));
199 assert!(stderr_path(root, &key).ends_with("stderr"));
200 }
201
202 #[test]
203 fn cache_013_outputs_dir_is_outputs_under_entry_dir() {
204 let key = key_with_first_byte(0xAB);
205 let root = Path::new("/ws/.haz/cache");
206 let od = outputs_dir(root, &key);
207 let expected = format!("/ws/.haz/cache/ab/{}/outputs", key.to_hex());
208 assert_eq!(od, Path::new(&expected));
209 }
210
211 #[test]
212 fn cache_013_output_blob_path_is_keyed_by_content_hash() {
213 let key = key_with_first_byte(0xAB);
214 let content_hash = [0xCDu8; 32];
215 let root = Path::new("/ws/.haz/cache");
216 let blob = output_blob_path(root, &key, &content_hash);
217 let expected = format!(
218 "/ws/.haz/cache/ab/{}/outputs/{}",
219 key.to_hex(),
220 "cd".repeat(32)
221 );
222 assert_eq!(blob, Path::new(&expected));
223 }
224
225 #[test]
226 fn cache_017_tmp_entry_dir_uses_dot_tmp_prefix_with_random_suffix() {
227 let key = key_with_first_byte(0xAB);
228 let root = Path::new("/ws/.haz/cache");
229 let tmp = tmp_entry_dir(root, &key, "r4nd0m");
230 let expected = format!("/ws/.haz/cache/ab/.tmp-{}-r4nd0m", key.to_hex());
231 assert_eq!(tmp, Path::new(&expected));
232 }
233
234 #[test]
235 fn cache_017_tmp_entry_dir_and_entry_dir_have_same_parent() {
236 let key = key_with_first_byte(0xAB);
240 let root = Path::new("/ws/.haz/cache");
241 let tmp = tmp_entry_dir(root, &key, "rnd");
242 let final_entry = entry_dir(root, &key);
243 assert_eq!(tmp.parent().unwrap(), final_entry.parent().unwrap());
244 }
245
246 #[test]
247 fn restore_staging_dir_lives_directly_under_cache_root() {
248 let key = key_with_first_byte(0xAB);
249 let root = Path::new("/ws/.haz/cache");
250 let staging = restore_staging_dir(root, &key, "r4nd0m");
251 let expected = format!("/ws/.haz/cache/.restore-{}-r4nd0m", key.to_hex());
252 assert_eq!(staging, Path::new(&expected));
253 }
254
255 #[test]
256 fn restore_staging_dir_name_is_distinct_from_tmp_entry_dir() {
257 let key = key_with_first_byte(0xAB);
262 let root = Path::new("/ws/.haz/cache");
263 let store_tmp = tmp_entry_dir(root, &key, "rnd");
264 let restore_staging = restore_staging_dir(root, &key, "rnd");
265 assert_ne!(store_tmp, restore_staging);
266 let store_name = store_tmp.file_name().unwrap().to_string_lossy();
267 let restore_name = restore_staging.file_name().unwrap().to_string_lossy();
268 assert!(store_name.starts_with(".tmp-"));
269 assert!(restore_name.starts_with(".restore-"));
270 }
271}