1use crate::CacheDigest;
2use serde::{Deserialize, Serialize};
3use std::io::{self, Read as _};
4use std::path::{Path, PathBuf};
5#[cfg(target_os = "linux")]
6use std::sync::{Mutex, OnceLock};
7use std::time::SystemTime;
8
9const DIGEST_BUFFER_BYTES: usize = 64 * 1024;
10const TIMESTAMP_MACROS: &[&[u8]] = &[b"__DATE__", b"__TIME__", b"__TIMESTAMP__"];
11
12#[cfg(target_os = "linux")]
13const STATX_IDENTITY_MASK: u32 = 0x100 | 0x200 | 0x40 | 0x1000;
14
15#[cfg(target_os = "linux")]
18#[repr(C)]
19struct LinuxStatxTimestamp {
20 seconds: i64,
21 nanos: u32,
22 reserved: i32,
23}
24
25#[cfg(target_os = "linux")]
26#[repr(C)]
27struct LinuxStatx {
28 mask: u32,
29 block_size: u32,
30 attributes: u64,
31 links: u32,
32 uid: u32,
33 gid: u32,
34 mode: u16,
35 reserved0: u16,
36 inode: u64,
37 size: u64,
38 blocks: u64,
39 attributes_mask: u64,
40 accessed: LinuxStatxTimestamp,
41 created: LinuxStatxTimestamp,
42 changed: LinuxStatxTimestamp,
43 modified: LinuxStatxTimestamp,
44 rdev_major: u32,
45 rdev_minor: u32,
46 device_major: u32,
47 device_minor: u32,
48 mount_id: u64,
49 direct_io_memory_alignment: u32,
50 direct_io_offset_alignment: u32,
51 subvolume: u64,
52 atomic_write_unit_min: u32,
53 atomic_write_unit_max: u32,
54 atomic_write_segments_max: u32,
55 direct_io_read_offset_alignment: u32,
56 atomic_write_unit_max_opt: u32,
57 reserved1: u32,
58 reserved2: [u64; 8],
59}
60
61#[cfg(target_os = "linux")]
62const _: () = assert!(std::mem::size_of::<LinuxStatx>() == 256);
63
64#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct FileIdentity {
77 pub path: PathBuf,
79 pub len: u64,
81 pub modified: SystemTime,
83 pub changed: Option<(i64, i64)>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub object: Option<FileObjectIdentity>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct FileObjectIdentity {
95 pub device_major: u32,
98 pub device_minor: u32,
101 pub mount_id: u64,
103 pub inode: u64,
105}
106
107impl FileIdentity {
108 pub fn describe(path: &Path, metadata: &std::fs::Metadata) -> Option<Self> {
111 Some(Self {
112 path: path.to_path_buf(),
113 len: metadata.len(),
114 modified: metadata.modified().ok()?,
115 changed: change_token(metadata),
116 object: metadata_object_identity(metadata),
117 })
118 }
119
120 pub fn for_digest_cache(path: &Path, metadata: &std::fs::Metadata) -> io::Result<Option<Self>> {
128 digest_cache_identity(
129 path,
130 metadata,
131 metadata_identity_is_unreliable(path, metadata)?,
132 )
133 }
134
135 pub fn still_describes(&self) -> std::io::Result<bool> {
146 let metadata = std::fs::metadata(&self.path)?;
147 Ok(Self::for_digest_cache(&self.path, &metadata)?.as_ref() == Some(self))
148 }
149
150 pub fn can_skip_content_verification(&self) -> bool {
152 self.changed.is_some() || self.object.is_some()
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct FileSnapshot {
170 identity: FileIdentity,
171 content: Option<CacheDigest>,
172 require_full_identity: bool,
173}
174
175impl FileSnapshot {
176 pub fn capture(path: &Path) -> io::Result<Option<Self>> {
178 Self::capture_with_cache(path, &NoFileDigestCache)
179 }
180
181 pub fn capture_with_cache(
184 path: &Path,
185 digests: &dyn FileDigestCache,
186 ) -> io::Result<Option<Self>> {
187 let observed_at = SystemTime::now();
188 let metadata = std::fs::metadata(path)?;
189 let unreliable = metadata_identity_is_unreliable(path, &metadata)?;
190 capture_file_snapshot(path, digests, unreliable, unreliable, metadata, observed_at)
191 }
192
193 pub fn capture_content_with_cache(
202 path: &Path,
203 digests: &dyn FileDigestCache,
204 ) -> io::Result<Option<Self>> {
205 let observed_at = SystemTime::now();
206 let metadata = std::fs::metadata(path)?;
207 let unreliable = metadata_identity_is_unreliable(path, &metadata)?;
211 capture_file_snapshot(path, digests, unreliable, true, metadata, observed_at)
212 }
213
214 pub fn matches(&self, identity: Option<&FileIdentity>, content: &CacheDigest) -> bool {
224 self.content.as_ref().map_or_else(
225 || identity == Some(&self.identity),
226 |before| {
227 before == content
228 && identity.is_some_and(|after| {
229 if self.require_full_identity {
230 &self.identity == after
231 } else {
232 self.identity.path == after.path
233 && self.identity.len == after.len
234 && self.identity.object == after.object
235 }
236 })
237 },
238 )
239 }
240
241 pub fn differs_only_in_change_token(&self, identity: Option<&FileIdentity>) -> bool {
251 self.content.is_none()
252 && identity.is_some_and(|after| {
253 after.changed != self.identity.changed
254 && after.path == self.identity.path
255 && after.len == self.identity.len
256 && after.modified == self.identity.modified
257 && after.object == self.identity.object
258 })
259 }
260
261 pub fn proves_content_change(&self) -> bool {
263 self.content.is_some() || self.identity.changed.is_some()
264 }
265}
266
267impl From<FileIdentity> for FileSnapshot {
268 fn from(identity: FileIdentity) -> Self {
269 Self {
270 identity,
271 content: None,
272 require_full_identity: true,
273 }
274 }
275}
276
277#[cfg(target_os = "linux")]
278fn metadata_identity_is_unreliable(path: &Path, metadata: &std::fs::Metadata) -> io::Result<bool> {
279 use std::mem::MaybeUninit;
280 use std::os::unix::ffi::OsStrExt as _;
281 use std::os::unix::fs::MetadataExt as _;
282
283 static FILESYSTEMS: OnceLock<Mutex<std::collections::BTreeMap<u64, bool>>> = OnceLock::new();
284 let filesystems = FILESYSTEMS.get_or_init(|| Mutex::new(std::collections::BTreeMap::new()));
285 if let Some(unreliable) = filesystems.lock().unwrap().get(&metadata.dev()).copied() {
286 return Ok(unreliable);
287 }
288
289 let path = std::ffi::CString::new(path.as_os_str().as_bytes())
290 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains a NUL byte"))?;
291 let mut status = MaybeUninit::<libc::statfs>::zeroed();
292 let result = unsafe { libc::statfs(path.as_ptr(), status.as_mut_ptr()) };
295 if result != 0 {
296 return Err(io::Error::last_os_error());
297 }
298 let status = unsafe { status.assume_init() };
300 let unreliable = status.f_type == 0x6969;
302 filesystems
303 .lock()
304 .unwrap()
305 .insert(metadata.dev(), unreliable);
306 Ok(unreliable)
307}
308
309#[cfg(not(target_os = "linux"))]
310fn metadata_identity_is_unreliable(
311 _path: &Path,
312 _metadata: &std::fs::Metadata,
313) -> io::Result<bool> {
314 Ok(false)
315}
316
317fn capture_file_snapshot(
321 path: &Path,
322 digests: &dyn FileDigestCache,
323 unreliable: bool,
324 content_identity: bool,
325 metadata: std::fs::Metadata,
326 observed_at: SystemTime,
327) -> io::Result<Option<FileSnapshot>> {
328 let cache_identity = digest_cache_identity(path, &metadata, unreliable)?;
329 let Some(identity) = cache_identity
330 .clone()
331 .or_else(|| FileIdentity::describe(path, &metadata))
332 else {
333 return Ok(None);
334 };
335 let racy = identity_is_racy(&identity, observed_at);
339 let cache_identity = cache_identity.filter(|_| !racy);
340 let (identity, content) = if content_identity || racy {
341 let resolved = cache_identity
342 .as_ref()
343 .and_then(|identity| {
344 digests
345 .resolve(FileDigestScope::Content, std::slice::from_ref(identity))
346 .pop()
347 })
348 .unwrap_or(FileDigestResolution::Unresolved);
349 let cached = match resolved {
350 FileDigestResolution::Digest(digest)
351 if cache_identity.as_ref().is_some_and(|identity| {
352 identity.len == digest.size && identity.still_describes().unwrap_or(false)
353 }) =>
354 {
355 Some(digest)
356 }
357 FileDigestResolution::Digest(_)
358 | FileDigestResolution::EmbeddedTimestampMacro
359 | FileDigestResolution::Unresolved => None,
360 };
361 if let Some(digest) = cached {
362 (identity, Some(digest))
363 } else {
364 let observed = digest_file_validated(FileDigestScope::Content, path)?
365 .ok_or_else(|| io::Error::other("could not establish a file digest identity"))?;
366 let digest = observed
367 .resolution
368 .into_digest()
369 .ok_or_else(|| io::Error::other("content digest resolution returned no digest"))?;
370 if let Some(file) = observed.cache_identity
371 && file.len == digest.size
372 {
373 digests.record(
374 FileDigestScope::Content,
375 vec![RecordedFileDigest {
376 file,
377 digest: digest.clone(),
378 }],
379 );
380 }
381 (observed.identity, Some(digest))
382 }
383 } else {
384 (identity, None)
385 };
386 Ok(Some(FileSnapshot {
387 identity,
388 content,
389 require_full_identity: !unreliable && !content_identity,
390 }))
391}
392
393fn digest_cache_identity(
394 path: &Path,
395 metadata: &std::fs::Metadata,
396 unreliable: bool,
397) -> io::Result<Option<FileIdentity>> {
398 if unreliable {
399 nfs_file_identity(path)
400 } else {
401 Ok(FileIdentity::describe(path, metadata))
402 }
403}
404
405#[cfg(target_os = "linux")]
406fn nfs_file_identity(path: &Path) -> io::Result<Option<FileIdentity>> {
407 use std::mem::MaybeUninit;
408 use std::os::unix::ffi::OsStrExt as _;
409
410 let path = std::ffi::CString::new(path.as_os_str().as_bytes())
411 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains a NUL byte"))?;
412 let mut status = MaybeUninit::<LinuxStatx>::zeroed();
413 let result = unsafe {
415 libc::syscall(
416 libc::SYS_statx,
417 libc::AT_FDCWD,
418 path.as_ptr(),
419 0x4000,
423 STATX_IDENTITY_MASK,
424 status.as_mut_ptr(),
425 )
426 };
427 if result != 0 {
428 let error = io::Error::last_os_error();
429 return match error.raw_os_error() {
430 Some(libc::ENOSYS | libc::EINVAL | libc::EOPNOTSUPP) => Ok(None),
431 _ => Err(error),
432 };
433 }
434 let status = unsafe { status.assume_init() };
436 nfs_identity_from_statx(
437 Path::new(std::ffi::OsStr::from_bytes(path.to_bytes())),
438 &status,
439 )
440}
441
442#[cfg(target_os = "linux")]
443fn nfs_identity_from_statx(path: &Path, status: &LinuxStatx) -> io::Result<Option<FileIdentity>> {
444 if status.mask & STATX_IDENTITY_MASK != STATX_IDENTITY_MASK
445 || status.modified.nanos >= 1_000_000_000
446 {
447 return Ok(None);
448 }
449 let modified = system_time(status.modified.seconds, status.modified.nanos)?;
450 Ok(Some(FileIdentity {
451 path: path.to_path_buf(),
452 len: status.size,
453 modified,
454 changed: None,
455 object: Some(FileObjectIdentity {
456 device_major: status.device_major,
457 device_minor: status.device_minor,
458 mount_id: status.mount_id,
459 inode: status.inode,
460 }),
461 }))
462}
463
464#[cfg(not(target_os = "linux"))]
465fn nfs_file_identity(_path: &Path) -> io::Result<Option<FileIdentity>> {
466 Ok(None)
467}
468
469#[cfg(target_os = "linux")]
470fn system_time(seconds: i64, nanos: u32) -> io::Result<SystemTime> {
471 if seconds >= 0 {
472 SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::new(seconds as u64, nanos))
473 } else {
474 SystemTime::UNIX_EPOCH
475 .checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs()))
476 .and_then(|time| time.checked_add(std::time::Duration::from_nanos(nanos.into())))
477 }
478 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "file timestamp is out of range"))
479}
480
481#[cfg(unix)]
483fn change_token(metadata: &std::fs::Metadata) -> Option<(i64, i64)> {
484 use std::os::unix::fs::MetadataExt;
485 Some((metadata.ctime(), metadata.ctime_nsec()))
486}
487
488#[cfg(not(unix))]
491fn change_token(_metadata: &std::fs::Metadata) -> Option<(i64, i64)> {
492 None
493}
494
495#[cfg(unix)]
496fn metadata_object_identity(metadata: &std::fs::Metadata) -> Option<FileObjectIdentity> {
497 use std::os::unix::fs::MetadataExt as _;
498
499 let device = metadata.dev();
500 Some(FileObjectIdentity {
501 device_major: (device >> 32) as u32,
502 device_minor: device as u32,
503 mount_id: 0,
504 inode: metadata.ino(),
505 })
506}
507
508#[cfg(not(unix))]
509fn metadata_object_identity(_metadata: &std::fs::Metadata) -> Option<FileObjectIdentity> {
510 None
511}
512
513#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
515#[serde(deny_unknown_fields)]
516pub struct RecordedFileDigest {
517 pub file: FileIdentity,
519 pub digest: CacheDigest,
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
531#[serde(rename_all = "snake_case")]
532pub enum FileDigestScope {
533 Content,
535 CcInput,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(tag = "kind", rename_all = "snake_case")]
543pub enum FileDigestResolution {
544 Digest(CacheDigest),
546 EmbeddedTimestampMacro,
548 Unresolved,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq)]
554pub struct ValidatedFileDigest {
555 pub resolution: FileDigestResolution,
557 pub identity: FileIdentity,
559 pub cache_identity: Option<FileIdentity>,
561 pub racy: bool,
564}
565
566impl FileDigestResolution {
567 pub fn into_digest(self) -> Option<CacheDigest> {
569 match self {
570 Self::Digest(digest) => Some(digest),
571 Self::EmbeddedTimestampMacro | Self::Unresolved => None,
572 }
573 }
574}
575
576pub fn digest_file(scope: FileDigestScope, path: &Path) -> io::Result<FileDigestResolution> {
579 let file = std::fs::File::open(path)?;
580 digest_reader(scope, &file).map(|(resolution, _)| resolution)
581}
582
583pub fn digest_file_validated(
586 scope: FileDigestScope,
587 path: &Path,
588) -> io::Result<Option<ValidatedFileDigest>> {
589 let started = SystemTime::now();
592 let file = std::fs::File::open(path)?;
593 let before = file.metadata()?;
594 if !before.is_file() {
595 return Err(io::Error::new(
596 io::ErrorKind::InvalidInput,
597 format!("digest path is not a regular file: {}", path.display()),
598 ));
599 }
600 let (resolution, size) = digest_reader(scope, &file)?;
601 let after = file.metadata()?;
602 let current_metadata = std::fs::metadata(path)?;
603 let unreliable = metadata_identity_is_unreliable(path, ¤t_metadata)?;
604 let cache_identity = digest_cache_identity(path, ¤t_metadata, unreliable)?;
605 let identity = cache_identity
606 .clone()
607 .or_else(|| FileIdentity::describe(path, ¤t_metadata));
608 let Some(identity) = identity else {
609 return Ok(None);
610 };
611 if !metadata_names_same_file(&after, ¤t_metadata) {
612 return Err(io::Error::new(
613 io::ErrorKind::WouldBlock,
614 "file was replaced while its contents were being read",
615 ));
616 }
617 if !handle_and_path_are_stable(path, &before, &after, &identity, unreliable, size)? {
618 return Err(io::Error::new(
619 io::ErrorKind::WouldBlock,
620 "file changed while its contents were being read",
621 ));
622 }
623 let racy = cache_identity
628 .as_ref()
629 .is_some_and(|identity| identity_is_racy(identity, started));
630 let cache_identity = cache_identity.filter(|_| !racy);
631 Ok(Some(ValidatedFileDigest {
632 resolution,
633 identity,
634 cache_identity,
635 racy,
636 }))
637}
638
639const FINE_TIMESTAMP_RACE_WINDOW: std::time::Duration = std::time::Duration::from_millis(20);
643
644const COARSE_TIMESTAMP_RACE_WINDOW: std::time::Duration = std::time::Duration::from_secs(2);
647
648fn identity_is_racy(identity: &FileIdentity, observed_at: SystemTime) -> bool {
657 let nanos = |time: SystemTime| {
658 time.duration_since(SystemTime::UNIX_EPOCH)
659 .map_or(0, |since| since.subsec_nanos())
660 };
661 let coarse = nanos(identity.modified) == 0
662 && identity
663 .changed
664 .is_none_or(|(_, changed_nanos)| changed_nanos == 0);
665 let window = if coarse {
666 COARSE_TIMESTAMP_RACE_WINDOW
667 } else {
668 FINE_TIMESTAMP_RACE_WINDOW
669 };
670 let recent = |time: SystemTime| {
671 observed_at
672 .duration_since(time)
673 .map_or(true, |age| age < window)
674 };
675 recent(identity.modified)
676 || identity.changed.is_some_and(|(seconds, changed_nanos)| {
677 u64::try_from(seconds).map_or(true, |seconds| {
678 recent(
679 SystemTime::UNIX_EPOCH
680 + std::time::Duration::new(seconds, changed_nanos.try_into().unwrap_or(0)),
681 )
682 })
683 })
684}
685
686fn digest_reader(
687 scope: FileDigestScope,
688 file: &std::fs::File,
689) -> io::Result<(FileDigestResolution, u64)> {
690 let mut reader = std::io::BufReader::new(file);
691 let mut hasher = blake3::Hasher::new();
692 let mut size = 0_u64;
693 let longest_macro = TIMESTAMP_MACROS
694 .iter()
695 .map(|macro_name| macro_name.len())
696 .max()
697 .unwrap_or_default();
698 let mut window = Vec::with_capacity(DIGEST_BUFFER_BYTES + longest_macro);
699 let mut chunk = vec![0_u8; DIGEST_BUFFER_BYTES];
700 let mut found_timestamp_macro = false;
701 loop {
702 let read = reader.read(&mut chunk)?;
703 if read == 0 {
704 break;
705 }
706 hasher.update(&chunk[..read]);
707 size = size
708 .checked_add(read as u64)
709 .ok_or_else(|| io::Error::other("file length overflowed u64"))?;
710 if scope == FileDigestScope::CcInput && !found_timestamp_macro {
711 window.extend_from_slice(&chunk[..read]);
712 found_timestamp_macro = TIMESTAMP_MACROS
713 .iter()
714 .any(|macro_name| memchr::memmem::find(&window, macro_name).is_some());
715 let keep = window.len().saturating_sub(longest_macro.saturating_sub(1));
716 window.drain(..keep);
717 }
718 }
719 if found_timestamp_macro {
720 Ok((FileDigestResolution::EmbeddedTimestampMacro, size))
721 } else {
722 Ok((
723 FileDigestResolution::Digest(CacheDigest {
724 algorithm: "blake3".into(),
725 hash: hasher.finalize().to_hex().to_string(),
726 size,
727 }),
728 size,
729 ))
730 }
731}
732
733fn handle_and_path_are_stable(
734 path: &Path,
735 before: &std::fs::Metadata,
736 after: &std::fs::Metadata,
737 current: &FileIdentity,
738 unreliable: bool,
739 digest_size: u64,
740) -> io::Result<bool> {
741 if before.len() != after.len() || after.len() != digest_size || current.len != digest_size {
742 return Ok(false);
743 }
744 if !metadata_names_same_file(before, after) {
745 return Ok(false);
746 }
747 if unreliable {
748 #[cfg(target_os = "linux")]
749 {
750 let before_modified = before.modified()?;
751 let after_modified = after.modified()?;
752 let object_matches = current.object.as_ref().is_none_or(|object| {
753 metadata_matches_object(before, object) && metadata_matches_object(after, object)
754 });
755 return Ok(before_modified == after_modified
756 && after_modified == current.modified
757 && object_matches);
758 }
759 #[cfg(not(target_os = "linux"))]
760 {
761 let _ = (path, before, after, current);
762 return Ok(false);
763 }
764 }
765 let before = FileIdentity::describe(path, before);
766 let after = FileIdentity::describe(path, after);
767 Ok(before.is_some() && before == after && after.as_ref() == Some(current))
768}
769
770#[cfg(unix)]
771fn metadata_names_same_file(left: &std::fs::Metadata, right: &std::fs::Metadata) -> bool {
772 use std::os::unix::fs::MetadataExt as _;
773
774 left.dev() == right.dev() && left.ino() == right.ino()
775}
776
777#[cfg(not(unix))]
778fn metadata_names_same_file(_left: &std::fs::Metadata, _right: &std::fs::Metadata) -> bool {
779 true
780}
781
782#[cfg(target_os = "linux")]
783fn metadata_matches_object(metadata: &std::fs::Metadata, object: &FileObjectIdentity) -> bool {
784 use std::os::unix::fs::MetadataExt as _;
785
786 let device = metadata.dev();
787 let major = ((device >> 8) & 0x0fff) | ((device >> 32) & 0xfffff000);
788 let minor = (device & 0x00ff) | ((device >> 12) & 0xffffff00);
789 u32::try_from(major).ok() == Some(object.device_major)
790 && u32::try_from(minor).ok() == Some(object.device_minor)
791 && metadata.ino() == object.inode
792}
793
794pub trait FileDigestCache: Send + Sync {
800 fn resolve(&self, scope: FileDigestScope, files: &[FileIdentity]) -> Vec<FileDigestResolution> {
802 self.find(scope, files)
803 .into_iter()
804 .map(|digest| {
805 digest.map_or(
806 FileDigestResolution::Unresolved,
807 FileDigestResolution::Digest,
808 )
809 })
810 .collect()
811 }
812 fn find(&self, scope: FileDigestScope, files: &[FileIdentity]) -> Vec<Option<CacheDigest>>;
814 fn record(&self, scope: FileDigestScope, entries: Vec<RecordedFileDigest>);
816}
817
818pub struct NoFileDigestCache;
820
821impl FileDigestCache for NoFileDigestCache {
822 fn resolve(
823 &self,
824 _scope: FileDigestScope,
825 files: &[FileIdentity],
826 ) -> Vec<FileDigestResolution> {
827 vec![FileDigestResolution::Unresolved; files.len()]
828 }
829
830 fn find(&self, _scope: FileDigestScope, files: &[FileIdentity]) -> Vec<Option<CacheDigest>> {
831 vec![None; files.len()]
832 }
833
834 fn record(&self, _scope: FileDigestScope, _entries: Vec<RecordedFileDigest>) {}
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840
841 #[cfg(target_os = "linux")]
842 #[test]
843 fn an_nfs_snapshot_consults_the_session_digest_ledger() {
844 struct CountingCache(std::sync::atomic::AtomicUsize);
845 impl FileDigestCache for CountingCache {
846 fn find(
847 &self,
848 scope: FileDigestScope,
849 files: &[FileIdentity],
850 ) -> Vec<Option<CacheDigest>> {
851 assert_eq!(scope, FileDigestScope::Content);
852 assert_eq!(files.len(), 1);
853 assert_eq!(files[0].changed, None);
854 assert!(files[0].object.is_some());
855 self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
856 vec![None]
857 }
858 fn record(&self, _scope: FileDigestScope, _entries: Vec<RecordedFileDigest>) {}
859 }
860 let directory = tempfile::tempdir().unwrap();
861 let path = directory.path().join("input.rlib");
862 std::fs::write(&path, b"nfs bytes").unwrap();
863 if nfs_file_identity(&path).unwrap().is_none() {
864 eprintln!("skipping: statx cannot supply the NFS ledger identity");
865 return;
866 }
867 let cache = CountingCache(std::sync::atomic::AtomicUsize::new(0));
868 let metadata = std::fs::metadata(&path).unwrap();
869 let observed_at = metadata.modified().unwrap() + std::time::Duration::from_secs(3);
871 let snapshot = capture_file_snapshot(&path, &cache, true, true, metadata, observed_at)
872 .unwrap()
873 .unwrap();
874 assert_eq!(snapshot.content, Some(CacheDigest::blake3(b"nfs bytes")));
875 assert_eq!(cache.0.load(std::sync::atomic::Ordering::Relaxed), 1);
876 }
877
878 #[test]
879 fn timestamp_macros_are_detected_at_every_chunk_boundary_split() {
880 let directory = tempfile::tempdir().unwrap();
881 let path = directory.path().join("input.h");
882 for macro_name in TIMESTAMP_MACROS {
883 for split in 0..=macro_name.len() {
885 let mut bytes = vec![b'x'; DIGEST_BUFFER_BYTES - split];
886 bytes.extend_from_slice(macro_name);
887 std::fs::write(&path, &bytes).unwrap();
888 assert_eq!(
889 digest_file(FileDigestScope::CcInput, &path).unwrap(),
890 FileDigestResolution::EmbeddedTimestampMacro,
891 "macro {macro_name:?}, split {split}"
892 );
893 assert_eq!(
895 digest_file(FileDigestScope::Content, &path).unwrap(),
896 FileDigestResolution::Digest(CacheDigest::blake3(&bytes))
897 );
898 }
899 }
900 }
901
902 #[test]
903 fn timestamp_scan_accepts_empty_short_and_near_match_inputs() {
904 let directory = tempfile::tempdir().unwrap();
905 let path = directory.path().join("input.h");
906 for bytes in [
907 b"".as_slice(),
908 b"_",
909 b"__DATE_ __TIME_ __TIMESTAMP_",
910 b"__date__ __time__ __timestamp__",
911 &[0xff, 0, b'_', b'_'],
912 ] {
913 std::fs::write(&path, bytes).unwrap();
914 assert_eq!(
915 digest_file(FileDigestScope::CcInput, &path).unwrap(),
916 FileDigestResolution::Digest(CacheDigest::blake3(bytes))
917 );
918 }
919 }
920
921 #[cfg(unix)]
926 #[test]
927 fn a_content_snapshot_survives_a_hardlink_and_a_metadata_one_does_not() {
928 let directory = tempfile::tempdir().unwrap();
929 let path = directory.path().join("libdep.rmeta");
930 std::fs::write(&path, b"metadata").unwrap();
931 let metadata = std::fs::metadata(&path).unwrap();
932 let observed_at = metadata.modified().unwrap() + std::time::Duration::from_secs(3);
933 let by_metadata = capture_file_snapshot(
934 &path,
935 &NoFileDigestCache,
936 false,
937 false,
938 metadata,
939 observed_at,
940 )
941 .unwrap()
942 .unwrap();
943 let by_content = FileSnapshot::capture_content_with_cache(&path, &NoFileDigestCache)
944 .unwrap()
945 .unwrap();
946 let digest = CacheDigest::blake3_file(&path).unwrap();
947 std::thread::sleep(std::time::Duration::from_millis(20));
949 std::fs::hard_link(&path, directory.path().join("metadata.rmeta")).unwrap();
950 let after = FileIdentity::describe(&path, &std::fs::metadata(&path).unwrap()).unwrap();
951
952 assert!(by_content.proves_content_change());
953 assert!(by_content.matches(Some(&after), &digest));
954 if by_metadata.proves_content_change() {
955 assert!(!by_metadata.matches(Some(&after), &digest));
956 }
957
958 std::fs::write(&path, b"rewritten").unwrap();
959 let rewritten = CacheDigest::blake3_file(&path).unwrap();
960 let after = FileIdentity::describe(&path, &std::fs::metadata(&path).unwrap()).unwrap();
961 assert!(!by_content.matches(Some(&after), &rewritten));
962 }
963
964 #[cfg(unix)]
965 struct RecordingDigestCache {
966 answer: Option<CacheDigest>,
967 recorded: std::sync::Mutex<Vec<RecordedFileDigest>>,
968 }
969
970 #[cfg(unix)]
971 impl FileDigestCache for RecordingDigestCache {
972 fn find(
973 &self,
974 _scope: FileDigestScope,
975 files: &[FileIdentity],
976 ) -> Vec<Option<CacheDigest>> {
977 assert_eq!(files.len(), 1);
978 vec![self.answer.clone()]
979 }
980
981 fn record(&self, _scope: FileDigestScope, entries: Vec<RecordedFileDigest>) {
982 self.recorded.lock().unwrap().extend(entries);
983 }
984 }
985
986 #[cfg(unix)]
987 #[test]
988 fn content_snapshot_records_a_replacement_under_its_own_identity() {
989 let directory = tempfile::tempdir().unwrap();
990 let path = directory.path().join("input.rlib");
991 let replacement = directory.path().join("replacement.rlib");
992 std::fs::write(&path, b"original....").unwrap();
993 std::fs::File::options()
994 .write(true)
995 .open(&path)
996 .unwrap()
997 .set_times(std::fs::FileTimes::new().set_modified(SystemTime::UNIX_EPOCH))
998 .unwrap();
999 std::fs::write(&replacement, b"replacement!").unwrap();
1000 std::fs::File::options()
1001 .write(true)
1002 .open(&replacement)
1003 .unwrap()
1004 .set_times(std::fs::FileTimes::new().set_modified(SystemTime::UNIX_EPOCH))
1005 .unwrap();
1006 let before = std::fs::metadata(&path).unwrap();
1007 let old_identity = FileIdentity::describe(&path, &before).unwrap();
1008 std::fs::remove_file(&path).unwrap();
1009 std::fs::rename(&replacement, &path).unwrap();
1010 let cache = RecordingDigestCache {
1011 answer: None,
1012 recorded: std::sync::Mutex::new(Vec::new()),
1013 };
1014
1015 settle_past_timestamp_race();
1016 let snapshot = capture_file_snapshot(&path, &cache, true, true, before, SystemTime::now())
1017 .unwrap()
1018 .unwrap();
1019 let current = FileIdentity::for_digest_cache(&path, &std::fs::metadata(&path).unwrap())
1020 .unwrap()
1021 .unwrap();
1022 let digest = CacheDigest::blake3_file(&path).unwrap();
1023
1024 assert_eq!(current.modified, old_identity.modified);
1025 assert_ne!(current.object, old_identity.object);
1026 assert_ne!(current, old_identity);
1027 assert!(snapshot.matches(Some(¤t), &digest));
1028 let recorded = cache.recorded.lock().unwrap();
1029 assert_eq!(recorded.len(), 1);
1030 assert_eq!(recorded[0].file, current);
1031 assert_eq!(recorded[0].digest, digest);
1032 }
1033
1034 #[cfg(unix)]
1035 #[test]
1036 fn content_snapshot_rejects_a_stale_digest_cache_hit() {
1037 let directory = tempfile::tempdir().unwrap();
1038 let path = directory.path().join("input.rlib");
1039 let replacement = directory.path().join("replacement.rlib");
1040 std::fs::write(&path, b"original....").unwrap();
1041 std::fs::File::options()
1042 .write(true)
1043 .open(&path)
1044 .unwrap()
1045 .set_times(std::fs::FileTimes::new().set_modified(SystemTime::UNIX_EPOCH))
1046 .unwrap();
1047 let before = std::fs::metadata(&path).unwrap();
1048 std::fs::write(&replacement, b"replacement!").unwrap();
1049 std::fs::File::options()
1050 .write(true)
1051 .open(&replacement)
1052 .unwrap()
1053 .set_times(std::fs::FileTimes::new().set_modified(SystemTime::UNIX_EPOCH))
1054 .unwrap();
1055 std::fs::remove_file(&path).unwrap();
1056 std::fs::rename(&replacement, &path).unwrap();
1057 let cache = RecordingDigestCache {
1058 answer: Some(CacheDigest::blake3(b"original....")),
1059 recorded: std::sync::Mutex::new(Vec::new()),
1060 };
1061
1062 settle_past_timestamp_race();
1063 let snapshot = capture_file_snapshot(&path, &cache, true, true, before, SystemTime::now())
1064 .unwrap()
1065 .unwrap();
1066 let current = FileIdentity::for_digest_cache(&path, &std::fs::metadata(&path).unwrap())
1067 .unwrap()
1068 .unwrap();
1069 let digest = CacheDigest::blake3_file(&path).unwrap();
1070
1071 assert!(snapshot.matches(Some(¤t), &digest));
1072 assert_ne!(snapshot.content, Some(CacheDigest::blake3(b"original....")));
1073 assert_eq!(cache.recorded.lock().unwrap()[0].digest, digest);
1074 }
1075
1076 #[cfg(unix)]
1077 #[test]
1078 fn a_replaced_path_does_not_validate_the_open_handle() {
1079 let directory = tempfile::tempdir().unwrap();
1080 let path = directory.path().join("input.rlib");
1081 let replacement = directory.path().join("replacement.rlib");
1082 std::fs::write(&path, b"old bytes").unwrap();
1083 std::fs::write(&replacement, b"new bytes").unwrap();
1084
1085 let file = std::fs::File::open(&path).unwrap();
1086 let before = file.metadata().unwrap();
1087 std::fs::rename(&replacement, &path).unwrap();
1088 let (_, size) = digest_reader(FileDigestScope::Content, &file).unwrap();
1089 let after = file.metadata().unwrap();
1090 let current_metadata = std::fs::metadata(&path).unwrap();
1091 let current = FileIdentity::describe(&path, ¤t_metadata).unwrap();
1092
1093 assert!(!metadata_names_same_file(&after, ¤t_metadata));
1094 assert!(
1095 !handle_and_path_are_stable(&path, &before, &after, ¤t, false, size).unwrap()
1096 );
1097 }
1098
1099 #[test]
1100 fn an_identity_describes_the_file_until_it_is_written_or_removed() {
1101 let directory = tempfile::tempdir().unwrap();
1102 let path = directory.path().join("input.rs");
1103 std::fs::write(&path, b"fn main() {}").unwrap();
1104 let identity = FileIdentity::describe(&path, &std::fs::metadata(&path).unwrap()).unwrap();
1105 assert!(identity.still_describes().unwrap());
1106
1107 std::thread::sleep(std::time::Duration::from_millis(20));
1108 std::fs::write(&path, b"fn main() { }").unwrap();
1109 assert!(!identity.still_describes().unwrap());
1110
1111 std::fs::remove_file(&path).unwrap();
1112 assert!(identity.still_describes().is_err());
1113 }
1114
1115 #[test]
1116 fn a_racy_snapshot_rejects_a_same_length_rewrite_with_identical_metadata() {
1117 let directory = tempfile::tempdir().unwrap();
1118 let path = directory.path().join("input.rs");
1119 std::fs::write(&path, b"fn main() {}").unwrap();
1120 let metadata = std::fs::metadata(&path).unwrap();
1121 let observed_at = metadata.modified().unwrap();
1122 let snapshot = capture_file_snapshot(
1123 &path,
1124 &NoFileDigestCache,
1125 false,
1126 false,
1127 metadata,
1128 observed_at,
1129 )
1130 .unwrap()
1131 .unwrap();
1132 let identity = snapshot.identity.clone();
1133 let original = CacheDigest::blake3_file(&path).unwrap();
1134 assert!(snapshot.matches(Some(&identity), &original));
1135
1136 std::fs::write(&path, b"fn main(){ }").unwrap();
1137 let rewritten = CacheDigest::blake3_file(&path).unwrap();
1138 assert_eq!(original.size, rewritten.size);
1139 assert_ne!(original, rewritten);
1140 assert!(!snapshot.matches(Some(&identity), &rewritten));
1143 assert!(snapshot.proves_content_change());
1144 }
1145
1146 #[cfg(unix)]
1147 #[test]
1148 fn a_racy_local_snapshot_rejects_an_edit_that_restores_the_original_bytes() {
1149 let directory = tempfile::tempdir().unwrap();
1150 let path = directory.path().join("input.rs");
1151 std::fs::write(&path, b"fn main() {}").unwrap();
1152 let metadata = std::fs::metadata(&path).unwrap();
1153 let observed_at = metadata.modified().unwrap();
1154 let snapshot = capture_file_snapshot(
1155 &path,
1156 &NoFileDigestCache,
1157 false,
1158 false,
1159 metadata,
1160 observed_at,
1161 )
1162 .unwrap()
1163 .unwrap();
1164 let original = CacheDigest::blake3_file(&path).unwrap();
1165 assert!(snapshot.matches(Some(&snapshot.identity), &original));
1166
1167 std::fs::write(&path, b"fn main(){ }").unwrap();
1168 std::fs::write(&path, b"fn main() {}").unwrap();
1169 let restored = CacheDigest::blake3_file(&path).unwrap();
1170 assert_eq!(original, restored);
1171 let mut after = snapshot.identity.clone();
1175 let (seconds, nanos) = after.changed.unwrap();
1176 after.changed = Some((seconds + 1, nanos));
1177 assert!(!snapshot.matches(Some(&after), &restored));
1178 }
1179
1180 #[test]
1181 fn a_settled_snapshot_keeps_the_metadata_only_path() {
1182 let directory = tempfile::tempdir().unwrap();
1183 let path = directory.path().join("input.rs");
1184 std::fs::write(&path, b"fn main() {}").unwrap();
1185 let metadata = std::fs::metadata(&path).unwrap();
1186 let observed_at = metadata.modified().unwrap() + std::time::Duration::from_secs(3);
1187 std::fs::remove_file(&path).unwrap();
1190 let snapshot = capture_file_snapshot(
1191 &path,
1192 &NoFileDigestCache,
1193 false,
1194 false,
1195 metadata,
1196 observed_at,
1197 )
1198 .unwrap()
1199 .unwrap();
1200 assert!(snapshot.content.is_none());
1201 }
1202
1203 #[cfg(unix)]
1204 #[test]
1205 fn a_racy_snapshot_does_not_reuse_a_digest_with_identical_metadata() {
1206 let directory = tempfile::tempdir().unwrap();
1207 let path = directory.path().join("input.rs");
1208 std::fs::write(&path, b"fn main() {}").unwrap();
1209 let metadata = std::fs::metadata(&path).unwrap();
1210 let observed_at = metadata.modified().unwrap();
1211 let cache = RecordingDigestCache {
1212 answer: Some(CacheDigest::blake3(b"fn main(){ }")),
1213 recorded: std::sync::Mutex::new(Vec::new()),
1214 };
1215 for content_identity in [false, true] {
1216 let snapshot = capture_file_snapshot(
1217 &path,
1218 &cache,
1219 false,
1220 content_identity,
1221 metadata.clone(),
1222 observed_at,
1223 )
1224 .unwrap()
1225 .unwrap();
1226 assert_eq!(snapshot.content, Some(CacheDigest::blake3(b"fn main() {}")));
1227 }
1228 }
1229
1230 #[test]
1231 fn a_metadata_snapshot_detects_a_metadata_change() {
1232 let directory = tempfile::tempdir().unwrap();
1233 let path = directory.path().join("input.rs");
1234 std::fs::write(&path, b"fn main() {}").unwrap();
1235 let metadata = std::fs::metadata(&path).unwrap();
1236 let observed_at = metadata.modified().unwrap() + std::time::Duration::from_secs(3);
1237 let snapshot = capture_file_snapshot(
1238 &path,
1239 &NoFileDigestCache,
1240 false,
1241 false,
1242 metadata,
1243 observed_at,
1244 )
1245 .unwrap()
1246 .unwrap();
1247
1248 std::fs::File::options()
1249 .write(true)
1250 .open(&path)
1251 .unwrap()
1252 .set_times(std::fs::FileTimes::new().set_modified(SystemTime::UNIX_EPOCH))
1253 .unwrap();
1254 let identity = FileIdentity::describe(&path, &std::fs::metadata(&path).unwrap());
1255 let digest = CacheDigest::blake3_file(&path).unwrap();
1256
1257 assert!(!snapshot.matches(identity.as_ref(), &digest));
1258 assert_eq!(snapshot.proves_content_change(), cfg!(unix));
1259 }
1260
1261 #[test]
1262 fn a_content_snapshot_ignores_timestamp_churn_but_detects_changed_bytes() {
1263 for (unreliable, content_identity) in [(true, false), (true, true), (false, true)] {
1264 let directory = tempfile::tempdir().unwrap();
1265 let path = directory.path().join("input.rs");
1266 std::fs::write(&path, b"fn main() {}").unwrap();
1267 let snapshot = capture_file_snapshot(
1268 &path,
1269 &NoFileDigestCache,
1270 unreliable,
1271 content_identity,
1272 std::fs::metadata(&path).unwrap(),
1273 SystemTime::now(),
1274 )
1275 .unwrap()
1276 .unwrap();
1277
1278 let mut identity = snapshot.identity.clone();
1279 identity.changed = identity
1280 .changed
1281 .map(|(seconds, nanos)| (seconds + 1, nanos));
1282 let digest = CacheDigest::blake3_file(&path).unwrap();
1283 assert!(snapshot.matches(Some(&identity), &digest));
1284 assert!(snapshot.proves_content_change());
1285
1286 identity.modified = SystemTime::UNIX_EPOCH;
1287 assert!(snapshot.matches(Some(&identity), &digest));
1288
1289 std::fs::write(&path, b"fn main(){ }").unwrap();
1290 let identity = FileIdentity::describe(&path, &std::fs::metadata(&path).unwrap());
1291 let digest = CacheDigest::blake3_file(&path).unwrap();
1292 assert!(!snapshot.matches(identity.as_ref(), &digest));
1293 }
1294 }
1295
1296 #[test]
1297 fn an_nfs_object_identity_can_skip_a_second_content_read() {
1298 let identity = FileIdentity {
1299 path: PathBuf::from("/nfs/input.rlib"),
1300 len: 9,
1301 modified: SystemTime::UNIX_EPOCH,
1302 changed: None,
1303 object: Some(FileObjectIdentity {
1304 device_major: 0,
1305 device_minor: 42,
1306 mount_id: 7,
1307 inode: 99,
1308 }),
1309 };
1310 assert!(identity.can_skip_content_verification());
1311 let without_object = FileIdentity {
1312 object: None,
1313 ..identity
1314 };
1315 assert!(!without_object.can_skip_content_verification());
1316 }
1317
1318 #[test]
1319 fn reliable_metadata_keeps_the_native_identity() {
1320 let directory = tempfile::tempdir().unwrap();
1321 let path = directory.path().join("input.rs");
1322 std::fs::write(&path, b"fn main() {}").unwrap();
1323 let metadata = std::fs::metadata(&path).unwrap();
1324
1325 let identity = digest_cache_identity(&path, &metadata, false)
1326 .unwrap()
1327 .unwrap();
1328
1329 assert_eq!(identity.path, path);
1330 assert_eq!(identity.len, 12);
1331 assert_eq!(identity.object.is_some(), cfg!(unix));
1332 }
1333
1334 #[test]
1335 fn content_snapshot_ignores_nfs_timestamp_churn_but_not_content_or_identity_changes() {
1336 let digest = CacheDigest::blake3(b"nfs bytes");
1337 let identity = FileIdentity {
1338 path: PathBuf::from("/nfs/input.rlib"),
1339 len: digest.size,
1340 modified: SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(10),
1341 changed: Some((10, 1)),
1342 object: Some(FileObjectIdentity {
1343 device_major: 0,
1344 device_minor: 42,
1345 mount_id: 7,
1346 inode: 99,
1347 }),
1348 };
1349 let snapshot = FileSnapshot {
1350 identity: identity.clone(),
1351 content: Some(digest.clone()),
1352 require_full_identity: false,
1353 };
1354 for seconds in [9, 11] {
1357 let mut after = identity.clone();
1358 after.changed = Some((seconds, 500));
1359 after.modified = SystemTime::UNIX_EPOCH + std::time::Duration::new(seconds as u64, 500);
1360 assert!(snapshot.matches(Some(&after), &digest));
1361 }
1362
1363 let changed = CacheDigest::blake3(b"NFS bytes");
1366 assert_eq!(changed.size, digest.size);
1367 assert!(!snapshot.matches(Some(&identity), &changed));
1368 assert!(!snapshot.matches(None, &digest));
1369
1370 let mut after = identity.clone();
1371 after.object.as_mut().unwrap().inode += 1;
1372 assert!(!snapshot.matches(Some(&after), &digest));
1373
1374 let mut after = identity.clone();
1375 after.len += 1;
1376 assert!(!snapshot.matches(Some(&after), &digest));
1377
1378 let mut after = identity;
1379 after.path.set_file_name("replacement.rlib");
1380 assert!(!snapshot.matches(Some(&after), &digest));
1381 }
1382
1383 #[cfg(target_os = "linux")]
1384 #[test]
1385 fn incomplete_statx_metadata_falls_back_to_content_hashing() {
1386 let status = unsafe { std::mem::zeroed::<LinuxStatx>() };
1388 assert!(
1389 nfs_identity_from_statx(Path::new("/nfs/input.rlib"), &status)
1390 .unwrap()
1391 .is_none()
1392 );
1393 }
1394
1395 fn settle_past_timestamp_race() {
1398 std::thread::sleep(FINE_TIMESTAMP_RACE_WINDOW * 2);
1399 }
1400
1401 #[test]
1404 fn a_digest_of_a_recently_changed_file_is_not_offered_for_reuse() {
1405 let directory = tempfile::tempdir().unwrap();
1406 let path = directory.path().join("val.rs");
1407 std::fs::write(&path, b"pub const VAL: &str = \"v1\";").unwrap();
1408 std::fs::File::options()
1409 .write(true)
1410 .open(&path)
1411 .unwrap()
1412 .set_times(
1413 std::fs::FileTimes::new()
1414 .set_modified(SystemTime::now() + std::time::Duration::from_secs(3600)),
1415 )
1416 .unwrap();
1417
1418 let fresh = digest_file_validated(FileDigestScope::Content, &path)
1419 .unwrap()
1420 .unwrap();
1421 assert!(fresh.resolution.into_digest().is_some());
1422 assert!(fresh.racy);
1423 assert_eq!(fresh.cache_identity, None);
1424 }
1425
1426 #[test]
1427 fn a_digest_of_a_settled_file_is_offered_for_reuse() {
1428 let directory = tempfile::tempdir().unwrap();
1429 let path = directory.path().join("val.rs");
1430 std::fs::write(&path, b"pub const VAL: &str = \"v1\";").unwrap();
1431
1432 settle_past_timestamp_race();
1433 let settled = digest_file_validated(FileDigestScope::Content, &path)
1434 .unwrap()
1435 .unwrap();
1436 assert!(!settled.racy);
1437 assert!(settled.cache_identity.is_some());
1438 }
1439
1440 #[test]
1441 fn racy_window_follows_the_timestamp_resolution() {
1442 let at = |seconds: u64, nanos: u32| {
1443 SystemTime::UNIX_EPOCH + std::time::Duration::new(seconds, nanos)
1444 };
1445 let identity = |modified: SystemTime, changed: Option<(i64, i64)>| FileIdentity {
1446 path: PathBuf::from("/x"),
1447 len: 1,
1448 modified,
1449 changed,
1450 object: None,
1451 };
1452 let now = at(1_000, 500_000_000);
1453 let fine = |age_ms: u64| {
1454 let when = now - std::time::Duration::from_millis(age_ms);
1455 let since = when.duration_since(SystemTime::UNIX_EPOCH).unwrap();
1456 identity(
1457 when,
1458 Some((since.as_secs() as i64, i64::from(since.subsec_nanos()))),
1459 )
1460 };
1461 assert!(identity_is_racy(&fine(5), now));
1462 assert!(!identity_is_racy(&fine(25), now));
1463 let touched = identity(at(10, 7), Some((1_000, 499_000_000)));
1465 assert!(identity_is_racy(&touched, now));
1466 for with_ctime in [false, true] {
1468 let coarse = |age: u64| {
1469 identity(
1470 at(1_000 - age, 0),
1471 with_ctime.then_some((1_000 - age as i64, 0)),
1472 )
1473 };
1474 assert!(identity_is_racy(&coarse(1), at(1_000, 0)));
1475 assert!(!identity_is_racy(&coarse(3), at(1_000, 0)));
1476 }
1477 assert!(identity_is_racy(&identity(at(2_000, 1), None), now));
1479 }
1480}