Skip to main content

epicenter_cli/
vault.rs

1//! The vault: a durable store of Cargo unit groups, addressed by Cargo's
2//! own fingerprint hashes.
3//!
4//! A *unit group* is exactly what Cargo needs to consider one built unit
5//! fresh: the 16-hex fingerprint hash file, the serialized `Fingerprint`
6//! JSON, the binary dep-info, and the unit's artifacts (`deps/…`, and the
7//! `build/<anchor>/…` tree for build-script units). The vault keeps one
8//! directory per generation, where a generation is one toolchain plus one
9//! flag environment (the *parity key*):
10//!
11//! ```text
12//! {cargo-cache-home}/epicenter/
13//!   store/<parity-key>/
14//!     manifest.json                file list, sizes, canonical stamps
15//!     units/<pkg>-<unit-hash>/     one unit group
16//!       <memo-file>                the authoritative 16-hex hash
17//!       <memo-file>.json           the serialized Fingerprint
18//!       dep-<memo-file>            EncodedDepInfo (mandatory for freshness)
19//!       deps/…  build/…            artifacts, as the profile dir holds them
20//!   build/…                        the shared working dir (a disposable view)
21//! ```
22//!
23//! Timestamps are the only lever besides the hash in Cargo's freshness
24//! check, so every file of a unit group carries ONE canonical second
25//! (`stamp`): the ceiling of the group's metadata output (`.rmeta`) when it
26//! has one — the moment dependents can compile against it — else of its
27//! newest file, and always ≥ every dependency's stamp (harvest clamps the
28//! store). Restore never stamps hard links: they share the store's inode,
29//! and a `touch` on a restored file would rewrite the store itself.
30
31use std::fs;
32use std::path::{Path, PathBuf};
33use std::process::Command;
34use std::time::{Duration, SystemTime, UNIX_EPOCH};
35
36use anyhow::{Context, Result};
37use serde::{Deserialize, Serialize};
38use walkdir::WalkDir;
39
40use crate::fingerprint::{self, ArtifactInfo, Unit};
41
42/// Seed for the flag digest, versioned: bumping it retires old generations.
43const PARITY_SEED: &str = "epicenter-parity-v1";
44
45// ── toolchain identity ──────────────────────────────────────────────────────
46
47/// The `rustc -vV` facts that name a vault generation.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Toolchain {
50    /// `1.99.0`
51    pub release: String,
52    /// Short commit hash (`b940084d7`) as rustc prints it.
53    pub commit: String,
54    /// `x86_64-unknown-linux-gnu`
55    pub host: String,
56}
57
58impl Toolchain {
59    /// `rustc 1.99.0 (b940084d7)` — for humans, not for keys.
60    pub fn version_line(&self) -> String {
61        format!("rustc {} ({})", self.release, self.commit)
62    }
63}
64
65/// Parse `rustc -vV` (first line + `host:`).
66pub fn toolchain() -> Result<Toolchain> {
67    let output = Command::new("rustc")
68        .arg("-vV")
69        .output()
70        .context("running `rustc -vV`")?;
71    if !output.status.success() {
72        anyhow::bail!("`rustc -vV` failed — is a toolchain installed?");
73    }
74    let text = String::from_utf8_lossy(&output.stdout);
75
76    let mut release = "unknown".to_string();
77    let mut commit = "unknown".to_string();
78    let mut host = "unknown".to_string();
79    for line in text.lines() {
80        if let Some(rest) = line.strip_prefix("rustc ") {
81            let mut parts = rest.split_whitespace();
82            if let Some(version) = parts.next() {
83                release = version.to_string();
84            }
85            // `rustc 1.99.0 (b940084d7 2026-09-28)` — the token starts `(`.
86            if let Some(commit_part) = parts.next() {
87                let value = commit_part.trim_start_matches('(');
88                if !value.is_empty() {
89                    commit = value.to_string();
90                }
91            }
92        } else if let Some(value) = line.strip_prefix("host: ") {
93            host = value.trim().to_string();
94        }
95    }
96    Ok(Toolchain {
97        release,
98        commit,
99        host,
100    })
101}
102
103/// Install time of the active toolchain: the real `rustc` binary in the
104/// sysroot (falling back to the sysroot directory). This is the reference
105/// for "was this unit built by the installed toolchain?".
106pub fn toolchain_install_time() -> Option<SystemTime> {
107    let output = Command::new("rustc")
108        .args(["--print", "sysroot"])
109        .output()
110        .ok()?;
111    if !output.status.success() {
112        return None;
113    }
114    let sysroot = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
115    let rustc = sysroot
116        .join("bin")
117        .join(if cfg!(windows) { "rustc.exe" } else { "rustc" });
118    fs::metadata(&rustc)
119        .and_then(|meta| meta.modified())
120        .ok()
121        .or_else(|| fs::metadata(&sysroot).and_then(|meta| meta.modified()).ok())
122}
123
124// ── parity key ──────────────────────────────────────────────────────────────
125
126/// One vault generation = toolchain + host + flag environment:
127/// `1.99.0-b940084d7-x86_64-unknown-linux-gnu-3f9a2c1b`.
128pub fn parity_key() -> Result<String> {
129    let tc = toolchain()?;
130    Ok(format!(
131        "{}-{}-{}-{}",
132        tc.release,
133        tc.commit,
134        tc.host,
135        flags_digest()
136    ))
137}
138
139/// Digest of the flags that live outside the fingerprint: the flag
140/// environment plus the flag-relevant keys of Cargo's global config.
141fn flags_digest() -> String {
142    let mut input = String::from(PARITY_SEED);
143    for name in [
144        "RUSTFLAGS",
145        "RUSTDOCFLAGS",
146        "CARGO_BUILD_RUSTFLAGS",
147        "CARGO_BUILD_RUSTDOCFLAGS",
148        "CARGO_BUILD_TARGET",
149    ] {
150        input.push('\0');
151        input.push_str(name);
152        input.push('=');
153        input.push_str(&std::env::var(name).unwrap_or_default());
154    }
155    if let Ok(path) = crate::config::cargo_config_path() {
156        if let Ok(text) = fs::read_to_string(path) {
157            for key in ["rustflags", "rustdocflags", "target"] {
158                if let Some(value) = crate::config::read_table_key(&text, "build", key) {
159                    input.push('\0');
160                    input.push_str(key);
161                    input.push('=');
162                    input.push_str(&value);
163                }
164            }
165        }
166    }
167    format!("{:08x}", (fnv1a64(input.as_bytes()) >> 32) as u32)
168}
169
170fn fnv1a64(bytes: &[u8]) -> u64 {
171    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
172    for byte in bytes {
173        hash ^= *byte as u64;
174        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
175    }
176    hash
177}
178
179// ── layout ──────────────────────────────────────────────────────────────────
180
181/// `<build>/../store` — the vault lives beside the shared build dir, so
182/// `cargo clean` can never touch it.
183pub fn store_root(build_root: &Path) -> PathBuf {
184    build_root
185        .parent()
186        .unwrap_or(build_root)
187        .join("store")
188}
189
190/// `store/<parity-key>/units`.
191pub fn units_dir(store_root: &Path, parity: &str) -> PathBuf {
192    store_root.join(parity).join("units")
193}
194
195fn manifest_path(store_root: &Path, parity: &str) -> PathBuf {
196    store_root.join(parity).join("manifest.json")
197}
198
199// ── manifest ────────────────────────────────────────────────────────────────
200
201/// `manifest.json` of one generation.
202#[derive(Debug, Clone, Default, Serialize, Deserialize)]
203pub struct Manifest {
204    pub parity_key: String,
205    pub toolchain: String,
206    /// Unix seconds of the first harvest of this generation.
207    pub created: u64,
208    /// Unix seconds of the last harvest.
209    pub updated: u64,
210    pub units: Vec<UnitEntry>,
211}
212
213/// One stored unit group.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct UnitEntry {
216    /// Fingerprint dir name: `<pkg>-<16hex>`.
217    pub name: String,
218    pub profile: String,
219    /// Memo file name, e.g. `lib-serde`.
220    pub memo: String,
221    /// Memo file content — the authoritative 16-hex hash.
222    pub hash: String,
223    /// Canonical mtime of the whole group (whole seconds).
224    pub stamp: u64,
225    pub files: Vec<FileEntry>,
226}
227
228/// One stored file.
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct FileEntry {
231    /// Path inside the unit group.
232    pub path: String,
233    /// Path relative to the profile dir in a build dir.
234    pub dest: String,
235    pub size: u64,
236    /// `memo` | `fingerprint` | `dep-info` | `artifact` | `build-tree`.
237    pub role: String,
238}
239
240fn load_manifest(store_root: &Path, parity: &str) -> Result<Option<Manifest>> {
241    let path = manifest_path(store_root, parity);
242    if !path.is_file() {
243        return Ok(None);
244    }
245    let text = fs::read_to_string(&path)
246        .with_context(|| format!("reading {}", path.display()))?;
247    let manifest = serde_json::from_str(&text)
248        .with_context(|| format!("parsing {}", path.display()))?;
249    Ok(Some(manifest))
250}
251
252fn save_manifest(store_root: &Path, parity: &str, manifest: &Manifest) -> Result<()> {
253    let dir = store_root.join(parity);
254    fs::create_dir_all(&dir)?;
255    let what = serde_json::to_string_pretty(manifest)?;
256    let tmp = dir.join(format!("manifest.json.tmp-{}", std::process::id()));
257    fs::write(&tmp, what + "\n")?;
258    fs::rename(&tmp, manifest_path(store_root, parity))?;
259    Ok(())
260}
261
262/// Names of the store generations (directories under `store/`).
263pub fn generation_names(store_root: &Path) -> Result<Vec<String>> {
264    if !store_root.is_dir() {
265        return Ok(Vec::new());
266    }
267    let mut names = Vec::new();
268    for entry in fs::read_dir(store_root)
269        .with_context(|| format!("reading {}", store_root.display()))?
270    {
271        let entry = entry?;
272        if !entry.file_type()?.is_dir() {
273            continue;
274        }
275        let name = entry.file_name().to_string_lossy().into_owned();
276        if !name.starts_with('.') {
277            names.push(name);
278        }
279    }
280    names.sort();
281    Ok(names)
282}
283
284// ── stored units (read / remove) ─────────────────────────────────────────────
285
286/// One unit as the vault holds it.
287#[derive(Debug, Clone)]
288pub struct StoredUnit {
289    /// The generation directory the unit lives in (the parity key).
290    pub generation: String,
291    /// The manifest entry: name, profile, hash, stamp, files.
292    pub entry: UnitEntry,
293    /// `store/<generation>/units/<name>`.
294    pub dir: PathBuf,
295}
296
297/// Every stored unit across all generations, manifest-driven.
298pub fn stored_units(store_root: &Path) -> Result<Vec<StoredUnit>> {
299    let mut out = Vec::new();
300    for generation in generation_names(store_root)? {
301        let Some(manifest) = load_manifest(store_root, &generation)? else {
302            continue;
303        };
304        let units = units_dir(store_root, &generation);
305        for entry in manifest.units {
306            let dir = units.join(&entry.name);
307            out.push(StoredUnit {
308                generation: generation.clone(),
309                entry,
310                dir,
311            });
312        }
313    }
314    Ok(out)
315}
316
317/// The `rustc` identity recorded in a stored unit's Fingerprint JSON.
318pub fn stored_unit_rustc(unit: &StoredUnit) -> Option<String> {
319    let text = fs::read_to_string(unit.dir.join(format!("{}.json", unit.entry.memo))).ok()?;
320    let value: serde_json::Value = serde_json::from_str(&text).ok()?;
321    match value.get("rustc") {
322        Some(serde_json::Value::Number(number)) => Some(number.to_string()),
323        Some(serde_json::Value::String(text)) => Some(text.clone()),
324        _ => None,
325    }
326}
327
328/// Remove stored unit groups: their directories go, the manifest is pruned,
329/// and a generation left without units disappears entirely. Returns how many
330/// groups were dropped; groups that could not be removed stay recorded.
331pub fn drop_units(store_root: &Path, generation: &str, names: &[String]) -> Result<usize> {
332    let Some(mut manifest) = load_manifest(store_root, generation)? else {
333        return Ok(0);
334    };
335    let units = units_dir(store_root, generation);
336    let mut dropped = 0;
337    manifest.units.retain(|entry| {
338        if !names.contains(&entry.name) {
339            return true;
340        }
341        match fs::remove_dir_all(units.join(&entry.name)) {
342            Ok(()) => {
343                dropped += 1;
344                false
345            }
346            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
347                dropped += 1;
348                false
349            }
350            Err(err) => {
351                eprintln!("could not remove {}: {err}", units.join(&entry.name).display());
352                true
353            }
354        }
355    });
356    if manifest.units.is_empty() {
357        let _ = fs::remove_dir_all(store_root.join(generation));
358    } else if dropped > 0 {
359        manifest.updated = now_secs();
360        save_manifest(store_root, generation, &manifest)?;
361    }
362    Ok(dropped)
363}
364
365// ── harvest ─────────────────────────────────────────────────────────────────
366
367/// What one harvest did.
368#[derive(Debug, Default)]
369pub struct HarvestReport {
370    pub parity: String,
371    pub toolchain: String,
372    pub stored: usize,
373    pub refreshed: usize,
374    pub unchanged: usize,
375    pub skipped_stale: usize,
376    pub skipped_incomplete: usize,
377    pub files: usize,
378    pub bytes: u64,
379    pub note: Option<String>,
380}
381
382/// Store unit groups from a build dir into the vault.
383///
384/// Only units built by the installed toolchain are stored: the newest unit's
385/// rustc cohort marks the active generation, and a cache that as a whole
386/// predates the installed toolchain produces a note instead of stale copies
387/// filed under the current parity key.
388pub fn harvest(
389    build_root: &Path,
390    store_root: &Path,
391    unit_filter: Option<&str>,
392    dry_run: bool,
393) -> Result<HarvestReport> {
394    let tc = toolchain()?;
395    let parity = parity_key()?;
396    let mut report = HarvestReport {
397        parity: parity.clone(),
398        toolchain: tc.version_line(),
399        ..Default::default()
400    };
401
402    // Self-heal debris from a crashed or failed store run (dot-dirs whose
403    // copies may have inherited read-only sources) before planning.
404    if !dry_run {
405        sweep_staging(&units_dir(store_root, &parity));
406    }
407
408    let rows: Vec<(Unit, ArtifactInfo)> = fingerprint::read_units(build_root)?
409        .into_iter()
410        .map(|unit| {
411            let arts = fingerprint::artifacts_for(build_root, &unit);
412            (unit, arts)
413        })
414        .collect();
415
416    // Freshness gate for the whole cache: nothing built since the toolchain
417    // was installed means the cache belongs to an older generation.
418    let newest_activity = rows.iter().filter_map(|(_, arts)| arts.newest).max();
419    if let (Some(newest), Some(installed)) = (newest_activity, toolchain_install_time()) {
420        if newest < installed {
421            report.note = Some(format!(
422                "the shared cache predates the installed toolchain ({}) — build once, then harvest",
423                tc.version_line()
424            ));
425            return Ok(report);
426        }
427    }
428
429    // Active generation = the rustc cohort of the most recently built unit
430    // that carries a fingerprint JSON. Units without one can't be placed and
431    // are harvested as-is (fail-open) rather than mislabeled.
432    let active_gen = rows
433        .iter()
434        .filter(|(unit, _)| crate::who::rustc_of(unit) != "unknown")
435        .max_by_key(|(_, arts)| arts.newest)
436        .map(|(unit, _)| crate::who::rustc_of(unit))
437        .unwrap_or_else(|| "unknown".into());
438
439    let mut manifest = load_manifest(store_root, &parity)?.unwrap_or_else(|| Manifest {
440        parity_key: parity.clone(),
441        toolchain: tc.version_line(),
442        created: now_secs(),
443        updated: 0,
444        units: Vec::new(),
445    });
446
447    // ── pass 1: collect the groups to store and their base stamps ──────────
448    struct GroupPlan {
449        unit: Unit,
450        files: Vec<(PathBuf, String, String, String)>,
451        memo: String,
452        hash: String,
453        base: u64,
454    }
455    let mut plans: Vec<GroupPlan> = Vec::new();
456
457    for (unit, arts) in &rows {
458        if let Some(query) = unit_filter {
459            if !unit_name_matches(&unit.dir_name, query) {
460                continue;
461            }
462        }
463        let cohort = crate::who::rustc_of(unit);
464        if cohort != "unknown" && active_gen != "unknown" && cohort != active_gen {
465            report.skipped_stale += 1;
466            continue;
467        }
468
469        let unit_dir = build_root
470            .join(&unit.profile)
471            .join(".fingerprint")
472            .join(&unit.dir_name);
473        let profile_dir = build_root.join(&unit.profile);
474        let memo = unit.memo_file.clone();
475        let mut files: Vec<(PathBuf, String, String, String)> = Vec::new();
476        // (source, store path, destination relative to the profile dir, role)
477
478        // The whole fingerprint dir except the volatile timestamp: hash
479        // files, their serialized fingerprints, and dep-info. Shared dirs
480        // (several memo files) are stored completely, so every mode of the
481        // unit stays recognizable to Cargo.
482        for entry in fs::read_dir(&unit_dir)
483            .with_context(|| format!("reading {}", unit_dir.display()))?
484        {
485            let entry = entry?;
486            if !entry.file_type()?.is_file() {
487                continue;
488            }
489            let name = entry.file_name().to_string_lossy().into_owned();
490            if is_volatile(&name) {
491                continue;
492            }
493            let role = if name.starts_with("dep-") {
494                "dep-info"
495            } else if name.ends_with(".json") {
496                "fingerprint"
497            } else if fs::read_to_string(entry.path())
498                .map(|text| crate::fingerprint::looks_like_hash(text.trim()))
499                .unwrap_or(false)
500            {
501                "memo"
502            } else {
503                // Cached diagnostics (`output-*`) and other cargo state.
504                "extra"
505            };
506            files.push((
507                entry.path(),
508                name.clone(),
509                format!(".fingerprint/{}/{name}", unit.dir_name),
510                role.into(),
511            ));
512        }
513
514        // Artifacts: `deps/…` and the `build/<anchor>/…` tree of build
515        // scripts. Wrapper internals and rustc's `.d` side files stay out.
516        let mut artifact_count = 0;
517        for (path, _, _) in &arts.paths {
518            let name = path
519                .file_name()
520                .map(|name| name.to_string_lossy().into_owned())
521                .unwrap_or_default();
522            if is_volatile(&name) {
523                continue;
524            }
525            let Ok(rel) = path.strip_prefix(&profile_dir) else {
526                continue;
527            };
528            let rel = rel.to_string_lossy().replace('\\', "/");
529            if rel.starts_with("deps/") && rel.ends_with(".d") {
530                continue;
531            }
532            let role = if rel.starts_with("build/") {
533                "build-tree"
534            } else {
535                "artifact"
536            };
537            files.push((path.clone(), rel.clone(), rel, role.into()));
538            artifact_count += 1;
539        }
540        if artifact_count == 0 {
541            report.skipped_incomplete += 1;
542            continue;
543        }
544
545        plans.push(GroupPlan {
546            base: group_stamp(&files)?,
547            memo,
548            hash: unit.memo_hash.clone().unwrap_or_default(),
549            files,
550            unit: unit.clone(),
551        });
552    }
553
554    // ── pass 2: enforce `T(unit) ≥ every dependency's T` (§11) ─────────────
555    //
556    // Base stamps restore the natural order inside one build view; a store
557    // fed from several views can still invert edges (a dependency harvested
558    // later than its dependent — e.g. a fragmented multi-project cache).
559    // Resolve the edges (a dependency's memo hash → the unit that owns it)
560    // and raise dependents until nothing is older than what it consumed.
561    // `verify`'s ordering audit stays the guard for anything left over.
562    let stored_stamps: std::collections::HashMap<String, u64> = manifest
563        .units
564        .iter()
565        .map(|entry| (entry.hash.clone(), entry.stamp))
566        .collect();
567    let index: std::collections::HashMap<&str, usize> = plans
568        .iter()
569        .enumerate()
570        .map(|(position, plan)| (plan.hash.as_str(), position))
571        .collect();
572    let mut plan_deps: Vec<Vec<usize>> = Vec::with_capacity(plans.len());
573    let mut stamps: Vec<u64> = Vec::with_capacity(plans.len());
574    for plan in &plans {
575        let mut deps = Vec::new();
576        let mut stamp = plan.base;
577        for dep in plan
578            .unit
579            .json
580            .as_ref()
581            .map(|json| json.deps.as_slice())
582            .unwrap_or(&[])
583        {
584            let Some(hash) = dep.memo_hash() else { continue };
585            if let Some(&at) = index.get(hash.as_str()) {
586                deps.push(at);
587            } else if let Some(known) = stored_stamps.get(hash.as_str()) {
588                stamp = stamp.max(*known);
589            }
590        }
591        plan_deps.push(deps);
592        stamps.push(stamp);
593    }
594    // Dependencies before dependents; a cycle (never observed) keeps its base
595    // order and the ordering audit reports whatever is left.
596    let mut order: Vec<usize> = Vec::with_capacity(plans.len());
597    let mut settled = vec![false; plans.len()];
598    loop {
599        let mut progressed = false;
600        for at in 0..plans.len() {
601            if settled[at] || !plan_deps[at].iter().all(|&dep| settled[dep]) {
602                continue;
603            }
604            settled[at] = true;
605            order.push(at);
606            progressed = true;
607        }
608        if !progressed {
609            break;
610        }
611    }
612    for at in 0..plans.len() {
613        if !settled[at] {
614            order.push(at);
615        }
616    }
617    for &at in &order {
618        for dep in &plan_deps[at] {
619            stamps[at] = stamps[at].max(stamps[*dep]);
620        }
621    }
622
623    // ── pass 3: store (or refresh) one group at a time ─────────────────────
624    for (at, plan) in plans.iter().enumerate() {
625        let unit = &plan.unit;
626        let files = &plan.files;
627        let stamp = stamps[at];
628        let group_dir = units_dir(store_root, &parity).join(&unit.dir_name);
629
630        // Unchanged = same hash, profile and stamp, same (path, role) file
631        // set, all present. Anything else is refreshed as a whole group.
632        let mut new_entries: Vec<(&str, &str)> = files
633            .iter()
634            .map(|(_, path, _, role)| (path.as_str(), role.as_str()))
635            .collect();
636        new_entries.sort_unstable();
637        let already = manifest
638            .units
639            .iter()
640            .find(|entry| entry.name == unit.dir_name)
641            .map(|entry| {
642                let mut stored: Vec<(&str, &str)> = entry
643                    .files
644                    .iter()
645                    .map(|file| (file.path.as_str(), file.role.as_str()))
646                    .collect();
647                stored.sort_unstable();
648                entry.hash == plan.hash
649                    && entry.profile == unit.profile
650                    && entry.stamp == stamp
651                    && stored == new_entries
652                    && entry
653                        .files
654                        .iter()
655                        .all(|file| group_dir.join(&file.path).is_file())
656            })
657            .unwrap_or(false);
658        if already {
659            report.unchanged += 1;
660            continue;
661        }
662
663        let bytes: u64 = files
664            .iter()
665            .filter_map(|(src, _, _, _)| fs::metadata(src).ok())
666            .map(|meta| meta.len())
667            .sum();
668        if dry_run {
669            report.stored += 1;
670            report.files += files.len();
671            report.bytes += bytes;
672            continue;
673        }
674
675        let mut entries = Vec::with_capacity(files.len());
676        store_group(store_root, &parity, &unit.dir_name, stamp, files, &mut entries)
677            .with_context(|| format!("storing unit {}", unit.dir_name))?;
678
679        let entry = UnitEntry {
680            name: unit.dir_name.clone(),
681            profile: unit.profile.clone(),
682            memo: plan.memo.clone(),
683            hash: plan.hash.clone(),
684            stamp,
685            files: entries,
686        };
687        match manifest
688            .units
689            .iter_mut()
690            .find(|existing| existing.name == entry.name)
691        {
692            Some(slot) => {
693                *slot = entry;
694                report.refreshed += 1;
695            }
696            None => {
697                manifest.units.push(entry);
698                report.stored += 1;
699            }
700        }
701        report.files += files.len();
702        report.bytes += bytes;
703    }
704
705    if !dry_run {
706        manifest.units.sort_by(|a, b| {
707            (a.profile.as_str(), a.name.as_str()).cmp(&(b.profile.as_str(), b.name.as_str()))
708        });
709        manifest.updated = now_secs();
710        save_manifest(store_root, &parity, &manifest)?;
711    }
712    Ok(report)
713}
714
715/// Copy one unit group into its final place: a temporary directory next to
716/// the target, stamped read-only files, then a swap that never leaves the
717/// group half-written under its real name.
718fn store_group(
719    store_root: &Path,
720    parity: &str,
721    name: &str,
722    stamp: u64,
723    files: &[(PathBuf, String, String, String)],
724    entries: &mut Vec<FileEntry>,
725) -> Result<()> {
726    let units = units_dir(store_root, parity);
727    let final_dir = units.join(name);
728    let tmp = units.join(format!(".{name}.tmp-{}", std::process::id()));
729    if tmp.is_dir() {
730        crate::util::force_remove_dir(&tmp)?;
731    }
732    fs::create_dir_all(&tmp)?;
733
734    if let Err(err) = stage_group(&tmp, files, stamp, entries) {
735        // Never leave a half-written staging dir behind: it would keep a
736        // failed unit's bytes on disk and hide the failure from the next run.
737        let _ = crate::util::force_remove_dir(&tmp);
738        return Err(err);
739    }
740
741    let old = units.join(format!(".{name}.old-{}", std::process::id()));
742    if final_dir.is_dir() {
743        fs::rename(&final_dir, &old)?;
744    }
745    fs::rename(&tmp, &final_dir)?;
746    if old.is_dir() {
747        let _ = fs::remove_dir_all(&old);
748    }
749    Ok(())
750}
751
752/// Copy one unit group's files into its staging dir. Every file is made
753/// writable before it is stamped and read-only again (on Unix) once the group
754/// is in place.
755///
756/// `fs::copy` carries the source's attributes along. A read-only source — a
757/// wrapper's hard-linked store file, a file restored from a backup — makes
758/// `File::set_modified` fail with "access denied" on Windows even though the
759/// copy lives in our own staging dir.
760fn stage_group(
761    tmp: &Path,
762    files: &[(PathBuf, String, String, String)],
763    stamp: u64,
764    entries: &mut Vec<FileEntry>,
765) -> Result<()> {
766    for (src, path, dest, role) in files {
767        let dst = tmp.join(path);
768        if let Some(parent) = dst.parent() {
769            fs::create_dir_all(parent)?;
770        }
771        fs::copy(src, &dst)
772            .with_context(|| format!("copying {} -> {}", src.display(), dst.display()))?;
773        make_writable(&dst).with_context(|| format!("making {} writable", dst.display()))?;
774        set_stamp(&dst, stamp)?;
775        make_read_only(&dst)?;
776        let size = fs::metadata(&dst).map(|meta| meta.len()).unwrap_or(0);
777        entries.push(FileEntry {
778            path: path.clone(),
779            dest: dest.clone(),
780            size,
781            role: role.clone(),
782        });
783    }
784    Ok(())
785}
786
787/// Remove staging leftovers (`.{unit}.tmp-{pid}`, `.{unit}.old-{pid}`) that a
788/// crashed or failed run left in the units dir. Fresh dirs are left alone: a
789/// concurrent harvest may be filling them right now.
790fn sweep_staging(units: &Path) {
791    let Ok(entries) = fs::read_dir(units) else {
792        return;
793    };
794    let now = SystemTime::now();
795    for entry in entries.flatten() {
796        let name = entry.file_name();
797        let name = name.to_string_lossy();
798        if !(name.contains(".tmp-") || name.contains(".old-")) {
799            continue;
800        }
801        let stale = entry
802            .metadata()
803            .and_then(|meta| meta.modified())
804            .ok()
805            .and_then(|modified| now.duration_since(modified).ok())
806            .is_some_and(|age| age > Duration::from_secs(600));
807        if stale {
808            let _ = crate::util::force_remove_dir(&entry.path());
809        }
810    }
811}
812
813/// The canonical stamp of a group: the ceiling (whole seconds) of its
814/// newest file, so every stored file is stamped at or after its true mtime
815/// and cross-filesystem second-resolution round-trips cannot make an output
816/// look newer than its own group.
817fn group_stamp(files: &[(PathBuf, String, String, String)]) -> Result<u64> {
818    let mut newest: Option<SystemTime> = None;
819    let mut metadata: Option<SystemTime> = None;
820    for (src, path, _, _) in files {
821        let modified = fs::metadata(src)
822            .and_then(|meta| meta.modified())
823            .with_context(|| format!("reading mtime of {}", src.display()))?;
824        if newest.map_or(true, |current| modified > current) {
825            newest = Some(modified);
826        }
827        if path.ends_with(".rmeta") && metadata.map_or(true, |current| modified > current) {
828            metadata = Some(modified);
829        }
830    }
831    // The metadata output, when present, is the moment dependents can compile
832    // against this unit: rustc writes `.rmeta` first and `.rlib` last, and a
833    // dependent routinely finishes before its dependency's `.rlib` exists.
834    // Stamping with the `.rlib` would make the group look newer than its own
835    // dependents and force spurious rebuilds after a restore.
836    Ok(match metadata.or(newest) {
837        Some(time) => ceil_secs(time),
838        None => now_secs(),
839    })
840}
841
842/// Round a timestamp up to whole seconds.
843fn ceil_secs(time: SystemTime) -> u64 {
844    match time.duration_since(UNIX_EPOCH) {
845        Ok(duration) => {
846            let secs = duration.as_secs();
847            if duration.subsec_nanos() > 0 { secs + 1 } else { secs }
848        }
849        Err(_) => 0,
850    }
851}
852
853fn now_secs() -> u64 {
854    SystemTime::now()
855        .duration_since(UNIX_EPOCH)
856        .map(|duration| duration.as_secs())
857        .unwrap_or(0)
858}
859
860fn set_stamp(path: &Path, stamp: u64) -> Result<()> {
861    let time = UNIX_EPOCH + Duration::from_secs(stamp);
862    let file = fs::File::open(path)
863        .with_context(|| format!("opening {} to stamp it", path.display()))?;
864    file.set_modified(time)
865        .with_context(|| format!("stamping {}", path.display()))?;
866    Ok(())
867}
868
869/// Copy a file into a view as a **private** inode: writable, and stamped
870/// with the canonical group time. Required for everything cargo writes in
871/// place (`O_TRUNC`) — a shared inode would either hit the read-only store
872/// file (EACCES) or mutate the store.
873fn copy_private(src: &Path, dst: &Path, stamp: u64) -> std::io::Result<()> {
874    fs::copy(src, dst)?;
875    make_writable(dst)?;
876    let _ = set_stamp(dst, stamp);
877    Ok(())
878}
879
880/// The store is read-only by contract; restored state must not be.
881#[cfg(unix)]
882fn make_writable(path: &Path) -> std::io::Result<()> {
883    use std::os::unix::fs::PermissionsExt;
884    let mut perms = fs::metadata(path)?.permissions();
885    perms.set_mode(0o644);
886    fs::set_permissions(path, perms)
887}
888
889#[cfg(not(unix))]
890fn make_writable(path: &Path) -> std::io::Result<()> {
891    let mut perms = fs::metadata(path)?.permissions();
892    perms.set_readonly(false);
893    fs::set_permissions(path, perms)
894}
895
896/// On Unix the store is read-only (owner can still stamp and delete; only
897/// content edits are refused). On Windows a read-only file blocks rename
898/// and delete, so the store keeps normal permissions there.
899fn make_read_only(path: &Path) -> Result<()> {
900    #[cfg(unix)]
901    {
902        use std::os::unix::fs::PermissionsExt;
903        fs::set_permissions(path, fs::Permissions::from_mode(0o444))?;
904    }
905    #[cfg(not(unix))]
906    {
907        let _ = path;
908    }
909    Ok(())
910}
911
912/// Files that never enter the store: the volatile invocation timestamp,
913/// and wrapper bookkeeping (`*.kache-*`, `.kache*`) that lives in the build
914/// dir but is no part of Cargo's state.
915fn is_volatile(name: &str) -> bool {
916    name == "invoked.timestamp" || name.contains(".kache")
917}
918
919// ── restore ─────────────────────────────────────────────────────────────────
920
921/// What one restore did.
922#[derive(Debug, Default)]
923pub struct RestoreReport {
924    pub parity: String,
925    pub target: PathBuf,
926    pub units_linked: usize,
927    pub units_already: usize,
928    pub files_linked: usize,
929    pub files_copied: usize,
930    pub files_existing: usize,
931    pub bytes: u64,
932    pub errors: usize,
933    pub note: Option<String>,
934}
935
936/// Link matching unit groups from the vault into a working dir.
937///
938/// `deps/` link outputs (`.rlib`, proc-macro `.so`) are hard-linked — rustc
939/// only ever replaces those atomically. Fingerprint state, build-script
940/// trees and `deps/*.rmeta` become private writable copies: rustc writes
941/// `.rmeta` in place, so a read-only link turns any rebuild of that unit
942/// into `output file … .rmeta is not writeable`. Hard links share the
943/// store's canonical stamp; private copies are stamped after copying. Never
944/// overwrites a file that already exists, never touches sources, and a
945/// partial vault is fine: Cargo recompiles whatever is missing.
946pub fn restore(
947    view_root: &Path,
948    store_root: &Path,
949    unit_filter: Option<&str>,
950    dry_run: bool,
951) -> Result<RestoreReport> {
952    let parity = parity_key()?;
953    let mut report = RestoreReport {
954        parity: parity.clone(),
955        target: view_root.to_path_buf(),
956        ..Default::default()
957    };
958    let Some(manifest) = load_manifest(store_root, &parity)? else {
959        report.note = Some(
960            "the vault holds no units for this generation yet — run `epicenter harvest` after a build"
961                .into(),
962        );
963        return Ok(report);
964    };
965
966    let units = units_dir(store_root, &parity);
967    for entry in &manifest.units {
968        if let Some(query) = unit_filter {
969            if !unit_name_matches(&entry.name, query) {
970                continue;
971            }
972        }
973        let fp_dir = view_root
974            .join(&entry.profile)
975            .join(".fingerprint")
976            .join(&entry.name);
977        if fs::read_to_string(fp_dir.join(&entry.memo))
978            .map(|text| text.trim() == entry.hash)
979            .unwrap_or(false)
980        {
981            report.units_already += 1;
982            continue;
983        }
984
985        let mut unit_touched = false;
986        for file in &entry.files {
987            let dst = view_root.join(&entry.profile).join(&file.dest);
988            if dst.exists() {
989                report.files_existing += 1;
990                continue;
991            }
992            // Only `deps/` link outputs (`.rlib`, `.so`) may share the
993            // store inode — rustc replaces those atomically. `.rmeta` and
994            // everything else are written in place by rustc, cargo or build
995            // scripts: private copies only.
996            let shared = file.path.starts_with("deps/") && !file.path.ends_with(".rmeta");
997            if dry_run {
998                unit_touched = true;
999                if shared {
1000                    report.files_linked += 1;
1001                } else {
1002                    report.files_copied += 1;
1003                }
1004                report.bytes += file.size;
1005                continue;
1006            }
1007            let src = units.join(&entry.name).join(&file.path);
1008            if let Some(parent) = dst.parent() {
1009                if let Err(err) = fs::create_dir_all(parent) {
1010                    report.errors += 1;
1011                    eprintln!("restore: creating {}: {err}", parent.display());
1012                    continue;
1013                }
1014            }
1015            let linked = shared && fs::hard_link(&src, &dst).is_ok();
1016            let result = if linked {
1017                Ok(())
1018            } else {
1019                copy_private(&src, &dst, entry.stamp)
1020            };
1021            match result {
1022                Ok(()) => {
1023                    if linked {
1024                        report.files_linked += 1;
1025                    } else {
1026                        report.files_copied += 1;
1027                    }
1028                    unit_touched = true;
1029                }
1030                Err(err) => {
1031                    report.errors += 1;
1032                    eprintln!("restore: writing {}: {err}", dst.display());
1033                    continue;
1034                }
1035            }
1036            report.bytes += file.size;
1037        }
1038        if unit_touched {
1039            report.units_linked += 1;
1040        }
1041    }
1042    Ok(report)
1043}
1044
1045// ── verify ──────────────────────────────────────────────────────────────────
1046
1047/// Result of auditing one generation.
1048#[derive(Debug, Default)]
1049pub struct VerifyReport {
1050    pub parity: String,
1051    pub units: usize,
1052    pub files: usize,
1053    pub bytes: u64,
1054    /// Human lines: missing files, size/hash/stamp drift, orphan groups.
1055    pub problems: Vec<String>,
1056    /// Not a failure, but worth knowing (e.g. unknown files lying around).
1057    pub warnings: Vec<String>,
1058    /// Units older than one of their dependencies. Cargo recompiles exactly
1059    /// these after a restore ("the dependency `X` was rebuilt") — a quality
1060    /// fact, not an integrity failure.
1061    pub ordering: Vec<String>,
1062}
1063
1064impl VerifyReport {
1065    pub fn is_clean(&self) -> bool {
1066        self.problems.is_empty()
1067    }
1068}
1069
1070/// Audit every generation in the vault: every manifest file present with the
1071/// recorded size, the canonical stamp, and — for memo files — the recorded
1072/// hash content; plus a count of unit groups the manifest doesn't know.
1073pub fn verify(store_root: &Path) -> Result<Vec<VerifyReport>> {
1074    let mut reports = Vec::new();
1075    for parity in generation_names(store_root)? {
1076        let mut report = VerifyReport {
1077            parity: parity.clone(),
1078            ..Default::default()
1079        };
1080        let Some(manifest) = load_manifest(store_root, &parity)? else {
1081            report.problems.push(format!(
1082                "generation {parity} has no readable manifest.json — it cannot be audited"
1083            ));
1084            reports.push(report);
1085            continue;
1086        };
1087        let units = units_dir(store_root, &parity);
1088        for entry in &manifest.units {
1089            for file in &entry.files {
1090                let path = units.join(&entry.name).join(&file.path);
1091                let meta = match fs::metadata(&path) {
1092                    Ok(meta) => meta,
1093                    Err(_) => {
1094                        report
1095                            .problems
1096                            .push(format!("missing: {}", display_join(&path, units.as_path())));
1097                        continue;
1098                    }
1099                };
1100                report.files += 1;
1101                report.bytes += meta.len();
1102                if meta.len() != file.size {
1103                    report.problems.push(format!(
1104                        "size drift: {} (recorded {}, found {})",
1105                        display_join(&path, &units),
1106                        file.size,
1107                        meta.len()
1108                    ));
1109                }
1110                let found_stamp = meta.modified().map(ceil_secs).unwrap_or(0);
1111                if found_stamp != entry.stamp {
1112                    report.problems.push(format!(
1113                        "stamp drift: {} (recorded {}, found {})",
1114                        display_join(&path, &units),
1115                        entry.stamp,
1116                        found_stamp
1117                    ));
1118                }
1119                if file.role == "memo" {
1120                    match fs::read_to_string(&path) {
1121                        Ok(text) if file.path == entry.memo && text.trim() != entry.hash => {
1122                            report.problems.push(format!(
1123                                "hash drift: {} (recorded {}, found {})",
1124                                display_join(&path, &units),
1125                                entry.hash,
1126                                text.trim()
1127                            ));
1128                        }
1129                        Ok(text)
1130                            if file.path != entry.memo
1131                                && !crate::fingerprint::looks_like_hash(text.trim()) =>
1132                        {
1133                            report.problems.push(format!(
1134                                "malformed memo file: {} ({:?})",
1135                                display_join(&path, &units),
1136                                text.trim()
1137                            ));
1138                        }
1139                        Ok(_) => {}
1140                        Err(err) => report.problems.push(format!(
1141                            "unreadable memo file {}: {err}",
1142                            display_join(&path, &units)
1143                        )),
1144                    }
1145                }
1146            }
1147        }
1148        // Ordering audit: a unit must be at least as new as its
1149        // dependencies, or cargo recompiles it after a restore ("the
1150        // dependency `X` was rebuilt"). Edges come from the stored
1151        // fingerprints; the join key is the dependency's memo hash — the
1152        // same value serde's `serde_core` edge (9416646317397223793) and
1153        // serde_core's memo file (71a9f323efa5ae82) share.
1154        let by_hash: std::collections::HashMap<&str, usize> = manifest
1155            .units
1156            .iter()
1157            .enumerate()
1158            .map(|(index, entry)| (entry.hash.as_str(), index))
1159            .collect();
1160        for entry in &manifest.units {
1161            let json = entry
1162                .files
1163                .iter()
1164                .find(|file| file.path.ends_with(".json"))
1165                .and_then(|file| {
1166                    fs::read_to_string(units.join(&entry.name).join(&file.path)).ok()
1167                })
1168                .and_then(|text| crate::fingerprint::parse_fingerprint(&text));
1169            let Some(json) = json else { continue };
1170            for dep in &json.deps {
1171                let Some(dep_index) = dep
1172                    .memo_hash()
1173                    .and_then(|hash| by_hash.get(hash.as_str()))
1174                    .copied()
1175                else {
1176                    continue;
1177                };
1178                let dep_entry = &manifest.units[dep_index];
1179                if dep_entry.stamp > entry.stamp {
1180                    report.ordering.push(format!(
1181                        "{} is older than its dependency {} ({} older) — cargo recompiles it after a restore",
1182                        entry.name,
1183                        dep_entry.name,
1184                        crate::util::human_duration(Duration::from_secs(dep_entry.stamp - entry.stamp))
1185                    ));
1186                }
1187            }
1188        }
1189        report.units = manifest.units.len();
1190
1191        // Unit groups on disk that the manifest doesn't record.
1192        if units.is_dir() {
1193            let known: std::collections::BTreeSet<&str> =
1194                manifest.units.iter().map(|unit| unit.name.as_str()).collect();
1195            for entry in fs::read_dir(&units)? {
1196                let entry = entry?;
1197                if !entry.file_type()?.is_dir() {
1198                    continue;
1199                }
1200                let name = entry.file_name().to_string_lossy().into_owned();
1201                if name.starts_with('.') {
1202                    continue; // in-flight temp dirs of a crashed harvest
1203                }
1204                if !known.contains(name.as_str()) {
1205                    report
1206                        .warnings
1207                        .push(format!("unrecorded unit group: units/{name}"));
1208                }
1209            }
1210        }
1211        reports.push(report);
1212    }
1213    Ok(reports)
1214}
1215
1216/// Path for humans: relative to `base` when possible.
1217fn display_join(path: &Path, base: &Path) -> String {
1218    path.strip_prefix(base)
1219        .unwrap_or(path)
1220        .to_string_lossy()
1221        .replace('\\', "/")
1222}
1223
1224// ── status ──────────────────────────────────────────────────────────────────
1225
1226/// One generation as the `vault` command shows it.
1227#[derive(Debug, Default)]
1228pub struct GenerationInfo {
1229    pub parity: String,
1230    /// Matches the installed toolchain + flag environment.
1231    pub active: bool,
1232    pub units: usize,
1233    pub files: usize,
1234    /// Bytes as the store holds them; a file linked into views counts once.
1235    pub bytes: u64,
1236    /// Store files that are also linked elsewhere (`nlink > 1`).
1237    pub linked: usize,
1238    pub created: Option<u64>,
1239    pub updated: Option<u64>,
1240}
1241
1242/// Summarize every generation of the vault.
1243pub fn generations(store_root: &Path) -> Result<Vec<GenerationInfo>> {
1244    let active = parity_key().ok();
1245    let mut out = Vec::new();
1246    for parity in generation_names(store_root)? {
1247        let mut info = GenerationInfo {
1248            parity: parity.clone(),
1249            active: active.as_deref() == Some(parity.as_str()),
1250            ..Default::default()
1251        };
1252        if let Some(manifest) = load_manifest(store_root, &parity)? {
1253            info.units = manifest.units.len();
1254            info.created = Some(manifest.created);
1255            info.updated = Some(manifest.updated);
1256        }
1257        let units = units_dir(store_root, &parity);
1258        if units.is_dir() {
1259            for entry in WalkDir::new(&units).min_depth(1) {
1260                let entry = entry?;
1261                if !entry.file_type().is_file() {
1262                    continue;
1263                }
1264                let meta = entry.metadata()?;
1265                info.files += 1;
1266                info.bytes += meta.len();
1267                if link_count(&meta) > 1 {
1268                    info.linked += 1;
1269                }
1270            }
1271        }
1272        out.push(info);
1273    }
1274    Ok(out)
1275}
1276
1277/// Hard links of one inode as the file system knows it (1 where unsupported).
1278pub(crate) fn link_count(meta: &fs::Metadata) -> u64 {
1279    #[cfg(unix)]
1280    {
1281        use std::os::unix::fs::MetadataExt;
1282        meta.nlink()
1283    }
1284    #[cfg(not(unix))]
1285    {
1286        let _ = meta;
1287        1
1288    }
1289}
1290
1291/// The vault's own matcher: package prefix or hash fragment of a group name.
1292fn unit_name_matches(name: &str, query: &str) -> bool {
1293    name.contains(query)
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298    use super::*;
1299
1300    /// A build dir in the shapes real Cargo writes: a lib unit, a
1301    /// build-script compile unit, and a build-script run unit without
1302    /// dep-info — plus volatile files and wrapper internals that must be
1303    /// filtered out.
1304    fn synthetic_build_dir(root: &Path) -> Result<()> {
1305        let jsons = r#"{"rustc":12019306335353385202,"features":"[]","declared_features":"[]","deps":[]}"#;
1306
1307        let lib = root.join("debug/.fingerprint/serde-102a33f055a8d229");
1308        fs::create_dir_all(&lib)?;
1309        fs::write(lib.join("lib-serde"), "5d82713a7f2d05e2")?;
1310        fs::write(lib.join("lib-serde.json"), jsons)?;
1311        fs::write(lib.join("dep-lib-serde"), [0u8; 24])?;
1312        fs::write(lib.join("invoked.timestamp"), "")?;
1313        fs::create_dir_all(root.join("debug/deps"))?;
1314        fs::write(
1315            root.join("debug/deps/libserde-102a33f055a8d229.rlib"),
1316            [7u8; 64],
1317        )?;
1318        fs::write(
1319            root.join("debug/deps/libserde-102a33f055a8d229.rmeta"),
1320            [6u8; 48],
1321        )?;
1322        fs::write(
1323            root.join("debug/deps/serde-102a33f055a8d229.d"),
1324            b"side file",
1325        )?;
1326        fs::write(
1327            root.join("debug/deps/libserde-102a33f055a8d229.kache-real"),
1328            b"wrapper",
1329        )?;
1330
1331        let compile = root.join("debug/.fingerprint/x-1122334455667788");
1332        fs::create_dir_all(&compile)?;
1333        fs::write(
1334            compile.join("build-script-build-script-build"),
1335            "aaaabbbbccccdddd",
1336        )?;
1337        fs::write(
1338            compile.join("build-script-build-script-build.json"),
1339            jsons,
1340        )?;
1341        fs::write(
1342            compile.join("dep-build-script-build-script-build"),
1343            [0u8; 8],
1344        )?;
1345        let compile_anchor = root.join("debug/build/x-1122334455667788");
1346        fs::create_dir_all(&compile_anchor)?;
1347        fs::write(
1348            compile_anchor.join("build_script_build-1122334455667788"),
1349            [3u8; 32],
1350        )?;
1351        fs::write(
1352            compile_anchor.join("build_script_build-1122334455667788.d"),
1353            b"dep",
1354        )?;
1355        fs::write(
1356            compile_anchor.join("build_script_build-1122334455667788.kache-real"),
1357            b"wrapper",
1358        )?;
1359        fs::write(compile_anchor.join("invoked.timestamp"), "")?;
1360
1361        // The run unit has no dep-info at all (real Cargo layout).
1362        let run = root.join("debug/.fingerprint/x-aabbccddeeff0011");
1363        fs::create_dir_all(&run)?;
1364        fs::write(
1365            run.join("run-build-script-build-script-build"),
1366            "eeeeffff00001111",
1367        )?;
1368        fs::write(
1369            run.join("run-build-script-build-script-build.json"),
1370            jsons,
1371        )?;
1372        let run_anchor = root.join("debug/build/x-aabbccddeeff0011");
1373        fs::create_dir_all(&run_anchor)?;
1374        fs::write(
1375            run_anchor.join("output"),
1376            b"cargo:rerun-if-changed=build.rs",
1377        )?;
1378        fs::write(run_anchor.join("stderr"), b"")?;
1379        fs::write(run_anchor.join("invoked.timestamp"), "")?;
1380        Ok(())
1381    }
1382
1383    /// A build dir with one incomplete unit (memo file, but no artifacts).
1384    fn synthetic_husk(root: &Path) -> Result<()> {
1385        let unit = root.join("debug/.fingerprint/husk-aaaabbbbccccdddd");
1386        fs::create_dir_all(&unit)?;
1387        fs::write(unit.join("lib-husk"), "0000111122223333")?;
1388        Ok(())
1389    }
1390
1391    #[test]
1392    fn parity_key_is_stable_and_shaped() -> Result<()> {
1393        let first = parity_key()?;
1394        let second = parity_key()?;
1395        assert_eq!(first, second);
1396        let tc = toolchain()?;
1397        assert!(first.starts_with(&format!("{}-{}", tc.release, tc.commit)));
1398        assert!(first.ends_with(&flags_digest()));
1399        Ok(())
1400    }
1401
1402    #[test]
1403    fn harvest_stores_groups_with_one_canonical_stamp() -> Result<()> {
1404        let build = tempfile::tempdir()?;
1405        let store = tempfile::tempdir()?;
1406        synthetic_build_dir(build.path())?;
1407
1408        let report = harvest(build.path(), store.path(), None, false)?;
1409        assert_eq!(report.stored, 3, "lib + build-script compile + run");
1410        assert_eq!(report.skipped_incomplete, 0);
1411        assert!(report.files >= 10, "got {}", report.files);
1412        assert!(report.bytes > 0);
1413
1414        let parity = parity_key()?;
1415        let manifest = load_manifest(store.path(), &parity)?.expect("manifest written");
1416        assert_eq!(manifest.units.len(), 3);
1417        let lib_entry = manifest
1418            .units
1419            .iter()
1420            .find(|unit| unit.name == "serde-102a33f055a8d229")
1421            .expect("lib unit recorded");
1422        assert_eq!(lib_entry.hash, "5d82713a7f2d05e2");
1423        assert_eq!(lib_entry.profile, "debug");
1424
1425        // Every stored file of the group carries the same stamp.
1426        let group = units_dir(store.path(), &parity).join(&lib_entry.name);
1427        for file in &lib_entry.files {
1428            let meta = fs::metadata(group.join(&file.path))?;
1429            assert_eq!(ceil_secs(meta.modified()?), lib_entry.stamp, "{}", file.path);
1430            assert_eq!(meta.len(), file.size);
1431        }
1432
1433        // Volatile files and wrapper internals never enter the store.
1434        assert!(!group.join("invoked.timestamp").exists());
1435        for file in &lib_entry.files {
1436            assert!(!file.path.contains("kache"), "{}", file.path);
1437            assert!(!file.path.ends_with(".d"), "{}", file.path);
1438        }
1439
1440        #[cfg(unix)]
1441        {
1442            use std::os::unix::fs::PermissionsExt;
1443            let meta = fs::metadata(group.join(&lib_entry.files[0].path))?;
1444            assert_eq!(meta.permissions().mode() & 0o777, 0o444, "store is read-only");
1445        }
1446        Ok(())
1447    }
1448
1449    #[test]
1450    fn harvest_is_idempotent_and_skips_incomplete_units() -> Result<()> {
1451        let build = tempfile::tempdir()?;
1452        let store = tempfile::tempdir()?;
1453        synthetic_build_dir(build.path())?;
1454        synthetic_husk(build.path())?;
1455
1456        let first = harvest(build.path(), store.path(), None, false)?;
1457        assert_eq!(first.stored, 3);
1458        assert_eq!(first.skipped_incomplete, 1, "husk without artifacts");
1459
1460        let second = harvest(build.path(), store.path(), None, false)?;
1461        assert_eq!(second.stored, 0);
1462        assert_eq!(second.unchanged, 3);
1463        assert_eq!(second.skipped_incomplete, 1);
1464        Ok(())
1465    }
1466
1467    #[test]
1468    fn harvest_refreshes_when_the_file_set_grows() -> Result<()> {
1469        let build = tempfile::tempdir()?;
1470        let store = tempfile::tempdir()?;
1471        synthetic_build_dir(build.path())?;
1472        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1473
1474        fs::write(
1475            build
1476                .path()
1477                .join("debug/deps/libserde-102a33f055a8d229.so"),
1478            [1u8; 16],
1479        )?;
1480        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1481        assert_eq!(report.refreshed, 1);
1482        assert_eq!(report.stored, 0);
1483
1484        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1485        let paths: Vec<&str> = manifest.units[0]
1486            .files
1487            .iter()
1488            .map(|file| file.path.as_str())
1489            .collect();
1490        assert!(
1491            paths.contains(&"deps/libserde-102a33f055a8d229.so"),
1492            "{paths:?}"
1493        );
1494        Ok(())
1495    }
1496
1497    #[test]
1498    fn harvest_stores_all_memos_of_shared_dirs() -> Result<()> {
1499        let build = tempfile::tempdir()?;
1500        let store = tempfile::tempdir()?;
1501        synthetic_build_dir(build.path())?;
1502        let lib = build.path().join("debug/.fingerprint/serde-102a33f055a8d229");
1503        fs::write(lib.join("bin-serde"), "1111222233334444")?;
1504        fs::write(lib.join("dep-bin-serde"), [0u8; 8])?;
1505
1506        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1507        assert_eq!(report.stored, 1);
1508        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1509        let entry = &manifest.units[0];
1510        let paths: Vec<&str> = entry.files.iter().map(|file| file.path.as_str()).collect();
1511        assert!(paths.contains(&"bin-serde"), "{paths:?}");
1512        assert!(paths.contains(&"dep-bin-serde"), "{paths:?}");
1513
1514        let reports = verify(store.path())?;
1515        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1516        Ok(())
1517    }
1518
1519    #[test]
1520    fn harvest_dry_run_writes_nothing() -> Result<()> {
1521        let build = tempfile::tempdir()?;
1522        let store = tempfile::tempdir()?;
1523        synthetic_build_dir(build.path())?;
1524
1525        let report = harvest(build.path(), store.path(), None, true)?;
1526        assert_eq!(report.stored, 3);
1527        assert_eq!(generation_names(store.path())?.len(), 0, "no generation");
1528        Ok(())
1529    }
1530
1531    #[test]
1532    fn harvest_unit_filter_selects_one_group() -> Result<()> {
1533        let build = tempfile::tempdir()?;
1534        let store = tempfile::tempdir()?;
1535        synthetic_build_dir(build.path())?;
1536
1537        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1538        assert_eq!(report.stored, 1);
1539        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1540        assert_eq!(manifest.units.len(), 1);
1541        assert_eq!(manifest.units[0].name, "serde-102a33f055a8d229");
1542        Ok(())
1543    }
1544
1545    #[test]
1546    fn restore_links_hardlinks_and_never_overwrites() -> Result<()> {
1547        let build = tempfile::tempdir()?;
1548        let store = tempfile::tempdir()?;
1549        let view = tempfile::tempdir()?;
1550        synthetic_build_dir(build.path())?;
1551        harvest(build.path(), store.path(), None, false)?;
1552
1553        // A pre-existing file must survive a restore untouched.
1554        let occupied = view.path().join("debug/deps/libserde-102a33f055a8d229.rlib");
1555        fs::create_dir_all(occupied.parent().unwrap())?;
1556        fs::write(&occupied, b"live artifact")?;
1557
1558        let report = restore(view.path(), store.path(), None, false)?;
1559        assert_eq!(report.units_linked, 3);
1560        assert_eq!(report.files_existing, 1, "the live artifact was skipped");
1561        assert_eq!(fs::read(&occupied)?, b"live artifact");
1562        // Only `deps/` artifacts are hard-linked; state and build trees are
1563        // private copies (cargo writes both in place). The single deps/
1564        // artifact of this fixture was the occupied one, so nothing linked.
1565        assert_eq!(report.files_linked, 0, "no other deps/ artifact");
1566        assert!(report.files_copied >= 12, "got {}", report.files_copied);
1567
1568        // Memo file content and hash landed in the right place.
1569        let memo = view
1570            .path()
1571            .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
1572        assert_eq!(fs::read_to_string(&memo)?.trim(), "5d82713a7f2d05e2");
1573        assert!(
1574            view.path()
1575                .join("debug/build/x-aabbccddeeff0011/output")
1576                .is_file()
1577        );
1578
1579        // Fingerprint state is a private writable copy: cargo rewrites it in
1580        // place after a rebuild, and a shared inode would EACCES on the
1581        // read-only store copy (or mutate the store).
1582        #[cfg(unix)]
1583        {
1584            use std::os::unix::fs::MetadataExt;
1585            use std::os::unix::fs::PermissionsExt;
1586            let parity = parity_key()?;
1587            let stored =
1588                units_dir(store.path(), &parity).join("serde-102a33f055a8d229/lib-serde");
1589            let view_meta = fs::metadata(&memo)?;
1590            assert_ne!(view_meta.ino(), fs::metadata(&stored)?.ino(), "state not shared");
1591            assert_eq!(view_meta.nlink(), 1, "state owns its inode");
1592            assert_ne!(view_meta.permissions().mode() & 0o200, 0, "state writable");
1593        }
1594
1595        // Second restore: everything already fresh.
1596        let again = restore(view.path(), store.path(), None, false)?;
1597        assert_eq!(again.units_already, 3);
1598        assert_eq!(again.files_linked, 0);
1599        assert_eq!(again.files_copied, 0);
1600        Ok(())
1601    }
1602
1603    #[test]
1604    fn restore_into_missing_profile_dirs_creates_them() -> Result<()> {
1605        let build = tempfile::tempdir()?;
1606        let store = tempfile::tempdir()?;
1607        let view = tempfile::tempdir()?;
1608        synthetic_build_dir(build.path())?;
1609        harvest(build.path(), store.path(), None, false)?;
1610
1611        let report = restore(view.path(), store.path(), None, false)?;
1612        assert_eq!(report.errors, 0);
1613        assert!(view.path().join("debug/.fingerprint").is_dir());
1614        Ok(())
1615    }
1616
1617    #[test]
1618    fn restore_without_a_generation_reports_fail_open() -> Result<()> {
1619        let view = tempfile::tempdir()?;
1620        let store = tempfile::tempdir()?;
1621        let report = restore(view.path(), store.path(), None, false)?;
1622        assert!(report.note.is_some());
1623        assert_eq!(report.files_linked, 0);
1624        Ok(())
1625    }
1626
1627    #[test]
1628    fn verify_passes_clean_and_flags_drift() -> Result<()> {
1629        let build = tempfile::tempdir()?;
1630        let store = tempfile::tempdir()?;
1631        synthetic_build_dir(build.path())?;
1632        harvest(build.path(), store.path(), None, false)?;
1633
1634        let reports = verify(store.path())?;
1635        assert_eq!(reports.len(), 1);
1636        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1637        assert_eq!(reports[0].units, 3);
1638
1639        // Damage one stored file: size + stamp drift must surface.
1640        let parity = parity_key()?;
1641        let victim =
1642            units_dir(store.path(), &parity).join("serde-102a33f055a8d229/dep-lib-serde");
1643        #[cfg(unix)]
1644        {
1645            use std::os::unix::fs::PermissionsExt;
1646            fs::set_permissions(&victim, fs::Permissions::from_mode(0o644))?;
1647        }
1648        fs::write(&victim, [9u8; 32])?;
1649
1650        let reports = verify(store.path())?;
1651        assert!(!reports[0].is_clean());
1652        assert!(
1653            reports[0]
1654                .problems
1655                .iter()
1656                .any(|line| line.contains("size drift")),
1657            "{:?}",
1658            reports[0].problems
1659        );
1660        Ok(())
1661    }
1662
1663    #[test]
1664    fn verify_reports_unrecorded_unit_groups() -> Result<()> {
1665        let build = tempfile::tempdir()?;
1666        let store = tempfile::tempdir()?;
1667        synthetic_build_dir(build.path())?;
1668        harvest(build.path(), store.path(), None, false)?;
1669
1670        let stray = units_dir(store.path(), &parity_key()?).join("stray-0000000000000000");
1671        fs::create_dir_all(stray.join("deps"))?;
1672        fs::write(stray.join("deps/x.rlib"), [0u8; 4])?;
1673
1674        let reports = verify(store.path())?;
1675        assert!(
1676            reports[0]
1677                .warnings
1678                .iter()
1679                .any(|line| line.contains("stray-0000000000000000")),
1680            "{:?}",
1681            reports[0].warnings
1682        );
1683        Ok(())
1684    }
1685
1686    #[test]
1687    fn generations_summary_counts_store_files() -> Result<()> {
1688        let build = tempfile::tempdir()?;
1689        let store = tempfile::tempdir()?;
1690        synthetic_build_dir(build.path())?;
1691        harvest(build.path(), store.path(), None, false)?;
1692
1693        let generations = generations(store.path())?;
1694        assert_eq!(generations.len(), 1);
1695        assert!(generations[0].active, "current toolchain generation");
1696        assert_eq!(generations[0].units, 3);
1697        assert!(generations[0].files >= 10);
1698        assert!(generations[0].linked <= generations[0].files);
1699        Ok(())
1700    }
1701
1702    #[test]
1703    fn cached_diagnostics_are_stored_as_extra_and_verify_clean() -> Result<()> {
1704        let build = tempfile::tempdir()?;
1705        let store = tempfile::tempdir()?;
1706        synthetic_build_dir(build.path())?;
1707        // Cargo's message cache lives in the fingerprint dir, next to the memo.
1708        fs::write(
1709            build
1710                .path()
1711                .join("debug/.fingerprint/serde-102a33f055a8d229/output-lib-serde"),
1712            r#"{"$message_type":"diagnostic","message":"warning: unused"}"#,
1713        )?;
1714
1715        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1716        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1717        let output = manifest.units[0]
1718            .files
1719            .iter()
1720            .find(|file| file.path == "output-lib-serde")
1721            .expect("cached diagnostics are stored");
1722        assert_eq!(output.role, "extra");
1723
1724        let reports = verify(store.path())?;
1725        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1726        Ok(())
1727    }
1728
1729    #[test]
1730    fn harvest_refreshes_when_a_role_drifted() -> Result<()> {
1731        let build = tempfile::tempdir()?;
1732        let store = tempfile::tempdir()?;
1733        synthetic_build_dir(build.path())?;
1734        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1735
1736        // A manifest from before roles were content-based: the memo is
1737        // recorded as something else.
1738        let parity = parity_key()?;
1739        let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1740        let memo_index = manifest.units[0]
1741            .files
1742            .iter()
1743            .position(|file| file.path == manifest.units[0].memo)
1744            .expect("memo recorded");
1745        manifest.units[0].files[memo_index].role = "extra".to_string();
1746        save_manifest(store.path(), &parity, &manifest)?;
1747
1748        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1749        assert_eq!(report.refreshed, 1, "role drift forces a refresh");
1750        let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1751        let role = &manifest.units[0].files[memo_index].role;
1752        assert_eq!(role, "memo", "roles are rewritten");
1753        Ok(())
1754    }
1755
1756    #[test]
1757    fn restore_shares_artifacts_but_copies_state_writable() -> Result<()> {
1758        let build = tempfile::tempdir()?;
1759        let store = tempfile::tempdir()?;
1760        synthetic_build_dir(build.path())?;
1761        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1762
1763        let view = tempfile::tempdir()?;
1764        restore(view.path(), store.path(), Some("102a33f0"), false)?;
1765
1766        let state = view
1767            .path()
1768            .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
1769        let artifact = view
1770            .path()
1771            .join("debug/deps/libserde-102a33f055a8d229.rlib");
1772        let metadata = view
1773            .path()
1774            .join("debug/deps/libserde-102a33f055a8d229.rmeta");
1775        assert!(state.is_file() && artifact.is_file() && metadata.is_file());
1776
1777        let parity = parity_key()?;
1778        let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1779        let entry = manifest
1780            .units
1781            .iter()
1782            .find(|entry| entry.name == "serde-102a33f055a8d229")
1783            .expect("stored unit");
1784        assert_eq!(ceil_secs(fs::metadata(&state)?.modified()?), entry.stamp);
1785
1786        // Rewriting restored state must not reach the store (the exact
1787        // failure cargo hits when it stores a rebuilt fingerprint).
1788        fs::write(&state, "ffffffffffffffff")?;
1789        let stored = units_dir(store.path(), &parity)
1790            .join("serde-102a33f055a8d229")
1791            .join("lib-serde");
1792        assert_eq!(fs::read_to_string(&stored)?, "5d82713a7f2d05e2");
1793
1794        #[cfg(unix)]
1795        {
1796            use std::os::unix::fs::MetadataExt;
1797            assert!(fs::metadata(&artifact)?.nlink() >= 2, "artifact shares the store inode");
1798            assert_eq!(fs::metadata(&state)?.nlink(), 1, "state is a private copy");
1799            assert_eq!(
1800                fs::metadata(&metadata)?.nlink(),
1801                1,
1802                ".rmeta is a private copy (rustc opens it for write)"
1803            );
1804        }
1805        Ok(())
1806    }
1807
1808    /// Two units with a resolved dependency edge: `serde` → `serde_core`.
1809    /// `dep_at`/`unit_at` are the stamps the files carry, so harvest stores
1810    /// exactly that ordering.
1811    fn synthetic_edge_dir(root: &Path, dep_at: u64, unit_at: u64) -> Result<()> {
1812        fs::create_dir_all(root.join("debug/deps"))?;
1813        let core = root.join("debug/.fingerprint/serde_core-b468be6fa8c1f936");
1814        fs::create_dir_all(&core)?;
1815        fs::write(core.join("lib-serde_core"), "71a9f323efa5ae82")?;
1816        fs::write(core.join("dep-lib-serde_core"), [0u8; 8])?;
1817        let core_rlib = root.join("debug/deps/libserde_core-b468be6fa8c1f936.rlib");
1818        fs::write(&core_rlib, [1u8; 32])?;
1819
1820        let unit = root.join("debug/.fingerprint/serde-746d173581f1a314");
1821        fs::create_dir_all(&unit)?;
1822        fs::write(unit.join("lib-serde"), "5d82713a7f2d05e2")?;
1823        fs::write(
1824            unit.join("lib-serde.json"),
1825            r#"{"rustc":5244445300240561903,"features":"[]","declared_features":"[]","deps":[[11029742160753049355,"serde_core",false,9416646317397223793]]}"#,
1826        )?;
1827        fs::write(unit.join("dep-lib-serde"), [0u8; 8])?;
1828        let unit_rlib = root.join("debug/deps/libserde-746d173581f1a314.rlib");
1829        fs::write(&unit_rlib, [1u8; 32])?;
1830
1831        set_stamp(&core.join("lib-serde_core"), dep_at)?;
1832        set_stamp(&core.join("dep-lib-serde_core"), dep_at)?;
1833        set_stamp(&core_rlib, dep_at)?;
1834        set_stamp(&unit.join("lib-serde"), unit_at)?;
1835        set_stamp(&unit.join("lib-serde.json"), unit_at)?;
1836        set_stamp(&unit.join("dep-lib-serde"), unit_at)?;
1837        set_stamp(&unit_rlib, unit_at)?;
1838        Ok(())
1839    }
1840
1841    #[test]
1842    fn harvest_clamps_a_dependent_to_its_dependency() -> Result<()> {
1843        let build = tempfile::tempdir()?;
1844        let store = tempfile::tempdir()?;
1845        let now = now_secs();
1846        // The view itself is inverted (a fragmented cache: the dependency was
1847        // rebuilt after its dependent) — harvest must clamp the dependent up.
1848        synthetic_edge_dir(build.path(), now, now - 3600)?;
1849        harvest(build.path(), store.path(), None, false)?;
1850
1851        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1852        let dependent = manifest
1853            .units
1854            .iter()
1855            .find(|entry| entry.name == "serde-746d173581f1a314")
1856            .expect("dependent stored");
1857        let dependency = manifest
1858            .units
1859            .iter()
1860            .find(|entry| entry.name == "serde_core-b468be6fa8c1f936")
1861            .expect("dependency stored");
1862        assert!(
1863            dependent.stamp >= dependency.stamp,
1864            "clamped: {} >= {}",
1865            dependent.stamp,
1866            dependency.stamp
1867        );
1868
1869        let reports = verify(store.path())?;
1870        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1871        assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
1872        Ok(())
1873    }
1874
1875    #[test]
1876    fn harvest_stamps_groups_by_the_metadata_output() -> Result<()> {
1877        let build = tempfile::tempdir()?;
1878        let store = tempfile::tempdir()?;
1879        synthetic_build_dir(build.path())?;
1880        let now = now_secs();
1881        // Rustc writes `.rmeta` first and `.rlib` last: the consumable
1882        // moment — and therefore the stamp — is the metadata.
1883        set_stamp(
1884            &build.path().join("debug/deps/libserde-102a33f055a8d229.rmeta"),
1885            now - 30,
1886        )?;
1887        set_stamp(
1888            &build.path().join("debug/deps/libserde-102a33f055a8d229.rlib"),
1889            now,
1890        )?;
1891        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1892
1893        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1894        assert_eq!(manifest.units[0].stamp, now - 30);
1895        Ok(())
1896    }
1897
1898    #[test]
1899    fn harvest_stores_read_only_sources() -> Result<()> {
1900        let build = tempfile::tempdir()?;
1901        let store = tempfile::tempdir()?;
1902        synthetic_build_dir(build.path())?;
1903        // Wrappers (kache) and backup tools leave read-only files in the build
1904        // dir; `fs::copy` carries that attribute along, and stamping the copy
1905        // must not depend on the source's permissions (Windows: access denied).
1906        let source = build
1907            .path()
1908            .join("debug/.fingerprint/serde-102a33f055a8d229/dep-lib-serde");
1909        let mut perms = fs::metadata(&source)?.permissions();
1910        perms.set_readonly(true);
1911        fs::set_permissions(&source, perms)?;
1912
1913        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1914        assert_eq!(report.stored, 1);
1915
1916        let parity = parity_key()?;
1917        let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1918        let stored = units_dir(store.path(), &parity)
1919            .join(&manifest.units[0].name)
1920            .join("dep-lib-serde");
1921        assert!(stored.is_file(), "the state file is stored");
1922        assert_eq!(
1923            ceil_secs(fs::metadata(&stored)?.modified()?),
1924            manifest.units[0].stamp,
1925            "the stored copy carries the canonical stamp"
1926        );
1927        Ok(())
1928    }
1929
1930    #[cfg(unix)]
1931    #[test]
1932    fn a_failed_store_drops_its_staging_dir() -> Result<()> {
1933        use std::os::unix::fs::MetadataExt;
1934
1935        let build = tempfile::tempdir()?;
1936        let store = tempfile::tempdir()?;
1937        synthetic_build_dir(build.path())?;
1938        if fs::metadata(build.path())?.uid() == 0 {
1939            return Ok(()); // root reads a 000 file anyway — nothing to inject
1940        }
1941        let artifact = build
1942            .path()
1943            .join("debug/deps/libserde-102a33f055a8d229.rlib");
1944        {
1945            use std::os::unix::fs::PermissionsExt;
1946            fs::set_permissions(&artifact, fs::Permissions::from_mode(0o000))?;
1947        }
1948
1949        let result = harvest(build.path(), store.path(), Some("102a33f0"), false);
1950        assert!(result.is_err(), "an unreadable artifact aborts the store");
1951
1952        let units = units_dir(store.path(), &parity_key()?);
1953        if units.is_dir() {
1954            for entry in fs::read_dir(&units)? {
1955                let name = entry?.file_name().to_string_lossy().into_owned();
1956                assert!(!name.contains(".tmp-"), "staging leftover: {name}");
1957            }
1958        }
1959        Ok(())
1960    }
1961
1962    #[cfg(unix)]
1963    #[test]
1964    fn harvest_sweeps_stale_staging_dirs() -> Result<()> {
1965        use std::os::unix::fs::PermissionsExt;
1966
1967        let build = tempfile::tempdir()?;
1968        let store = tempfile::tempdir()?;
1969        synthetic_build_dir(build.path())?;
1970        let units = units_dir(store.path(), &parity_key()?);
1971
1972        // A crashed store run from yesterday: dot-dir, read-only leftovers.
1973        let stale = units.join(".serde-102a33f055a8d229.tmp-99999");
1974        fs::create_dir_all(&stale)?;
1975        let leftover = stale.join("dep-lib-serde");
1976        fs::write(&leftover, b"x")?;
1977        fs::set_permissions(&leftover, fs::Permissions::from_mode(0o444))?;
1978        set_stamp(&stale, now_secs() - 7200)?;
1979
1980        // A concurrent harvest's staging dir must survive the sweep.
1981        let fresh = units.join(".serde-102a33f055a8d229.tmp-11111");
1982        fs::create_dir_all(&fresh)?;
1983
1984        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1985        assert!(!stale.exists(), "stale staging dir swept");
1986        assert!(fresh.exists(), "fresh staging dir left alone");
1987        Ok(())
1988    }
1989
1990    #[test]
1991    fn verify_flags_an_inverted_store_stamp() -> Result<()> {
1992        let build = tempfile::tempdir()?;
1993        let store = tempfile::tempdir()?;
1994        let now = now_secs();
1995        synthetic_edge_dir(build.path(), now - 3600, now)?;
1996        harvest(build.path(), store.path(), None, false)?;
1997
1998        // Simulate a store written before the clamp existed: re-stamp the
1999        // dependent (files and manifest) older than its dependency.
2000        let parity = parity_key()?;
2001        let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2002        let units = units_dir(store.path(), &parity);
2003        let entry = manifest
2004            .units
2005            .iter_mut()
2006            .find(|entry| entry.name == "serde-746d173581f1a314")
2007            .expect("dependent stored");
2008        entry.stamp = now - 7200;
2009        for file in &entry.files {
2010            set_stamp(&units.join(&entry.name).join(&file.path), now - 7200)?;
2011        }
2012        save_manifest(store.path(), &parity, &manifest)?;
2013
2014        let reports = verify(store.path())?;
2015        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2016        assert_eq!(reports[0].ordering.len(), 1, "{:?}", reports[0].ordering);
2017        assert!(reports[0].ordering[0].contains("serde-746d173581f1a314"));
2018        assert!(reports[0].ordering[0].contains("serde_core-b468be6fa8c1f936"));
2019        Ok(())
2020    }
2021
2022    #[test]
2023    fn verify_ordering_is_quiet_when_units_are_current() -> Result<()> {
2024        let build = tempfile::tempdir()?;
2025        let store = tempfile::tempdir()?;
2026        let now = now_secs();
2027        synthetic_edge_dir(build.path(), now - 3600, now)?;
2028        harvest(build.path(), store.path(), None, false)?;
2029
2030        let reports = verify(store.path())?;
2031        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2032        assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
2033        Ok(())
2034    }
2035}