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::io::Write;
33use std::path::{Path, PathBuf};
34use std::time::{Duration, SystemTime, UNIX_EPOCH};
35
36use anyhow::{Context, Result};
37
38use crate::util::{self, FileLock};
39use serde::{Deserialize, Serialize};
40use walkdir::WalkDir;
41
42use crate::fingerprint::{self, ArtifactInfo, Unit};
43
44/// Seed for the flag digest, versioned: bumping it retires old generations.
45const PARITY_SEED: &str = "epicenter-parity-v2";
46
47// ── toolchain identity ──────────────────────────────────────────────────────
48
49/// The `rustc -vV` facts that name a vault generation.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Toolchain {
52    /// `1.99.0`
53    pub release: String,
54    /// Short commit hash (`b940084d7`) as rustc prints it.
55    pub commit: String,
56    /// `x86_64-unknown-linux-gnu`
57    pub host: String,
58}
59
60impl Toolchain {
61    /// `rustc 1.99.0 (b940084d7)` — for humans, not for keys.
62    pub fn version_line(&self) -> String {
63        format!("rustc {} ({})", self.release, self.commit)
64    }
65}
66
67/// Parse `rustc -vV` (first line + `host:`).
68pub fn toolchain() -> Result<Toolchain> {
69    let mut command = util::command_for("rustc")?;
70    let output = command.arg("-vV").output().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    util::validate_component(&release, "rustc release")?;
97    util::validate_component(&commit, "rustc commit")?;
98    util::validate_component(&host, "rustc host")?;
99    Ok(Toolchain {
100        release,
101        commit,
102        host,
103    })
104}
105
106/// Install time of the active toolchain: the real `rustc` binary in the
107/// sysroot (falling back to the sysroot directory). This is the reference
108/// for "was this unit built by the installed toolchain?".
109pub fn toolchain_install_time() -> Option<SystemTime> {
110    let mut command = util::command_for("rustc").ok()?;
111    let output = command.args(["--print", "sysroot"]).output().ok()?;
112    if !output.status.success() {
113        return None;
114    }
115    let sysroot = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
116    let rustc = sysroot
117        .join("bin")
118        .join(if cfg!(windows) { "rustc.exe" } else { "rustc" });
119    fs::metadata(&rustc)
120        .and_then(|meta| meta.modified())
121        .ok()
122        .or_else(|| fs::metadata(&sysroot).and_then(|meta| meta.modified()).ok())
123}
124
125// ── parity key ──────────────────────────────────────────────────────────────
126
127/// One vault generation = toolchain + host + flag environment:
128/// `1.99.0-b940084d7-x86_64-unknown-linux-gnu-3f9a2c1b`.
129pub fn parity_key() -> Result<String> {
130    let tc = toolchain()?;
131    let key = format!(
132        "{}-{}-{}-{}",
133        tc.release,
134        tc.commit,
135        tc.host,
136        flags_digest()
137    );
138    // The key becomes a directory name: never allow traversal via a fake
139    // `rustc -vV` on PATH (e.g. release `../../x`).
140    util::validate_component(&key, "parity key")?;
141    Ok(key)
142}
143
144/// Digest of the flags that live outside the fingerprint: the flag
145/// environment plus the flag-relevant keys of Cargo's global config.
146fn flags_digest() -> String {
147    let mut input = String::from(PARITY_SEED);
148    for name in [
149        "RUSTFLAGS",
150        "RUSTDOCFLAGS",
151        "CARGO_BUILD_RUSTFLAGS",
152        "CARGO_BUILD_RUSTDOCFLAGS",
153        "CARGO_BUILD_TARGET",
154        "RUSTC_WRAPPER",
155        "CARGO_BUILD_RUSTC_WRAPPER",
156        "RUSTC",
157    ] {
158        input.push('\0');
159        input.push_str(name);
160        input.push('=');
161        input.push_str(&std::env::var(name).unwrap_or_default());
162    }
163    if let Ok(path) = crate::config::cargo_config_path() {
164        if let Ok(text) = fs::read_to_string(path) {
165            for key in ["rustflags", "rustdocflags", "target", "rustc-wrapper"] {
166                if let Some(value) = crate::config::read_table_key(&text, "build", key) {
167                    input.push('\0');
168                    input.push_str(key);
169                    input.push('=');
170                    input.push_str(&value);
171                }
172            }
173        }
174    }
175    if let Ok(mut cargo) = util::command_for("cargo") {
176        if let Ok(output) = cargo.arg("-V").output() {
177            if output.status.success() {
178                input.push('\0');
179                input.push_str("cargo=");
180                input.push_str(String::from_utf8_lossy(&output.stdout).trim());
181            }
182        }
183    }
184    format!("{:08x}", (fnv1a64(input.as_bytes()) >> 32) as u32)
185}
186
187fn fnv1a64(bytes: &[u8]) -> u64 {
188    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
189    for byte in bytes {
190        hash ^= *byte as u64;
191        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
192    }
193    hash
194}
195
196// ── layout ──────────────────────────────────────────────────────────────────
197
198/// `<build>/../store` — the vault lives beside the shared build dir, so
199/// `cargo clean` can never touch it.
200pub fn store_root(build_root: &Path) -> PathBuf {
201    build_root.parent().unwrap_or(build_root).join("store")
202}
203
204/// `store/<parity-key>/units`.
205pub fn units_dir(store_root: &Path, parity: &str) -> PathBuf {
206    store_root.join(parity).join("units")
207}
208
209fn manifest_path(store_root: &Path, parity: &str) -> PathBuf {
210    store_root.join(parity).join("manifest.json")
211}
212
213/// The per-generation lock lives **outside** the generation dir so an empty
214/// generation can be removed while the lock is held (Windows).
215fn store_lock_path(store_root: &Path, parity: &str) -> PathBuf {
216    store_root.join(format!(".lock-{parity}"))
217}
218
219fn ensure_store_root(store_root: &Path) -> Result<()> {
220    match fs::symlink_metadata(store_root) {
221        Ok(meta) => {
222            if !meta.is_dir() || meta.file_type().is_symlink() {
223                anyhow::bail!(
224                    "store root {} is not a real directory",
225                    store_root.display()
226                );
227            }
228            util::ensure_private_dir(store_root)?;
229        }
230        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
231            util::create_private_dir_all(store_root)?;
232        }
233        Err(err) => {
234            return Err(err).with_context(|| format!("inspecting {}", store_root.display()));
235        }
236    }
237    Ok(())
238}
239
240// ── manifest ────────────────────────────────────────────────────────────────
241
242/// `manifest.json` of one generation.
243#[derive(Debug, Clone, Default, Serialize, Deserialize)]
244pub struct Manifest {
245    pub parity_key: String,
246    pub toolchain: String,
247    /// Unix seconds of the first harvest of this generation.
248    pub created: u64,
249    /// Unix seconds of the last harvest.
250    pub updated: u64,
251    pub units: Vec<UnitEntry>,
252}
253
254/// One stored unit group.
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct UnitEntry {
257    /// Fingerprint dir name: `<pkg>-<16hex>`.
258    pub name: String,
259    pub profile: String,
260    /// Memo file name, e.g. `lib-serde`.
261    pub memo: String,
262    /// Memo file content — the authoritative 16-hex hash.
263    pub hash: String,
264    /// Canonical mtime of the whole group (whole seconds).
265    pub stamp: u64,
266    pub files: Vec<FileEntry>,
267}
268
269/// One stored file.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct FileEntry {
272    /// Path inside the unit group.
273    pub path: String,
274    /// Path relative to the profile dir in a build dir.
275    pub dest: String,
276    pub size: u64,
277    /// SHA-256 of the file content (empty only in pre-fix manifests).
278    #[serde(default)]
279    pub sha256: String,
280    /// `memo` | `fingerprint` | `dep-info` | `artifact` | `build-tree`.
281    pub role: String,
282}
283
284impl Manifest {
285    /// Reject every path that could escape the store or the view. This runs
286    /// before any consumer (restore/verify/evict) touches a manifest path.
287    pub fn validate(&self) -> Result<()> {
288        for unit in &self.units {
289            util::validate_component(&unit.name, "unit name")?;
290            util::validate_component(&unit.profile, "profile")?;
291            util::validate_component(&unit.memo, "memo file")?;
292            if !unit.hash.is_empty() && !fingerprint::looks_like_hash(&unit.hash) {
293                anyhow::bail!("invalid memo hash {:?} in unit {}", unit.hash, unit.name);
294            }
295            for file in &unit.files {
296                util::validate_rel(&file.path, "stored file path")?;
297                util::validate_rel(&file.dest, "destination path")?;
298                if !file.sha256.is_empty()
299                    && (file.sha256.len() != 64
300                        || !file.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()))
301                {
302                    anyhow::bail!("invalid SHA-256 for {} in unit {}", file.path, unit.name);
303                }
304            }
305        }
306        Ok(())
307    }
308}
309
310fn load_manifest(store_root: &Path, parity: &str) -> Result<Option<Manifest>> {
311    let path = manifest_path(store_root, parity);
312    if !path.is_file() {
313        return Ok(None);
314    }
315    let text = fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
316    let manifest: Manifest =
317        serde_json::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
318    manifest
319        .validate()
320        .with_context(|| format!("validating {}", path.display()))?;
321    Ok(Some(manifest))
322}
323
324fn save_manifest(store_root: &Path, parity: &str, manifest: &Manifest) -> Result<()> {
325    util::validate_component(parity, "generation")?;
326    let dir = store_root.join(parity);
327    util::create_private_dir_all(&dir)?;
328    let what = serde_json::to_string_pretty(manifest)?;
329    // O_EXCL + a symlink-free path: never write through a planted temp link.
330    let mut tmp = None;
331    for attempt in 0..100u32 {
332        let candidate = dir.join(format!(
333            "manifest.json.tmp-{}-{attempt}",
334            std::process::id()
335        ));
336        match fs::OpenOptions::new()
337            .write(true)
338            .create_new(true)
339            .open(&candidate)
340        {
341            Ok(file) => {
342                tmp = Some((candidate, file));
343                break;
344            }
345            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
346            Err(err) => {
347                return Err(err).with_context(|| format!("creating {}", candidate.display()));
348            }
349        }
350    }
351    let Some((tmp, mut file)) = tmp else {
352        anyhow::bail!(
353            "could not create a unique manifest temp file in {}",
354            dir.display()
355        );
356    };
357    file.write_all(what.as_bytes())?;
358    file.write_all(b"\n")?;
359    file.sync_all()?;
360    drop(file);
361    fs::rename(&tmp, manifest_path(store_root, parity))?;
362    #[cfg(unix)]
363    if let Ok(parent) = fs::File::open(&dir) {
364        let _ = parent.sync_all();
365    }
366    Ok(())
367}
368
369/// Names of the store generations (directories under `store/`).
370pub fn generation_names(store_root: &Path) -> Result<Vec<String>> {
371    if !store_root.is_dir() {
372        return Ok(Vec::new());
373    }
374    let mut names = Vec::new();
375    for entry in
376        fs::read_dir(store_root).with_context(|| format!("reading {}", store_root.display()))?
377    {
378        let entry = entry?;
379        if !entry.file_type()?.is_dir() {
380            continue;
381        }
382        let name = entry.file_name().to_string_lossy().into_owned();
383        if !name.starts_with('.') {
384            names.push(name);
385        }
386    }
387    names.sort();
388    Ok(names)
389}
390
391// ── stored units (read / remove) ─────────────────────────────────────────────
392
393/// One unit as the vault holds it.
394#[derive(Debug, Clone)]
395pub struct StoredUnit {
396    /// The generation directory the unit lives in (the parity key).
397    pub generation: String,
398    /// The manifest entry: name, profile, hash, stamp, files.
399    pub entry: UnitEntry,
400    /// `store/<generation>/units/<name>`.
401    pub dir: PathBuf,
402}
403
404/// Every stored unit across all generations, manifest-driven.
405pub fn stored_units(store_root: &Path) -> Result<Vec<StoredUnit>> {
406    let mut out = Vec::new();
407    for generation in generation_names(store_root)? {
408        let Some(manifest) = load_manifest(store_root, &generation)? else {
409            continue;
410        };
411        let units = units_dir(store_root, &generation);
412        for entry in manifest.units {
413            let dir = units.join(&entry.name);
414            out.push(StoredUnit {
415                generation: generation.clone(),
416                entry,
417                dir,
418            });
419        }
420    }
421    Ok(out)
422}
423
424/// The `rustc` identity recorded in a stored unit's Fingerprint JSON.
425pub fn stored_unit_rustc(unit: &StoredUnit) -> Option<String> {
426    let dir_meta = fs::symlink_metadata(&unit.dir).ok()?;
427    if !dir_meta.is_dir() || dir_meta.file_type().is_symlink() {
428        return None;
429    }
430    let rel = PathBuf::from(format!("{}.json", unit.entry.memo));
431    util::check_regular_chain(&unit.dir, &rel).ok()?;
432    let text = fs::read_to_string(unit.dir.join(&rel)).ok()?;
433    let value: serde_json::Value = serde_json::from_str(&text).ok()?;
434    match value.get("rustc") {
435        Some(serde_json::Value::Number(number)) => Some(number.to_string()),
436        Some(serde_json::Value::String(text)) => Some(text.clone()),
437        _ => None,
438    }
439}
440
441/// Remove stored unit groups: their directories go, the manifest is pruned,
442/// and a generation left without units disappears entirely. Returns how many
443/// groups were dropped; groups that could not be removed stay recorded.
444pub fn drop_units(store_root: &Path, generation: &str, names: &[String]) -> Result<usize> {
445    util::validate_component(generation, "generation")?;
446    let _lock = FileLock::acquire(&store_lock_path(store_root, generation))?;
447    let Some(mut manifest) = load_manifest(store_root, generation)? else {
448        return Ok(0);
449    };
450    let units = units_dir(store_root, generation);
451    let mut kept = Vec::with_capacity(manifest.units.len());
452    let mut dropped = 0;
453    for entry in manifest.units.drain(..) {
454        if !names.contains(&entry.name) {
455            kept.push(entry);
456            continue;
457        }
458        util::validate_component(&entry.name, "unit name")?;
459        let dir = units.join(&entry.name);
460        match fs::symlink_metadata(&dir) {
461            Ok(meta) if meta.file_type().is_symlink() => {
462                eprintln!("could not remove symlinked unit {}: refused", dir.display());
463                kept.push(entry);
464            }
465            Ok(meta) if meta.is_dir() => match fs::remove_dir_all(&dir) {
466                Ok(()) => {
467                    dropped += 1;
468                }
469                Err(err) => {
470                    eprintln!("could not remove {}: {err}", dir.display());
471                    kept.push(entry);
472                }
473            },
474            Ok(_) => {
475                eprintln!("could not remove {}: not a directory", dir.display());
476                kept.push(entry);
477            }
478            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
479                dropped += 1;
480            }
481            Err(err) => {
482                eprintln!("could not remove {}: {err}", dir.display());
483                kept.push(entry);
484            }
485        }
486    }
487    manifest.units = kept;
488    if manifest.units.is_empty() {
489        let _ = fs::remove_dir_all(store_root.join(generation));
490    } else if dropped > 0 {
491        manifest.updated = now_secs();
492        save_manifest(store_root, generation, &manifest)?;
493    }
494    Ok(dropped)
495}
496// ── harvest ─────────────────────────────────────────────────────────────────
497
498/// What one harvest did.
499#[derive(Debug, Default)]
500pub struct HarvestReport {
501    pub parity: String,
502    pub toolchain: String,
503    pub stored: usize,
504    pub refreshed: usize,
505    pub unchanged: usize,
506    pub skipped_stale: usize,
507    pub skipped_incomplete: usize,
508    pub files: usize,
509    pub bytes: u64,
510    pub note: Option<String>,
511}
512
513/// Store unit groups from a build dir into the vault.
514///
515/// Only units built by the installed toolchain are stored: the newest unit's
516/// rustc cohort marks the active generation, and a cache that as a whole
517/// predates the installed toolchain produces a note instead of stale copies
518/// filed under the current parity key.
519pub fn harvest(
520    build_root: &Path,
521    store_root: &Path,
522    unit_filter: Option<&str>,
523    dry_run: bool,
524) -> Result<HarvestReport> {
525    let tc = toolchain()?;
526    let parity = parity_key()?;
527    let mut report = HarvestReport {
528        parity: parity.clone(),
529        toolchain: tc.version_line(),
530        ..Default::default()
531    };
532
533    // Serialize every manifest read-modify-write. Parallel `exec --harvest`
534    // jobs otherwise lose each other's units (last writer wins).
535    let _lock = if dry_run {
536        None
537    } else {
538        ensure_store_root(store_root)?;
539        util::create_private_dir_all(&store_root.join(&parity))?;
540        util::create_private_dir_all(&units_dir(store_root, &parity))?;
541        let lock = FileLock::acquire(&store_lock_path(store_root, &parity))?;
542        // Self-heal debris from a crashed or failed store run (dot-dirs whose
543        // copies may have inherited read-only sources) before planning.
544        sweep_staging(&units_dir(store_root, &parity));
545        Some(lock)
546    };
547
548    let rows: Vec<(Unit, ArtifactInfo)> = fingerprint::read_units(build_root)?
549        .into_iter()
550        .map(|unit| {
551            let arts = fingerprint::artifacts_for(build_root, &unit);
552            (unit, arts)
553        })
554        .collect();
555
556    // Freshness gate for the whole cache: nothing built since the toolchain
557    // was installed means the cache belongs to an older generation.
558    let newest_activity = rows.iter().filter_map(|(_, arts)| arts.newest).max();
559    if let (Some(newest), Some(installed)) = (newest_activity, toolchain_install_time()) {
560        if newest < installed {
561            report.note = Some(format!(
562                "the shared cache predates the installed toolchain ({}) — build once, then harvest",
563                tc.version_line()
564            ));
565            return Ok(report);
566        }
567    }
568
569    // Active generation = the rustc cohort of the most recently built unit
570    // that carries a fingerprint JSON. Units without one can't be placed and
571    // are harvested as-is (fail-open) rather than mislabeled.
572    let active_gen = rows
573        .iter()
574        .filter(|(unit, _)| crate::who::rustc_of(unit) != "unknown")
575        .max_by_key(|(_, arts)| arts.newest)
576        .map(|(unit, _)| crate::who::rustc_of(unit))
577        .unwrap_or_else(|| "unknown".into());
578
579    let mut manifest = load_manifest(store_root, &parity)?.unwrap_or_else(|| Manifest {
580        parity_key: parity.clone(),
581        toolchain: tc.version_line(),
582        created: now_secs(),
583        updated: 0,
584        units: Vec::new(),
585    });
586
587    // ── pass 1: collect the groups to store and their base stamps ──────────
588    struct GroupPlan {
589        unit: Unit,
590        files: Vec<(PathBuf, String, String, String)>,
591        memo: String,
592        hash: String,
593        base: u64,
594    }
595    let mut plans: Vec<GroupPlan> = Vec::new();
596
597    for (unit, arts) in &rows {
598        if let Some(query) = unit_filter {
599            if !unit_name_matches(&unit.dir_name, query) {
600                continue;
601            }
602        }
603        let cohort = crate::who::rustc_of(unit);
604        if cohort != "unknown" && active_gen != "unknown" && cohort != active_gen {
605            report.skipped_stale += 1;
606            continue;
607        }
608
609        let unit_dir = build_root
610            .join(&unit.profile)
611            .join(".fingerprint")
612            .join(&unit.dir_name);
613        let profile_dir = build_root.join(&unit.profile);
614        let memo = unit.memo_file.clone();
615        let mut files: Vec<(PathBuf, String, String, String)> = Vec::new();
616        // (source, store path, destination relative to the profile dir, role)
617
618        // The whole fingerprint dir except the volatile timestamp: hash
619        // files, their serialized fingerprints, and dep-info. Shared dirs
620        // (several memo files) are stored completely, so every mode of the
621        // unit stays recognizable to Cargo.
622        for entry in
623            fs::read_dir(&unit_dir).with_context(|| format!("reading {}", unit_dir.display()))?
624        {
625            let entry = entry?;
626            if !entry.file_type()?.is_file() {
627                continue;
628            }
629            let name = entry.file_name().to_string_lossy().into_owned();
630            if is_volatile(&name) {
631                continue;
632            }
633            let role = if name.starts_with("dep-") {
634                "dep-info"
635            } else if name.ends_with(".json") {
636                "fingerprint"
637            } else if fs::read_to_string(entry.path())
638                .map(|text| crate::fingerprint::looks_like_hash(text.trim()))
639                .unwrap_or(false)
640            {
641                "memo"
642            } else {
643                // Cached diagnostics (`output-*`) and other cargo state.
644                "extra"
645            };
646            files.push((
647                entry.path(),
648                name.clone(),
649                format!(".fingerprint/{}/{name}", unit.dir_name),
650                role.into(),
651            ));
652        }
653
654        // Artifacts: `deps/…` and the `build/<anchor>/…` tree of build
655        // scripts. Wrapper internals and rustc's `.d` side files stay out.
656        let mut artifact_count = 0;
657        for (path, _, _) in &arts.paths {
658            let name = path
659                .file_name()
660                .map(|name| name.to_string_lossy().into_owned())
661                .unwrap_or_default();
662            if is_volatile(&name) {
663                continue;
664            }
665            let Ok(rel) = path.strip_prefix(&profile_dir) else {
666                continue;
667            };
668            let rel = rel.to_string_lossy().replace('\\', "/");
669            if rel.starts_with("deps/") && rel.ends_with(".d") {
670                continue;
671            }
672            let role = if rel.starts_with("build/") {
673                "build-tree"
674            } else {
675                "artifact"
676            };
677            files.push((path.clone(), rel.clone(), rel, role.into()));
678            artifact_count += 1;
679        }
680        if artifact_count == 0 {
681            report.skipped_incomplete += 1;
682            continue;
683        }
684
685        plans.push(GroupPlan {
686            base: group_stamp(&files)?,
687            memo,
688            hash: unit.memo_hash.clone().unwrap_or_default(),
689            files,
690            unit: unit.clone(),
691        });
692    }
693
694    // ── pass 2: enforce `T(unit) ≥ every dependency's T` (§11) ─────────────
695    //
696    // Base stamps restore the natural order inside one build view; a store
697    // fed from several views can still invert edges (a dependency harvested
698    // later than its dependent — e.g. a fragmented multi-project cache).
699    // Resolve the edges (a dependency's memo hash → the unit that owns it)
700    // and raise dependents until nothing is older than what it consumed.
701    // `verify`'s ordering audit stays the guard for anything left over.
702    let stored_stamps: std::collections::HashMap<String, u64> = manifest
703        .units
704        .iter()
705        .map(|entry| (entry.hash.clone(), entry.stamp))
706        .collect();
707    let index: std::collections::HashMap<&str, usize> = plans
708        .iter()
709        .enumerate()
710        .map(|(position, plan)| (plan.hash.as_str(), position))
711        .collect();
712    let mut plan_deps: Vec<Vec<usize>> = Vec::with_capacity(plans.len());
713    let mut stamps: Vec<u64> = Vec::with_capacity(plans.len());
714    for plan in &plans {
715        let mut deps = Vec::new();
716        let mut stamp = plan.base;
717        for dep in plan
718            .unit
719            .json
720            .as_ref()
721            .map(|json| json.deps.as_slice())
722            .unwrap_or(&[])
723        {
724            let Some(hash) = dep.memo_hash() else {
725                continue;
726            };
727            if let Some(&at) = index.get(hash.as_str()) {
728                deps.push(at);
729            } else if let Some(known) = stored_stamps.get(hash.as_str()) {
730                stamp = stamp.max(*known);
731            }
732        }
733        plan_deps.push(deps);
734        stamps.push(stamp);
735    }
736    // Dependencies before dependents; a cycle (never observed) keeps its base
737    // order and the ordering audit reports whatever is left.
738    let mut order: Vec<usize> = Vec::with_capacity(plans.len());
739    let mut settled = vec![false; plans.len()];
740    loop {
741        let mut progressed = false;
742        for at in 0..plans.len() {
743            if settled[at] || !plan_deps[at].iter().all(|&dep| settled[dep]) {
744                continue;
745            }
746            settled[at] = true;
747            order.push(at);
748            progressed = true;
749        }
750        if !progressed {
751            break;
752        }
753    }
754    for at in 0..plans.len() {
755        if !settled[at] {
756            order.push(at);
757        }
758    }
759    for &at in &order {
760        for dep in &plan_deps[at] {
761            stamps[at] = stamps[at].max(stamps[*dep]);
762        }
763    }
764
765    // ── pass 3: store (or refresh) one group at a time ─────────────────────
766    for (at, plan) in plans.iter().enumerate() {
767        let unit = &plan.unit;
768        let files = &plan.files;
769        let stamp = stamps[at];
770        let group_dir = units_dir(store_root, &parity).join(&unit.dir_name);
771
772        // Unchanged = same hash, profile and stamp, same (path, role) file
773        // set, all present. Anything else is refreshed as a whole group.
774        let mut new_entries: Vec<(&str, &str)> = files
775            .iter()
776            .map(|(_, path, _, role)| (path.as_str(), role.as_str()))
777            .collect();
778        new_entries.sort_unstable();
779        let already = manifest
780            .units
781            .iter()
782            .find(|entry| entry.name == unit.dir_name)
783            .map(|entry| {
784                let mut stored: Vec<(&str, &str)> = entry
785                    .files
786                    .iter()
787                    .map(|file| (file.path.as_str(), file.role.as_str()))
788                    .collect();
789                stored.sort_unstable();
790                entry.hash == plan.hash
791                    && entry.profile == unit.profile
792                    && entry.stamp == stamp
793                    && stored == new_entries
794                    && entry
795                        .files
796                        .iter()
797                        .all(|file| !file.sha256.is_empty() && group_dir.join(&file.path).is_file())
798            })
799            .unwrap_or(false);
800        if already {
801            report.unchanged += 1;
802            continue;
803        }
804
805        let bytes: u64 = files
806            .iter()
807            .filter_map(|(src, _, _, _)| fs::metadata(src).ok())
808            .map(|meta| meta.len())
809            .sum();
810        if dry_run {
811            report.stored += 1;
812            report.files += files.len();
813            report.bytes += bytes;
814            continue;
815        }
816
817        let mut entries = Vec::with_capacity(files.len());
818        store_group(
819            store_root,
820            &parity,
821            &unit.dir_name,
822            stamp,
823            files,
824            &mut entries,
825        )
826        .with_context(|| format!("storing unit {}", unit.dir_name))?;
827
828        let entry = UnitEntry {
829            name: unit.dir_name.clone(),
830            profile: unit.profile.clone(),
831            memo: plan.memo.clone(),
832            hash: plan.hash.clone(),
833            stamp,
834            files: entries,
835        };
836        match manifest
837            .units
838            .iter_mut()
839            .find(|existing| existing.name == entry.name)
840        {
841            Some(slot) => {
842                *slot = entry;
843                report.refreshed += 1;
844            }
845            None => {
846                manifest.units.push(entry);
847                report.stored += 1;
848            }
849        }
850        report.files += files.len();
851        report.bytes += bytes;
852    }
853
854    if !dry_run {
855        manifest.units.sort_by(|a, b| {
856            (a.profile.as_str(), a.name.as_str()).cmp(&(b.profile.as_str(), b.name.as_str()))
857        });
858        manifest.updated = now_secs();
859        save_manifest(store_root, &parity, &manifest)?;
860    }
861    Ok(report)
862}
863
864/// Copy one unit group into its final place: a temporary directory next to
865/// the target, stamped read-only files, then a swap that never leaves the
866/// group half-written under its real name.
867fn store_group(
868    store_root: &Path,
869    parity: &str,
870    name: &str,
871    stamp: u64,
872    files: &[(PathBuf, String, String, String)],
873    entries: &mut Vec<FileEntry>,
874) -> Result<()> {
875    util::validate_component(name, "unit name")?;
876    let units = units_dir(store_root, parity);
877    let final_dir = units.join(name);
878    // Unique staging names + O_EXCL directory creation: a pre-planted symlink
879    // or temp dir can never be followed/truncated.
880    let mut tmp = None;
881    for attempt in 0..100u32 {
882        let candidate = units.join(format!(".{name}.tmp-{}-{attempt}", std::process::id()));
883        match fs::create_dir(&candidate) {
884            Ok(()) => {
885                tmp = Some(candidate);
886                break;
887            }
888            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
889            Err(err) => {
890                return Err(err)
891                    .with_context(|| format!("creating staging dir {}", candidate.display()));
892            }
893        }
894    }
895    let Some(tmp) = tmp else {
896        anyhow::bail!(
897            "could not create a unique staging dir in {}",
898            units.display()
899        );
900    };
901    #[cfg(unix)]
902    {
903        use std::os::unix::fs::PermissionsExt;
904        let _ = fs::set_permissions(&tmp, fs::Permissions::from_mode(0o700));
905    }
906
907    if let Err(err) = stage_group(&tmp, files, stamp, entries) {
908        // Never leave a half-written staging dir behind: it would keep a
909        // failed unit's bytes on disk and hide the failure from the next run.
910        let _ = crate::util::force_remove_dir(&tmp);
911        return Err(err);
912    }
913
914    let old = units.join(format!(".{name}.old-{}-{}", std::process::id(), now_secs()));
915    match fs::symlink_metadata(&final_dir) {
916        Ok(meta) if meta.file_type().is_symlink() => {
917            let _ = crate::util::force_remove_dir(&tmp);
918            anyhow::bail!("refusing symlinked store unit {}", final_dir.display());
919        }
920        Ok(meta) if meta.is_dir() => fs::rename(&final_dir, &old)?,
921        Ok(_) => {
922            let _ = crate::util::force_remove_dir(&tmp);
923            anyhow::bail!("store unit path {} is not a directory", final_dir.display());
924        }
925        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
926        Err(err) => return Err(err).context("inspecting store unit"),
927    }
928    fs::rename(&tmp, &final_dir)?;
929    if fs::symlink_metadata(&old)
930        .map(|meta| meta.is_dir() && !meta.file_type().is_symlink())
931        .unwrap_or(false)
932    {
933        let _ = fs::remove_dir_all(&old);
934    }
935    Ok(())
936}
937
938/// Copy one unit group's files into its staging dir. Every file is made
939/// writable before it is stamped and read-only again (on Unix) once the group
940/// is in place.
941///
942/// `fs::copy` carries the source's attributes along. A read-only source — a
943/// wrapper's hard-linked store file, a file restored from a backup — makes
944/// `File::set_modified` fail with "access denied" on Windows even though the
945/// copy lives in our own staging dir.
946fn stage_group(
947    tmp: &Path,
948    files: &[(PathBuf, String, String, String)],
949    stamp: u64,
950    entries: &mut Vec<FileEntry>,
951) -> Result<()> {
952    for (src, path, dest, role) in files {
953        let rel = util::validate_rel(path, "stored file path")?;
954        let dst = tmp.join(&rel);
955        if let Some(parent) = dst.parent() {
956            util::create_private_dir_all(parent)?;
957        }
958        let (size, sha256) = util::copy_file_hashed(src, &dst)
959            .with_context(|| format!("copying {} -> {}", src.display(), dst.display()))?;
960        make_writable(&dst).with_context(|| format!("making {} writable", dst.display()))?;
961        set_stamp(&dst, stamp)?;
962        make_read_only(&dst)?;
963        entries.push(FileEntry {
964            path: path.clone(),
965            dest: dest.clone(),
966            size,
967            sha256,
968            role: role.clone(),
969        });
970    }
971    Ok(())
972}
973
974/// Remove staging leftovers (`.{unit}.tmp-{pid}`, `.{unit}.old-{pid}`) that a
975/// crashed or failed run left in the units dir. Fresh dirs are left alone: a
976/// concurrent harvest may be filling them right now.
977fn sweep_staging(units: &Path) {
978    let Ok(entries) = fs::read_dir(units) else {
979        return;
980    };
981    let now = SystemTime::now();
982    for entry in entries.flatten() {
983        let name = entry.file_name();
984        let name = name.to_string_lossy();
985        if !(name.contains(".tmp-") || name.contains(".old-")) {
986            continue;
987        }
988        let meta = match fs::symlink_metadata(entry.path()) {
989            Ok(meta) => meta,
990            Err(_) => continue,
991        };
992        // Unlink a planted symlink itself; never traverse it.
993        if meta.file_type().is_symlink() {
994            let _ = fs::remove_file(entry.path());
995            continue;
996        }
997        if !meta.is_dir() {
998            continue;
999        }
1000        let stale = meta
1001            .modified()
1002            .ok()
1003            .and_then(|modified| now.duration_since(modified).ok())
1004            .is_some_and(|age| age > Duration::from_secs(600));
1005        if stale {
1006            let _ = crate::util::force_remove_dir(&entry.path());
1007        }
1008    }
1009}
1010
1011/// The canonical stamp of a group: the ceiling (whole seconds) of its
1012/// newest file, so every stored file is stamped at or after its true mtime
1013/// and cross-filesystem second-resolution round-trips cannot make an output
1014/// look newer than its own group.
1015fn group_stamp(files: &[(PathBuf, String, String, String)]) -> Result<u64> {
1016    let mut newest: Option<SystemTime> = None;
1017    let mut metadata: Option<SystemTime> = None;
1018    for (src, path, _, _) in files {
1019        let modified = fs::metadata(src)
1020            .and_then(|meta| meta.modified())
1021            .with_context(|| format!("reading mtime of {}", src.display()))?;
1022        if newest.map_or(true, |current| modified > current) {
1023            newest = Some(modified);
1024        }
1025        if path.ends_with(".rmeta") && metadata.map_or(true, |current| modified > current) {
1026            metadata = Some(modified);
1027        }
1028    }
1029    // The metadata output, when present, is the moment dependents can compile
1030    // against this unit: rustc writes `.rmeta` first and `.rlib` last, and a
1031    // dependent routinely finishes before its dependency's `.rlib` exists.
1032    // Stamping with the `.rlib` would make the group look newer than its own
1033    // dependents and force spurious rebuilds after a restore.
1034    Ok(match metadata.or(newest) {
1035        Some(time) => ceil_secs(time),
1036        None => now_secs(),
1037    })
1038}
1039
1040/// Round a timestamp up to whole seconds.
1041fn ceil_secs(time: SystemTime) -> u64 {
1042    match time.duration_since(UNIX_EPOCH) {
1043        Ok(duration) => {
1044            let secs = duration.as_secs();
1045            if duration.subsec_nanos() > 0 {
1046                secs + 1
1047            } else {
1048                secs
1049            }
1050        }
1051        Err(_) => 0,
1052    }
1053}
1054
1055fn now_secs() -> u64 {
1056    SystemTime::now()
1057        .duration_since(UNIX_EPOCH)
1058        .map(|duration| duration.as_secs())
1059        .unwrap_or(0)
1060}
1061
1062fn set_stamp(path: &Path, stamp: u64) -> Result<()> {
1063    let time = UNIX_EPOCH + Duration::from_secs(stamp);
1064    let file =
1065        fs::File::open(path).with_context(|| format!("opening {} to stamp it", path.display()))?;
1066    file.set_modified(time)
1067        .with_context(|| format!("stamping {}", path.display()))?;
1068    Ok(())
1069}
1070
1071/// The store is read-only by contract; restored state must not be.
1072#[cfg(unix)]
1073fn make_writable(path: &Path) -> std::io::Result<()> {
1074    use std::os::unix::fs::PermissionsExt;
1075    let mut perms = fs::metadata(path)?.permissions();
1076    perms.set_mode(0o644);
1077    fs::set_permissions(path, perms)
1078}
1079
1080#[cfg(not(unix))]
1081fn make_writable(path: &Path) -> std::io::Result<()> {
1082    let mut perms = fs::metadata(path)?.permissions();
1083    perms.set_readonly(false);
1084    fs::set_permissions(path, perms)
1085}
1086
1087/// On Unix the store is read-only (owner can still stamp and delete; only
1088/// content edits are refused). On Windows a read-only file blocks rename
1089/// and delete, so the store keeps normal permissions there.
1090fn make_read_only(path: &Path) -> Result<()> {
1091    #[cfg(unix)]
1092    {
1093        use std::os::unix::fs::PermissionsExt;
1094        fs::set_permissions(path, fs::Permissions::from_mode(0o444))?;
1095    }
1096    #[cfg(not(unix))]
1097    {
1098        let _ = path;
1099    }
1100    Ok(())
1101}
1102
1103/// Files that never enter the store: the volatile invocation timestamp,
1104/// and wrapper bookkeeping (`*.kache-*`, `.kache*`) that lives in the build
1105/// dir but is no part of Cargo's state.
1106fn is_volatile(name: &str) -> bool {
1107    name == "invoked.timestamp" || name.contains(".kache")
1108}
1109
1110// ── restore ─────────────────────────────────────────────────────────────────
1111
1112/// What one restore did.
1113#[derive(Debug, Default)]
1114pub struct RestoreReport {
1115    pub parity: String,
1116    pub target: PathBuf,
1117    pub units_linked: usize,
1118    pub units_already: usize,
1119    pub files_linked: usize,
1120    pub files_copied: usize,
1121    pub files_existing: usize,
1122    pub files_missing: usize,
1123    pub bytes: u64,
1124    pub errors: usize,
1125    /// Integrity failures (hash/size/owner/symlink). These abort `exec` and
1126    /// make `restore` exit non-zero; a merely missing unit stays fail-open.
1127    pub fatal_errors: usize,
1128    pub note: Option<String>,
1129}
1130
1131/// Link matching unit groups from the vault into a working dir.
1132///
1133/// `deps/` link outputs (`.rlib`, proc-macro `.so`) are hard-linked — rustc
1134/// only ever replaces those atomically. Fingerprint state, build-script
1135/// trees and `deps/*.rmeta` become private writable copies: rustc writes
1136/// `.rmeta` in place, so a read-only link turns any rebuild of that unit
1137/// into `output file … .rmeta is not writeable`. Hard links share the
1138/// store's canonical stamp; private copies are stamped after copying. Never
1139/// overwrites a file that already exists, never touches sources, and a
1140/// partial vault is fine: Cargo recompiles whatever is missing.
1141pub fn restore(
1142    view_root: &Path,
1143    store_root: &Path,
1144    unit_filter: Option<&str>,
1145    dry_run: bool,
1146) -> Result<RestoreReport> {
1147    let parity = parity_key()?;
1148    let mut report = RestoreReport {
1149        parity: parity.clone(),
1150        target: view_root.to_path_buf(),
1151        ..Default::default()
1152    };
1153    let Some(manifest) = load_manifest(store_root, &parity)? else {
1154        report.note = Some(
1155            "the vault holds no units for this generation yet — run `epicenter harvest` after a build"
1156                .into(),
1157        );
1158        return Ok(report);
1159    };
1160    check_store_trust(store_root)?;
1161    if !dry_run {
1162        prepare_view_root(view_root)?;
1163    }
1164
1165    let units = units_dir(store_root, &parity);
1166    for entry in &manifest.units {
1167        if let Some(query) = unit_filter {
1168            if !unit_name_matches(&entry.name, query) {
1169                continue;
1170            }
1171        }
1172        let fp_dir = view_root
1173            .join(&entry.profile)
1174            .join(".fingerprint")
1175            .join(&entry.name);
1176        if read_small_regular(&fp_dir.join(&entry.memo))
1177            .map(|text| text.trim() == entry.hash)
1178            .unwrap_or(false)
1179        {
1180            report.units_already += 1;
1181            continue;
1182        }
1183
1184        let mut unit_touched = false;
1185        for file in &entry.files {
1186            // Both components were validated by Manifest::validate; keep the
1187            // join explicit and reject any symlink in the destination chain.
1188            let dest_rel = Path::new(&entry.profile).join(&file.dest);
1189            let dst = view_root.join(&dest_rel);
1190            match fs::symlink_metadata(&dst) {
1191                Ok(meta) => {
1192                    if meta.file_type().is_symlink() {
1193                        report.errors += 1;
1194                        report.fatal_errors += 1;
1195                        eprintln!("restore: refusing symlink destination {}", dst.display());
1196                    } else {
1197                        report.files_existing += 1;
1198                    }
1199                    continue;
1200                }
1201                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1202                Err(err) => {
1203                    report.errors += 1;
1204                    eprintln!("restore: inspecting {}: {err}", dst.display());
1205                    continue;
1206                }
1207            }
1208            // Only `deps/` link outputs (`.rlib`, proc-macro `.so`) may share
1209            // the store inode — rustc replaces those atomically. `.rmeta` and
1210            // everything else are written in place by rustc, cargo or build
1211            // scripts: private verified copies only.
1212            let shared = file.path.starts_with("deps/") && !file.path.ends_with(".rmeta");
1213            if dry_run {
1214                unit_touched = true;
1215                if shared {
1216                    report.files_linked += 1;
1217                } else {
1218                    report.files_copied += 1;
1219                }
1220                report.bytes += file.size;
1221                continue;
1222            }
1223            if let Some(parent) = dest_rel.parent() {
1224                if let Err(err) = util::create_dir_chain_nofollow(view_root, parent) {
1225                    report.errors += 1;
1226                    eprintln!("restore: creating {}: {err}", parent.display());
1227                    continue;
1228                }
1229            }
1230            let src = units.join(&entry.name).join(&file.path);
1231            let rel = Path::new(&entry.name).join(&file.path);
1232            match fs::symlink_metadata(&src) {
1233                Ok(_) => {}
1234                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1235                    // Fail-open: a partial vault means Cargo rebuilds it.
1236                    report.files_missing += 1;
1237                    continue;
1238                }
1239                Err(err) => {
1240                    report.errors += 1;
1241                    report.fatal_errors += 1;
1242                    eprintln!("restore: inspecting {}: {err}", src.display());
1243                    continue;
1244                }
1245            }
1246            let result = if shared {
1247                util::check_regular_chain(&units, &rel)
1248                    .and_then(|_| verify_store_file(&src, file))
1249                    .and_then(|_| fs::hard_link(&src, &dst).map_err(Into::into))
1250            } else {
1251                util::check_regular_chain(&units, &rel)
1252                    .and_then(|_| install_private_copy(&src, &dst, entry.stamp, file))
1253            };
1254            match result {
1255                Ok(()) => {
1256                    if shared {
1257                        report.files_linked += 1;
1258                    } else {
1259                        report.files_copied += 1;
1260                    }
1261                    report.bytes += file.size;
1262                    unit_touched = true;
1263                }
1264                Err(err) => {
1265                    report.errors += 1;
1266                    report.fatal_errors += 1;
1267                    eprintln!("restore: writing {}: {err:#}", dst.display());
1268                    continue;
1269                }
1270            }
1271        }
1272        if unit_touched {
1273            report.units_linked += 1;
1274        }
1275    }
1276    Ok(report)
1277}
1278
1279/// The store is a trust boundary: refuse a symlinked, foreign-owned, or
1280/// group/world-writable store. A root process must not consume a user-owned
1281/// vault (that is the hard-link privesc this fix closes).
1282fn check_store_trust(store_root: &Path) -> Result<()> {
1283    let meta = fs::symlink_metadata(store_root)
1284        .with_context(|| format!("inspecting store {}", store_root.display()))?;
1285    if !meta.is_dir() || meta.file_type().is_symlink() {
1286        anyhow::bail!("store {} is not a real directory", store_root.display());
1287    }
1288    if !util::owned_by_us(&meta) {
1289        anyhow::bail!(
1290            "store {} is owned by another user — refusing to execute its artifacts",
1291            store_root.display()
1292        );
1293    }
1294    // Repair the common pre-fix 0755 store instead of making every old vault
1295    // unusable; the directory is ours, so 0700 is always safe (and keeps
1296    // artifacts unreadable by other local users).
1297    util::ensure_private_dir(store_root)?;
1298    Ok(())
1299}
1300
1301fn prepare_view_root(view_root: &Path) -> Result<()> {
1302    match fs::symlink_metadata(view_root) {
1303        Ok(meta) => {
1304            if !meta.is_dir() || meta.file_type().is_symlink() {
1305                anyhow::bail!("view {} is not a real directory", view_root.display());
1306            }
1307            if !util::owned_by_us(&meta) {
1308                anyhow::bail!(
1309                    "view {} is owned by another user — refusing to write into it",
1310                    view_root.display()
1311                );
1312            }
1313        }
1314        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1315            util::create_private_dir_all(view_root)?;
1316        }
1317        Err(err) => {
1318            return Err(err).with_context(|| format!("inspecting view {}", view_root.display()));
1319        }
1320    }
1321    Ok(())
1322}
1323
1324fn verify_store_file(src: &Path, file: &FileEntry) -> Result<()> {
1325    let meta = fs::symlink_metadata(src)
1326        .with_context(|| format!("inspecting stored file {}", src.display()))?;
1327    if !meta.file_type().is_file() || meta.file_type().is_symlink() {
1328        anyhow::bail!("{} is not a regular file", src.display());
1329    }
1330    if !util::owned_by_us(&meta) {
1331        anyhow::bail!("{} is owned by another user", src.display());
1332    }
1333    if meta.len() != file.size {
1334        anyhow::bail!(
1335            "{} size drift (recorded {}, found {})",
1336            src.display(),
1337            file.size,
1338            meta.len()
1339        );
1340    }
1341    if file.sha256.is_empty() {
1342        anyhow::bail!("{} has no recorded SHA-256", src.display());
1343    }
1344    let found = util::sha256_file(src)?;
1345    if found != file.sha256 {
1346        anyhow::bail!("{} failed its SHA-256 check", src.display());
1347    }
1348    Ok(())
1349}
1350
1351fn install_private_copy(src: &Path, dst: &Path, stamp: u64, file: &FileEntry) -> Result<()> {
1352    let parent = dst
1353        .parent()
1354        .with_context(|| format!("destination {} has no parent", dst.display()))?;
1355    let nonce = SystemTime::now()
1356        .duration_since(UNIX_EPOCH)
1357        .map(|duration| duration.as_nanos())
1358        .unwrap_or(0);
1359    let tmp = parent.join(format!(".epicenter-copy-{}-{nonce}", std::process::id()));
1360    util::copy_file_verified(src, &tmp, file.size, &file.sha256)?;
1361    make_writable(&tmp)?;
1362    set_stamp(&tmp, stamp)?;
1363    // Private copies stay writable: rustc/cargo rewrite .rmeta and
1364    // fingerprint state in place. Only shared `deps/` artifacts are 0444.
1365    // Link, never rename: this keeps "never overwrite an existing file".
1366    let installed = fs::hard_link(&tmp, dst);
1367    let _ = fs::remove_file(&tmp);
1368    installed.with_context(|| format!("installing {}", dst.display()))?;
1369    Ok(())
1370}
1371
1372/// Read a small regular file without following a symlink.
1373fn read_small_regular(path: &Path) -> Option<String> {
1374    let meta = fs::symlink_metadata(path).ok()?;
1375    if !meta.file_type().is_file() || meta.file_type().is_symlink() || meta.len() > 4 * 1024 * 1024
1376    {
1377        return None;
1378    }
1379    fs::read_to_string(path).ok()
1380}
1381
1382// ── verify ──────────────────────────────────────────────────────────────────
1383
1384/// Result of auditing one generation.
1385#[derive(Debug, Default)]
1386pub struct VerifyReport {
1387    pub parity: String,
1388    pub units: usize,
1389    pub files: usize,
1390    pub bytes: u64,
1391    /// Human lines: missing files, size/hash/stamp drift, orphan groups.
1392    pub problems: Vec<String>,
1393    /// Not a failure, but worth knowing (e.g. unknown files lying around).
1394    pub warnings: Vec<String>,
1395    /// Units older than one of their dependencies. Cargo recompiles exactly
1396    /// these after a restore ("the dependency `X` was rebuilt") — a quality
1397    /// fact, not an integrity failure.
1398    pub ordering: Vec<String>,
1399}
1400
1401impl VerifyReport {
1402    pub fn is_clean(&self) -> bool {
1403        self.problems.is_empty()
1404    }
1405}
1406
1407/// Audit every generation in the vault: every manifest file present with the
1408/// recorded size, the canonical stamp, and — for memo files — the recorded
1409/// hash content; plus a count of unit groups the manifest doesn't know.
1410pub fn verify(store_root: &Path) -> Result<Vec<VerifyReport>> {
1411    let mut reports = Vec::new();
1412    for parity in generation_names(store_root)? {
1413        let mut report = VerifyReport {
1414            parity: parity.clone(),
1415            ..Default::default()
1416        };
1417        if let Err(err) = check_store_trust(store_root) {
1418            report
1419                .problems
1420                .push(format!("unsafe store: {err:#} — no artifact was read"));
1421            reports.push(report);
1422            continue;
1423        }
1424        let Some(manifest) = load_manifest(store_root, &parity)? else {
1425            report.problems.push(format!(
1426                "generation {parity} has no readable manifest.json — it cannot be audited"
1427            ));
1428            reports.push(report);
1429            continue;
1430        };
1431        let units = units_dir(store_root, &parity);
1432        for entry in &manifest.units {
1433            for file in &entry.files {
1434                let path = units.join(&entry.name).join(&file.path);
1435                let rel = Path::new(&entry.name).join(&file.path);
1436                if let Err(err) = util::check_regular_chain(&units, &rel) {
1437                    report.problems.push(format!(
1438                        "unsafe path: {} ({err:#})",
1439                        display_join(&path, &units)
1440                    ));
1441                    continue;
1442                }
1443                let meta = match fs::symlink_metadata(&path) {
1444                    Ok(meta) => meta,
1445                    Err(_) => {
1446                        report
1447                            .problems
1448                            .push(format!("missing: {}", display_join(&path, units.as_path())));
1449                        continue;
1450                    }
1451                };
1452                if meta.file_type().is_symlink() {
1453                    report.problems.push(format!(
1454                        "symlink in store (refused): {}",
1455                        display_join(&path, &units)
1456                    ));
1457                    continue;
1458                }
1459                if !meta.is_file() {
1460                    report.problems.push(format!(
1461                        "not a regular file: {}",
1462                        display_join(&path, &units)
1463                    ));
1464                    continue;
1465                }
1466                if !util::owned_by_us(&meta) {
1467                    report.problems.push(format!(
1468                        "foreign-owned file: {}",
1469                        display_join(&path, &units)
1470                    ));
1471                }
1472                report.files += 1;
1473                report.bytes += meta.len();
1474                if meta.len() != file.size {
1475                    report.problems.push(format!(
1476                        "size drift: {} (recorded {}, found {})",
1477                        display_join(&path, &units),
1478                        file.size,
1479                        meta.len()
1480                    ));
1481                }
1482                let found_stamp = meta.modified().map(ceil_secs).unwrap_or(0);
1483                if found_stamp != entry.stamp {
1484                    report.problems.push(format!(
1485                        "stamp drift: {} (recorded {}, found {})",
1486                        display_join(&path, &units),
1487                        entry.stamp,
1488                        found_stamp
1489                    ));
1490                }
1491                if file.sha256.is_empty() {
1492                    report.problems.push(format!(
1493                        "no recorded SHA-256: {} (re-harvest this generation)",
1494                        display_join(&path, &units)
1495                    ));
1496                } else {
1497                    match util::sha256_file(&path) {
1498                        Ok(found) if found != file.sha256 => report.problems.push(format!(
1499                            "content drift: {} (recorded {}, found {})",
1500                            display_join(&path, &units),
1501                            &file.sha256[..file.sha256.len().min(12)],
1502                            &found[..found.len().min(12)]
1503                        )),
1504                        Ok(_) => {}
1505                        Err(err) => report.problems.push(format!(
1506                            "unreadable: {}: {err}",
1507                            display_join(&path, &units)
1508                        )),
1509                    }
1510                }
1511                if file.role == "memo" {
1512                    match read_small_regular(&path) {
1513                        Some(text) if file.path == entry.memo && text.trim() != entry.hash => {
1514                            let found = text.trim();
1515                            let detail = if crate::fingerprint::looks_like_hash(found) {
1516                                format!("found {found}")
1517                            } else {
1518                                "found non-hash content (not printed)".to_string()
1519                            };
1520                            report.problems.push(format!(
1521                                "hash drift: {} (recorded {}; {detail})",
1522                                display_join(&path, &units),
1523                                entry.hash
1524                            ));
1525                        }
1526                        Some(text)
1527                            if file.path != entry.memo
1528                                && !crate::fingerprint::looks_like_hash(text.trim()) =>
1529                        {
1530                            report.problems.push(format!(
1531                                "malformed memo file: {} (content is not a 16-hex hash)",
1532                                display_join(&path, &units)
1533                            ));
1534                        }
1535                        Some(_) => {}
1536                        None => report.problems.push(format!(
1537                            "unreadable/symlinked memo file: {}",
1538                            display_join(&path, &units)
1539                        )),
1540                    }
1541                }
1542            }
1543        }
1544        // Ordering audit: a unit must be at least as new as its
1545        // dependencies, or cargo recompiles it after a restore ("the
1546        // dependency `X` was rebuilt"). Edges come from the stored
1547        // fingerprints; the join key is the dependency's memo hash — the
1548        // same value serde's `serde_core` edge (9416646317397223793) and
1549        // serde_core's memo file (71a9f323efa5ae82) share.
1550        let by_hash: std::collections::HashMap<&str, usize> = manifest
1551            .units
1552            .iter()
1553            .enumerate()
1554            .map(|(index, entry)| (entry.hash.as_str(), index))
1555            .collect();
1556        for entry in &manifest.units {
1557            let json = entry
1558                .files
1559                .iter()
1560                .find(|file| file.path.ends_with(".json"))
1561                .and_then(|file| read_small_regular(&units.join(&entry.name).join(&file.path)))
1562                .and_then(|text| crate::fingerprint::parse_fingerprint(&text));
1563            let Some(json) = json else { continue };
1564            for dep in &json.deps {
1565                let Some(dep_index) = dep
1566                    .memo_hash()
1567                    .and_then(|hash| by_hash.get(hash.as_str()))
1568                    .copied()
1569                else {
1570                    continue;
1571                };
1572                let dep_entry = &manifest.units[dep_index];
1573                if dep_entry.stamp > entry.stamp {
1574                    report.ordering.push(format!(
1575                        "{} is older than its dependency {} ({} older) — cargo recompiles it after a restore",
1576                        entry.name,
1577                        dep_entry.name,
1578                        crate::util::human_duration(Duration::from_secs(dep_entry.stamp - entry.stamp))
1579                    ));
1580                }
1581            }
1582        }
1583        report.units = manifest.units.len();
1584
1585        // Unit groups on disk that the manifest doesn't record.
1586        if units.is_dir() {
1587            let known: std::collections::BTreeSet<&str> = manifest
1588                .units
1589                .iter()
1590                .map(|unit| unit.name.as_str())
1591                .collect();
1592            for entry in fs::read_dir(&units)? {
1593                let entry = entry?;
1594                if !entry.file_type()?.is_dir() {
1595                    continue;
1596                }
1597                let name = entry.file_name().to_string_lossy().into_owned();
1598                if name.starts_with('.') {
1599                    continue; // in-flight temp dirs of a crashed harvest
1600                }
1601                if !known.contains(name.as_str()) {
1602                    report
1603                        .warnings
1604                        .push(format!("unrecorded unit group: units/{name}"));
1605                }
1606            }
1607        }
1608        reports.push(report);
1609    }
1610    Ok(reports)
1611}
1612
1613/// Path for humans: relative to `base` when possible.
1614fn display_join(path: &Path, base: &Path) -> String {
1615    path.strip_prefix(base)
1616        .unwrap_or(path)
1617        .to_string_lossy()
1618        .replace('\\', "/")
1619}
1620
1621// ── status ──────────────────────────────────────────────────────────────────
1622
1623/// One generation as the `vault` command shows it.
1624#[derive(Debug, Default)]
1625pub struct GenerationInfo {
1626    pub parity: String,
1627    /// Matches the installed toolchain + flag environment.
1628    pub active: bool,
1629    pub units: usize,
1630    pub files: usize,
1631    /// Bytes as the store holds them; a file linked into views counts once.
1632    pub bytes: u64,
1633    /// Store files that are also linked elsewhere (`nlink > 1`).
1634    pub linked: usize,
1635    pub created: Option<u64>,
1636    pub updated: Option<u64>,
1637}
1638
1639/// Summarize every generation of the vault.
1640pub fn generations(store_root: &Path) -> Result<Vec<GenerationInfo>> {
1641    let active = parity_key().ok();
1642    let mut out = Vec::new();
1643    for parity in generation_names(store_root)? {
1644        let mut info = GenerationInfo {
1645            parity: parity.clone(),
1646            active: active.as_deref() == Some(parity.as_str()),
1647            ..Default::default()
1648        };
1649        if let Some(manifest) = load_manifest(store_root, &parity)? {
1650            info.units = manifest.units.len();
1651            info.created = Some(manifest.created);
1652            info.updated = Some(manifest.updated);
1653        }
1654        let units = units_dir(store_root, &parity);
1655        if units.is_dir() {
1656            for entry in WalkDir::new(&units).min_depth(1) {
1657                let entry = entry?;
1658                if !entry.file_type().is_file() {
1659                    continue;
1660                }
1661                let meta = entry.metadata()?;
1662                info.files += 1;
1663                info.bytes += meta.len();
1664                if link_count(&meta) > 1 {
1665                    info.linked += 1;
1666                }
1667            }
1668        }
1669        out.push(info);
1670    }
1671    Ok(out)
1672}
1673
1674/// Hard links of one inode as the file system knows it (1 where unsupported).
1675pub(crate) fn link_count(meta: &fs::Metadata) -> u64 {
1676    #[cfg(unix)]
1677    {
1678        use std::os::unix::fs::MetadataExt;
1679        meta.nlink()
1680    }
1681    #[cfg(not(unix))]
1682    {
1683        let _ = meta;
1684        1
1685    }
1686}
1687
1688/// The vault's own matcher: package prefix or hash fragment of a group name.
1689fn unit_name_matches(name: &str, query: &str) -> bool {
1690    name.contains(query)
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695    use super::*;
1696
1697    /// A build dir in the shapes real Cargo writes: a lib unit, a
1698    /// build-script compile unit, and a build-script run unit without
1699    /// dep-info — plus volatile files and wrapper internals that must be
1700    /// filtered out.
1701    fn synthetic_build_dir(root: &Path) -> Result<()> {
1702        let jsons =
1703            r#"{"rustc":12019306335353385202,"features":"[]","declared_features":"[]","deps":[]}"#;
1704
1705        let lib = root.join("debug/.fingerprint/serde-102a33f055a8d229");
1706        fs::create_dir_all(&lib)?;
1707        fs::write(lib.join("lib-serde"), "5d82713a7f2d05e2")?;
1708        fs::write(lib.join("lib-serde.json"), jsons)?;
1709        fs::write(lib.join("dep-lib-serde"), [0u8; 24])?;
1710        fs::write(lib.join("invoked.timestamp"), "")?;
1711        fs::create_dir_all(root.join("debug/deps"))?;
1712        fs::write(
1713            root.join("debug/deps/libserde-102a33f055a8d229.rlib"),
1714            [7u8; 64],
1715        )?;
1716        fs::write(
1717            root.join("debug/deps/libserde-102a33f055a8d229.rmeta"),
1718            [6u8; 48],
1719        )?;
1720        fs::write(
1721            root.join("debug/deps/serde-102a33f055a8d229.d"),
1722            b"side file",
1723        )?;
1724        fs::write(
1725            root.join("debug/deps/libserde-102a33f055a8d229.kache-real"),
1726            b"wrapper",
1727        )?;
1728
1729        let compile = root.join("debug/.fingerprint/x-1122334455667788");
1730        fs::create_dir_all(&compile)?;
1731        fs::write(
1732            compile.join("build-script-build-script-build"),
1733            "aaaabbbbccccdddd",
1734        )?;
1735        fs::write(compile.join("build-script-build-script-build.json"), jsons)?;
1736        fs::write(
1737            compile.join("dep-build-script-build-script-build"),
1738            [0u8; 8],
1739        )?;
1740        let compile_anchor = root.join("debug/build/x-1122334455667788");
1741        fs::create_dir_all(&compile_anchor)?;
1742        fs::write(
1743            compile_anchor.join("build_script_build-1122334455667788"),
1744            [3u8; 32],
1745        )?;
1746        fs::write(
1747            compile_anchor.join("build_script_build-1122334455667788.d"),
1748            b"dep",
1749        )?;
1750        fs::write(
1751            compile_anchor.join("build_script_build-1122334455667788.kache-real"),
1752            b"wrapper",
1753        )?;
1754        fs::write(compile_anchor.join("invoked.timestamp"), "")?;
1755
1756        // The run unit has no dep-info at all (real Cargo layout).
1757        let run = root.join("debug/.fingerprint/x-aabbccddeeff0011");
1758        fs::create_dir_all(&run)?;
1759        fs::write(
1760            run.join("run-build-script-build-script-build"),
1761            "eeeeffff00001111",
1762        )?;
1763        fs::write(run.join("run-build-script-build-script-build.json"), jsons)?;
1764        let run_anchor = root.join("debug/build/x-aabbccddeeff0011");
1765        fs::create_dir_all(&run_anchor)?;
1766        fs::write(
1767            run_anchor.join("output"),
1768            b"cargo:rerun-if-changed=build.rs",
1769        )?;
1770        fs::write(run_anchor.join("stderr"), b"")?;
1771        fs::write(run_anchor.join("invoked.timestamp"), "")?;
1772        Ok(())
1773    }
1774
1775    /// A build dir with one incomplete unit (memo file, but no artifacts).
1776    fn synthetic_husk(root: &Path) -> Result<()> {
1777        let unit = root.join("debug/.fingerprint/husk-aaaabbbbccccdddd");
1778        fs::create_dir_all(&unit)?;
1779        fs::write(unit.join("lib-husk"), "0000111122223333")?;
1780        Ok(())
1781    }
1782
1783    #[test]
1784    fn parity_key_is_stable_and_shaped() -> Result<()> {
1785        let first = parity_key()?;
1786        let second = parity_key()?;
1787        assert_eq!(first, second);
1788        let tc = toolchain()?;
1789        assert!(first.starts_with(&format!("{}-{}", tc.release, tc.commit)));
1790        assert!(first.ends_with(&flags_digest()));
1791        Ok(())
1792    }
1793
1794    #[test]
1795    fn harvest_stores_groups_with_one_canonical_stamp() -> Result<()> {
1796        let build = tempfile::tempdir()?;
1797        let store = tempfile::tempdir()?;
1798        synthetic_build_dir(build.path())?;
1799
1800        let report = harvest(build.path(), store.path(), None, false)?;
1801        assert_eq!(report.stored, 3, "lib + build-script compile + run");
1802        assert_eq!(report.skipped_incomplete, 0);
1803        assert!(report.files >= 10, "got {}", report.files);
1804        assert!(report.bytes > 0);
1805
1806        let parity = parity_key()?;
1807        let manifest = load_manifest(store.path(), &parity)?.expect("manifest written");
1808        assert_eq!(manifest.units.len(), 3);
1809        let lib_entry = manifest
1810            .units
1811            .iter()
1812            .find(|unit| unit.name == "serde-102a33f055a8d229")
1813            .expect("lib unit recorded");
1814        assert_eq!(lib_entry.hash, "5d82713a7f2d05e2");
1815        assert_eq!(lib_entry.profile, "debug");
1816
1817        // Every stored file of the group carries the same stamp.
1818        let group = units_dir(store.path(), &parity).join(&lib_entry.name);
1819        for file in &lib_entry.files {
1820            let meta = fs::metadata(group.join(&file.path))?;
1821            assert_eq!(
1822                ceil_secs(meta.modified()?),
1823                lib_entry.stamp,
1824                "{}",
1825                file.path
1826            );
1827            assert_eq!(meta.len(), file.size);
1828        }
1829
1830        // Volatile files and wrapper internals never enter the store.
1831        assert!(!group.join("invoked.timestamp").exists());
1832        for file in &lib_entry.files {
1833            assert!(!file.path.contains("kache"), "{}", file.path);
1834            assert!(!file.path.ends_with(".d"), "{}", file.path);
1835        }
1836
1837        #[cfg(unix)]
1838        {
1839            use std::os::unix::fs::PermissionsExt;
1840            let meta = fs::metadata(group.join(&lib_entry.files[0].path))?;
1841            assert_eq!(
1842                meta.permissions().mode() & 0o777,
1843                0o444,
1844                "store is read-only"
1845            );
1846        }
1847        Ok(())
1848    }
1849
1850    #[test]
1851    fn harvest_is_idempotent_and_skips_incomplete_units() -> Result<()> {
1852        let build = tempfile::tempdir()?;
1853        let store = tempfile::tempdir()?;
1854        synthetic_build_dir(build.path())?;
1855        synthetic_husk(build.path())?;
1856
1857        let first = harvest(build.path(), store.path(), None, false)?;
1858        assert_eq!(first.stored, 3);
1859        assert_eq!(first.skipped_incomplete, 1, "husk without artifacts");
1860
1861        let second = harvest(build.path(), store.path(), None, false)?;
1862        assert_eq!(second.stored, 0);
1863        assert_eq!(second.unchanged, 3);
1864        assert_eq!(second.skipped_incomplete, 1);
1865        Ok(())
1866    }
1867
1868    #[test]
1869    fn harvest_refreshes_when_the_file_set_grows() -> Result<()> {
1870        let build = tempfile::tempdir()?;
1871        let store = tempfile::tempdir()?;
1872        synthetic_build_dir(build.path())?;
1873        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1874
1875        fs::write(
1876            build.path().join("debug/deps/libserde-102a33f055a8d229.so"),
1877            [1u8; 16],
1878        )?;
1879        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1880        assert_eq!(report.refreshed, 1);
1881        assert_eq!(report.stored, 0);
1882
1883        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1884        let paths: Vec<&str> = manifest.units[0]
1885            .files
1886            .iter()
1887            .map(|file| file.path.as_str())
1888            .collect();
1889        assert!(
1890            paths.contains(&"deps/libserde-102a33f055a8d229.so"),
1891            "{paths:?}"
1892        );
1893        Ok(())
1894    }
1895
1896    #[test]
1897    fn harvest_stores_all_memos_of_shared_dirs() -> Result<()> {
1898        let build = tempfile::tempdir()?;
1899        let store = tempfile::tempdir()?;
1900        synthetic_build_dir(build.path())?;
1901        let lib = build
1902            .path()
1903            .join("debug/.fingerprint/serde-102a33f055a8d229");
1904        fs::write(lib.join("bin-serde"), "1111222233334444")?;
1905        fs::write(lib.join("dep-bin-serde"), [0u8; 8])?;
1906
1907        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1908        assert_eq!(report.stored, 1);
1909        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1910        let entry = &manifest.units[0];
1911        let paths: Vec<&str> = entry.files.iter().map(|file| file.path.as_str()).collect();
1912        assert!(paths.contains(&"bin-serde"), "{paths:?}");
1913        assert!(paths.contains(&"dep-bin-serde"), "{paths:?}");
1914
1915        let reports = verify(store.path())?;
1916        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1917        Ok(())
1918    }
1919
1920    #[test]
1921    fn harvest_dry_run_writes_nothing() -> Result<()> {
1922        let build = tempfile::tempdir()?;
1923        let store = tempfile::tempdir()?;
1924        synthetic_build_dir(build.path())?;
1925
1926        let report = harvest(build.path(), store.path(), None, true)?;
1927        assert_eq!(report.stored, 3);
1928        assert_eq!(generation_names(store.path())?.len(), 0, "no generation");
1929        Ok(())
1930    }
1931
1932    #[test]
1933    fn harvest_unit_filter_selects_one_group() -> Result<()> {
1934        let build = tempfile::tempdir()?;
1935        let store = tempfile::tempdir()?;
1936        synthetic_build_dir(build.path())?;
1937
1938        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1939        assert_eq!(report.stored, 1);
1940        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1941        assert_eq!(manifest.units.len(), 1);
1942        assert_eq!(manifest.units[0].name, "serde-102a33f055a8d229");
1943        Ok(())
1944    }
1945
1946    #[test]
1947    fn restore_links_hardlinks_and_never_overwrites() -> Result<()> {
1948        let build = tempfile::tempdir()?;
1949        let store = tempfile::tempdir()?;
1950        let view = tempfile::tempdir()?;
1951        synthetic_build_dir(build.path())?;
1952        harvest(build.path(), store.path(), None, false)?;
1953
1954        // A pre-existing file must survive a restore untouched.
1955        let occupied = view
1956            .path()
1957            .join("debug/deps/libserde-102a33f055a8d229.rlib");
1958        fs::create_dir_all(occupied.parent().unwrap())?;
1959        fs::write(&occupied, b"live artifact")?;
1960
1961        let report = restore(view.path(), store.path(), None, false)?;
1962        assert_eq!(report.units_linked, 3);
1963        assert_eq!(report.files_existing, 1, "the live artifact was skipped");
1964        assert_eq!(fs::read(&occupied)?, b"live artifact");
1965        // Only `deps/` artifacts are hard-linked; state and build trees are
1966        // private copies (cargo writes both in place). The single deps/
1967        // artifact of this fixture was the occupied one, so nothing linked.
1968        assert_eq!(report.files_linked, 0, "no other deps/ artifact");
1969        assert!(report.files_copied >= 12, "got {}", report.files_copied);
1970
1971        // Memo file content and hash landed in the right place.
1972        let memo = view
1973            .path()
1974            .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
1975        assert_eq!(fs::read_to_string(&memo)?.trim(), "5d82713a7f2d05e2");
1976        assert!(
1977            view.path()
1978                .join("debug/build/x-aabbccddeeff0011/output")
1979                .is_file()
1980        );
1981
1982        // Fingerprint state is a private writable copy: cargo rewrites it in
1983        // place after a rebuild, and a shared inode would EACCES on the
1984        // read-only store copy (or mutate the store).
1985        #[cfg(unix)]
1986        {
1987            use std::os::unix::fs::MetadataExt;
1988            use std::os::unix::fs::PermissionsExt;
1989            let parity = parity_key()?;
1990            let stored = units_dir(store.path(), &parity).join("serde-102a33f055a8d229/lib-serde");
1991            let view_meta = fs::metadata(&memo)?;
1992            assert_ne!(
1993                view_meta.ino(),
1994                fs::metadata(&stored)?.ino(),
1995                "state not shared"
1996            );
1997            assert_eq!(view_meta.nlink(), 1, "state owns its inode");
1998            assert_ne!(view_meta.permissions().mode() & 0o200, 0, "state writable");
1999        }
2000
2001        // Second restore: everything already fresh.
2002        let again = restore(view.path(), store.path(), None, false)?;
2003        assert_eq!(again.units_already, 3);
2004        assert_eq!(again.files_linked, 0);
2005        assert_eq!(again.files_copied, 0);
2006        Ok(())
2007    }
2008
2009    #[test]
2010    fn restore_into_missing_profile_dirs_creates_them() -> Result<()> {
2011        let build = tempfile::tempdir()?;
2012        let store = tempfile::tempdir()?;
2013        let view = tempfile::tempdir()?;
2014        synthetic_build_dir(build.path())?;
2015        harvest(build.path(), store.path(), None, false)?;
2016
2017        let report = restore(view.path(), store.path(), None, false)?;
2018        assert_eq!(report.errors, 0);
2019        assert!(view.path().join("debug/.fingerprint").is_dir());
2020        Ok(())
2021    }
2022
2023    #[test]
2024    fn restore_without_a_generation_reports_fail_open() -> Result<()> {
2025        let view = tempfile::tempdir()?;
2026        let store = tempfile::tempdir()?;
2027        let report = restore(view.path(), store.path(), None, false)?;
2028        assert!(report.note.is_some());
2029        assert_eq!(report.files_linked, 0);
2030        Ok(())
2031    }
2032
2033    #[test]
2034    fn verify_passes_clean_and_flags_drift() -> Result<()> {
2035        let build = tempfile::tempdir()?;
2036        let store = tempfile::tempdir()?;
2037        synthetic_build_dir(build.path())?;
2038        harvest(build.path(), store.path(), None, false)?;
2039
2040        let reports = verify(store.path())?;
2041        assert_eq!(reports.len(), 1);
2042        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2043        assert_eq!(reports[0].units, 3);
2044
2045        // Damage one stored file: size + stamp drift must surface.
2046        let parity = parity_key()?;
2047        let victim = units_dir(store.path(), &parity).join("serde-102a33f055a8d229/dep-lib-serde");
2048        #[cfg(unix)]
2049        {
2050            use std::os::unix::fs::PermissionsExt;
2051            fs::set_permissions(&victim, fs::Permissions::from_mode(0o644))?;
2052        }
2053        fs::write(&victim, [9u8; 32])?;
2054
2055        let reports = verify(store.path())?;
2056        assert!(!reports[0].is_clean());
2057        assert!(
2058            reports[0]
2059                .problems
2060                .iter()
2061                .any(|line| line.contains("size drift")),
2062            "{:?}",
2063            reports[0].problems
2064        );
2065        Ok(())
2066    }
2067
2068    #[test]
2069    fn verify_reports_unrecorded_unit_groups() -> Result<()> {
2070        let build = tempfile::tempdir()?;
2071        let store = tempfile::tempdir()?;
2072        synthetic_build_dir(build.path())?;
2073        harvest(build.path(), store.path(), None, false)?;
2074
2075        let stray = units_dir(store.path(), &parity_key()?).join("stray-0000000000000000");
2076        fs::create_dir_all(stray.join("deps"))?;
2077        fs::write(stray.join("deps/x.rlib"), [0u8; 4])?;
2078
2079        let reports = verify(store.path())?;
2080        assert!(
2081            reports[0]
2082                .warnings
2083                .iter()
2084                .any(|line| line.contains("stray-0000000000000000")),
2085            "{:?}",
2086            reports[0].warnings
2087        );
2088        Ok(())
2089    }
2090
2091    #[test]
2092    fn generations_summary_counts_store_files() -> Result<()> {
2093        let build = tempfile::tempdir()?;
2094        let store = tempfile::tempdir()?;
2095        synthetic_build_dir(build.path())?;
2096        harvest(build.path(), store.path(), None, false)?;
2097
2098        let generations = generations(store.path())?;
2099        assert_eq!(generations.len(), 1);
2100        assert!(generations[0].active, "current toolchain generation");
2101        assert_eq!(generations[0].units, 3);
2102        assert!(generations[0].files >= 10);
2103        assert!(generations[0].linked <= generations[0].files);
2104        Ok(())
2105    }
2106
2107    #[test]
2108    fn cached_diagnostics_are_stored_as_extra_and_verify_clean() -> Result<()> {
2109        let build = tempfile::tempdir()?;
2110        let store = tempfile::tempdir()?;
2111        synthetic_build_dir(build.path())?;
2112        // Cargo's message cache lives in the fingerprint dir, next to the memo.
2113        fs::write(
2114            build
2115                .path()
2116                .join("debug/.fingerprint/serde-102a33f055a8d229/output-lib-serde"),
2117            r#"{"$message_type":"diagnostic","message":"warning: unused"}"#,
2118        )?;
2119
2120        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2121        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2122        let output = manifest.units[0]
2123            .files
2124            .iter()
2125            .find(|file| file.path == "output-lib-serde")
2126            .expect("cached diagnostics are stored");
2127        assert_eq!(output.role, "extra");
2128
2129        let reports = verify(store.path())?;
2130        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2131        Ok(())
2132    }
2133
2134    #[test]
2135    fn harvest_refreshes_when_a_role_drifted() -> Result<()> {
2136        let build = tempfile::tempdir()?;
2137        let store = tempfile::tempdir()?;
2138        synthetic_build_dir(build.path())?;
2139        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2140
2141        // A manifest from before roles were content-based: the memo is
2142        // recorded as something else.
2143        let parity = parity_key()?;
2144        let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2145        let memo_index = manifest.units[0]
2146            .files
2147            .iter()
2148            .position(|file| file.path == manifest.units[0].memo)
2149            .expect("memo recorded");
2150        manifest.units[0].files[memo_index].role = "extra".to_string();
2151        save_manifest(store.path(), &parity, &manifest)?;
2152
2153        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2154        assert_eq!(report.refreshed, 1, "role drift forces a refresh");
2155        let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2156        let role = &manifest.units[0].files[memo_index].role;
2157        assert_eq!(role, "memo", "roles are rewritten");
2158        Ok(())
2159    }
2160
2161    #[test]
2162    fn restore_shares_artifacts_but_copies_state_writable() -> Result<()> {
2163        let build = tempfile::tempdir()?;
2164        let store = tempfile::tempdir()?;
2165        synthetic_build_dir(build.path())?;
2166        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2167
2168        let view = tempfile::tempdir()?;
2169        restore(view.path(), store.path(), Some("102a33f0"), false)?;
2170
2171        let state = view
2172            .path()
2173            .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
2174        let artifact = view
2175            .path()
2176            .join("debug/deps/libserde-102a33f055a8d229.rlib");
2177        let metadata = view
2178            .path()
2179            .join("debug/deps/libserde-102a33f055a8d229.rmeta");
2180        assert!(state.is_file() && artifact.is_file() && metadata.is_file());
2181
2182        let parity = parity_key()?;
2183        let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2184        let entry = manifest
2185            .units
2186            .iter()
2187            .find(|entry| entry.name == "serde-102a33f055a8d229")
2188            .expect("stored unit");
2189        assert_eq!(ceil_secs(fs::metadata(&state)?.modified()?), entry.stamp);
2190
2191        // Rewriting restored state must not reach the store (the exact
2192        // failure cargo hits when it stores a rebuilt fingerprint).
2193        fs::write(&state, "ffffffffffffffff")?;
2194        let stored = units_dir(store.path(), &parity)
2195            .join("serde-102a33f055a8d229")
2196            .join("lib-serde");
2197        assert_eq!(fs::read_to_string(&stored)?, "5d82713a7f2d05e2");
2198
2199        #[cfg(unix)]
2200        {
2201            use std::os::unix::fs::MetadataExt;
2202            assert!(
2203                fs::metadata(&artifact)?.nlink() >= 2,
2204                "artifact shares the store inode"
2205            );
2206            assert_eq!(fs::metadata(&state)?.nlink(), 1, "state is a private copy");
2207            assert_eq!(
2208                fs::metadata(&metadata)?.nlink(),
2209                1,
2210                ".rmeta is a private copy (rustc opens it for write)"
2211            );
2212        }
2213        Ok(())
2214    }
2215
2216    /// Two units with a resolved dependency edge: `serde` → `serde_core`.
2217    /// `dep_at`/`unit_at` are the stamps the files carry, so harvest stores
2218    /// exactly that ordering.
2219    fn synthetic_edge_dir(root: &Path, dep_at: u64, unit_at: u64) -> Result<()> {
2220        fs::create_dir_all(root.join("debug/deps"))?;
2221        let core = root.join("debug/.fingerprint/serde_core-b468be6fa8c1f936");
2222        fs::create_dir_all(&core)?;
2223        fs::write(core.join("lib-serde_core"), "71a9f323efa5ae82")?;
2224        fs::write(core.join("dep-lib-serde_core"), [0u8; 8])?;
2225        let core_rlib = root.join("debug/deps/libserde_core-b468be6fa8c1f936.rlib");
2226        fs::write(&core_rlib, [1u8; 32])?;
2227
2228        let unit = root.join("debug/.fingerprint/serde-746d173581f1a314");
2229        fs::create_dir_all(&unit)?;
2230        fs::write(unit.join("lib-serde"), "5d82713a7f2d05e2")?;
2231        fs::write(
2232            unit.join("lib-serde.json"),
2233            r#"{"rustc":5244445300240561903,"features":"[]","declared_features":"[]","deps":[[11029742160753049355,"serde_core",false,9416646317397223793]]}"#,
2234        )?;
2235        fs::write(unit.join("dep-lib-serde"), [0u8; 8])?;
2236        let unit_rlib = root.join("debug/deps/libserde-746d173581f1a314.rlib");
2237        fs::write(&unit_rlib, [1u8; 32])?;
2238
2239        set_stamp(&core.join("lib-serde_core"), dep_at)?;
2240        set_stamp(&core.join("dep-lib-serde_core"), dep_at)?;
2241        set_stamp(&core_rlib, dep_at)?;
2242        set_stamp(&unit.join("lib-serde"), unit_at)?;
2243        set_stamp(&unit.join("lib-serde.json"), unit_at)?;
2244        set_stamp(&unit.join("dep-lib-serde"), unit_at)?;
2245        set_stamp(&unit_rlib, unit_at)?;
2246        Ok(())
2247    }
2248
2249    #[test]
2250    fn harvest_clamps_a_dependent_to_its_dependency() -> Result<()> {
2251        let build = tempfile::tempdir()?;
2252        let store = tempfile::tempdir()?;
2253        let now = now_secs();
2254        // The view itself is inverted (a fragmented cache: the dependency was
2255        // rebuilt after its dependent) — harvest must clamp the dependent up.
2256        synthetic_edge_dir(build.path(), now, now - 3600)?;
2257        harvest(build.path(), store.path(), None, false)?;
2258
2259        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2260        let dependent = manifest
2261            .units
2262            .iter()
2263            .find(|entry| entry.name == "serde-746d173581f1a314")
2264            .expect("dependent stored");
2265        let dependency = manifest
2266            .units
2267            .iter()
2268            .find(|entry| entry.name == "serde_core-b468be6fa8c1f936")
2269            .expect("dependency stored");
2270        assert!(
2271            dependent.stamp >= dependency.stamp,
2272            "clamped: {} >= {}",
2273            dependent.stamp,
2274            dependency.stamp
2275        );
2276
2277        let reports = verify(store.path())?;
2278        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2279        assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
2280        Ok(())
2281    }
2282
2283    #[test]
2284    fn harvest_stamps_groups_by_the_metadata_output() -> Result<()> {
2285        let build = tempfile::tempdir()?;
2286        let store = tempfile::tempdir()?;
2287        synthetic_build_dir(build.path())?;
2288        let now = now_secs();
2289        // Rustc writes `.rmeta` first and `.rlib` last: the consumable
2290        // moment — and therefore the stamp — is the metadata.
2291        set_stamp(
2292            &build
2293                .path()
2294                .join("debug/deps/libserde-102a33f055a8d229.rmeta"),
2295            now - 30,
2296        )?;
2297        set_stamp(
2298            &build
2299                .path()
2300                .join("debug/deps/libserde-102a33f055a8d229.rlib"),
2301            now,
2302        )?;
2303        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2304
2305        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2306        assert_eq!(manifest.units[0].stamp, now - 30);
2307        Ok(())
2308    }
2309
2310    #[test]
2311    fn harvest_stores_read_only_sources() -> Result<()> {
2312        let build = tempfile::tempdir()?;
2313        let store = tempfile::tempdir()?;
2314        synthetic_build_dir(build.path())?;
2315        // Wrappers (kache) and backup tools leave read-only files in the build
2316        // dir; `fs::copy` carries that attribute along, and stamping the copy
2317        // must not depend on the source's permissions (Windows: access denied).
2318        let source = build
2319            .path()
2320            .join("debug/.fingerprint/serde-102a33f055a8d229/dep-lib-serde");
2321        let mut perms = fs::metadata(&source)?.permissions();
2322        perms.set_readonly(true);
2323        fs::set_permissions(&source, perms)?;
2324
2325        let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2326        assert_eq!(report.stored, 1);
2327
2328        let parity = parity_key()?;
2329        let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2330        let stored = units_dir(store.path(), &parity)
2331            .join(&manifest.units[0].name)
2332            .join("dep-lib-serde");
2333        assert!(stored.is_file(), "the state file is stored");
2334        assert_eq!(
2335            ceil_secs(fs::metadata(&stored)?.modified()?),
2336            manifest.units[0].stamp,
2337            "the stored copy carries the canonical stamp"
2338        );
2339        Ok(())
2340    }
2341
2342    #[cfg(unix)]
2343    #[test]
2344    fn a_failed_store_drops_its_staging_dir() -> Result<()> {
2345        use std::os::unix::fs::MetadataExt;
2346
2347        let build = tempfile::tempdir()?;
2348        let store = tempfile::tempdir()?;
2349        synthetic_build_dir(build.path())?;
2350        if fs::metadata(build.path())?.uid() == 0 {
2351            return Ok(()); // root reads a 000 file anyway — nothing to inject
2352        }
2353        let artifact = build
2354            .path()
2355            .join("debug/deps/libserde-102a33f055a8d229.rlib");
2356        {
2357            use std::os::unix::fs::PermissionsExt;
2358            fs::set_permissions(&artifact, fs::Permissions::from_mode(0o000))?;
2359        }
2360
2361        let result = harvest(build.path(), store.path(), Some("102a33f0"), false);
2362        assert!(result.is_err(), "an unreadable artifact aborts the store");
2363
2364        let units = units_dir(store.path(), &parity_key()?);
2365        if units.is_dir() {
2366            for entry in fs::read_dir(&units)? {
2367                let name = entry?.file_name().to_string_lossy().into_owned();
2368                assert!(!name.contains(".tmp-"), "staging leftover: {name}");
2369            }
2370        }
2371        Ok(())
2372    }
2373
2374    #[cfg(unix)]
2375    #[test]
2376    fn harvest_sweeps_stale_staging_dirs() -> Result<()> {
2377        use std::os::unix::fs::PermissionsExt;
2378
2379        let build = tempfile::tempdir()?;
2380        let store = tempfile::tempdir()?;
2381        synthetic_build_dir(build.path())?;
2382        let units = units_dir(store.path(), &parity_key()?);
2383
2384        // A crashed store run from yesterday: dot-dir, read-only leftovers.
2385        let stale = units.join(".serde-102a33f055a8d229.tmp-99999");
2386        fs::create_dir_all(&stale)?;
2387        let leftover = stale.join("dep-lib-serde");
2388        fs::write(&leftover, b"x")?;
2389        fs::set_permissions(&leftover, fs::Permissions::from_mode(0o444))?;
2390        set_stamp(&stale, now_secs() - 7200)?;
2391
2392        // A concurrent harvest's staging dir must survive the sweep.
2393        let fresh = units.join(".serde-102a33f055a8d229.tmp-11111");
2394        fs::create_dir_all(&fresh)?;
2395
2396        harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2397        assert!(!stale.exists(), "stale staging dir swept");
2398        assert!(fresh.exists(), "fresh staging dir left alone");
2399        Ok(())
2400    }
2401
2402    #[test]
2403    fn verify_flags_an_inverted_store_stamp() -> Result<()> {
2404        let build = tempfile::tempdir()?;
2405        let store = tempfile::tempdir()?;
2406        let now = now_secs();
2407        synthetic_edge_dir(build.path(), now - 3600, now)?;
2408        harvest(build.path(), store.path(), None, false)?;
2409
2410        // Simulate a store written before the clamp existed: re-stamp the
2411        // dependent (files and manifest) older than its dependency.
2412        let parity = parity_key()?;
2413        let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2414        let units = units_dir(store.path(), &parity);
2415        let entry = manifest
2416            .units
2417            .iter_mut()
2418            .find(|entry| entry.name == "serde-746d173581f1a314")
2419            .expect("dependent stored");
2420        entry.stamp = now - 7200;
2421        for file in &entry.files {
2422            set_stamp(&units.join(&entry.name).join(&file.path), now - 7200)?;
2423        }
2424        save_manifest(store.path(), &parity, &manifest)?;
2425
2426        let reports = verify(store.path())?;
2427        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2428        assert_eq!(reports[0].ordering.len(), 1, "{:?}", reports[0].ordering);
2429        assert!(reports[0].ordering[0].contains("serde-746d173581f1a314"));
2430        assert!(reports[0].ordering[0].contains("serde_core-b468be6fa8c1f936"));
2431        Ok(())
2432    }
2433
2434    #[test]
2435    fn manifest_validation_rejects_escapes() {
2436        let unit = |name: &str, profile: &str, path: &str, dest: &str| UnitEntry {
2437            name: name.into(),
2438            profile: profile.into(),
2439            memo: "lib-x".into(),
2440            hash: "0123456789abcdef".into(),
2441            stamp: 1,
2442            files: vec![FileEntry {
2443                path: path.into(),
2444                dest: dest.into(),
2445                size: 1,
2446                sha256: String::new(),
2447                role: "artifact".into(),
2448            }],
2449        };
2450        let base = Manifest {
2451            parity_key: "p".into(),
2452            toolchain: "t".into(),
2453            created: 1,
2454            updated: 1,
2455            units: vec![],
2456        };
2457        for unit in [
2458            unit("../escape", "debug", "deps/x", "deps/x"),
2459            unit("ok", "/etc", "deps/x", "deps/x"),
2460            unit("ok", "debug", "/etc/passwd", "deps/x"),
2461            unit("ok", "debug", "deps/x", "/root/.ssh/authorized_keys"),
2462            unit("ok", "debug", "deps/../../x", "deps/x"),
2463        ] {
2464            let manifest = Manifest {
2465                units: vec![unit],
2466                ..base.clone()
2467            };
2468            assert!(manifest.validate().is_err(), "must reject {manifest:?}");
2469        }
2470    }
2471
2472    #[test]
2473    fn harvest_records_sha256_and_verify_flags_content_drift() -> Result<()> {
2474        let build = tempfile::tempdir()?;
2475        let store = tempfile::tempdir()?;
2476        synthetic_build_dir(build.path())?;
2477        harvest(build.path(), store.path(), None, false)?;
2478
2479        let parity = parity_key()?;
2480        let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2481        assert!(
2482            manifest
2483                .units
2484                .iter()
2485                .flat_map(|unit| &unit.files)
2486                .all(|file| !file.sha256.is_empty()),
2487            "every stored file must carry a hash"
2488        );
2489
2490        let entry = manifest
2491            .units
2492            .iter()
2493            .find(|entry| entry.name == "serde-102a33f055a8d229")
2494            .expect("unit");
2495        let file = entry
2496            .files
2497            .iter()
2498            .find(|file| file.path == "deps/libserde-102a33f055a8d229.rlib")
2499            .expect("rlib");
2500        let path = units_dir(store.path(), &parity)
2501            .join(&entry.name)
2502            .join(&file.path);
2503        let mut bytes = fs::read(&path)?;
2504        let last = bytes.len() - 1;
2505        bytes[last] ^= 0x01; // same size, different content
2506        #[cfg(unix)]
2507        {
2508            use std::os::unix::fs::PermissionsExt;
2509            fs::set_permissions(&path, fs::Permissions::from_mode(0o644))?;
2510        }
2511        fs::write(&path, &bytes)?;
2512        let stamp = UNIX_EPOCH + Duration::from_secs(entry.stamp);
2513        fs::File::open(&path)?.set_modified(stamp)?;
2514        #[cfg(unix)]
2515        {
2516            use std::os::unix::fs::PermissionsExt;
2517            fs::set_permissions(&path, fs::Permissions::from_mode(0o444))?;
2518        }
2519
2520        let reports = verify(store.path())?;
2521        assert!(
2522            reports[0]
2523                .problems
2524                .iter()
2525                .any(|problem| problem.contains("content drift")),
2526            "{:?}",
2527            reports[0].problems
2528        );
2529
2530        let view = tempfile::tempdir()?;
2531        let report = restore(view.path(), store.path(), None, false)?;
2532        assert!(report.errors > 0, "tampered artifact must not install");
2533        assert!(
2534            !view.path().join(&file.dest).exists(),
2535            "tampered artifact must be absent from the view"
2536        );
2537        Ok(())
2538    }
2539
2540    #[test]
2541    fn concurrent_harvest_keeps_every_unit() -> Result<()> {
2542        let store = tempfile::tempdir()?;
2543        let mut builds = Vec::new();
2544        for index in 0..6u32 {
2545            let build = tempfile::tempdir()?;
2546            let hex = format!("{:016x}", index + 1);
2547            let unit = build
2548                .path()
2549                .join("debug/.fingerprint")
2550                .join(format!("u{index}-{hex}"));
2551            fs::create_dir_all(&unit)?;
2552            fs::write(unit.join(format!("lib-u{index}")), "0123456789abcdef")?;
2553            fs::write(
2554                unit.join(format!("lib-u{index}.json")),
2555                r#"{"rustc":1,"features":"[]","declared_features":"[]","deps":[]}"#,
2556            )?;
2557            fs::create_dir_all(build.path().join("debug/deps"))?;
2558            fs::write(
2559                build
2560                    .path()
2561                    .join("debug/deps")
2562                    .join(format!("libu{index}-{hex}.rlib")),
2563                [index as u8; 8],
2564            )?;
2565            builds.push(build);
2566        }
2567        std::thread::scope(|scope| {
2568            for build in &builds {
2569                scope.spawn(|| harvest(build.path(), store.path(), None, false));
2570            }
2571        });
2572        let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2573        assert_eq!(manifest.units.len(), builds.len(), "{:?}", manifest.units);
2574        Ok(())
2575    }
2576
2577    #[cfg(unix)]
2578    #[test]
2579    fn store_directories_are_private() -> Result<()> {
2580        use std::os::unix::fs::MetadataExt;
2581        let build = tempfile::tempdir()?;
2582        let store = tempfile::tempdir()?;
2583        synthetic_build_dir(build.path())?;
2584        harvest(build.path(), store.path(), None, false)?;
2585        assert_eq!(
2586            fs::metadata(store.path())?.mode() & 0o077,
2587            0,
2588            "store root 0700"
2589        );
2590        let generation = store.path().join(parity_key()?);
2591        assert_eq!(
2592            fs::metadata(&generation)?.mode() & 0o077,
2593            0,
2594            "generation 0700"
2595        );
2596        Ok(())
2597    }
2598
2599    #[test]
2600    fn verify_ordering_is_quiet_when_units_are_current() -> Result<()> {
2601        let build = tempfile::tempdir()?;
2602        let store = tempfile::tempdir()?;
2603        let now = now_secs();
2604        synthetic_edge_dir(build.path(), now - 3600, now)?;
2605        harvest(build.path(), store.path(), None, false)?;
2606
2607        let reports = verify(store.path())?;
2608        assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2609        assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
2610        Ok(())
2611    }
2612}