use std::{ cmp::Ordering, time::{SystemTime, SystemTimeError},}; use filetime::FileTime; use crate::entry::Stat; impl Stat { /// Detect whether this stat entry is racy if stored in a file index with `timestamp`. /// /// An index entry is considered racy if it's `mtime` is larger or equal to the index `timestamp`. /// The index `timestamp` marks the point in time before which we definitely resolved the racy git problem /// for all index entries so any index entries that changed afterwards will need to be examined for /// changes by actually reading the file from disk at least once. pub fn is_racy( &self, timestamp: FileTime, Options { check_stat, use_nsec, .. }: Options, ) -> bool { match timestamp.unix_seconds().cmp(&i64::from(self.mtime.secs)) { Ordering::Less => true, Ordering::Equal if use_nsec && check_stat => timestamp.nanoseconds() <= self.mtime.nsecs, Ordering::Equal => true, Ordering::Greater => false, } } /// Compares the stat information of two index entries. /// /// Intuitively this is basically equivalent to `self == other`. /// However there a lot of nobs in git that tweak whether certain stat information is used when checking /// equality, see [`Options`]. /// This function respects those options while performing the stat comparison and may therefore ignore some fields. pub fn matches( &self, other: &Self, Options { trust_ctime, check_stat, use_nsec, use_stdev, }: Options, ) -> bool { if self.mtime.secs != other.mtime.secs { return false; } if check_stat && use_nsec && self.mtime.nsecs != other.mtime.nsecs { return false; } if self.size != other.size { return false; } if trust_ctime { if self.ctime.secs != other.ctime.secs { return false; } if check_stat && use_nsec && self.ctime.nsecs != other.ctime.nsecs { return false; } } if check_stat { if use_stdev && self.dev != other.dev { return false; } self.ino == other.ino && self.gid == other.gid && self.uid == other.uid } else { true } } /// Creates stat information from file metadata. /// /// The information passed to this function should originate from a function like /// `symlink_metadata`/`lstat` or `File::metadata`/`fstat`. /// /// The data are adjusted for use in the index, using default values of fields that are not /// meaningful on the target operating system or that are unavailable, and truncating data /// where doing so does not lose essential information for keeping track of file status. pub fn from_fs(stat: &crate::fs::Metadata) -> Result<Stat, SystemTimeError> { let mtime = stat.modified().unwrap_or(std::time::UNIX_EPOCH); let ctime = stat.created().unwrap_or(std::time::UNIX_EPOCH); #[cfg(windows)] let res = Stat { mtime: mtime.try_into()?, ctime: ctime.try_into()?, dev: 0, ino: 0, uid: 0, gid: 0, // Truncation to 32 bits is on purpose (git does the same). size: stat.len() as u32, }; #[cfg(not(windows))] let res = { Stat { mtime: mtime.try_into().unwrap_or_default(), ctime: ctime.try_into().unwrap_or_default(), // Truncating the device and inode numbers to 32 bits should be fine even on // targets where they are represented as 64 bits, since we do not use them // precisely for tracking changes and we do not map them back to the inode. dev: stat.dev() as u32, ino: stat.ino() as u32, uid: stat.uid(), gid: stat.gid(), // Truncation to 32 bits is on purpose (git does the same). size: stat.len() as u32, } }; Ok(res) }} impl TryFrom<SystemTime> for Time { type Error = SystemTimeError; fn try_from(s: SystemTime) -> Result<Self, SystemTimeError> { let d = s.duration_since(std::time::UNIX_EPOCH)?; Ok(Time { // truncation to 32 bits is on purpose (we only compare the low bits) secs: d.as_secs() as u32, nsecs: d.subsec_nanos(), }) }} impl From<Time> for SystemTime { fn from(s: Time) -> Self { std::time::UNIX_EPOCH + std::time::Duration::new(s.secs.into(), s.nsecs) }} /// The time component in a [`Stat`] struct.#[derive(Debug, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)]#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]pub struct Time { /// The amount of seconds elapsed since EPOCH. pub secs: u32, /// The amount of nanoseconds elapsed in the current second, ranging from 0 to 999.999.999 . pub nsecs: u32,} impl From<FileTime> for Time { fn from(value: FileTime) -> Self { Time { secs: value.unix_seconds().try_into().expect("can't represent non-unix times"), nsecs: value.nanoseconds(), } }} impl PartialEq<FileTime> for Time { fn eq(&self, other: &FileTime) -> bool { *self == Time::from(*other) }} impl PartialOrd<FileTime> for Time { fn partial_cmp(&self, other: &FileTime) -> Option<Ordering> { self.partial_cmp(&Time::from(*other)) }} /// Configuration for comparing stat entries#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]pub struct Options { /// If true, a files creation time is taken into consideration when checking if a file changed. /// Can be set to false in case other tools alter the creation time in ways that interfere with our operation. /// /// Default `true`. pub trust_ctime: bool, /// If true, all stat fields will be used when checking for up-to-date'ness of the entry. Otherwise /// nano-second parts of mtime and ctime,uid, gid, inode and device number _will not_ be used, leaving only /// the whole-second part of ctime and mtime and the file size to be checked. /// /// Default `true`. pub check_stat: bool, /// Whether to compare nano secs when comparing timestamps. This currently /// leads to many false positives on linux and is therefore disabled there. /// /// Default `false` pub use_nsec: bool, /// Whether to compare network devices secs when comparing timestamps. /// Disabled by default because this can cause many false positives on network /// devices where the device number is not stable /// /// Default `false`. pub use_stdev: bool,} impl Default for Options { fn default() -> Self { Self { trust_ctime: true, check_stat: true, use_nsec: false, use_stdev: false, } }}
use std::{
cmp::Ordering,
time::{SystemTime, SystemTimeError},
};
use filetime::FileTime;
use crate::entry::Stat;
impl Stat {
/// Detect whether this stat entry is racy if stored in a file index with `timestamp`.
///
/// An index entry is considered racy if it's `mtime` is larger or equal to the index `timestamp`.
/// The index `timestamp` marks the point in time before which we definitely resolved the racy git problem
/// for all index entries so any index entries that changed afterwards will need to be examined for
/// changes by actually reading the file from disk at least once.
pub fn is_racy(
&self,
timestamp: FileTime,
Options {
check_stat, use_nsec, ..
}: Options,
) -> bool {
match timestamp.unix_seconds().cmp(&i64::from(self.mtime.secs)) {
Ordering::Less => true,
Ordering::Equal if use_nsec && check_stat => timestamp.nanoseconds() <= self.mtime.nsecs,
Ordering::Equal => true,
Ordering::Greater => false,
}
}
/// Compares the stat information of two index entries.
///
/// Intuitively this is basically equivalent to `self == other`.
/// However there a lot of nobs in git that tweak whether certain stat information is used when checking
/// equality, see [`Options`].
/// This function respects those options while performing the stat comparison and may therefore ignore some fields.
pub fn matches(
&self,
other: &Self,
Options {
trust_ctime,
check_stat,
use_nsec,
use_stdev,
}: Options,
) -> bool {
if self.mtime.secs != other.mtime.secs {
return false;
}
if check_stat && use_nsec && self.mtime.nsecs != other.mtime.nsecs {
return false;
}
if self.size != other.size {
return false;
}
if trust_ctime {
if self.ctime.secs != other.ctime.secs {
return false;
}
if check_stat && use_nsec && self.ctime.nsecs != other.ctime.nsecs {
return false;
}
}
if check_stat {
if use_stdev && self.dev != other.dev {
return false;
}
self.ino == other.ino && self.gid == other.gid && self.uid == other.uid
} else {
true
}
}
/// Creates stat information from file metadata.
///
/// The information passed to this function should originate from a function like
/// `symlink_metadata`/`lstat` or `File::metadata`/`fstat`.
///
/// The data are adjusted for use in the index, using default values of fields that are not
/// meaningful on the target operating system or that are unavailable, and truncating data
/// where doing so does not lose essential information for keeping track of file status.
pub fn from_fs(stat: &crate::fs::Metadata) -> Result<Stat, SystemTimeError> {
let mtime = stat.modified().unwrap_or(std::time::UNIX_EPOCH);
let ctime = stat.created().unwrap_or(std::time::UNIX_EPOCH);
#[cfg(windows)]
let res = Stat {
mtime: mtime.try_into()?,
ctime: ctime.try_into()?,
dev: 0,
ino: 0,
uid: 0,
gid: 0,
// Truncation to 32 bits is on purpose (git does the same).
size: stat.len() as u32,
};
#[cfg(not(windows))]
let res = {
Stat {
mtime: mtime.try_into().unwrap_or_default(),
ctime: ctime.try_into().unwrap_or_default(),
// Truncating the device and inode numbers to 32 bits should be fine even on
// targets where they are represented as 64 bits, since we do not use them
// precisely for tracking changes and we do not map them back to the inode.
dev: stat.dev() as u32,
ino: stat.ino() as u32,
uid: stat.uid(),
gid: stat.gid(),
// Truncation to 32 bits is on purpose (git does the same).
size: stat.len() as u32,
}
};
Ok(res)
}
}
impl TryFrom<SystemTime> for Time {
type Error = SystemTimeError;
fn try_from(s: SystemTime) -> Result<Self, SystemTimeError> {
let d = s.duration_since(std::time::UNIX_EPOCH)?;
Ok(Time {
// truncation to 32 bits is on purpose (we only compare the low bits)
secs: d.as_secs() as u32,
nsecs: d.subsec_nanos(),
})
}
}
impl From<Time> for SystemTime {
fn from(s: Time) -> Self {
std::time::UNIX_EPOCH + std::time::Duration::new(s.secs.into(), s.nsecs)
}
}
/// The time component in a [`Stat`] struct.
#[derive(Debug, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Time {
/// The amount of seconds elapsed since EPOCH.
pub secs: u32,
/// The amount of nanoseconds elapsed in the current second, ranging from 0 to 999.999.999 .
pub nsecs: u32,
}
impl From<FileTime> for Time {
fn from(value: FileTime) -> Self {
Time {
secs: value.unix_seconds().try_into().expect("can't represent non-unix times"),
nsecs: value.nanoseconds(),
}
}
}
impl PartialEq<FileTime> for Time {
fn eq(&self, other: &FileTime) -> bool {
*self == Time::from(*other)
}
}
impl PartialOrd<FileTime> for Time {
fn partial_cmp(&self, other: &FileTime) -> Option<Ordering> {
self.partial_cmp(&Time::from(*other))
}
}
/// Configuration for comparing stat entries
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub struct Options {
/// If true, a files creation time is taken into consideration when checking if a file changed.
/// Can be set to false in case other tools alter the creation time in ways that interfere with our operation.
///
/// Default `true`.
pub trust_ctime: bool,
/// If true, all stat fields will be used when checking for up-to-date'ness of the entry. Otherwise
/// nano-second parts of mtime and ctime,uid, gid, inode and device number _will not_ be used, leaving only
/// the whole-second part of ctime and mtime and the file size to be checked.
///
/// Default `true`.
pub check_stat: bool,
/// Whether to compare nano secs when comparing timestamps. This currently
/// leads to many false positives on linux and is therefore disabled there.
///
/// Default `false`
pub use_nsec: bool,
/// Whether to compare network devices secs when comparing timestamps.
/// Disabled by default because this can cause many false positives on network
/// devices where the device number is not stable
///
/// Default `false`.
pub use_stdev: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
trust_ctime: true,
check_stat: true,
use_nsec: false,
use_stdev: false,
}
}
}