mbx_cache_core/agent/file_digest.rs
1use crate::CacheDigest;
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use std::time::SystemTime;
5
6/// The on-disk identity a recorded file digest describes.
7///
8/// The same trade [`VerifiedBlob`] makes for CAS reads, offered to shims for
9/// the files they hash: an overwrite moves the modification time and a
10/// truncation changes the length, so a digest recorded against both stands
11/// until either does. Where the platform reports a metadata-change time the
12/// identity carries that too, and it is the part a writer cannot restore: a
13/// rewrite that puts the modification time back still moves the change time,
14/// so only filesystems without one fall back to the freshness model the
15/// surrounding build tool already lives on.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct FileIdentity {
19 /// Absolute path of the file.
20 pub path: PathBuf,
21 /// Length of the file in bytes.
22 pub len: u64,
23 /// Modification time of the file.
24 pub modified: SystemTime,
25 /// Platform metadata-change token, where one exists.
26 pub changed: Option<(i64, i64)>,
27}
28
29impl FileIdentity {
30 /// Describe a file from metadata already in hand, or nothing when the
31 /// filesystem reports no modification time to compare against later.
32 pub fn describe(path: &Path, metadata: &std::fs::Metadata) -> Option<Self> {
33 Some(Self {
34 path: path.to_path_buf(),
35 len: metadata.len(),
36 modified: metadata.modified().ok()?,
37 changed: change_token(metadata),
38 })
39 }
40}
41
42/// The metadata-change time as an opaque token, where the platform has one.
43#[cfg(unix)]
44fn change_token(metadata: &std::fs::Metadata) -> Option<(i64, i64)> {
45 use std::os::unix::fs::MetadataExt;
46 Some((metadata.ctime(), metadata.ctime_nsec()))
47}
48
49/// Windows reports creation rather than metadata-change time, which a rewrite
50/// does not move, so no token is better than a misleading one.
51#[cfg(not(unix))]
52fn change_token(_metadata: &std::fs::Metadata) -> Option<(i64, i64)> {
53 None
54}
55
56/// A file digest recorded against the identity it was read under.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct RecordedFileDigest {
60 /// Identity of the file when its contents were hashed.
61 pub file: FileIdentity,
62 /// Digest of those contents.
63 pub digest: CacheDigest,
64}
65
66/// What a recorded file digest may stand in for.
67///
68/// Adapters prove different things when they read a file: the cc adapter's
69/// input scan also establishes that a source embeds no timestamp macro, which
70/// a digest recorded by the rustc adapter never checked. Scoping the ledger
71/// keeps one adapter's shortcut from resting on a property another adapter
72/// never established.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum FileDigestScope {
76 /// The digest describes the file's contents and nothing more.
77 Content,
78 /// The digest describes a cc compiler input that also passed the
79 /// timestamp-macro scan.
80 CcInput,
81}
82
83/// Recorded digests a session may consult instead of rehashing a file.
84///
85/// The agent's file-digest ledger answers through this everywhere a caller
86/// holds one; the no-op implementation stands in where reuse must not happen,
87/// such as under verification.
88pub trait FileDigestCache: Send + Sync {
89 /// Recorded digests for these identities, in request order.
90 fn find(&self, scope: FileDigestScope, files: &[FileIdentity]) -> Vec<Option<CacheDigest>>;
91 /// Record digests for files read under these identities.
92 fn record(&self, scope: FileDigestScope, entries: Vec<RecordedFileDigest>);
93}
94
95/// A [`FileDigestCache`] that remembers nothing and finds nothing.
96pub struct NoFileDigestCache;
97
98impl FileDigestCache for NoFileDigestCache {
99 fn find(&self, _scope: FileDigestScope, files: &[FileIdentity]) -> Vec<Option<CacheDigest>> {
100 vec![None; files.len()]
101 }
102
103 fn record(&self, _scope: FileDigestScope, _entries: Vec<RecordedFileDigest>) {}
104}