1use 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 sha2::{Digest, Sha256};
41use walkdir::WalkDir;
42
43use crate::fingerprint::{self, ArtifactInfo, Unit};
44
45const PARITY_SEED: &str = "epicenter-parity-v3";
47
48#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Toolchain {
53 pub release: String,
55 pub commit: String,
57 pub host: String,
59}
60
61impl Toolchain {
62 pub fn version_line(&self) -> String {
64 format!("rustc {} ({})", self.release, self.commit)
65 }
66}
67
68pub fn toolchain() -> Result<Toolchain> {
70 let mut command = util::command_for("rustc")?;
71 let output = command.arg("-vV").output().context("running `rustc -vV`")?;
72 if !output.status.success() {
73 anyhow::bail!("`rustc -vV` failed — is a toolchain installed?");
74 }
75 let text = String::from_utf8_lossy(&output.stdout);
76
77 let mut release = "unknown".to_string();
78 let mut commit = "unknown".to_string();
79 let mut host = "unknown".to_string();
80 for line in text.lines() {
81 if let Some(rest) = line.strip_prefix("rustc ") {
82 let mut parts = rest.split_whitespace();
83 if let Some(version) = parts.next() {
84 release = version.to_string();
85 }
86 if let Some(commit_part) = parts.next() {
88 let value = commit_part.trim_start_matches('(');
89 if !value.is_empty() {
90 commit = value.to_string();
91 }
92 }
93 } else if let Some(value) = line.strip_prefix("host: ") {
94 host = value.trim().to_string();
95 }
96 }
97 util::validate_component(&release, "rustc release")?;
98 util::validate_component(&commit, "rustc commit")?;
99 util::validate_component(&host, "rustc host")?;
100 Ok(Toolchain {
101 release,
102 commit,
103 host,
104 })
105}
106
107pub fn toolchain_install_time() -> Option<SystemTime> {
111 let mut command = util::command_for("rustc").ok()?;
112 let output = command.args(["--print", "sysroot"]).output().ok()?;
113 if !output.status.success() {
114 return None;
115 }
116 let sysroot = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
117 let rustc = sysroot
118 .join("bin")
119 .join(if cfg!(windows) { "rustc.exe" } else { "rustc" });
120 fs::metadata(&rustc)
121 .and_then(|meta| meta.modified())
122 .ok()
123 .or_else(|| fs::metadata(&sysroot).and_then(|meta| meta.modified()).ok())
124}
125
126pub fn parity_key() -> Result<String> {
131 let tc = toolchain()?;
132 let key = format!(
133 "{}-{}-{}-{}",
134 tc.release,
135 tc.commit,
136 tc.host,
137 flags_digest()
138 );
139 util::validate_component(&key, "parity key")?;
142 Ok(key)
143}
144
145fn flags_digest() -> String {
148 let mut input = String::from(PARITY_SEED);
149 for name in [
150 "RUSTFLAGS",
151 "RUSTDOCFLAGS",
152 "CARGO_BUILD_RUSTFLAGS",
153 "CARGO_BUILD_RUSTDOCFLAGS",
154 "CARGO_BUILD_TARGET",
155 "RUSTC_WRAPPER",
156 "CARGO_BUILD_RUSTC_WRAPPER",
157 "RUSTC",
158 ] {
159 input.push('\0');
160 input.push_str(name);
161 input.push('=');
162 input.push_str(&std::env::var(name).unwrap_or_default());
163 }
164 if let Ok(path) = crate::config::cargo_config_path() {
165 if let Ok(text) = crate::util::read_trusted_config(&path, 1024 * 1024) {
166 for key in ["rustflags", "rustdocflags", "target", "rustc-wrapper"] {
167 if let Some(value) = crate::config::read_table_key(&text, "build", key) {
168 input.push('\0');
169 input.push_str(key);
170 input.push('=');
171 input.push_str(&value);
172 }
173 }
174 }
175 }
176 if let Ok(mut cargo) = util::command_for("cargo") {
177 if let Ok(output) = cargo.arg("-V").output() {
178 if output.status.success() {
179 input.push('\0');
180 input.push_str("cargo=");
181 input.push_str(String::from_utf8_lossy(&output.stdout).trim());
182 }
183 }
184 }
185 format!("{:08x}", (fnv1a64(input.as_bytes()) >> 32) as u32)
186}
187
188fn fnv1a64(bytes: &[u8]) -> u64 {
189 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
190 for byte in bytes {
191 hash ^= *byte as u64;
192 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
193 }
194 hash
195}
196
197pub fn store_root(build_root: &Path) -> PathBuf {
202 build_root.parent().unwrap_or(build_root).join("store")
203}
204
205pub fn units_dir(store_root: &Path, parity: &str) -> PathBuf {
207 store_root.join(parity).join("units")
208}
209
210fn manifest_path(store_root: &Path, parity: &str) -> PathBuf {
211 store_root.join(parity).join("manifest.json")
212}
213
214fn store_lock_path(store_root: &Path, parity: &str) -> PathBuf {
217 store_root.join(format!(".lock-{parity}"))
218}
219
220fn ensure_store_root(store_root: &Path) -> Result<()> {
221 util::ensure_windows_user_path(store_root, "store")?;
222 match fs::symlink_metadata(store_root) {
223 Ok(meta) => {
224 if !meta.is_dir() || meta.file_type().is_symlink() {
225 anyhow::bail!(
226 "store root {} is not a real directory",
227 store_root.display()
228 );
229 }
230 util::ensure_private_dir(store_root)?;
231 }
232 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
233 util::create_private_dir_all(store_root)?;
234 }
235 Err(err) => {
236 return Err(err).with_context(|| format!("inspecting {}", store_root.display()));
237 }
238 }
239 Ok(())
240}
241
242#[derive(Debug, Clone, Default, Serialize, Deserialize)]
246pub struct Manifest {
247 pub parity_key: String,
248 pub toolchain: String,
249 pub created: u64,
251 pub updated: u64,
253 pub units: Vec<UnitEntry>,
254 #[serde(default, skip_serializing_if = "String::is_empty")]
257 pub mac: String,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct UnitEntry {
263 pub name: String,
265 pub profile: String,
266 pub memo: String,
268 pub hash: String,
270 pub stamp: u64,
272 pub files: Vec<FileEntry>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct FileEntry {
278 pub path: String,
280 pub dest: String,
282 pub size: u64,
283 #[serde(default)]
285 pub sha256: String,
286 pub role: String,
288}
289
290impl Manifest {
291 pub fn validate(&self) -> Result<()> {
294 for unit in &self.units {
295 util::validate_component(&unit.name, "unit name")?;
296 util::validate_component(&unit.profile, "profile")?;
297 util::validate_component(&unit.memo, "memo file")?;
298 if !unit.hash.is_empty() && !fingerprint::looks_like_hash(&unit.hash) {
299 anyhow::bail!("invalid memo hash {:?} in unit {}", unit.hash, unit.name);
300 }
301 for file in &unit.files {
302 util::validate_rel(&file.path, "stored file path")?;
303 util::validate_rel(&file.dest, "destination path")?;
304 if !file.sha256.is_empty()
305 && (file.sha256.len() != 64
306 || !file.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()))
307 {
308 anyhow::bail!("invalid SHA-256 for {} in unit {}", file.path, unit.name);
309 }
310 }
311 }
312 Ok(())
313 }
314}
315
316fn manifest_key_path() -> Result<PathBuf> {
319 let path = crate::paths::cargo_home()?
320 .join("epicenter")
321 .join("manifest.key");
322 #[cfg(windows)]
323 crate::util::ensure_windows_user_path(&path, "vault key")?;
324 Ok(path)
325}
326
327fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
328 let mut block = [0u8; 64];
329 if key.len() > block.len() {
330 let digest = Sha256::digest(key);
331 block[..32].copy_from_slice(&digest);
332 } else {
333 block[..key.len()].copy_from_slice(key);
334 }
335 let mut inner = Sha256::new();
336 let mut outer = Sha256::new();
337 let mut ipad = [0x36u8; 64];
338 let mut opad = [0x5cu8; 64];
339 for index in 0..64 {
340 ipad[index] ^= block[index];
341 opad[index] ^= block[index];
342 }
343 inner.update(ipad);
344 inner.update(data);
345 let inner = inner.finalize();
346 outer.update(opad);
347 outer.update(inner);
348 let out = outer.finalize();
349 let mut result = [0u8; 32];
350 result.copy_from_slice(&out);
351 result
352}
353
354fn hex32(bytes: &[u8; 32]) -> String {
355 let mut out = String::with_capacity(64);
356 for byte in bytes {
357 out.push_str(&format!("{byte:02x}"));
358 }
359 out
360}
361
362fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
363 if left.len() != right.len() {
364 return false;
365 }
366 let mut diff = 0u8;
367 for (a, b) in left.iter().zip(right) {
368 diff |= a ^ b;
369 }
370 diff == 0
371}
372
373fn read_key_file(path: &Path) -> Result<Vec<u8>> {
374 let mut options = fs::OpenOptions::new();
375 options.read(true);
376 #[cfg(unix)]
377 {
378 use std::os::unix::fs::OpenOptionsExt;
379 options.custom_flags(libc::O_NOFOLLOW);
380 }
381 let mut file = options
382 .open(path)
383 .with_context(|| format!("opening vault key {}", path.display()))?;
384 let meta = file.metadata()?;
385 if !meta.is_file() || util::is_reparse_point(&meta) {
386 anyhow::bail!("vault key {} is not a regular file", path.display());
387 }
388 if !util::owned_by_us(&meta) {
389 anyhow::bail!("vault key {} is owned by another user", path.display());
390 }
391 #[cfg(unix)]
392 {
393 use std::os::unix::fs::MetadataExt;
394 if meta.mode() & 0o077 != 0 {
395 anyhow::bail!(
396 "vault key {} is accessible by other users (mode {:o})",
397 path.display(),
398 meta.mode() & 0o777
399 );
400 }
401 }
402 let mut key = Vec::new();
403 std::io::Read::read_to_end(&mut file, &mut key)?;
404 if key.len() != 32 {
405 anyhow::bail!("vault key {} is malformed", path.display());
406 }
407 Ok(key)
408}
409
410fn manifest_key() -> Result<Vec<u8>> {
411 let path = manifest_key_path()?;
412 if let Ok(key) = read_key_file(&path) {
413 return Ok(key);
414 }
415 let parent = path
416 .parent()
417 .with_context(|| format!("{} has no parent", path.display()))?;
418 util::create_private_dir_all(parent)?;
419 let mut key = [0u8; 32];
420 getrandom::fill(&mut key).map_err(|err| anyhow::anyhow!("generating vault key: {err}"))?;
421 let mut options = fs::OpenOptions::new();
422 options.write(true).create_new(true);
423 #[cfg(unix)]
424 {
425 use std::os::unix::fs::OpenOptionsExt;
426 options.mode(0o600);
427 }
428 match options.open(&path) {
429 Ok(mut file) => {
430 file.write_all(&key)?;
431 file.sync_all()?;
432 Ok(key.to_vec())
433 }
434 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => read_key_file(&path),
435 Err(err) => Err(err).with_context(|| format!("creating vault key {}", path.display())),
436 }
437}
438
439fn manifest_mac(manifest: &Manifest) -> Result<String> {
440 let mut unsigned = manifest.clone();
441 unsigned.mac.clear();
442 let body = serde_json::to_vec(&unsigned)?;
443 Ok(hex32(&hmac_sha256(&manifest_key()?, &body)))
444}
445
446fn load_manifest(store_root: &Path, parity: &str) -> Result<Option<Manifest>> {
447 let path = manifest_path(store_root, parity);
448 let meta = match fs::symlink_metadata(&path) {
449 Ok(meta) => meta,
450 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
451 Err(err) => {
452 return Err(err).with_context(|| format!("inspecting {}", path.display()));
453 }
454 };
455 if !meta.is_file() || meta.file_type().is_symlink() || util::is_reparse_point(&meta) {
458 anyhow::bail!("{} is not a regular manifest file", path.display());
459 }
460 if !util::owned_by_us(&meta) {
461 anyhow::bail!("{} is owned by another user", path.display());
462 }
463 #[cfg(unix)]
464 {
465 use std::os::unix::fs::MetadataExt;
466 if meta.mode() & 0o022 != 0 {
467 anyhow::bail!(
468 "{} is group/world-writable (mode {:o})",
469 path.display(),
470 meta.mode() & 0o777
471 );
472 }
473 }
474 let text = crate::util::read_trusted_config(&path, 16 * 1024 * 1024)
475 .with_context(|| format!("reading {}", path.display()))?;
476 let manifest: Manifest =
477 serde_json::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
478 if manifest.mac.len() != 64 {
479 anyhow::bail!(
480 "manifest {} is unsigned (re-harvest this generation)",
481 path.display()
482 );
483 }
484 let expected = manifest_mac(&manifest)?;
485 if !constant_time_eq(manifest.mac.as_bytes(), expected.as_bytes()) {
486 anyhow::bail!("manifest {} failed its MAC check", path.display());
487 }
488 manifest
489 .validate()
490 .with_context(|| format!("validating {}", path.display()))?;
491 Ok(Some(manifest))
492}
493
494pub(crate) fn save_manifest(store_root: &Path, parity: &str, manifest: &Manifest) -> Result<()> {
495 util::validate_component(parity, "generation")?;
496 let dir = store_root.join(parity);
497 util::create_private_dir_all(&dir)?;
498 let mut signed = manifest.clone();
499 signed.mac.clear();
500 signed.mac = manifest_mac(&signed)?;
501 let what = serde_json::to_string_pretty(&signed)?;
502 let mut tmp = None;
504 for attempt in 0..100u32 {
505 let candidate = dir.join(format!(
506 "manifest.json.tmp-{}-{attempt}",
507 std::process::id()
508 ));
509 match fs::OpenOptions::new()
510 .write(true)
511 .create_new(true)
512 .open(&candidate)
513 {
514 Ok(file) => {
515 #[cfg(unix)]
516 {
517 use std::os::unix::fs::PermissionsExt;
518 let _ = file.set_permissions(fs::Permissions::from_mode(0o600));
519 }
520 tmp = Some((candidate, file));
521 break;
522 }
523 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
524 Err(err) => {
525 return Err(err).with_context(|| format!("creating {}", candidate.display()));
526 }
527 }
528 }
529 let Some((tmp, mut file)) = tmp else {
530 anyhow::bail!(
531 "could not create a unique manifest temp file in {}",
532 dir.display()
533 );
534 };
535 file.write_all(what.as_bytes())?;
536 file.write_all(b"\n")?;
537 file.sync_all()?;
538 drop(file);
539 fs::rename(&tmp, manifest_path(store_root, parity))?;
540 #[cfg(unix)]
541 if let Ok(parent) = fs::File::open(&dir) {
542 let _ = parent.sync_all();
543 }
544 Ok(())
545}
546
547pub fn generation_names(store_root: &Path) -> Result<Vec<String>> {
549 if !store_root.is_dir() {
550 return Ok(Vec::new());
551 }
552 let mut names = Vec::new();
553 for entry in
554 fs::read_dir(store_root).with_context(|| format!("reading {}", store_root.display()))?
555 {
556 let entry = entry?;
557 if !entry.file_type()?.is_dir() {
558 continue;
559 }
560 let name = entry.file_name().to_string_lossy().into_owned();
561 if !name.starts_with('.') {
562 names.push(name);
563 }
564 }
565 names.sort();
566 Ok(names)
567}
568
569#[derive(Debug, Clone)]
573pub struct StoredUnit {
574 pub generation: String,
576 pub entry: UnitEntry,
578 pub dir: PathBuf,
580}
581
582pub fn stored_units(store_root: &Path) -> Result<Vec<StoredUnit>> {
584 if store_root.exists() {
585 check_store_trust(store_root)?;
586 }
587 let mut out = Vec::new();
588 for generation in generation_names(store_root)? {
589 let Some(manifest) = load_manifest(store_root, &generation)? else {
590 continue;
591 };
592 let units = units_dir(store_root, &generation);
593 for entry in manifest.units {
594 let dir = units.join(&entry.name);
595 out.push(StoredUnit {
596 generation: generation.clone(),
597 entry,
598 dir,
599 });
600 }
601 }
602 Ok(out)
603}
604
605pub fn stored_unit_rustc(unit: &StoredUnit) -> Option<String> {
607 let dir_meta = fs::symlink_metadata(&unit.dir).ok()?;
608 if !dir_meta.is_dir() || dir_meta.file_type().is_symlink() || util::is_reparse_point(&dir_meta)
609 {
610 return None;
611 }
612 let rel = PathBuf::from(format!("{}.json", unit.entry.memo));
613 util::check_regular_chain(&unit.dir, &rel).ok()?;
614 let text = fs::read_to_string(unit.dir.join(&rel)).ok()?;
615 let value: serde_json::Value = serde_json::from_str(&text).ok()?;
616 match value.get("rustc") {
617 Some(serde_json::Value::Number(number)) => Some(number.to_string()),
618 Some(serde_json::Value::String(text)) => Some(text.clone()),
619 _ => None,
620 }
621}
622
623pub fn drop_units(store_root: &Path, generation: &str, names: &[String]) -> Result<usize> {
627 util::validate_component(generation, "generation")?;
628 if store_root.exists() {
629 check_store_trust(store_root)?;
630 }
631 let _lock = FileLock::acquire(&store_lock_path(store_root, generation))?;
632 let Some(mut manifest) = load_manifest(store_root, generation)? else {
633 return Ok(0);
634 };
635 let units = units_dir(store_root, generation);
636 match fs::symlink_metadata(&units) {
637 Ok(meta)
638 if meta.is_dir()
639 && !meta.file_type().is_symlink()
640 && !util::is_reparse_point(&meta) => {}
641 Ok(_) => {
642 anyhow::bail!(
643 "units dir {} is a symlink/reparse or not a directory",
644 units.display()
645 );
646 }
647 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
648 }
650 Err(err) => {
651 return Err(err).with_context(|| format!("inspecting {}", units.display()));
652 }
653 }
654 let mut kept = Vec::with_capacity(manifest.units.len());
655 let mut dropped = 0;
656 for entry in manifest.units.drain(..) {
657 if !names.contains(&entry.name) {
658 kept.push(entry);
659 continue;
660 }
661 util::validate_component(&entry.name, "unit name")?;
662 let dir = units.join(&entry.name);
663 match fs::symlink_metadata(&dir) {
664 Ok(meta) if meta.file_type().is_symlink() || util::is_reparse_point(&meta) => {
665 eprintln!(
666 "could not remove symlinked/reparse unit {}: refused",
667 dir.display()
668 );
669 kept.push(entry);
670 }
671 Ok(meta) if meta.is_dir() => {
672 if let Ok(base) = fs::canonicalize(&units) {
675 match fs::canonicalize(&dir) {
676 Ok(canon) if canon.parent() == Some(base.as_path()) && canon != base => {}
677 _ => {
678 eprintln!(
679 "could not remove {}: escapes {}",
680 dir.display(),
681 units.display()
682 );
683 kept.push(entry);
684 continue;
685 }
686 }
687 }
688 match fs::remove_dir_all(&dir) {
689 Ok(()) => {
690 dropped += 1;
691 }
692 Err(err) => {
693 eprintln!("could not remove {}: {err}", dir.display());
694 kept.push(entry);
695 }
696 }
697 }
698 Ok(_) => {
699 eprintln!("could not remove {}: not a directory", dir.display());
700 kept.push(entry);
701 }
702 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
703 dropped += 1;
704 }
705 Err(err) => {
706 eprintln!("could not remove {}: {err}", dir.display());
707 kept.push(entry);
708 }
709 }
710 }
711 manifest.units = kept;
712 if manifest.units.is_empty() {
713 let _ = fs::remove_dir_all(store_root.join(generation));
714 } else if dropped > 0 {
715 manifest.updated = now_secs();
716 save_manifest(store_root, generation, &manifest)?;
717 }
718 Ok(dropped)
719}
720#[derive(Debug, Default)]
724pub struct HarvestReport {
725 pub parity: String,
726 pub toolchain: String,
727 pub stored: usize,
728 pub refreshed: usize,
729 pub unchanged: usize,
730 pub skipped_stale: usize,
731 pub skipped_incomplete: usize,
732 pub files: usize,
733 pub bytes: u64,
734 pub note: Option<String>,
735}
736
737pub fn harvest(
744 build_root: &Path,
745 store_root: &Path,
746 unit_filter: Option<&str>,
747 dry_run: bool,
748) -> Result<HarvestReport> {
749 let tc = toolchain()?;
750 let parity = parity_key()?;
751 let mut report = HarvestReport {
752 parity: parity.clone(),
753 toolchain: tc.version_line(),
754 ..Default::default()
755 };
756
757 let _lock = if dry_run {
760 None
761 } else {
762 ensure_store_root(store_root)?;
763 util::create_private_dir_all(&store_root.join(&parity))?;
764 util::create_private_dir_all(&units_dir(store_root, &parity))?;
765 let lock = FileLock::acquire(&store_lock_path(store_root, &parity))?;
766 sweep_staging(&units_dir(store_root, &parity));
769 Some(lock)
770 };
771
772 let rows: Vec<(Unit, ArtifactInfo)> = fingerprint::read_units(build_root)?
773 .into_iter()
774 .map(|unit| {
775 let arts = fingerprint::artifacts_for(build_root, &unit);
776 (unit, arts)
777 })
778 .collect();
779
780 let newest_activity = rows.iter().filter_map(|(_, arts)| arts.newest).max();
783 if let (Some(newest), Some(installed)) = (newest_activity, toolchain_install_time()) {
784 if newest < installed {
785 report.note = Some(format!(
786 "the shared cache predates the installed toolchain ({}) — build once, then harvest",
787 tc.version_line()
788 ));
789 return Ok(report);
790 }
791 }
792
793 let active_gen = rows
797 .iter()
798 .filter(|(unit, _)| crate::who::rustc_of(unit) != "unknown")
799 .max_by_key(|(_, arts)| arts.newest)
800 .map(|(unit, _)| crate::who::rustc_of(unit))
801 .unwrap_or_else(|| "unknown".into());
802
803 let mut manifest = load_manifest(store_root, &parity)?.unwrap_or_else(|| Manifest {
804 parity_key: parity.clone(),
805 toolchain: tc.version_line(),
806 created: now_secs(),
807 updated: 0,
808 units: Vec::new(),
809 mac: String::new(),
810 });
811
812 struct GroupPlan {
814 unit: Unit,
815 files: Vec<(PathBuf, String, String, String)>,
816 memo: String,
817 hash: String,
818 base: u64,
819 }
820 let mut plans: Vec<GroupPlan> = Vec::new();
821
822 for (unit, arts) in &rows {
823 if let Some(query) = unit_filter {
824 if !unit_name_matches(&unit.dir_name, query) {
825 continue;
826 }
827 }
828 let cohort = crate::who::rustc_of(unit);
829 if cohort != "unknown" && active_gen != "unknown" && cohort != active_gen {
830 report.skipped_stale += 1;
831 continue;
832 }
833
834 let unit_dir = build_root
835 .join(&unit.profile)
836 .join(".fingerprint")
837 .join(&unit.dir_name);
838 let profile_dir = build_root.join(&unit.profile);
839 let memo = unit.memo_file.clone();
840 let mut files: Vec<(PathBuf, String, String, String)> = Vec::new();
841 for entry in
848 fs::read_dir(&unit_dir).with_context(|| format!("reading {}", unit_dir.display()))?
849 {
850 let entry = entry?;
851 if !entry.file_type()?.is_file() {
852 continue;
853 }
854 let name = entry.file_name().to_string_lossy().into_owned();
855 if is_volatile(&name) {
856 continue;
857 }
858 let role = if name.starts_with("dep-") {
859 "dep-info"
860 } else if name.ends_with(".json") {
861 "fingerprint"
862 } else if fs::read_to_string(entry.path())
863 .map(|text| crate::fingerprint::looks_like_hash(text.trim()))
864 .unwrap_or(false)
865 {
866 "memo"
867 } else {
868 "extra"
870 };
871 files.push((
872 entry.path(),
873 name.clone(),
874 format!(".fingerprint/{}/{name}", unit.dir_name),
875 role.into(),
876 ));
877 }
878
879 let mut artifact_count = 0;
882 for (path, _, _) in &arts.paths {
883 let name = path
884 .file_name()
885 .map(|name| name.to_string_lossy().into_owned())
886 .unwrap_or_default();
887 if is_volatile(&name) {
888 continue;
889 }
890 let Ok(rel) = path.strip_prefix(&profile_dir) else {
891 continue;
892 };
893 let rel = rel.to_string_lossy().replace('\\', "/");
894 if rel.starts_with("deps/") && rel.ends_with(".d") {
895 continue;
896 }
897 let role = if rel.starts_with("build/") {
898 "build-tree"
899 } else {
900 "artifact"
901 };
902 files.push((path.clone(), rel.clone(), rel, role.into()));
903 artifact_count += 1;
904 }
905 if artifact_count == 0 {
906 report.skipped_incomplete += 1;
907 continue;
908 }
909
910 plans.push(GroupPlan {
911 base: group_stamp(&files)?,
912 memo,
913 hash: unit.memo_hash.clone().unwrap_or_default(),
914 files,
915 unit: unit.clone(),
916 });
917 }
918
919 let stored_stamps: std::collections::HashMap<String, u64> = manifest
928 .units
929 .iter()
930 .map(|entry| (entry.hash.clone(), entry.stamp))
931 .collect();
932 let index: std::collections::HashMap<&str, usize> = plans
933 .iter()
934 .enumerate()
935 .map(|(position, plan)| (plan.hash.as_str(), position))
936 .collect();
937 let mut plan_deps: Vec<Vec<usize>> = Vec::with_capacity(plans.len());
938 let mut stamps: Vec<u64> = Vec::with_capacity(plans.len());
939 for plan in &plans {
940 let mut deps = Vec::new();
941 let mut stamp = plan.base;
942 for dep in plan
943 .unit
944 .json
945 .as_ref()
946 .map(|json| json.deps.as_slice())
947 .unwrap_or(&[])
948 {
949 let Some(hash) = dep.memo_hash() else {
950 continue;
951 };
952 if let Some(&at) = index.get(hash.as_str()) {
953 deps.push(at);
954 } else if let Some(known) = stored_stamps.get(hash.as_str()) {
955 stamp = stamp.max(*known);
956 }
957 }
958 plan_deps.push(deps);
959 stamps.push(stamp);
960 }
961 let mut order: Vec<usize> = Vec::with_capacity(plans.len());
964 let mut settled = vec![false; plans.len()];
965 loop {
966 let mut progressed = false;
967 for at in 0..plans.len() {
968 if settled[at] || !plan_deps[at].iter().all(|&dep| settled[dep]) {
969 continue;
970 }
971 settled[at] = true;
972 order.push(at);
973 progressed = true;
974 }
975 if !progressed {
976 break;
977 }
978 }
979 for at in 0..plans.len() {
980 if !settled[at] {
981 order.push(at);
982 }
983 }
984 for &at in &order {
985 for dep in &plan_deps[at] {
986 stamps[at] = stamps[at].max(stamps[*dep]);
987 }
988 }
989
990 for (at, plan) in plans.iter().enumerate() {
992 let unit = &plan.unit;
993 let files = &plan.files;
994 let stamp = stamps[at];
995 let group_dir = units_dir(store_root, &parity).join(&unit.dir_name);
996
997 let mut new_entries: Vec<(&str, &str)> = files
1000 .iter()
1001 .map(|(_, path, _, role)| (path.as_str(), role.as_str()))
1002 .collect();
1003 new_entries.sort_unstable();
1004 let already = manifest
1005 .units
1006 .iter()
1007 .find(|entry| entry.name == unit.dir_name)
1008 .map(|entry| {
1009 let mut stored: Vec<(&str, &str)> = entry
1010 .files
1011 .iter()
1012 .map(|file| (file.path.as_str(), file.role.as_str()))
1013 .collect();
1014 stored.sort_unstable();
1015 entry.hash == plan.hash
1016 && entry.profile == unit.profile
1017 && entry.stamp == stamp
1018 && stored == new_entries
1019 && entry
1020 .files
1021 .iter()
1022 .all(|file| !file.sha256.is_empty() && group_dir.join(&file.path).is_file())
1023 })
1024 .unwrap_or(false);
1025 if already {
1026 report.unchanged += 1;
1027 continue;
1028 }
1029
1030 let bytes: u64 = files
1031 .iter()
1032 .filter_map(|(src, _, _, _)| fs::metadata(src).ok())
1033 .map(|meta| meta.len())
1034 .sum();
1035 if dry_run {
1036 report.stored += 1;
1037 report.files += files.len();
1038 report.bytes += bytes;
1039 continue;
1040 }
1041
1042 let mut entries = Vec::with_capacity(files.len());
1043 store_group(
1044 store_root,
1045 &parity,
1046 &unit.dir_name,
1047 stamp,
1048 files,
1049 &mut entries,
1050 )
1051 .with_context(|| format!("storing unit {}", unit.dir_name))?;
1052
1053 let entry = UnitEntry {
1054 name: unit.dir_name.clone(),
1055 profile: unit.profile.clone(),
1056 memo: plan.memo.clone(),
1057 hash: plan.hash.clone(),
1058 stamp,
1059 files: entries,
1060 };
1061 match manifest
1062 .units
1063 .iter_mut()
1064 .find(|existing| existing.name == entry.name)
1065 {
1066 Some(slot) => {
1067 *slot = entry;
1068 report.refreshed += 1;
1069 }
1070 None => {
1071 manifest.units.push(entry);
1072 report.stored += 1;
1073 }
1074 }
1075 report.files += files.len();
1076 report.bytes += bytes;
1077 }
1078
1079 if !dry_run {
1080 manifest.units.sort_by(|a, b| {
1081 (a.profile.as_str(), a.name.as_str()).cmp(&(b.profile.as_str(), b.name.as_str()))
1082 });
1083 manifest.updated = now_secs();
1084 save_manifest(store_root, &parity, &manifest)?;
1085 }
1086 Ok(report)
1087}
1088
1089fn store_group(
1093 store_root: &Path,
1094 parity: &str,
1095 name: &str,
1096 stamp: u64,
1097 files: &[(PathBuf, String, String, String)],
1098 entries: &mut Vec<FileEntry>,
1099) -> Result<()> {
1100 util::validate_component(name, "unit name")?;
1101 let units = units_dir(store_root, parity);
1102 let final_dir = units.join(name);
1103 let mut tmp = None;
1106 for attempt in 0..100u32 {
1107 let candidate = units.join(format!(".{name}.tmp-{}-{attempt}", std::process::id()));
1108 match fs::create_dir(&candidate) {
1109 Ok(()) => {
1110 tmp = Some(candidate);
1111 break;
1112 }
1113 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
1114 Err(err) => {
1115 return Err(err)
1116 .with_context(|| format!("creating staging dir {}", candidate.display()));
1117 }
1118 }
1119 }
1120 let Some(tmp) = tmp else {
1121 anyhow::bail!(
1122 "could not create a unique staging dir in {}",
1123 units.display()
1124 );
1125 };
1126 #[cfg(unix)]
1127 {
1128 use std::os::unix::fs::PermissionsExt;
1129 let _ = fs::set_permissions(&tmp, fs::Permissions::from_mode(0o700));
1130 }
1131
1132 if let Err(err) = stage_group(&tmp, files, stamp, entries) {
1133 let _ = crate::util::force_remove_dir(&tmp);
1136 return Err(err);
1137 }
1138
1139 let old = units.join(format!(".{name}.old-{}-{}", std::process::id(), now_secs()));
1140 match fs::symlink_metadata(&final_dir) {
1141 Ok(meta) if meta.file_type().is_symlink() || util::is_reparse_point(&meta) => {
1142 let _ = crate::util::force_remove_dir(&tmp);
1143 anyhow::bail!(
1144 "refusing symlinked/reparse store unit {}",
1145 final_dir.display()
1146 );
1147 }
1148 Ok(meta) if meta.is_dir() => fs::rename(&final_dir, &old)?,
1149 Ok(_) => {
1150 let _ = crate::util::force_remove_dir(&tmp);
1151 anyhow::bail!("store unit path {} is not a directory", final_dir.display());
1152 }
1153 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1154 Err(err) => return Err(err).context("inspecting store unit"),
1155 }
1156 fs::rename(&tmp, &final_dir)?;
1157 if fs::symlink_metadata(&old)
1158 .map(|meta| {
1159 meta.is_dir() && !meta.file_type().is_symlink() && !util::is_reparse_point(&meta)
1160 })
1161 .unwrap_or(false)
1162 {
1163 let _ = fs::remove_dir_all(&old);
1164 }
1165 Ok(())
1166}
1167
1168fn stage_group(
1177 tmp: &Path,
1178 files: &[(PathBuf, String, String, String)],
1179 stamp: u64,
1180 entries: &mut Vec<FileEntry>,
1181) -> Result<()> {
1182 for (src, path, dest, role) in files {
1183 let rel = util::validate_rel(path, "stored file path")?;
1184 let dst = tmp.join(&rel);
1185 if let Some(parent) = dst.parent() {
1186 util::create_private_dir_all(parent)?;
1187 }
1188 let (size, sha256) = util::copy_file_hashed(src, &dst)
1189 .with_context(|| format!("copying {} -> {}", src.display(), dst.display()))?;
1190 make_writable(&dst).with_context(|| format!("making {} writable", dst.display()))?;
1191 set_stamp(&dst, stamp)?;
1192 make_read_only(&dst)?;
1193 entries.push(FileEntry {
1194 path: path.clone(),
1195 dest: dest.clone(),
1196 size,
1197 sha256,
1198 role: role.clone(),
1199 });
1200 }
1201 Ok(())
1202}
1203
1204fn sweep_staging(units: &Path) {
1208 let Ok(entries) = fs::read_dir(units) else {
1209 return;
1210 };
1211 let now = SystemTime::now();
1212 for entry in entries.flatten() {
1213 let name = entry.file_name();
1214 let name = name.to_string_lossy();
1215 if !(name.contains(".tmp-") || name.contains(".old-")) {
1216 continue;
1217 }
1218 let meta = match fs::symlink_metadata(entry.path()) {
1219 Ok(meta) => meta,
1220 Err(_) => continue,
1221 };
1222 if meta.file_type().is_symlink() || util::is_reparse_point(&meta) {
1224 let _ = fs::remove_file(entry.path());
1225 continue;
1226 }
1227 if !meta.is_dir() {
1228 continue;
1229 }
1230 let stale = meta
1231 .modified()
1232 .ok()
1233 .and_then(|modified| now.duration_since(modified).ok())
1234 .is_some_and(|age| age > Duration::from_secs(600));
1235 if stale {
1236 let _ = crate::util::force_remove_dir(&entry.path());
1237 }
1238 }
1239}
1240
1241fn group_stamp(files: &[(PathBuf, String, String, String)]) -> Result<u64> {
1246 let mut newest: Option<SystemTime> = None;
1247 let mut metadata: Option<SystemTime> = None;
1248 for (src, path, _, _) in files {
1249 let modified = fs::metadata(src)
1250 .and_then(|meta| meta.modified())
1251 .with_context(|| format!("reading mtime of {}", src.display()))?;
1252 if newest.map_or(true, |current| modified > current) {
1253 newest = Some(modified);
1254 }
1255 if path.ends_with(".rmeta") && metadata.map_or(true, |current| modified > current) {
1256 metadata = Some(modified);
1257 }
1258 }
1259 Ok(match metadata.or(newest) {
1265 Some(time) => ceil_secs(time),
1266 None => now_secs(),
1267 })
1268}
1269
1270fn ceil_secs(time: SystemTime) -> u64 {
1272 match time.duration_since(UNIX_EPOCH) {
1273 Ok(duration) => {
1274 let secs = duration.as_secs();
1275 if duration.subsec_nanos() > 0 {
1276 secs + 1
1277 } else {
1278 secs
1279 }
1280 }
1281 Err(_) => 0,
1282 }
1283}
1284
1285fn now_secs() -> u64 {
1286 SystemTime::now()
1287 .duration_since(UNIX_EPOCH)
1288 .map(|duration| duration.as_secs())
1289 .unwrap_or(0)
1290}
1291
1292fn set_stamp(path: &Path, stamp: u64) -> Result<()> {
1293 let time = UNIX_EPOCH + Duration::from_secs(stamp);
1294 #[cfg(windows)]
1298 let file = {
1299 use std::os::windows::fs::OpenOptionsExt;
1300 fs::OpenOptions::new()
1301 .access_mode(0x0100) .open(path)
1303 .or_else(|_| fs::OpenOptions::new().write(true).open(path))
1304 .with_context(|| format!("opening {} to stamp it", path.display()))?
1305 };
1306 #[cfg(not(windows))]
1307 let file =
1308 fs::File::open(path).with_context(|| format!("opening {} to stamp it", path.display()))?;
1309 file.set_modified(time)
1310 .with_context(|| format!("stamping {}", path.display()))?;
1311 Ok(())
1312}
1313
1314#[cfg(unix)]
1316fn make_writable(path: &Path) -> std::io::Result<()> {
1317 use std::os::unix::fs::PermissionsExt;
1318 let mut perms = fs::metadata(path)?.permissions();
1319 perms.set_mode(0o644);
1320 fs::set_permissions(path, perms)
1321}
1322
1323#[cfg(not(unix))]
1324fn make_writable(path: &Path) -> std::io::Result<()> {
1325 let mut perms = fs::metadata(path)?.permissions();
1326 perms.set_readonly(false);
1327 fs::set_permissions(path, perms)
1328}
1329
1330fn make_read_only(path: &Path) -> Result<()> {
1334 #[cfg(unix)]
1335 {
1336 use std::os::unix::fs::PermissionsExt;
1337 fs::set_permissions(path, fs::Permissions::from_mode(0o444))?;
1338 }
1339 #[cfg(not(unix))]
1340 {
1341 let _ = path;
1342 }
1343 Ok(())
1344}
1345
1346fn is_volatile(name: &str) -> bool {
1350 name == "invoked.timestamp" || name.contains(".kache")
1351}
1352
1353#[derive(Debug, Default)]
1357pub struct RestoreReport {
1358 pub parity: String,
1359 pub target: PathBuf,
1360 pub units_linked: usize,
1361 pub units_already: usize,
1362 pub files_linked: usize,
1363 pub files_copied: usize,
1364 pub files_existing: usize,
1365 pub files_missing: usize,
1366 pub bytes: u64,
1367 pub errors: usize,
1368 pub fatal_errors: usize,
1371 pub note: Option<String>,
1372}
1373
1374#[cfg(unix)]
1375enum RestoreOutcome {
1376 Existing,
1377 Missing,
1378 Linked,
1379 Copied,
1380}
1381
1382#[cfg(unix)]
1387fn restore_file_at(
1388 view_root: &Path,
1389 dest_rel: &Path,
1390 src: &Path,
1391 stamp: u64,
1392 file: &FileEntry,
1393 shared: bool,
1394) -> Result<RestoreOutcome> {
1395 let parent_rel = dest_rel.parent().unwrap_or_else(|| Path::new(""));
1396 let dst_name = dest_rel
1397 .file_name()
1398 .with_context(|| format!("destination {} has no file name", dest_rel.display()))?;
1399 let parent = util::open_dir_chain_at(view_root, parent_rel)?;
1400 if parent.exists(dst_name)? {
1401 return Ok(RestoreOutcome::Existing);
1402 }
1403 let mut source = match util::open_file_nofollow(src) {
1404 Ok(file) => file,
1405 Err(err)
1406 if err
1407 .root_cause()
1408 .downcast_ref::<std::io::Error>()
1409 .map_or(false, |io| io.kind() == std::io::ErrorKind::NotFound) =>
1410 {
1411 return Ok(RestoreOutcome::Missing);
1412 }
1413 Err(err)
1414 if err
1415 .downcast_ref::<std::io::Error>()
1416 .map_or(false, |io| io.kind() == std::io::ErrorKind::NotFound) =>
1417 {
1418 return Ok(RestoreOutcome::Missing);
1419 }
1420 Err(err) => return Err(err),
1421 };
1422 let meta = source.metadata()?;
1423 if !meta.is_file() || !util::owned_by_us(&meta) {
1424 anyhow::bail!("stored file {} is not a regular owned file", src.display());
1425 }
1426 if meta.len() != file.size {
1427 anyhow::bail!(
1428 "stored file {} size drift (recorded {}, found {})",
1429 src.display(),
1430 file.size,
1431 meta.len()
1432 );
1433 }
1434 if shared {
1435 if file.sha256.is_empty() {
1436 anyhow::bail!("{} has no recorded SHA-256", src.display());
1437 }
1438 let found = util::sha256_file_fd(&mut source)?;
1439 if found != file.sha256 {
1440 anyhow::bail!("{} failed its SHA-256 check", src.display());
1441 }
1442 parent.link_path(src, dst_name)?;
1443 Ok(RestoreOutcome::Linked)
1444 } else {
1445 let mut tmp = parent.create_excl(dst_name, 0o600)?;
1450 let result = util::copy_fd_verified(&mut source, &mut tmp, file.size, &file.sha256)
1451 .and_then(|_| util::set_stamp_fd(&tmp, stamp).map_err(anyhow::Error::from));
1452 if let Err(err) = result {
1453 let _ = parent.unlink(dst_name);
1454 return Err(err);
1455 }
1456 Ok(RestoreOutcome::Copied)
1457 }
1458}
1459
1460pub fn restore(
1471 view_root: &Path,
1472 store_root: &Path,
1473 unit_filter: Option<&str>,
1474 dry_run: bool,
1475) -> Result<RestoreReport> {
1476 let parity = parity_key()?;
1477 let mut report = RestoreReport {
1478 parity: parity.clone(),
1479 target: view_root.to_path_buf(),
1480 ..Default::default()
1481 };
1482 if store_root.exists() {
1485 check_store_trust(store_root)?;
1486 }
1487 let Some(manifest) = load_manifest(store_root, &parity)? else {
1488 report.note = Some(
1489 "the vault holds no units for this generation yet — run `epicenter harvest` after a build"
1490 .into(),
1491 );
1492 return Ok(report);
1493 };
1494 if !dry_run {
1495 prepare_view_root(view_root)?;
1496 }
1497
1498 let units = units_dir(store_root, &parity);
1499 for entry in &manifest.units {
1500 if let Some(query) = unit_filter {
1501 if !unit_name_matches(&entry.name, query) {
1502 continue;
1503 }
1504 }
1505 let fp_dir = view_root
1506 .join(&entry.profile)
1507 .join(".fingerprint")
1508 .join(&entry.name);
1509 if read_small_regular(&fp_dir.join(&entry.memo))
1510 .map(|text| text.trim() == entry.hash)
1511 .unwrap_or(false)
1512 {
1513 report.units_already += 1;
1514 continue;
1515 }
1516
1517 let mut unit_touched = false;
1518 for file in &entry.files {
1519 let dest_rel = Path::new(&entry.profile).join(&file.dest);
1522 let dst = view_root.join(&dest_rel);
1523 match fs::symlink_metadata(&dst) {
1524 Ok(meta) => {
1525 if meta.file_type().is_symlink() || util::is_reparse_point(&meta) {
1526 report.errors += 1;
1527 report.fatal_errors += 1;
1528 eprintln!(
1529 "restore: refusing symlink/reparse destination {}",
1530 dst.display()
1531 );
1532 } else {
1533 report.files_existing += 1;
1534 }
1535 continue;
1536 }
1537 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1538 Err(err) => {
1539 report.errors += 1;
1540 eprintln!("restore: inspecting {}: {err}", dst.display());
1541 continue;
1542 }
1543 }
1544 let shared = file.path.starts_with("deps/") && !file.path.ends_with(".rmeta");
1549 if dry_run {
1550 unit_touched = true;
1551 if shared {
1552 report.files_linked += 1;
1553 } else {
1554 report.files_copied += 1;
1555 }
1556 report.bytes += file.size;
1557 continue;
1558 }
1559 let src = units.join(&entry.name).join(&file.path);
1560 let rel = Path::new(&entry.name).join(&file.path);
1561 #[cfg(unix)]
1562 {
1563 if let Err(err) = util::check_regular_chain(&units, &rel) {
1564 report.errors += 1;
1565 report.fatal_errors += 1;
1566 eprintln!("restore: writing {}: {err:#}", dst.display());
1567 continue;
1568 }
1569 match restore_file_at(view_root, &dest_rel, &src, entry.stamp, file, shared) {
1570 Ok(RestoreOutcome::Existing) => {
1571 report.files_existing += 1;
1572 }
1573 Ok(RestoreOutcome::Missing) => {
1574 report.files_missing += 1;
1575 }
1576 Ok(RestoreOutcome::Linked) => {
1577 report.files_linked += 1;
1578 report.bytes += file.size;
1579 unit_touched = true;
1580 }
1581 Ok(RestoreOutcome::Copied) => {
1582 report.files_copied += 1;
1583 report.bytes += file.size;
1584 unit_touched = true;
1585 }
1586 Err(err) => {
1587 report.errors += 1;
1588 report.fatal_errors += 1;
1589 eprintln!("restore: writing {}: {err:#}", dst.display());
1590 }
1591 }
1592 continue;
1593 }
1594 #[cfg(not(unix))]
1595 {
1596 if let Some(parent) = dest_rel.parent() {
1597 if let Err(err) = util::create_dir_chain_nofollow(view_root, parent) {
1598 report.errors += 1;
1599 report.fatal_errors += 1;
1600 eprintln!("restore: creating {}: {err}", parent.display());
1601 continue;
1602 }
1603 }
1604 match fs::symlink_metadata(&src) {
1605 Ok(_) => {}
1606 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1607 report.files_missing += 1;
1609 continue;
1610 }
1611 Err(err) => {
1612 report.errors += 1;
1613 report.fatal_errors += 1;
1614 eprintln!("restore: inspecting {}: {err}", src.display());
1615 continue;
1616 }
1617 }
1618 let result = if shared {
1619 util::check_regular_chain(&units, &rel)
1620 .and_then(|_| verify_store_file(&src, file))
1621 .and_then(|_| fs::hard_link(&src, &dst).map_err(Into::into))
1622 } else {
1623 util::check_regular_chain(&units, &rel)
1624 .and_then(|_| install_private_copy(&src, &dst, entry.stamp, file))
1625 };
1626 match result {
1627 Ok(()) => {
1628 if shared {
1629 report.files_linked += 1;
1630 } else {
1631 report.files_copied += 1;
1632 }
1633 report.bytes += file.size;
1634 unit_touched = true;
1635 }
1636 Err(err) => {
1637 report.errors += 1;
1638 report.fatal_errors += 1;
1639 eprintln!("restore: writing {}: {err:#}", dst.display());
1640 }
1641 }
1642 }
1643 }
1644 if unit_touched {
1645 report.units_linked += 1;
1646 }
1647 }
1648 Ok(report)
1649}
1650
1651fn check_store_trust(store_root: &Path) -> Result<()> {
1655 util::ensure_windows_user_path(store_root, "store")?;
1656 let meta = fs::symlink_metadata(store_root)
1657 .with_context(|| format!("inspecting store {}", store_root.display()))?;
1658 if !meta.is_dir() || meta.file_type().is_symlink() || util::is_reparse_point(&meta) {
1659 anyhow::bail!("store {} is not a real directory", store_root.display());
1660 }
1661 if !util::owned_by_us(&meta) {
1662 anyhow::bail!(
1663 "store {} is owned by another user — refusing to execute its artifacts",
1664 store_root.display()
1665 );
1666 }
1667 util::ensure_private_dir(store_root)?;
1671 Ok(())
1672}
1673
1674fn prepare_view_root(view_root: &Path) -> Result<()> {
1675 match fs::symlink_metadata(view_root) {
1676 Ok(meta) => {
1677 if !meta.is_dir() || meta.file_type().is_symlink() || util::is_reparse_point(&meta) {
1678 anyhow::bail!("view {} is not a real directory", view_root.display());
1679 }
1680 if !util::owned_by_us(&meta) {
1681 anyhow::bail!(
1682 "view {} is owned by another user — refusing to write into it",
1683 view_root.display()
1684 );
1685 }
1686 #[cfg(unix)]
1687 {
1688 use std::os::unix::fs::MetadataExt;
1689 if meta.mode() & 0o022 != 0 {
1690 anyhow::bail!(
1691 "view {} is group/world-writable (mode {:o}) — refusing a shared restore target; use a 0700 dir",
1692 view_root.display(),
1693 meta.mode() & 0o777
1694 );
1695 }
1696 }
1697 }
1698 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1699 util::create_private_dir_all(view_root)?;
1700 }
1701 Err(err) => {
1702 return Err(err).with_context(|| format!("inspecting view {}", view_root.display()));
1703 }
1704 }
1705 Ok(())
1711}
1712
1713#[cfg(not(unix))]
1714fn verify_store_file(src: &Path, file: &FileEntry) -> Result<()> {
1715 let meta = fs::symlink_metadata(src)
1716 .with_context(|| format!("inspecting stored file {}", src.display()))?;
1717 if !meta.file_type().is_file() || meta.file_type().is_symlink() || util::is_reparse_point(&meta)
1718 {
1719 anyhow::bail!("{} is not a regular file", src.display());
1720 }
1721 if !util::owned_by_us(&meta) {
1722 anyhow::bail!("{} is owned by another user", src.display());
1723 }
1724 if meta.len() != file.size {
1725 anyhow::bail!(
1726 "{} size drift (recorded {}, found {})",
1727 src.display(),
1728 file.size,
1729 meta.len()
1730 );
1731 }
1732 if file.sha256.is_empty() {
1733 anyhow::bail!("{} has no recorded SHA-256", src.display());
1734 }
1735 let found = util::sha256_file(src)?;
1736 if found != file.sha256 {
1737 anyhow::bail!("{} failed its SHA-256 check", src.display());
1738 }
1739 Ok(())
1740}
1741
1742#[cfg(not(unix))]
1743fn install_private_copy(src: &Path, dst: &Path, stamp: u64, file: &FileEntry) -> Result<()> {
1744 let parent = dst
1745 .parent()
1746 .with_context(|| format!("destination {} has no parent", dst.display()))?;
1747 let nonce = SystemTime::now()
1748 .duration_since(UNIX_EPOCH)
1749 .map(|duration| duration.as_nanos())
1750 .unwrap_or(0);
1751 let tmp = parent.join(format!(".epicenter-copy-{}-{nonce}", std::process::id()));
1752 util::copy_file_verified(src, &tmp, file.size, &file.sha256)?;
1753 make_writable(&tmp)?;
1754 set_stamp(&tmp, stamp)?;
1755 let installed = fs::hard_link(&tmp, dst);
1759 let _ = fs::remove_file(&tmp);
1760 installed.with_context(|| format!("installing {}", dst.display()))?;
1761 Ok(())
1762}
1763
1764fn read_small_regular(path: &Path) -> Option<String> {
1766 util::read_small_text(path, 4 * 1024 * 1024).ok()
1767}
1768
1769#[derive(Debug, Default)]
1773pub struct VerifyReport {
1774 pub parity: String,
1775 pub units: usize,
1776 pub files: usize,
1777 pub bytes: u64,
1778 pub problems: Vec<String>,
1780 pub warnings: Vec<String>,
1782 pub ordering: Vec<String>,
1786}
1787
1788impl VerifyReport {
1789 pub fn is_clean(&self) -> bool {
1790 self.problems.is_empty()
1791 }
1792}
1793
1794pub fn verify(store_root: &Path) -> Result<Vec<VerifyReport>> {
1798 let mut reports = Vec::new();
1799 for parity in generation_names(store_root)? {
1800 let mut report = VerifyReport {
1801 parity: parity.clone(),
1802 ..Default::default()
1803 };
1804 if let Err(err) = check_store_trust(store_root) {
1805 report
1806 .problems
1807 .push(format!("unsafe store: {err:#} — no artifact was read"));
1808 reports.push(report);
1809 continue;
1810 }
1811 let Some(manifest) = load_manifest(store_root, &parity)? else {
1812 report.problems.push(format!(
1813 "generation {parity} has no readable manifest.json — it cannot be audited"
1814 ));
1815 reports.push(report);
1816 continue;
1817 };
1818 let units = units_dir(store_root, &parity);
1819 for entry in &manifest.units {
1820 for file in &entry.files {
1821 let path = units.join(&entry.name).join(&file.path);
1822 let rel = Path::new(&entry.name).join(&file.path);
1823 if let Err(err) = util::check_regular_chain(&units, &rel) {
1824 report.problems.push(format!(
1825 "unsafe path: {} ({err:#})",
1826 display_join(&path, &units)
1827 ));
1828 continue;
1829 }
1830 let meta = match fs::symlink_metadata(&path) {
1831 Ok(meta) => meta,
1832 Err(_) => {
1833 report
1834 .problems
1835 .push(format!("missing: {}", display_join(&path, units.as_path())));
1836 continue;
1837 }
1838 };
1839 if meta.file_type().is_symlink() || util::is_reparse_point(&meta) {
1840 report.problems.push(format!(
1841 "symlink/reparse in store (refused): {}",
1842 display_join(&path, &units)
1843 ));
1844 continue;
1845 }
1846 if !meta.is_file() {
1847 report.problems.push(format!(
1848 "not a regular file: {}",
1849 display_join(&path, &units)
1850 ));
1851 continue;
1852 }
1853 if !util::owned_by_us(&meta) {
1854 report.problems.push(format!(
1855 "foreign-owned file: {}",
1856 display_join(&path, &units)
1857 ));
1858 }
1859 report.files += 1;
1860 report.bytes += meta.len();
1861 if meta.len() != file.size {
1862 report.problems.push(format!(
1863 "size drift: {} (recorded {}, found {})",
1864 display_join(&path, &units),
1865 file.size,
1866 meta.len()
1867 ));
1868 }
1869 let found_stamp = meta.modified().map(ceil_secs).unwrap_or(0);
1870 if found_stamp != entry.stamp {
1871 report.problems.push(format!(
1872 "stamp drift: {} (recorded {}, found {})",
1873 display_join(&path, &units),
1874 entry.stamp,
1875 found_stamp
1876 ));
1877 }
1878 if file.sha256.is_empty() {
1879 report.problems.push(format!(
1880 "no recorded SHA-256: {} (re-harvest this generation)",
1881 display_join(&path, &units)
1882 ));
1883 } else {
1884 match util::sha256_file(&path) {
1885 Ok(found) if found != file.sha256 => report.problems.push(format!(
1886 "content drift: {} (recorded {}, found {})",
1887 display_join(&path, &units),
1888 &file.sha256[..file.sha256.len().min(12)],
1889 &found[..found.len().min(12)]
1890 )),
1891 Ok(_) => {}
1892 Err(err) => report.problems.push(format!(
1893 "unreadable: {}: {err}",
1894 display_join(&path, &units)
1895 )),
1896 }
1897 }
1898 if file.role == "memo" {
1899 match read_small_regular(&path) {
1900 Some(text) if file.path == entry.memo && text.trim() != entry.hash => {
1901 let found = text.trim();
1902 let detail = if crate::fingerprint::looks_like_hash(found) {
1903 format!("found {found}")
1904 } else {
1905 "found non-hash content (not printed)".to_string()
1906 };
1907 report.problems.push(format!(
1908 "hash drift: {} (recorded {}; {detail})",
1909 display_join(&path, &units),
1910 entry.hash
1911 ));
1912 }
1913 Some(text)
1914 if file.path != entry.memo
1915 && !crate::fingerprint::looks_like_hash(text.trim()) =>
1916 {
1917 report.problems.push(format!(
1918 "malformed memo file: {} (content is not a 16-hex hash)",
1919 display_join(&path, &units)
1920 ));
1921 }
1922 Some(_) => {}
1923 None => report.problems.push(format!(
1924 "unreadable/symlinked memo file: {}",
1925 display_join(&path, &units)
1926 )),
1927 }
1928 }
1929 }
1930 }
1931 for entry in &manifest.units {
1934 let unit_dir = units.join(&entry.name);
1935 let expected: std::collections::BTreeSet<String> =
1936 entry.files.iter().map(|file| file.path.clone()).collect();
1937 let walk = match WalkDir::new(&unit_dir)
1938 .min_depth(1)
1939 .into_iter()
1940 .collect::<Result<Vec<_>, _>>()
1941 {
1942 Ok(walk) => walk,
1943 Err(err) => {
1944 report
1945 .problems
1946 .push(format!("unreadable unit dir: units/{} ({err})", entry.name));
1947 continue;
1948 }
1949 };
1950 for file in walk {
1951 if !file.file_type().is_file() {
1952 continue;
1953 }
1954 let rel = file
1955 .path()
1956 .strip_prefix(&unit_dir)
1957 .map(|rel| rel.to_string_lossy().replace('\\', "/"))
1958 .unwrap_or_default();
1959 if !expected.contains(&rel) {
1960 report
1961 .problems
1962 .push(format!("unrecorded file: units/{}/{}", entry.name, rel));
1963 }
1964 }
1965 }
1966
1967 let by_hash: std::collections::HashMap<&str, usize> = manifest
1974 .units
1975 .iter()
1976 .enumerate()
1977 .map(|(index, entry)| (entry.hash.as_str(), index))
1978 .collect();
1979 for entry in &manifest.units {
1980 let json = entry
1981 .files
1982 .iter()
1983 .find(|file| file.path.ends_with(".json"))
1984 .and_then(|file| read_small_regular(&units.join(&entry.name).join(&file.path)))
1985 .and_then(|text| crate::fingerprint::parse_fingerprint(&text));
1986 let Some(json) = json else { continue };
1987 for dep in &json.deps {
1988 let Some(dep_index) = dep
1989 .memo_hash()
1990 .and_then(|hash| by_hash.get(hash.as_str()))
1991 .copied()
1992 else {
1993 continue;
1994 };
1995 let dep_entry = &manifest.units[dep_index];
1996 if dep_entry.stamp > entry.stamp {
1997 report.ordering.push(format!(
1998 "{} is older than its dependency {} ({} older) — cargo recompiles it after a restore",
1999 entry.name,
2000 dep_entry.name,
2001 crate::util::human_duration(Duration::from_secs(dep_entry.stamp - entry.stamp))
2002 ));
2003 }
2004 }
2005 }
2006 report.units = manifest.units.len();
2007
2008 match fs::symlink_metadata(&units) {
2010 Ok(meta)
2011 if meta.is_dir()
2012 && !meta.file_type().is_symlink()
2013 && !util::is_reparse_point(&meta) => {}
2014 Ok(_) => {
2015 report.problems.push(format!(
2016 "units directory {} is a symlink/reparse or not a directory",
2017 units.display()
2018 ));
2019 }
2020 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
2021 Err(err) => report.problems.push(format!(
2022 "unreadable units directory {}: {err}",
2023 units.display()
2024 )),
2025 }
2026 if units.is_dir() {
2027 let known: std::collections::BTreeSet<&str> = manifest
2028 .units
2029 .iter()
2030 .map(|unit| unit.name.as_str())
2031 .collect();
2032 for entry in fs::read_dir(&units)? {
2033 let entry = entry?;
2034 if !entry.file_type()?.is_dir() {
2035 continue;
2036 }
2037 let name = entry.file_name().to_string_lossy().into_owned();
2038 if name.starts_with('.') {
2039 continue; }
2041 if !known.contains(name.as_str()) {
2042 report
2047 .problems
2048 .push(format!("unrecorded unit group: units/{name}"));
2049 }
2050 }
2051 }
2052 reports.push(report);
2053 }
2054 Ok(reports)
2055}
2056
2057fn display_join(path: &Path, base: &Path) -> String {
2059 path.strip_prefix(base)
2060 .unwrap_or(path)
2061 .to_string_lossy()
2062 .replace('\\', "/")
2063}
2064
2065#[derive(Debug, Default)]
2069pub struct GenerationInfo {
2070 pub parity: String,
2071 pub active: bool,
2073 pub units: usize,
2074 pub files: usize,
2075 pub bytes: u64,
2077 pub linked: usize,
2079 pub created: Option<u64>,
2080 pub updated: Option<u64>,
2081}
2082
2083pub fn generations(store_root: &Path) -> Result<Vec<GenerationInfo>> {
2085 let active = parity_key().ok();
2086 let mut out = Vec::new();
2087 for parity in generation_names(store_root)? {
2088 let mut info = GenerationInfo {
2089 parity: parity.clone(),
2090 active: active.as_deref() == Some(parity.as_str()),
2091 ..Default::default()
2092 };
2093 if let Some(manifest) = load_manifest(store_root, &parity)? {
2094 info.units = manifest.units.len();
2095 info.created = Some(manifest.created);
2096 info.updated = Some(manifest.updated);
2097 }
2098 let units = units_dir(store_root, &parity);
2099 if units.is_dir() {
2100 for entry in WalkDir::new(&units).min_depth(1) {
2101 let entry = entry?;
2102 if !entry.file_type().is_file() {
2103 continue;
2104 }
2105 let meta = entry.metadata()?;
2106 info.files += 1;
2107 info.bytes += meta.len();
2108 if link_count(&meta) > 1 {
2109 info.linked += 1;
2110 }
2111 }
2112 }
2113 out.push(info);
2114 }
2115 Ok(out)
2116}
2117
2118pub(crate) fn link_count(meta: &fs::Metadata) -> u64 {
2120 #[cfg(unix)]
2121 {
2122 use std::os::unix::fs::MetadataExt;
2123 meta.nlink()
2124 }
2125 #[cfg(not(unix))]
2126 {
2127 let _ = meta;
2128 1
2129 }
2130}
2131
2132fn unit_name_matches(name: &str, query: &str) -> bool {
2134 name.contains(query)
2135}
2136
2137#[cfg(test)]
2138mod tests {
2139 use super::*;
2140
2141 fn synthetic_build_dir(root: &Path) -> Result<()> {
2146 let jsons =
2147 r#"{"rustc":12019306335353385202,"features":"[]","declared_features":"[]","deps":[]}"#;
2148
2149 let lib = root.join("debug/.fingerprint/serde-102a33f055a8d229");
2150 fs::create_dir_all(&lib)?;
2151 fs::write(lib.join("lib-serde"), "5d82713a7f2d05e2")?;
2152 fs::write(lib.join("lib-serde.json"), jsons)?;
2153 fs::write(lib.join("dep-lib-serde"), [0u8; 24])?;
2154 fs::write(lib.join("invoked.timestamp"), "")?;
2155 fs::create_dir_all(root.join("debug/deps"))?;
2156 fs::write(
2157 root.join("debug/deps/libserde-102a33f055a8d229.rlib"),
2158 [7u8; 64],
2159 )?;
2160 fs::write(
2161 root.join("debug/deps/libserde-102a33f055a8d229.rmeta"),
2162 [6u8; 48],
2163 )?;
2164 fs::write(
2165 root.join("debug/deps/serde-102a33f055a8d229.d"),
2166 b"side file",
2167 )?;
2168 fs::write(
2169 root.join("debug/deps/libserde-102a33f055a8d229.kache-real"),
2170 b"wrapper",
2171 )?;
2172
2173 let compile = root.join("debug/.fingerprint/x-1122334455667788");
2174 fs::create_dir_all(&compile)?;
2175 fs::write(
2176 compile.join("build-script-build-script-build"),
2177 "aaaabbbbccccdddd",
2178 )?;
2179 fs::write(compile.join("build-script-build-script-build.json"), jsons)?;
2180 fs::write(
2181 compile.join("dep-build-script-build-script-build"),
2182 [0u8; 8],
2183 )?;
2184 let compile_anchor = root.join("debug/build/x-1122334455667788");
2185 fs::create_dir_all(&compile_anchor)?;
2186 fs::write(
2187 compile_anchor.join("build_script_build-1122334455667788"),
2188 [3u8; 32],
2189 )?;
2190 fs::write(
2191 compile_anchor.join("build_script_build-1122334455667788.d"),
2192 b"dep",
2193 )?;
2194 fs::write(
2195 compile_anchor.join("build_script_build-1122334455667788.kache-real"),
2196 b"wrapper",
2197 )?;
2198 fs::write(compile_anchor.join("invoked.timestamp"), "")?;
2199
2200 let run = root.join("debug/.fingerprint/x-aabbccddeeff0011");
2202 fs::create_dir_all(&run)?;
2203 fs::write(
2204 run.join("run-build-script-build-script-build"),
2205 "eeeeffff00001111",
2206 )?;
2207 fs::write(run.join("run-build-script-build-script-build.json"), jsons)?;
2208 let run_anchor = root.join("debug/build/x-aabbccddeeff0011");
2209 fs::create_dir_all(&run_anchor)?;
2210 fs::write(
2211 run_anchor.join("output"),
2212 b"cargo:rerun-if-changed=build.rs",
2213 )?;
2214 fs::write(run_anchor.join("stderr"), b"")?;
2215 fs::write(run_anchor.join("invoked.timestamp"), "")?;
2216 Ok(())
2217 }
2218
2219 fn synthetic_husk(root: &Path) -> Result<()> {
2221 let unit = root.join("debug/.fingerprint/husk-aaaabbbbccccdddd");
2222 fs::create_dir_all(&unit)?;
2223 fs::write(unit.join("lib-husk"), "0000111122223333")?;
2224 Ok(())
2225 }
2226
2227 #[test]
2228 fn parity_key_is_stable_and_shaped() -> Result<()> {
2229 let first = parity_key()?;
2230 let second = parity_key()?;
2231 assert_eq!(first, second);
2232 let tc = toolchain()?;
2233 assert!(first.starts_with(&format!("{}-{}", tc.release, tc.commit)));
2234 assert!(first.ends_with(&flags_digest()));
2235 Ok(())
2236 }
2237
2238 #[test]
2239 fn harvest_stores_groups_with_one_canonical_stamp() -> Result<()> {
2240 let build = tempfile::tempdir()?;
2241 let store = tempfile::tempdir()?;
2242 synthetic_build_dir(build.path())?;
2243
2244 let report = harvest(build.path(), store.path(), None, false)?;
2245 assert_eq!(report.stored, 3, "lib + build-script compile + run");
2246 assert_eq!(report.skipped_incomplete, 0);
2247 assert!(report.files >= 10, "got {}", report.files);
2248 assert!(report.bytes > 0);
2249
2250 let parity = parity_key()?;
2251 let manifest = load_manifest(store.path(), &parity)?.expect("manifest written");
2252 assert_eq!(manifest.units.len(), 3);
2253 let lib_entry = manifest
2254 .units
2255 .iter()
2256 .find(|unit| unit.name == "serde-102a33f055a8d229")
2257 .expect("lib unit recorded");
2258 assert_eq!(lib_entry.hash, "5d82713a7f2d05e2");
2259 assert_eq!(lib_entry.profile, "debug");
2260
2261 let group = units_dir(store.path(), &parity).join(&lib_entry.name);
2263 for file in &lib_entry.files {
2264 let meta = fs::metadata(group.join(&file.path))?;
2265 assert_eq!(
2266 ceil_secs(meta.modified()?),
2267 lib_entry.stamp,
2268 "{}",
2269 file.path
2270 );
2271 assert_eq!(meta.len(), file.size);
2272 }
2273
2274 assert!(!group.join("invoked.timestamp").exists());
2276 for file in &lib_entry.files {
2277 assert!(!file.path.contains("kache"), "{}", file.path);
2278 assert!(!file.path.ends_with(".d"), "{}", file.path);
2279 }
2280
2281 #[cfg(unix)]
2282 {
2283 use std::os::unix::fs::PermissionsExt;
2284 let meta = fs::metadata(group.join(&lib_entry.files[0].path))?;
2285 assert_eq!(
2286 meta.permissions().mode() & 0o777,
2287 0o444,
2288 "store is read-only"
2289 );
2290 }
2291 Ok(())
2292 }
2293
2294 #[test]
2295 fn harvest_is_idempotent_and_skips_incomplete_units() -> Result<()> {
2296 let build = tempfile::tempdir()?;
2297 let store = tempfile::tempdir()?;
2298 synthetic_build_dir(build.path())?;
2299 synthetic_husk(build.path())?;
2300
2301 let first = harvest(build.path(), store.path(), None, false)?;
2302 assert_eq!(first.stored, 3);
2303 assert_eq!(first.skipped_incomplete, 1, "husk without artifacts");
2304
2305 let second = harvest(build.path(), store.path(), None, false)?;
2306 assert_eq!(second.stored, 0);
2307 assert_eq!(second.unchanged, 3);
2308 assert_eq!(second.skipped_incomplete, 1);
2309 Ok(())
2310 }
2311
2312 #[test]
2313 fn harvest_refreshes_when_the_file_set_grows() -> Result<()> {
2314 let build = tempfile::tempdir()?;
2315 let store = tempfile::tempdir()?;
2316 synthetic_build_dir(build.path())?;
2317 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2318
2319 fs::write(
2320 build.path().join("debug/deps/libserde-102a33f055a8d229.so"),
2321 [1u8; 16],
2322 )?;
2323 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2324 assert_eq!(report.refreshed, 1);
2325 assert_eq!(report.stored, 0);
2326
2327 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2328 let paths: Vec<&str> = manifest.units[0]
2329 .files
2330 .iter()
2331 .map(|file| file.path.as_str())
2332 .collect();
2333 assert!(
2334 paths.contains(&"deps/libserde-102a33f055a8d229.so"),
2335 "{paths:?}"
2336 );
2337 Ok(())
2338 }
2339
2340 #[test]
2341 fn harvest_stores_all_memos_of_shared_dirs() -> Result<()> {
2342 let build = tempfile::tempdir()?;
2343 let store = tempfile::tempdir()?;
2344 synthetic_build_dir(build.path())?;
2345 let lib = build
2346 .path()
2347 .join("debug/.fingerprint/serde-102a33f055a8d229");
2348 fs::write(lib.join("bin-serde"), "1111222233334444")?;
2349 fs::write(lib.join("dep-bin-serde"), [0u8; 8])?;
2350
2351 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2352 assert_eq!(report.stored, 1);
2353 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2354 let entry = &manifest.units[0];
2355 let paths: Vec<&str> = entry.files.iter().map(|file| file.path.as_str()).collect();
2356 assert!(paths.contains(&"bin-serde"), "{paths:?}");
2357 assert!(paths.contains(&"dep-bin-serde"), "{paths:?}");
2358
2359 let reports = verify(store.path())?;
2360 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2361 Ok(())
2362 }
2363
2364 #[test]
2365 fn harvest_dry_run_writes_nothing() -> Result<()> {
2366 let build = tempfile::tempdir()?;
2367 let store = tempfile::tempdir()?;
2368 synthetic_build_dir(build.path())?;
2369
2370 let report = harvest(build.path(), store.path(), None, true)?;
2371 assert_eq!(report.stored, 3);
2372 assert_eq!(generation_names(store.path())?.len(), 0, "no generation");
2373 Ok(())
2374 }
2375
2376 #[test]
2377 fn harvest_unit_filter_selects_one_group() -> Result<()> {
2378 let build = tempfile::tempdir()?;
2379 let store = tempfile::tempdir()?;
2380 synthetic_build_dir(build.path())?;
2381
2382 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2383 assert_eq!(report.stored, 1);
2384 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2385 assert_eq!(manifest.units.len(), 1);
2386 assert_eq!(manifest.units[0].name, "serde-102a33f055a8d229");
2387 Ok(())
2388 }
2389
2390 #[test]
2391 fn restore_links_hardlinks_and_never_overwrites() -> Result<()> {
2392 let build = tempfile::tempdir()?;
2393 let store = tempfile::tempdir()?;
2394 let view = tempfile::tempdir()?;
2395 synthetic_build_dir(build.path())?;
2396 harvest(build.path(), store.path(), None, false)?;
2397
2398 let occupied = view
2400 .path()
2401 .join("debug/deps/libserde-102a33f055a8d229.rlib");
2402 fs::create_dir_all(occupied.parent().unwrap())?;
2403 fs::write(&occupied, b"live artifact")?;
2404
2405 let report = restore(view.path(), store.path(), None, false)?;
2406 assert_eq!(report.units_linked, 3);
2407 assert_eq!(report.files_existing, 1, "the live artifact was skipped");
2408 assert_eq!(fs::read(&occupied)?, b"live artifact");
2409 assert_eq!(report.files_linked, 0, "no other deps/ artifact");
2413 assert!(report.files_copied >= 12, "got {}", report.files_copied);
2414
2415 let memo = view
2417 .path()
2418 .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
2419 assert_eq!(fs::read_to_string(&memo)?.trim(), "5d82713a7f2d05e2");
2420 assert!(
2421 view.path()
2422 .join("debug/build/x-aabbccddeeff0011/output")
2423 .is_file()
2424 );
2425
2426 #[cfg(unix)]
2430 {
2431 use std::os::unix::fs::MetadataExt;
2432 use std::os::unix::fs::PermissionsExt;
2433 let parity = parity_key()?;
2434 let stored = units_dir(store.path(), &parity).join("serde-102a33f055a8d229/lib-serde");
2435 let view_meta = fs::metadata(&memo)?;
2436 assert_ne!(
2437 view_meta.ino(),
2438 fs::metadata(&stored)?.ino(),
2439 "state not shared"
2440 );
2441 assert_eq!(view_meta.nlink(), 1, "state owns its inode");
2442 assert_ne!(view_meta.permissions().mode() & 0o200, 0, "state writable");
2443 }
2444
2445 let again = restore(view.path(), store.path(), None, false)?;
2447 assert_eq!(again.units_already, 3);
2448 assert_eq!(again.files_linked, 0);
2449 assert_eq!(again.files_copied, 0);
2450 Ok(())
2451 }
2452
2453 #[test]
2454 fn restore_into_missing_profile_dirs_creates_them() -> Result<()> {
2455 let build = tempfile::tempdir()?;
2456 let store = tempfile::tempdir()?;
2457 let view = tempfile::tempdir()?;
2458 synthetic_build_dir(build.path())?;
2459 harvest(build.path(), store.path(), None, false)?;
2460
2461 let report = restore(view.path(), store.path(), None, false)?;
2462 assert_eq!(report.errors, 0);
2463 assert!(view.path().join("debug/.fingerprint").is_dir());
2464 Ok(())
2465 }
2466
2467 #[test]
2468 fn restore_without_a_generation_reports_fail_open() -> Result<()> {
2469 let view = tempfile::tempdir()?;
2470 let store = tempfile::tempdir()?;
2471 let report = restore(view.path(), store.path(), None, false)?;
2472 assert!(report.note.is_some());
2473 assert_eq!(report.files_linked, 0);
2474 Ok(())
2475 }
2476
2477 #[test]
2478 fn verify_passes_clean_and_flags_drift() -> Result<()> {
2479 let build = tempfile::tempdir()?;
2480 let store = tempfile::tempdir()?;
2481 synthetic_build_dir(build.path())?;
2482 harvest(build.path(), store.path(), None, false)?;
2483
2484 let reports = verify(store.path())?;
2485 assert_eq!(reports.len(), 1);
2486 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2487 assert_eq!(reports[0].units, 3);
2488
2489 let parity = parity_key()?;
2491 let victim = units_dir(store.path(), &parity).join("serde-102a33f055a8d229/dep-lib-serde");
2492 #[cfg(unix)]
2493 {
2494 use std::os::unix::fs::PermissionsExt;
2495 fs::set_permissions(&victim, fs::Permissions::from_mode(0o644))?;
2496 }
2497 fs::write(&victim, [9u8; 32])?;
2498
2499 let reports = verify(store.path())?;
2500 assert!(!reports[0].is_clean());
2501 assert!(
2502 reports[0]
2503 .problems
2504 .iter()
2505 .any(|line| line.contains("size drift")),
2506 "{:?}",
2507 reports[0].problems
2508 );
2509 Ok(())
2510 }
2511
2512 #[test]
2513 fn verify_reports_unrecorded_unit_groups() -> Result<()> {
2514 let build = tempfile::tempdir()?;
2515 let store = tempfile::tempdir()?;
2516 synthetic_build_dir(build.path())?;
2517 harvest(build.path(), store.path(), None, false)?;
2518
2519 let stray = units_dir(store.path(), &parity_key()?).join("stray-0000000000000000");
2520 fs::create_dir_all(stray.join("deps"))?;
2521 fs::write(stray.join("deps/x.rlib"), [0u8; 4])?;
2522
2523 let reports = verify(store.path())?;
2524 assert!(
2525 reports[0]
2526 .problems
2527 .iter()
2528 .any(|line| line.contains("stray-0000000000000000")),
2529 "{:?}",
2530 reports[0].problems
2531 );
2532 assert!(!reports[0].is_clean());
2533 Ok(())
2534 }
2535
2536 #[test]
2537 fn generations_summary_counts_store_files() -> Result<()> {
2538 let build = tempfile::tempdir()?;
2539 let store = tempfile::tempdir()?;
2540 synthetic_build_dir(build.path())?;
2541 harvest(build.path(), store.path(), None, false)?;
2542
2543 let generations = generations(store.path())?;
2544 assert_eq!(generations.len(), 1);
2545 assert!(generations[0].active, "current toolchain generation");
2546 assert_eq!(generations[0].units, 3);
2547 assert!(generations[0].files >= 10);
2548 assert!(generations[0].linked <= generations[0].files);
2549 Ok(())
2550 }
2551
2552 #[test]
2553 fn cached_diagnostics_are_stored_as_extra_and_verify_clean() -> Result<()> {
2554 let build = tempfile::tempdir()?;
2555 let store = tempfile::tempdir()?;
2556 synthetic_build_dir(build.path())?;
2557 fs::write(
2559 build
2560 .path()
2561 .join("debug/.fingerprint/serde-102a33f055a8d229/output-lib-serde"),
2562 r#"{"$message_type":"diagnostic","message":"warning: unused"}"#,
2563 )?;
2564
2565 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2566 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2567 let output = manifest.units[0]
2568 .files
2569 .iter()
2570 .find(|file| file.path == "output-lib-serde")
2571 .expect("cached diagnostics are stored");
2572 assert_eq!(output.role, "extra");
2573
2574 let reports = verify(store.path())?;
2575 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2576 Ok(())
2577 }
2578
2579 #[test]
2580 fn harvest_refreshes_when_a_role_drifted() -> Result<()> {
2581 let build = tempfile::tempdir()?;
2582 let store = tempfile::tempdir()?;
2583 synthetic_build_dir(build.path())?;
2584 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2585
2586 let parity = parity_key()?;
2589 let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2590 let memo_index = manifest.units[0]
2591 .files
2592 .iter()
2593 .position(|file| file.path == manifest.units[0].memo)
2594 .expect("memo recorded");
2595 manifest.units[0].files[memo_index].role = "extra".to_string();
2596 save_manifest(store.path(), &parity, &manifest)?;
2597
2598 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2599 assert_eq!(report.refreshed, 1, "role drift forces a refresh");
2600 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2601 let role = &manifest.units[0].files[memo_index].role;
2602 assert_eq!(role, "memo", "roles are rewritten");
2603 Ok(())
2604 }
2605
2606 #[test]
2607 fn restore_shares_artifacts_but_copies_state_writable() -> Result<()> {
2608 let build = tempfile::tempdir()?;
2609 let store = tempfile::tempdir()?;
2610 synthetic_build_dir(build.path())?;
2611 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2612
2613 let view = tempfile::tempdir()?;
2614 restore(view.path(), store.path(), Some("102a33f0"), false)?;
2615
2616 let state = view
2617 .path()
2618 .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
2619 let artifact = view
2620 .path()
2621 .join("debug/deps/libserde-102a33f055a8d229.rlib");
2622 let metadata = view
2623 .path()
2624 .join("debug/deps/libserde-102a33f055a8d229.rmeta");
2625 assert!(state.is_file() && artifact.is_file() && metadata.is_file());
2626
2627 let parity = parity_key()?;
2628 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2629 let entry = manifest
2630 .units
2631 .iter()
2632 .find(|entry| entry.name == "serde-102a33f055a8d229")
2633 .expect("stored unit");
2634 assert_eq!(ceil_secs(fs::metadata(&state)?.modified()?), entry.stamp);
2635
2636 fs::write(&state, "ffffffffffffffff")?;
2639 let stored = units_dir(store.path(), &parity)
2640 .join("serde-102a33f055a8d229")
2641 .join("lib-serde");
2642 assert_eq!(fs::read_to_string(&stored)?, "5d82713a7f2d05e2");
2643
2644 #[cfg(unix)]
2645 {
2646 use std::os::unix::fs::MetadataExt;
2647 assert!(
2648 fs::metadata(&artifact)?.nlink() >= 2,
2649 "artifact shares the store inode"
2650 );
2651 assert_eq!(fs::metadata(&state)?.nlink(), 1, "state is a private copy");
2652 assert_eq!(
2653 fs::metadata(&metadata)?.nlink(),
2654 1,
2655 ".rmeta is a private copy (rustc opens it for write)"
2656 );
2657 }
2658 Ok(())
2659 }
2660
2661 fn synthetic_edge_dir(root: &Path, dep_at: u64, unit_at: u64) -> Result<()> {
2665 fs::create_dir_all(root.join("debug/deps"))?;
2666 let core = root.join("debug/.fingerprint/serde_core-b468be6fa8c1f936");
2667 fs::create_dir_all(&core)?;
2668 fs::write(core.join("lib-serde_core"), "71a9f323efa5ae82")?;
2669 fs::write(core.join("dep-lib-serde_core"), [0u8; 8])?;
2670 let core_rlib = root.join("debug/deps/libserde_core-b468be6fa8c1f936.rlib");
2671 fs::write(&core_rlib, [1u8; 32])?;
2672
2673 let unit = root.join("debug/.fingerprint/serde-746d173581f1a314");
2674 fs::create_dir_all(&unit)?;
2675 fs::write(unit.join("lib-serde"), "5d82713a7f2d05e2")?;
2676 fs::write(
2677 unit.join("lib-serde.json"),
2678 r#"{"rustc":5244445300240561903,"features":"[]","declared_features":"[]","deps":[[11029742160753049355,"serde_core",false,9416646317397223793]]}"#,
2679 )?;
2680 fs::write(unit.join("dep-lib-serde"), [0u8; 8])?;
2681 let unit_rlib = root.join("debug/deps/libserde-746d173581f1a314.rlib");
2682 fs::write(&unit_rlib, [1u8; 32])?;
2683
2684 set_stamp(&core.join("lib-serde_core"), dep_at)?;
2685 set_stamp(&core.join("dep-lib-serde_core"), dep_at)?;
2686 set_stamp(&core_rlib, dep_at)?;
2687 set_stamp(&unit.join("lib-serde"), unit_at)?;
2688 set_stamp(&unit.join("lib-serde.json"), unit_at)?;
2689 set_stamp(&unit.join("dep-lib-serde"), unit_at)?;
2690 set_stamp(&unit_rlib, unit_at)?;
2691 Ok(())
2692 }
2693
2694 #[test]
2695 fn harvest_clamps_a_dependent_to_its_dependency() -> Result<()> {
2696 let build = tempfile::tempdir()?;
2697 let store = tempfile::tempdir()?;
2698 let now = now_secs();
2699 synthetic_edge_dir(build.path(), now, now - 3600)?;
2702 harvest(build.path(), store.path(), None, false)?;
2703
2704 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2705 let dependent = manifest
2706 .units
2707 .iter()
2708 .find(|entry| entry.name == "serde-746d173581f1a314")
2709 .expect("dependent stored");
2710 let dependency = manifest
2711 .units
2712 .iter()
2713 .find(|entry| entry.name == "serde_core-b468be6fa8c1f936")
2714 .expect("dependency stored");
2715 assert!(
2716 dependent.stamp >= dependency.stamp,
2717 "clamped: {} >= {}",
2718 dependent.stamp,
2719 dependency.stamp
2720 );
2721
2722 let reports = verify(store.path())?;
2723 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2724 assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
2725 Ok(())
2726 }
2727
2728 #[test]
2729 fn harvest_stamps_groups_by_the_metadata_output() -> Result<()> {
2730 let build = tempfile::tempdir()?;
2731 let store = tempfile::tempdir()?;
2732 synthetic_build_dir(build.path())?;
2733 let now = now_secs();
2734 set_stamp(
2737 &build
2738 .path()
2739 .join("debug/deps/libserde-102a33f055a8d229.rmeta"),
2740 now - 30,
2741 )?;
2742 set_stamp(
2743 &build
2744 .path()
2745 .join("debug/deps/libserde-102a33f055a8d229.rlib"),
2746 now,
2747 )?;
2748 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2749
2750 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
2751 assert_eq!(manifest.units[0].stamp, now - 30);
2752 Ok(())
2753 }
2754
2755 #[test]
2756 fn harvest_stores_read_only_sources() -> Result<()> {
2757 let build = tempfile::tempdir()?;
2758 let store = tempfile::tempdir()?;
2759 synthetic_build_dir(build.path())?;
2760 let source = build
2764 .path()
2765 .join("debug/.fingerprint/serde-102a33f055a8d229/dep-lib-serde");
2766 let mut perms = fs::metadata(&source)?.permissions();
2767 perms.set_readonly(true);
2768 fs::set_permissions(&source, perms)?;
2769
2770 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2771 assert_eq!(report.stored, 1);
2772
2773 let parity = parity_key()?;
2774 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2775 let stored = units_dir(store.path(), &parity)
2776 .join(&manifest.units[0].name)
2777 .join("dep-lib-serde");
2778 assert!(stored.is_file(), "the state file is stored");
2779 assert_eq!(
2780 ceil_secs(fs::metadata(&stored)?.modified()?),
2781 manifest.units[0].stamp,
2782 "the stored copy carries the canonical stamp"
2783 );
2784 Ok(())
2785 }
2786
2787 #[cfg(windows)]
2791 #[test]
2792 fn set_stamp_sets_times_on_windows() -> Result<()> {
2793 let dir = tempfile::tempdir()?;
2794 let file = dir.path().join("dep-lib-x");
2795 fs::write(&file, b"x")?;
2796
2797 set_stamp(&file, 1_700_000_000)?;
2798 assert_eq!(
2799 ceil_secs(fs::metadata(&file)?.modified()?),
2800 1_700_000_000,
2801 "a writable copy must be stampable"
2802 );
2803
2804 let mut perms = fs::metadata(&file)?.permissions();
2807 perms.set_readonly(true);
2808 fs::set_permissions(&file, perms)?;
2809 make_writable(&file)?;
2810 set_stamp(&file, 1_700_000_100)?;
2811 assert_eq!(
2812 ceil_secs(fs::metadata(&file)?.modified()?),
2813 1_700_000_100,
2814 "a copy of a read-only source must be stampable"
2815 );
2816 Ok(())
2817 }
2818
2819 #[cfg(unix)]
2820 #[test]
2821 fn a_failed_store_drops_its_staging_dir() -> Result<()> {
2822 use std::os::unix::fs::MetadataExt;
2823
2824 let build = tempfile::tempdir()?;
2825 let store = tempfile::tempdir()?;
2826 synthetic_build_dir(build.path())?;
2827 if fs::metadata(build.path())?.uid() == 0 {
2828 return Ok(()); }
2830 let artifact = build
2831 .path()
2832 .join("debug/deps/libserde-102a33f055a8d229.rlib");
2833 {
2834 use std::os::unix::fs::PermissionsExt;
2835 fs::set_permissions(&artifact, fs::Permissions::from_mode(0o000))?;
2836 }
2837
2838 let result = harvest(build.path(), store.path(), Some("102a33f0"), false);
2839 assert!(result.is_err(), "an unreadable artifact aborts the store");
2840
2841 let units = units_dir(store.path(), &parity_key()?);
2842 if units.is_dir() {
2843 for entry in fs::read_dir(&units)? {
2844 let name = entry?.file_name().to_string_lossy().into_owned();
2845 assert!(!name.contains(".tmp-"), "staging leftover: {name}");
2846 }
2847 }
2848 Ok(())
2849 }
2850
2851 #[cfg(unix)]
2852 #[test]
2853 fn harvest_sweeps_stale_staging_dirs() -> Result<()> {
2854 use std::os::unix::fs::PermissionsExt;
2855
2856 let build = tempfile::tempdir()?;
2857 let store = tempfile::tempdir()?;
2858 synthetic_build_dir(build.path())?;
2859 let units = units_dir(store.path(), &parity_key()?);
2860
2861 let stale = units.join(".serde-102a33f055a8d229.tmp-99999");
2863 fs::create_dir_all(&stale)?;
2864 let leftover = stale.join("dep-lib-serde");
2865 fs::write(&leftover, b"x")?;
2866 fs::set_permissions(&leftover, fs::Permissions::from_mode(0o444))?;
2867 set_stamp(&stale, now_secs() - 7200)?;
2868
2869 let fresh = units.join(".serde-102a33f055a8d229.tmp-11111");
2871 fs::create_dir_all(&fresh)?;
2872
2873 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
2874 assert!(!stale.exists(), "stale staging dir swept");
2875 assert!(fresh.exists(), "fresh staging dir left alone");
2876 Ok(())
2877 }
2878
2879 #[test]
2880 fn verify_flags_an_inverted_store_stamp() -> Result<()> {
2881 let build = tempfile::tempdir()?;
2882 let store = tempfile::tempdir()?;
2883 let now = now_secs();
2884 synthetic_edge_dir(build.path(), now - 3600, now)?;
2885 harvest(build.path(), store.path(), None, false)?;
2886
2887 let parity = parity_key()?;
2890 let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2891 let units = units_dir(store.path(), &parity);
2892 let entry = manifest
2893 .units
2894 .iter_mut()
2895 .find(|entry| entry.name == "serde-746d173581f1a314")
2896 .expect("dependent stored");
2897 entry.stamp = now - 7200;
2898 for file in &entry.files {
2899 set_stamp(&units.join(&entry.name).join(&file.path), now - 7200)?;
2900 }
2901 save_manifest(store.path(), &parity, &manifest)?;
2902
2903 let reports = verify(store.path())?;
2904 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2905 assert_eq!(reports[0].ordering.len(), 1, "{:?}", reports[0].ordering);
2906 assert!(reports[0].ordering[0].contains("serde-746d173581f1a314"));
2907 assert!(reports[0].ordering[0].contains("serde_core-b468be6fa8c1f936"));
2908 Ok(())
2909 }
2910
2911 #[test]
2912 fn manifest_validation_rejects_escapes() {
2913 let unit = |name: &str, profile: &str, path: &str, dest: &str| UnitEntry {
2914 name: name.into(),
2915 profile: profile.into(),
2916 memo: "lib-x".into(),
2917 hash: "0123456789abcdef".into(),
2918 stamp: 1,
2919 files: vec![FileEntry {
2920 path: path.into(),
2921 dest: dest.into(),
2922 size: 1,
2923 sha256: String::new(),
2924 role: "artifact".into(),
2925 }],
2926 };
2927 let base = Manifest {
2928 parity_key: "p".into(),
2929 toolchain: "t".into(),
2930 created: 1,
2931 updated: 1,
2932 units: vec![],
2933 mac: String::new(),
2934 };
2935 for unit in [
2936 unit("../escape", "debug", "deps/x", "deps/x"),
2937 unit("ok", "/etc", "deps/x", "deps/x"),
2938 unit("ok", "debug", "/etc/passwd", "deps/x"),
2939 unit("ok", "debug", "deps/x", "/root/.ssh/authorized_keys"),
2940 unit("ok", "debug", "deps/../../x", "deps/x"),
2941 unit(".. ", "debug", "deps/x", "deps/x"),
2942 unit("...", "debug", "deps/x", "deps/x"),
2943 unit("CON.txt", "debug", "deps/x", "deps/x"),
2944 ] {
2945 let manifest = Manifest {
2946 units: vec![unit],
2947 ..base.clone()
2948 };
2949 assert!(manifest.validate().is_err(), "must reject {manifest:?}");
2950 }
2951 }
2952
2953 #[test]
2954 fn harvest_records_sha256_and_verify_flags_content_drift() -> Result<()> {
2955 let build = tempfile::tempdir()?;
2956 let store = tempfile::tempdir()?;
2957 synthetic_build_dir(build.path())?;
2958 harvest(build.path(), store.path(), None, false)?;
2959
2960 let parity = parity_key()?;
2961 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2962 assert!(
2963 manifest
2964 .units
2965 .iter()
2966 .flat_map(|unit| &unit.files)
2967 .all(|file| !file.sha256.is_empty()),
2968 "every stored file must carry a hash"
2969 );
2970
2971 let entry = manifest
2972 .units
2973 .iter()
2974 .find(|entry| entry.name == "serde-102a33f055a8d229")
2975 .expect("unit");
2976 let file = entry
2977 .files
2978 .iter()
2979 .find(|file| file.path == "deps/libserde-102a33f055a8d229.rlib")
2980 .expect("rlib");
2981 let path = units_dir(store.path(), &parity)
2982 .join(&entry.name)
2983 .join(&file.path);
2984 let mut bytes = fs::read(&path)?;
2985 let last = bytes.len() - 1;
2986 bytes[last] ^= 0x01; #[cfg(unix)]
2988 {
2989 use std::os::unix::fs::PermissionsExt;
2990 fs::set_permissions(&path, fs::Permissions::from_mode(0o644))?;
2991 }
2992 fs::write(&path, &bytes)?;
2993 let stamp = UNIX_EPOCH + Duration::from_secs(entry.stamp);
2994 fs::OpenOptions::new()
2995 .write(true)
2996 .open(&path)?
2997 .set_modified(stamp)?;
2998 #[cfg(unix)]
2999 {
3000 use std::os::unix::fs::PermissionsExt;
3001 fs::set_permissions(&path, fs::Permissions::from_mode(0o444))?;
3002 }
3003
3004 let reports = verify(store.path())?;
3005 assert!(
3006 reports[0]
3007 .problems
3008 .iter()
3009 .any(|problem| problem.contains("content drift")),
3010 "{:?}",
3011 reports[0].problems
3012 );
3013
3014 let view = tempfile::tempdir()?;
3015 let report = restore(view.path(), store.path(), None, false)?;
3016 assert!(report.errors > 0, "tampered artifact must not install");
3017 assert!(
3018 !view.path().join(&file.dest).exists(),
3019 "tampered artifact must be absent from the view"
3020 );
3021 Ok(())
3022 }
3023
3024 #[test]
3025 fn concurrent_harvest_keeps_every_unit() -> Result<()> {
3026 let store = tempfile::tempdir()?;
3027 let mut builds = Vec::new();
3028 for index in 0..6u32 {
3029 let build = tempfile::tempdir()?;
3030 let hex = format!("{:016x}", index + 1);
3031 let unit = build
3032 .path()
3033 .join("debug/.fingerprint")
3034 .join(format!("u{index}-{hex}"));
3035 fs::create_dir_all(&unit)?;
3036 fs::write(unit.join(format!("lib-u{index}")), "0123456789abcdef")?;
3037 fs::write(
3038 unit.join(format!("lib-u{index}.json")),
3039 r#"{"rustc":1,"features":"[]","declared_features":"[]","deps":[]}"#,
3040 )?;
3041 fs::create_dir_all(build.path().join("debug/deps"))?;
3042 fs::write(
3043 build
3044 .path()
3045 .join("debug/deps")
3046 .join(format!("libu{index}-{hex}.rlib")),
3047 [index as u8; 8],
3048 )?;
3049 builds.push(build);
3050 }
3051 std::thread::scope(|scope| {
3052 for build in &builds {
3053 scope.spawn(|| harvest(build.path(), store.path(), None, false));
3054 }
3055 });
3056 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
3057 assert_eq!(manifest.units.len(), builds.len(), "{:?}", manifest.units);
3058 Ok(())
3059 }
3060
3061 #[cfg(unix)]
3062 #[test]
3063 fn store_directories_are_private() -> Result<()> {
3064 use std::os::unix::fs::MetadataExt;
3065 let build = tempfile::tempdir()?;
3066 let store = tempfile::tempdir()?;
3067 synthetic_build_dir(build.path())?;
3068 harvest(build.path(), store.path(), None, false)?;
3069 assert_eq!(
3070 fs::metadata(store.path())?.mode() & 0o077,
3071 0,
3072 "store root 0700"
3073 );
3074 let generation = store.path().join(parity_key()?);
3075 assert_eq!(
3076 fs::metadata(&generation)?.mode() & 0o077,
3077 0,
3078 "generation 0700"
3079 );
3080 Ok(())
3081 }
3082
3083 #[test]
3084 fn verify_ordering_is_quiet_when_units_are_current() -> Result<()> {
3085 let build = tempfile::tempdir()?;
3086 let store = tempfile::tempdir()?;
3087 let now = now_secs();
3088 synthetic_edge_dir(build.path(), now - 3600, now)?;
3089 harvest(build.path(), store.path(), None, false)?;
3090
3091 let reports = verify(store.path())?;
3092 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
3093 assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
3094 Ok(())
3095 }
3096}