1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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(&(self.mtime.secs as i64)) {
            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 the result of `symlink_metadata`.
    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 to 32 bits is fine here because
                // that's what the linux syscalls returns
                // just rust upcasts to 64 bits for some reason?
                // numbers this large are impractical anyway (that's a lot of hard-drives).
                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,
        }
    }
}