cap_primitives/rustix/fs/
metadata_ext.rs

1#![allow(clippy::useless_conversion)]
2
3use crate::fs::{ImplFileTypeExt, ImplPermissionsExt, Metadata};
4use crate::time::{Duration, SystemClock, SystemTime};
5#[cfg(target_os = "linux")]
6use rustix::fs::{makedev, Statx, StatxFlags};
7use rustix::fs::{RawMode, Stat};
8use std::{fs, io};
9
10#[derive(Debug, Clone)]
11pub(crate) struct ImplMetadataExt {
12    dev: u64,
13    ino: u64,
14    #[cfg(not(target_os = "wasi"))]
15    mode: u32,
16    nlink: u64,
17    #[cfg(not(target_os = "wasi"))]
18    uid: u32,
19    #[cfg(not(target_os = "wasi"))]
20    gid: u32,
21    #[cfg(not(target_os = "wasi"))]
22    rdev: u64,
23    size: u64,
24    #[cfg(not(target_os = "wasi"))]
25    atime: i64,
26    #[cfg(not(target_os = "wasi"))]
27    atime_nsec: i64,
28    #[cfg(not(target_os = "wasi"))]
29    mtime: i64,
30    #[cfg(not(target_os = "wasi"))]
31    mtime_nsec: i64,
32    #[cfg(not(target_os = "wasi"))]
33    ctime: i64,
34    #[cfg(not(target_os = "wasi"))]
35    ctime_nsec: i64,
36    #[cfg(not(target_os = "wasi"))]
37    blksize: u64,
38    #[cfg(not(target_os = "wasi"))]
39    blocks: u64,
40    #[cfg(target_os = "wasi")]
41    atim: u64,
42    #[cfg(target_os = "wasi")]
43    mtim: u64,
44    #[cfg(target_os = "wasi")]
45    ctim: u64,
46}
47
48impl ImplMetadataExt {
49    /// Constructs a new instance of `Self` from the given [`std::fs::File`]
50    /// and [`std::fs::Metadata`].
51    #[inline]
52    #[allow(clippy::unnecessary_wraps)]
53    pub(crate) fn from(_file: &fs::File, std: &fs::Metadata) -> io::Result<Self> {
54        // On `rustix`-style platforms, the `Metadata` has everything we need.
55        Ok(Self::from_just_metadata(std))
56    }
57
58    /// Constructs a new instance of `Self` from the given
59    /// [`std::fs::Metadata`].
60    #[inline]
61    pub(crate) fn from_just_metadata(std: &fs::Metadata) -> Self {
62        use rustix::fs::MetadataExt;
63        Self {
64            dev: std.dev(),
65            ino: std.ino(),
66            #[cfg(not(target_os = "wasi"))]
67            mode: std.mode(),
68            nlink: std.nlink(),
69            #[cfg(not(target_os = "wasi"))]
70            uid: std.uid(),
71            #[cfg(not(target_os = "wasi"))]
72            gid: std.gid(),
73            #[cfg(not(target_os = "wasi"))]
74            rdev: std.rdev(),
75            size: std.size(),
76            #[cfg(not(target_os = "wasi"))]
77            atime: std.atime(),
78            #[cfg(not(target_os = "wasi"))]
79            atime_nsec: std.atime_nsec(),
80            #[cfg(not(target_os = "wasi"))]
81            mtime: std.mtime(),
82            #[cfg(not(target_os = "wasi"))]
83            mtime_nsec: std.mtime_nsec(),
84            #[cfg(not(target_os = "wasi"))]
85            ctime: std.ctime(),
86            #[cfg(not(target_os = "wasi"))]
87            ctime_nsec: std.ctime_nsec(),
88            #[cfg(not(target_os = "wasi"))]
89            blksize: std.blksize(),
90            #[cfg(not(target_os = "wasi"))]
91            blocks: std.blocks(),
92            #[cfg(target_os = "wasi")]
93            atim: std.atim(),
94            #[cfg(target_os = "wasi")]
95            mtim: std.mtim(),
96            #[cfg(target_os = "wasi")]
97            ctim: std.ctim(),
98        }
99    }
100
101    /// Constructs a new instance of `Metadata` from the given `Stat`.
102    #[inline]
103    #[allow(unused_comparisons)] // NB: rust-lang/rust#115823 requires this here instead of on `st_dev` processing below
104    pub(crate) fn from_rustix(stat: Stat) -> Metadata {
105        Metadata {
106            file_type: ImplFileTypeExt::from_raw_mode(stat.st_mode as RawMode),
107            len: u64::try_from(stat.st_size).unwrap(),
108            #[cfg(not(target_os = "wasi"))]
109            permissions: ImplPermissionsExt::from_raw_mode(stat.st_mode as RawMode),
110            #[cfg(target_os = "wasi")]
111            permissions: ImplPermissionsExt::default(),
112
113            #[cfg(not(target_os = "wasi"))]
114            modified: system_time_from_rustix(
115                stat.st_mtime.try_into().unwrap(),
116                stat.st_mtime_nsec as _,
117            ),
118            #[cfg(not(target_os = "wasi"))]
119            accessed: system_time_from_rustix(
120                stat.st_atime.try_into().unwrap(),
121                stat.st_atime_nsec as _,
122            ),
123
124            #[cfg(target_os = "wasi")]
125            modified: system_time_from_rustix(stat.st_mtim.tv_sec, stat.st_mtim.tv_nsec as _),
126            #[cfg(target_os = "wasi")]
127            accessed: system_time_from_rustix(stat.st_atim.tv_sec, stat.st_atim.tv_nsec as _),
128
129            #[cfg(any(
130                target_os = "freebsd",
131                target_os = "openbsd",
132                target_os = "netbsd",
133                target_os = "macos",
134                target_os = "ios",
135                target_os = "tvos",
136                target_os = "watchos",
137                target_os = "visionos",
138            ))]
139            created: system_time_from_rustix(
140                stat.st_birthtime.try_into().unwrap(),
141                stat.st_birthtime_nsec as _,
142            ),
143
144            // `stat.st_ctime` is the latest status change; we want the creation.
145            #[cfg(not(any(
146                target_os = "freebsd",
147                target_os = "openbsd",
148                target_os = "macos",
149                target_os = "ios",
150                target_os = "tvos",
151                target_os = "watchos",
152                target_os = "visionos",
153                target_os = "netbsd"
154            )))]
155            created: None,
156
157            ext: Self {
158                // The type of `st_dev` is `dev_t` which is signed on some
159                // platforms and unsigned on other platforms. A `u64` is enough
160                // to work for all unsigned platforms, and for signed platforms
161                // perform a sign extension to `i64` and then view that as an
162                // unsigned 64-bit number instead.
163                //
164                // Note that the `unused_comparisons` is ignored here for
165                // platforms where it's unsigned since the first branch here
166                // will never be taken.
167                dev: if stat.st_dev < 0 {
168                    i64::try_from(stat.st_dev).unwrap() as u64
169                } else {
170                    u64::try_from(stat.st_dev).unwrap()
171                },
172                ino: stat.st_ino.into(),
173                #[cfg(not(target_os = "wasi"))]
174                mode: u32::from(stat.st_mode),
175                nlink: u64::from(stat.st_nlink),
176                #[cfg(not(target_os = "wasi"))]
177                uid: stat.st_uid,
178                #[cfg(not(target_os = "wasi"))]
179                gid: stat.st_gid,
180                #[cfg(not(target_os = "wasi"))]
181                rdev: u64::try_from(stat.st_rdev).unwrap(),
182                size: u64::try_from(stat.st_size).unwrap(),
183                #[cfg(not(target_os = "wasi"))]
184                atime: i64::try_from(stat.st_atime).unwrap(),
185                #[cfg(not(target_os = "wasi"))]
186                atime_nsec: stat.st_atime_nsec as _,
187                #[cfg(not(target_os = "wasi"))]
188                mtime: i64::try_from(stat.st_mtime).unwrap(),
189                #[cfg(not(target_os = "wasi"))]
190                mtime_nsec: stat.st_mtime_nsec as _,
191                #[cfg(not(target_os = "wasi"))]
192                ctime: i64::try_from(stat.st_ctime).unwrap(),
193                #[cfg(not(target_os = "wasi"))]
194                ctime_nsec: stat.st_ctime_nsec as _,
195                #[cfg(not(target_os = "wasi"))]
196                blksize: u64::try_from(stat.st_blksize).unwrap(),
197                #[cfg(not(target_os = "wasi"))]
198                blocks: u64::try_from(stat.st_blocks).unwrap(),
199                #[cfg(target_os = "wasi")]
200                atim: u64::try_from(
201                    stat.st_atim.tv_sec as u64 * 1000000000 + stat.st_atim.tv_nsec as u64,
202                )
203                .unwrap(),
204                #[cfg(target_os = "wasi")]
205                mtim: u64::try_from(
206                    stat.st_mtim.tv_sec as u64 * 1000000000 + stat.st_mtim.tv_nsec as u64,
207                )
208                .unwrap(),
209                #[cfg(target_os = "wasi")]
210                ctim: u64::try_from(
211                    stat.st_ctim.tv_sec as u64 * 1000000000 + stat.st_ctim.tv_nsec as u64,
212                )
213                .unwrap(),
214            },
215        }
216    }
217
218    /// Constructs a new instance of `Metadata` from the given `Statx`.
219    #[cfg(target_os = "linux")]
220    #[inline]
221    pub(crate) fn from_rustix_statx(statx: Statx) -> Metadata {
222        Metadata {
223            file_type: ImplFileTypeExt::from_raw_mode(RawMode::from(statx.stx_mode)),
224            len: u64::try_from(statx.stx_size).unwrap(),
225            permissions: ImplPermissionsExt::from_raw_mode(RawMode::from(statx.stx_mode)),
226            modified: if statx.stx_mask & StatxFlags::MTIME.bits() != 0 {
227                system_time_from_rustix(statx.stx_mtime.tv_sec, statx.stx_mtime.tv_nsec as _)
228            } else {
229                None
230            },
231            accessed: if statx.stx_mask & StatxFlags::ATIME.bits() != 0 {
232                system_time_from_rustix(statx.stx_atime.tv_sec, statx.stx_atime.tv_nsec as _)
233            } else {
234                None
235            },
236            created: if statx.stx_mask & StatxFlags::BTIME.bits() != 0 {
237                system_time_from_rustix(statx.stx_btime.tv_sec, statx.stx_btime.tv_nsec as _)
238            } else {
239                None
240            },
241
242            ext: Self {
243                dev: makedev(statx.stx_dev_major, statx.stx_dev_minor),
244                ino: statx.stx_ino.into(),
245                mode: u32::from(statx.stx_mode),
246                nlink: u64::from(statx.stx_nlink),
247                uid: statx.stx_uid,
248                gid: statx.stx_gid,
249                rdev: makedev(statx.stx_rdev_major, statx.stx_rdev_minor),
250                size: statx.stx_size,
251                atime: i64::from(statx.stx_atime.tv_sec),
252                atime_nsec: statx.stx_atime.tv_nsec as _,
253                mtime: i64::from(statx.stx_mtime.tv_sec),
254                mtime_nsec: statx.stx_mtime.tv_nsec as _,
255                ctime: i64::from(statx.stx_ctime.tv_sec),
256                ctime_nsec: statx.stx_ctime.tv_nsec as _,
257                blksize: u64::from(statx.stx_blksize),
258                blocks: statx.stx_blocks,
259            },
260        }
261    }
262
263    /// Determine if `self` and `other` refer to the same inode on the same
264    /// device.
265    pub(crate) const fn is_same_file(&self, other: &Self) -> bool {
266        self.dev == other.dev && self.ino == other.ino
267    }
268}
269
270#[allow(clippy::similar_names)]
271fn system_time_from_rustix(sec: i64, nsec: u64) -> Option<SystemTime> {
272    if sec >= 0 {
273        SystemClock::UNIX_EPOCH.checked_add(Duration::new(u64::try_from(sec).unwrap(), nsec as _))
274    } else {
275        SystemClock::UNIX_EPOCH
276            .checked_sub(Duration::new(u64::try_from(-sec).unwrap(), 0))
277            .map(|t| t.checked_add(Duration::new(0, nsec as u32)))
278            .flatten()
279    }
280}
281
282impl crate::fs::MetadataExt for ImplMetadataExt {
283    #[inline]
284    fn dev(&self) -> u64 {
285        self.dev
286    }
287
288    #[inline]
289    fn ino(&self) -> u64 {
290        self.ino
291    }
292
293    #[cfg(not(target_os = "wasi"))]
294    #[inline]
295    fn mode(&self) -> u32 {
296        self.mode
297    }
298
299    #[inline]
300    fn nlink(&self) -> u64 {
301        self.nlink
302    }
303
304    #[cfg(not(target_os = "wasi"))]
305    #[inline]
306    fn uid(&self) -> u32 {
307        self.uid
308    }
309
310    #[cfg(not(target_os = "wasi"))]
311    #[inline]
312    fn gid(&self) -> u32 {
313        self.gid
314    }
315
316    #[cfg(not(target_os = "wasi"))]
317    #[inline]
318    fn rdev(&self) -> u64 {
319        self.rdev
320    }
321
322    #[inline]
323    fn size(&self) -> u64 {
324        self.size
325    }
326
327    #[cfg(not(target_os = "wasi"))]
328    #[inline]
329    fn atime(&self) -> i64 {
330        self.atime
331    }
332
333    #[cfg(not(target_os = "wasi"))]
334    #[inline]
335    fn atime_nsec(&self) -> i64 {
336        self.atime_nsec
337    }
338
339    #[cfg(not(target_os = "wasi"))]
340    #[inline]
341    fn mtime(&self) -> i64 {
342        self.mtime
343    }
344
345    #[cfg(not(target_os = "wasi"))]
346    #[inline]
347    fn mtime_nsec(&self) -> i64 {
348        self.mtime_nsec
349    }
350
351    #[cfg(not(target_os = "wasi"))]
352    #[inline]
353    fn ctime(&self) -> i64 {
354        self.ctime
355    }
356
357    #[cfg(not(target_os = "wasi"))]
358    #[inline]
359    fn ctime_nsec(&self) -> i64 {
360        self.ctime_nsec
361    }
362
363    #[cfg(not(target_os = "wasi"))]
364    #[inline]
365    fn blksize(&self) -> u64 {
366        self.blksize
367    }
368
369    #[cfg(not(target_os = "wasi"))]
370    #[inline]
371    fn blocks(&self) -> u64 {
372        self.blocks
373    }
374
375    #[cfg(target_os = "wasi")]
376    fn atim(&self) -> u64 {
377        self.atim
378    }
379
380    #[cfg(target_os = "wasi")]
381    fn mtim(&self) -> u64 {
382        self.mtim
383    }
384
385    #[cfg(target_os = "wasi")]
386    fn ctim(&self) -> u64 {
387        self.ctim
388    }
389}
390
391/// It should be possible to represent times before the Epoch.
392/// https://github.com/bytecodealliance/cap-std/issues/328
393#[test]
394fn negative_time() {
395    let system_time = system_time_from_rustix(-1, 1).unwrap();
396    let d = SystemClock::UNIX_EPOCH.duration_since(system_time).unwrap();
397    assert_eq!(d.as_secs(), 0);
398    if !cfg!(emulate_second_only_system) {
399        assert_eq!(d.subsec_nanos(), 999999999);
400    }
401}