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 /// Whether the file at this identity's path still has exactly this
42 /// identity, so the digest recorded against it still describes the bytes on
43 /// disk without reading them again.
44 ///
45 /// Length alone would miss an overwrite that keeps the size, and the
46 /// modification time can be put back by whoever rewrote the file. The
47 /// change time cannot be set from user space, so where the platform reports
48 /// one a rewrite that restores the old modification time still shows. A
49 /// file that has vanished is an error rather than a change, so the caller
50 /// can tell the two apart.
51 pub fn still_describes(&self) -> std::io::Result<bool> {
52 let metadata = std::fs::metadata(&self.path)?;
53 Ok(Self::describe(&self.path, &metadata).as_ref() == Some(self))
54 }
55}
56
57/// The metadata-change time as an opaque token, where the platform has one.
58#[cfg(unix)]
59fn change_token(metadata: &std::fs::Metadata) -> Option<(i64, i64)> {
60 use std::os::unix::fs::MetadataExt;
61 Some((metadata.ctime(), metadata.ctime_nsec()))
62}
63
64/// Windows reports creation rather than metadata-change time, which a rewrite
65/// does not move, so no token is better than a misleading one.
66#[cfg(not(unix))]
67fn change_token(_metadata: &std::fs::Metadata) -> Option<(i64, i64)> {
68 None
69}
70
71/// A file digest recorded against the identity it was read under.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct RecordedFileDigest {
75 /// Identity of the file when its contents were hashed.
76 pub file: FileIdentity,
77 /// Digest of those contents.
78 pub digest: CacheDigest,
79}
80
81/// What a recorded file digest may stand in for.
82///
83/// Adapters prove different things when they read a file: the cc adapter's
84/// input scan also establishes that a source embeds no timestamp macro, which
85/// a digest recorded by the rustc adapter never checked. Scoping the ledger
86/// keeps one adapter's shortcut from resting on a property another adapter
87/// never established.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum FileDigestScope {
91 /// The digest describes the file's contents and nothing more.
92 Content,
93 /// The digest describes a cc compiler input that also passed the
94 /// timestamp-macro scan.
95 CcInput,
96}
97
98/// Recorded digests a session may consult instead of rehashing a file.
99///
100/// The agent's file-digest ledger answers through this everywhere a caller
101/// holds one; the no-op implementation stands in where reuse must not happen,
102/// such as under verification.
103pub trait FileDigestCache: Send + Sync {
104 /// Recorded digests for these identities, in request order.
105 fn find(&self, scope: FileDigestScope, files: &[FileIdentity]) -> Vec<Option<CacheDigest>>;
106 /// Record digests for files read under these identities.
107 fn record(&self, scope: FileDigestScope, entries: Vec<RecordedFileDigest>);
108}
109
110/// A [`FileDigestCache`] that remembers nothing and finds nothing.
111pub struct NoFileDigestCache;
112
113impl FileDigestCache for NoFileDigestCache {
114 fn find(&self, _scope: FileDigestScope, files: &[FileIdentity]) -> Vec<Option<CacheDigest>> {
115 vec![None; files.len()]
116 }
117
118 fn record(&self, _scope: FileDigestScope, _entries: Vec<RecordedFileDigest>) {}
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn an_identity_describes_the_file_until_it_is_written_or_removed() {
127 let directory = tempfile::tempdir().unwrap();
128 let path = directory.path().join("input.rs");
129 std::fs::write(&path, b"fn main() {}").unwrap();
130 let identity = FileIdentity::describe(&path, &std::fs::metadata(&path).unwrap()).unwrap();
131 assert!(identity.still_describes().unwrap());
132
133 std::thread::sleep(std::time::Duration::from_millis(20));
134 std::fs::write(&path, b"fn main() { }").unwrap();
135 assert!(!identity.still_describes().unwrap());
136
137 std::fs::remove_file(&path).unwrap();
138 assert!(identity.still_describes().is_err());
139 }
140}