gix_index/
fs.rs

1//! This module contains a `Metadata` implementation that must be used instead of `std::fs::Metadata` to assure
2//! that the `ctime` information is populated exactly like the one in `git`, which wouldn't be the case on unix.
3#![allow(clippy::useless_conversion)] // on some MacOOS conversions are required, but on linux usually not.
4#![allow(clippy::unnecessary_cast)]
5
6// it's allowed for good measure, in case there are systems that use different types for that.
7use std::{path::Path, time::SystemTime};
8
9/// A structure to partially mirror [`std::fs::Metadata`].
10#[cfg(not(windows))]
11pub struct Metadata(rustix::fs::Stat);
12
13#[cfg(windows)]
14/// A structure to partially mirror [`std::fs::Metadata`].
15pub struct Metadata(std::fs::Metadata);
16
17/// Lifecycle
18impl Metadata {
19    /// Obtain the metadata at `path` without following symlinks.
20    pub fn from_path_no_follow(path: &Path) -> Result<Self, std::io::Error> {
21        #[cfg(not(windows))]
22        {
23            rustix::fs::lstat(path).map(Metadata).map_err(Into::into)
24        }
25        #[cfg(windows)]
26        path.symlink_metadata().map(Metadata)
27    }
28
29    /// Obtain the metadata at `path` without following symlinks.
30    pub fn from_file(file: &std::fs::File) -> Result<Self, std::io::Error> {
31        #[cfg(not(windows))]
32        {
33            rustix::fs::fstat(file).map(Metadata).map_err(Into::into)
34        }
35        #[cfg(windows)]
36        file.metadata().map(Metadata)
37    }
38}
39
40/// Access
41#[allow(clippy::len_without_is_empty)]
42impl Metadata {
43    /// Return true if the metadata belongs to a directory
44    pub fn is_dir(&self) -> bool {
45        #[cfg(not(windows))]
46        {
47            (self.0.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFDIR as u32
48        }
49        #[cfg(windows)]
50        self.0.is_dir()
51    }
52
53    /// Return the time at which the underlying file was modified.
54    pub fn modified(&self) -> Option<SystemTime> {
55        #[cfg(not(windows))]
56        {
57            #[cfg(not(any(target_os = "aix", target_os = "hurd")))]
58            let seconds = self.0.st_mtime;
59            #[cfg(any(target_os = "aix", target_os = "hurd"))]
60            let seconds = self.0.st_mtim.tv_sec;
61
62            #[cfg(not(any(target_os = "netbsd", target_os = "aix", target_os = "hurd")))]
63            let nanoseconds = self.0.st_mtime_nsec;
64            #[cfg(target_os = "netbsd")]
65            let nanoseconds = self.0.st_mtimensec;
66            #[cfg(any(target_os = "aix", target_os = "hurd"))]
67            let nanoseconds = self.0.st_mtim.tv_nsec;
68
69            // All operating systems treat the seconds as offset from unix epoch, hence it must
70            // be signed in order to deal with dates before epoch.
71            // Rustix seems to think this value is u64, but we fix it here for now.
72            let seconds = seconds as i64;
73            system_time_from_secs_nanos(seconds, nanoseconds.try_into().ok()?)
74        }
75        #[cfg(windows)]
76        self.0.modified().ok()
77    }
78
79    /// Return the time at which the underlying file was created.
80    ///
81    /// Note that this differs from [`std::fs::Metadata::created()`] which would return
82    /// the inode birth time, which is notably different to what `git` does.
83    pub fn created(&self) -> Option<SystemTime> {
84        #[cfg(not(windows))]
85        {
86            #[cfg(not(any(target_os = "aix", target_os = "hurd")))]
87            let seconds = self.0.st_ctime;
88            #[cfg(any(target_os = "aix", target_os = "hurd"))]
89            let seconds = self.0.st_ctim.tv_sec;
90
91            #[cfg(not(any(target_os = "netbsd", target_os = "aix", target_os = "hurd")))]
92            let nanoseconds = self.0.st_ctime_nsec;
93            #[cfg(target_os = "netbsd")]
94            let nanoseconds = self.0.st_ctimensec;
95            #[cfg(any(target_os = "aix", target_os = "hurd"))]
96            let nanoseconds = self.0.st_ctim.tv_nsec;
97
98            // All operating systems treat the seconds as offset from unix epoch, hence it must
99            // be signed in order to deal with dates before epoch.
100            // Rustix seems to think this value is u64, but we fix it here for now.
101            let seconds = seconds as i64;
102            system_time_from_secs_nanos(seconds, nanoseconds.try_into().ok()?)
103        }
104        #[cfg(windows)]
105        self.0.created().ok()
106    }
107
108    /// Return the size of the file in bytes.
109    pub fn len(&self) -> u64 {
110        #[cfg(not(windows))]
111        {
112            self.0.st_size as u64
113        }
114        #[cfg(windows)]
115        self.0.len()
116    }
117
118    /// Return the device id on which the file is located, or 0 on windows.
119    pub fn dev(&self) -> u64 {
120        #[cfg(not(windows))]
121        {
122            self.0.st_dev as u64
123        }
124        #[cfg(windows)]
125        0
126    }
127
128    /// Return the inode id tracking the file, or 0 on windows.
129    pub fn ino(&self) -> u64 {
130        #[cfg(not(windows))]
131        {
132            self.0.st_ino as u64
133        }
134        #[cfg(windows)]
135        0
136    }
137
138    /// Return the user-id of the file or 0 on windows.
139    pub fn uid(&self) -> u32 {
140        #[cfg(not(windows))]
141        {
142            self.0.st_uid as u32
143        }
144        #[cfg(windows)]
145        0
146    }
147
148    /// Return the group-id of the file or 0 on windows.
149    pub fn gid(&self) -> u32 {
150        #[cfg(not(windows))]
151        {
152            self.0.st_gid as u32
153        }
154        #[cfg(windows)]
155        0
156    }
157
158    /// Return `true` if the file's executable bit is set, or `false` on windows.
159    pub fn is_executable(&self) -> bool {
160        #[cfg(not(windows))]
161        {
162            (self.0.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFREG as u32
163                && self.0.st_mode as u32 & libc::S_IXUSR as u32 == libc::S_IXUSR as u32
164        }
165        #[cfg(windows)]
166        gix_fs::is_executable(&self.0)
167    }
168
169    /// Return `true` if the file's is a symbolic link.
170    pub fn is_symlink(&self) -> bool {
171        #[cfg(not(windows))]
172        {
173            (self.0.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFLNK as u32
174        }
175        #[cfg(windows)]
176        self.0.is_symlink()
177    }
178
179    /// Return `true` if this is a regular file, executable or not.
180    pub fn is_file(&self) -> bool {
181        #[cfg(not(windows))]
182        {
183            (self.0.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFREG as u32
184        }
185        #[cfg(windows)]
186        self.0.is_file()
187    }
188}
189
190#[cfg(not(windows))]
191fn system_time_from_secs_nanos(secs: i64, nanos: i32) -> Option<SystemTime> {
192    // Copied from https://github.com/rust-lang/rust at a8ece1190bf6b340175bc5b688e52bd29924f483, MIT licensed, and adapted.
193    // On Apple OS, dates before epoch are represented differently than on other
194    // Unix platforms: e.g. 1/10th of a second before epoch is represented as `seconds=-1`
195    // and `nanoseconds=100_000_000` on other platforms, but is `seconds=0` and
196    // `nanoseconds=-900_000_000` on Apple OS.
197    //
198    // To compensate, we first detect this special case by checking if both
199    // seconds and nanoseconds are in range, and then correct the value for seconds
200    // and nanoseconds to match the common unix representation.
201    //
202    // Please note that Apple OS nonetheless accepts the standard unix format when
203    // setting file times, which makes this compensation round-trippable and generally
204    // transparent.
205    #[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos"))]
206    let (secs, nanos) = if (secs <= 0 && secs > i64::MIN) && (nanos < 0 && nanos > -1_000_000_000) {
207        (secs - 1, nanos + 1_000_000_000)
208    } else {
209        (secs, nanos)
210    };
211    let d = std::time::Duration::new(secs.abs_diff(0), nanos.try_into().ok()?);
212    Some(if secs < 0 {
213        std::time::UNIX_EPOCH - d
214    } else {
215        std::time::UNIX_EPOCH + d
216    })
217}