1use std::fs;
16use std::io::Write;
17use std::path::Path;
18
19use concinnity_core::blob::{
20 CACHE_SEGMENT_VERSION, CacheEntry, CacheEntryKind, CacheMeta, encode_cnb, parse_cnb,
21};
22
23struct Item {
26 kind: CacheEntryKind,
27 key: String,
28 bytes: Vec<u8>,
29 used: bool,
30}
31
32pub struct Segment {
41 items: Vec<Item>,
42 toolchain: String,
43 dirty: bool,
44}
45
46impl Segment {
47 pub fn read_from(path: &Path) -> Self {
51 let Ok(image) = fs::read(path) else {
52 return Self::empty();
53 };
54 let Ok((meta, payload_start)) = parse_cnb::<CacheMeta>(CACHE_SEGMENT_VERSION, &image)
55 else {
56 return Self::empty();
57 };
58 Self {
59 items: meta
60 .entries
61 .iter()
62 .filter_map(|entry| {
63 Some(Item {
64 kind: entry.kind,
65 key: entry.key.clone(),
66 bytes: entry_bytes(&image, payload_start, entry)?.to_vec(),
67 used: false,
68 })
69 })
70 .collect(),
71 toolchain: meta.toolchain,
72 dirty: false,
73 }
74 }
75
76 fn empty() -> Self {
77 Self {
78 items: Vec::new(),
79 toolchain: String::new(),
80 dirty: false,
81 }
82 }
83
84 pub fn get(&mut self, kind: CacheEntryKind, key: &str) -> Option<&[u8]> {
88 let item = self
89 .items
90 .iter_mut()
91 .find(|i| i.kind == kind && i.key == key)?;
92 item.used = true;
93 Some(&item.bytes)
94 }
95
96 pub fn put(&mut self, kind: CacheEntryKind, key: &str, bytes: &[u8]) -> bool {
105 if bytes.is_empty() {
106 return false;
107 }
108 match self
109 .items
110 .iter_mut()
111 .find(|i| i.kind == kind && i.key == key)
112 {
113 Some(item) if bytes.len() <= item.bytes.len() => {
114 item.used = true;
115 return false;
116 }
117 Some(item) => {
118 item.bytes.clear();
119 item.bytes.extend_from_slice(bytes);
120 item.used = true;
121 }
122 None => self.items.push(Item {
123 kind,
124 key: key.to_owned(),
125 bytes: bytes.to_vec(),
126 used: true,
127 }),
128 }
129 self.dirty = true;
130 true
131 }
132
133 pub(super) fn remove(&mut self, kind: CacheEntryKind, key: &str) {
135 let before = self.items.len();
136 self.items.retain(|i| !(i.kind == kind && i.key == key));
137 self.dirty |= self.items.len() != before;
138 }
139
140 pub(super) fn adopt_toolchain(&mut self, id: &str) -> bool {
148 if self.toolchain == id {
149 return false;
150 }
151 let discarded = !self.toolchain.is_empty() && !self.items.is_empty();
152 if discarded {
153 self.items.clear();
154 }
155 self.toolchain = id.to_owned();
156 self.dirty = true;
157 discarded
158 }
159
160 pub fn write_to(&mut self, path: &Path, budget: u64) -> bool {
167 if !self.dirty {
168 return false;
169 }
170 self.evict_to(budget);
171 if self.items.is_empty() {
172 let _ = fs::remove_file(path);
175 self.dirty = false;
176 return false;
177 }
178 let mut payload = Vec::with_capacity(self.items.iter().map(|i| i.bytes.len()).sum());
179 let entries = self
180 .items
181 .iter()
182 .map(|item| {
183 let entry = CacheEntry {
184 kind: item.kind,
185 key: item.key.clone(),
186 offset: payload.len() as u64,
187 len: item.bytes.len() as u64,
188 };
189 payload.extend_from_slice(&item.bytes);
190 entry
191 })
192 .collect();
193 let meta = CacheMeta {
194 toolchain: self.toolchain.clone(),
195 entries,
196 };
197 let Ok(image) = encode_cnb(CACHE_SEGMENT_VERSION, &meta, &payload) else {
198 return false;
199 };
200 if !crate::store::atomic::replace(path, |out| out.write_all(&image)) {
201 return false;
202 }
203 self.dirty = false;
204 true
205 }
206
207 fn evict_to(&mut self, budget: u64) {
213 let mut total: u64 = self.items.iter().map(|i| i.bytes.len() as u64).sum();
214 if total <= budget {
215 return;
216 }
217 let before = self.items.len();
218 self.items.retain(|item| {
219 if total <= budget || item.used {
220 return true;
221 }
222 total -= item.bytes.len() as u64;
223 false
224 });
225 self.dirty |= self.items.len() != before;
226 }
227}
228
229fn entry_bytes<'a>(image: &'a [u8], payload_start: usize, entry: &CacheEntry) -> Option<&'a [u8]> {
232 let start = payload_start.checked_add(usize::try_from(entry.offset).ok()?)?;
233 let end = start.checked_add(usize::try_from(entry.len).ok()?)?;
234 image.get(start..end)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 const PIPELINE: CacheEntryKind = CacheEntryKind::Pipeline;
242 const SHADER: CacheEntryKind = CacheEntryKind::Shader;
243 const BUDGET: u64 = 1024;
244
245 fn segment_path(dir: &tempfile::TempDir) -> std::path::PathBuf {
246 dir.path().join("cache").join("0")
247 }
248
249 fn written(path: &Path, entries: &[(CacheEntryKind, &str, &[u8])]) -> Segment {
252 let mut segment = Segment::read_from(path);
253 for (kind, key, bytes) in entries {
254 segment.put(*kind, key, bytes);
255 }
256 segment.write_to(path, BUDGET);
257 segment
258 }
259
260 #[test]
261 fn an_entry_round_trips_through_a_segment() {
262 let dir = tempfile::tempdir().unwrap();
263 let path = segment_path(&dir);
264 written(&path, &[(PIPELINE, "vk-aa", &[1, 2, 3])]);
265
266 let mut reread = Segment::read_from(&path);
267 assert_eq!(reread.get(PIPELINE, "vk-aa"), Some(&[1, 2, 3][..]));
268
269 let leftovers = fs::read_dir(path.parent().unwrap())
270 .unwrap()
271 .flatten()
272 .filter(|e| e.path().extension().is_some_and(|x| x == "tmp"))
273 .count();
274 assert_eq!(leftovers, 0, "temp files must not survive a write");
275 }
276
277 #[test]
281 fn one_entry_does_not_clobber_another() {
282 let dir = tempfile::tempdir().unwrap();
283 let path = segment_path(&dir);
284 written(
285 &path,
286 &[
287 (PIPELINE, "vk-aa", &[1, 2, 3]),
288 (PIPELINE, "vk-bb", &[9]),
289 (SHADER, "vk-aa", &[4, 4]),
290 ],
291 );
292
293 let mut reread = Segment::read_from(&path);
294 assert_eq!(reread.get(PIPELINE, "vk-aa"), Some(&[1, 2, 3][..]));
295 assert_eq!(reread.get(PIPELINE, "vk-bb"), Some(&[9][..]));
296 assert_eq!(reread.get(SHADER, "vk-aa"), Some(&[4, 4][..]));
297
298 reread.put(PIPELINE, "vk-aa", &[4, 5, 6, 7]);
300 reread.write_to(&path, BUDGET);
301 let mut last = Segment::read_from(&path);
302 assert_eq!(last.get(PIPELINE, "vk-aa"), Some(&[4, 5, 6, 7][..]));
303 assert_eq!(last.get(PIPELINE, "vk-bb"), Some(&[9][..]));
304 assert_eq!(last.get(SHADER, "vk-aa"), Some(&[4, 4][..]));
305 }
306
307 #[test]
310 fn a_segment_nothing_changed_is_not_rewritten() {
311 let dir = tempfile::tempdir().unwrap();
312 let path = segment_path(&dir);
313 written(&path, &[(SHADER, "cafe", &[1, 2, 3])]);
314 let before = fs::metadata(&path).unwrap().len();
315
316 let mut warm = Segment::read_from(&path);
317 assert_eq!(warm.get(SHADER, "cafe"), Some(&[1, 2, 3][..]));
318 assert!(
319 !warm.write_to(&path, BUDGET),
320 "a read-only run writes nothing"
321 );
322 assert_eq!(fs::metadata(&path).unwrap().len(), before);
323 }
324
325 #[test]
328 fn only_growth_replaces_an_entry() {
329 let dir = tempfile::tempdir().unwrap();
330 let path = segment_path(&dir);
331 let mut segment = written(&path, &[(PIPELINE, "vk-aa", &[5, 5])]);
332
333 assert!(!segment.put(PIPELINE, "vk-aa", &[5, 5]), "unchanged");
334 assert!(!segment.put(PIPELINE, "vk-aa", &[6, 5]), "reshuffled");
335 assert!(!segment.put(PIPELINE, "vk-aa", &[5]), "shrunk");
336 assert!(!segment.put(PIPELINE, "vk-bb", &[]), "empty");
337 assert!(!segment.write_to(&path, BUDGET), "none of those is content");
338
339 assert!(segment.put(PIPELINE, "vk-aa", &[5, 5, 6]), "grew");
340 assert!(segment.write_to(&path, BUDGET));
341 let mut reread = Segment::read_from(&path);
342 assert_eq!(reread.get(PIPELINE, "vk-aa"), Some(&[5, 5, 6][..]));
343 }
344
345 #[test]
349 fn an_absent_segment_reads_as_a_miss_and_is_recreated() {
350 let dir = tempfile::tempdir().unwrap();
351 let path = segment_path(&dir);
352 assert!(!path.exists());
353 assert_eq!(Segment::read_from(&path).get(PIPELINE, "vk-aa"), None);
354
355 written(&path, &[(PIPELINE, "vk-aa", &[1])]);
356 fs::remove_dir_all(path.parent().unwrap()).unwrap();
357 assert_eq!(Segment::read_from(&path).get(PIPELINE, "vk-aa"), None);
358 written(&path, &[(PIPELINE, "vk-aa", &[1])]);
359 assert_eq!(
360 Segment::read_from(&path).get(PIPELINE, "vk-aa"),
361 Some(&[1][..])
362 );
363 }
364
365 #[test]
366 fn a_missing_entry_reads_as_a_miss() {
367 let dir = tempfile::tempdir().unwrap();
368 let path = segment_path(&dir);
369 let mut segment = written(&path, &[(PIPELINE, "vk-aa", &[1])]);
370 assert_eq!(segment.get(PIPELINE, "vk-bb"), None);
371 }
372
373 #[test]
374 fn a_removed_entry_is_gone_and_the_last_one_takes_the_file() {
375 let dir = tempfile::tempdir().unwrap();
376 let path = segment_path(&dir);
377 let mut segment = written(
378 &path,
379 &[(PIPELINE, "vk-aa", &[1]), (PIPELINE, "vk-bb", &[2])],
380 );
381
382 segment.remove(PIPELINE, "vk-aa");
383 assert!(segment.write_to(&path, BUDGET));
384 let mut reread = Segment::read_from(&path);
385 assert_eq!(reread.get(PIPELINE, "vk-aa"), None);
386 assert_eq!(reread.get(PIPELINE, "vk-bb"), Some(&[2][..]));
387
388 reread.remove(PIPELINE, "vk-bb");
389 assert!(!reread.write_to(&path, BUDGET));
390 assert!(!path.exists(), "an empty segment leaves no file behind");
391 reread.remove(PIPELINE, "vk-bb");
393 assert!(!reread.write_to(&path, BUDGET));
394 assert!(!path.exists());
395 }
396
397 #[test]
400 fn a_corrupt_segment_reads_empty_and_is_replaced() {
401 let dir = tempfile::tempdir().unwrap();
402 let path = segment_path(&dir);
403 fs::create_dir_all(path.parent().unwrap()).unwrap();
404 fs::write(&path, b"not a segment at all").unwrap();
405 assert_eq!(Segment::read_from(&path).get(PIPELINE, "vk-aa"), None);
406
407 written(&path, &[(PIPELINE, "vk-aa", &[7])]);
408 assert_eq!(
409 Segment::read_from(&path).get(PIPELINE, "vk-aa"),
410 Some(&[7][..])
411 );
412 }
413
414 #[test]
417 fn an_entry_pointing_past_the_image_reads_as_a_miss() {
418 let dir = tempfile::tempdir().unwrap();
419 let path = segment_path(&dir);
420 written(&path, &[(PIPELINE, "vk-aa", &[1, 2, 3])]);
421 let image = fs::read(&path).unwrap();
422 fs::write(&path, &image[..image.len() - 1]).unwrap();
423 assert_eq!(Segment::read_from(&path).get(PIPELINE, "vk-aa"), None);
424 }
425
426 #[test]
429 fn a_sibling_segment_is_left_alone() {
430 let dir = tempfile::tempdir().unwrap();
431 let path = segment_path(&dir);
432 let sibling = path.parent().unwrap().join("1");
433 fs::create_dir_all(path.parent().unwrap()).unwrap();
434 fs::write(&sibling, b"build segment").unwrap();
435
436 let mut segment = written(&path, &[(PIPELINE, "vk-aa", &[1])]);
437 segment.remove(PIPELINE, "vk-aa");
438 segment.write_to(&path, BUDGET);
439 assert!(!path.exists());
440 assert_eq!(fs::read(&sibling).unwrap(), b"build segment");
441 }
442
443 #[test]
446 fn a_toolchain_change_discards_the_entries() {
447 let dir = tempfile::tempdir().unwrap();
448 let path = segment_path(&dir);
449 let mut segment = written(&path, &[(SHADER, "cafe", &[1, 2])]);
450 assert!(!segment.adopt_toolchain("slang 2026.1"));
454 assert!(segment.write_to(&path, BUDGET), "the stamp is a change");
455
456 let mut warm = Segment::read_from(&path);
458 assert!(!warm.adopt_toolchain("slang 2026.1"));
459 assert_eq!(warm.get(SHADER, "cafe"), Some(&[1, 2][..]));
460 assert!(!warm.write_to(&path, BUDGET));
461
462 let mut upgraded = Segment::read_from(&path);
464 assert!(upgraded.adopt_toolchain("slang 2026.2"), "discarded");
465 assert_eq!(upgraded.get(SHADER, "cafe"), None);
466 upgraded.put(SHADER, "f00d", &[3]);
467 upgraded.write_to(&path, BUDGET);
468 let mut reread = Segment::read_from(&path);
469 assert_eq!(reread.get(SHADER, "cafe"), None);
470 assert_eq!(reread.get(SHADER, "f00d"), Some(&[3][..]));
471 assert!(!reread.adopt_toolchain("slang 2026.2"), "stamp persisted");
472 }
473
474 #[test]
475 fn nothing_is_evicted_under_budget() {
476 let dir = tempfile::tempdir().unwrap();
477 let path = segment_path(&dir);
478 let mut segment = written(&path, &[(SHADER, "a", &[0; 8]), (SHADER, "b", &[0; 8])]);
479 segment.evict_to(64);
480 assert!(segment.get(SHADER, "a").is_some());
481 assert!(segment.get(SHADER, "b").is_some());
482 }
483
484 #[test]
488 fn eviction_drops_oldest_first_and_spares_what_this_run_used() {
489 let dir = tempfile::tempdir().unwrap();
490 let path = segment_path(&dir);
491 written(
492 &path,
493 &[
494 (SHADER, "oldest", &[0; 40]),
495 (SHADER, "middle", &[0; 40]),
496 (SHADER, "newest", &[0; 40]),
497 ],
498 );
499
500 let mut warm = Segment::read_from(&path);
501 assert!(warm.get(SHADER, "oldest").is_some(), "this run needs it");
502 warm.evict_to(80);
503 assert!(warm.get(SHADER, "oldest").is_some());
504 assert!(warm.get(SHADER, "middle").is_none());
505 assert!(warm.get(SHADER, "newest").is_some());
506
507 assert!(warm.write_to(&path, 1024));
509 assert!(Segment::read_from(&path).get(SHADER, "middle").is_none());
510 }
511
512 #[test]
515 fn eviction_stops_at_the_entries_this_run_used() {
516 let dir = tempfile::tempdir().unwrap();
517 let path = segment_path(&dir);
518 let mut segment = written(&path, &[(SHADER, "a", &[0; 40]), (SHADER, "b", &[0; 40])]);
519 segment.evict_to(0);
520 assert!(segment.get(SHADER, "a").is_some());
521 assert!(segment.get(SHADER, "b").is_some());
522 }
523}