Skip to main content

dolang_vfs/
metadata.rs

1//! File and filesystem metadata returned by a [`crate::Vfs`].
2
3use dolang_winterop::security::Sid;
4use serde::{Deserialize, Serialize};
5
6use crate::security::{OwnershipIdentity, Permission};
7
8/// The kind of filesystem object described by [`Metadata`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum FileType {
11    /// Regular file.
12    File,
13    /// Directory.
14    Dir,
15    /// Symbolic link.
16    Symlink,
17    /// FIFO or named pipe.
18    Fifo,
19    /// Character device.
20    CharacterDevice,
21    /// Block device.
22    BlockDevice,
23    /// Unix-domain socket.
24    Socket,
25    /// Unrecognized file type.
26    Unknown,
27}
28
29bitflags::bitflags! {
30    /// Portable Unix-style mode bits: permissions, the setuid/setgid/sticky
31    /// bits, and the `S_IFMT` file-type nibble.
32    ///
33    /// The file-type bits are retained for inspection/round-tripping but are
34    /// not decoded structurally here -- [`FileType`] is the source of truth
35    /// for a filesystem object's kind, since the type nibble is fragile and
36    /// platform-dependent.
37    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
38    pub struct Mode: u32 {
39        const SET_UID = 0o4000;
40        const SET_GID = 0o2000;
41        const STICKY = 0o1000;
42        const OWNER_READ = 0o400;
43        const OWNER_WRITE = 0o200;
44        const OWNER_EXECUTE = 0o100;
45        const GROUP_READ = 0o40;
46        const GROUP_WRITE = 0o20;
47        const GROUP_EXECUTE = 0o10;
48        const OTHER_READ = 0o4;
49        const OTHER_WRITE = 0o2;
50        const OTHER_EXECUTE = 0o1;
51        const IFIFO = 0o010000;
52        const IFCHR = 0o020000;
53        const IFDIR = 0o040000;
54        const IFBLK = 0o060000;
55        const IFREG = 0o100000;
56        const IFLNK = 0o120000;
57        const IFSOCK = 0o140000;
58    }
59}
60
61impl Mode {
62    /// Projects the owning user's read/write/execute bits.
63    pub fn owner(self) -> Permission {
64        Permission::from_bits_truncate((self.bits() >> 6) as u8 & 0o7)
65    }
66    /// Projects the owning group's read/write/execute bits.
67    pub fn group(self) -> Permission {
68        Permission::from_bits_truncate((self.bits() >> 3) as u8 & 0o7)
69    }
70    /// Projects other users' read/write/execute bits.
71    pub fn other(self) -> Permission {
72        Permission::from_bits_truncate(self.bits() as u8 & 0o7)
73    }
74}
75
76/// Metadata for one filesystem object.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Metadata {
79    /// File length in bytes.
80    pub(crate) len: u64,
81    /// File kind.
82    pub(crate) file_type: FileType,
83    /// Last-access time in seconds since the Unix epoch.
84    pub(crate) atime: i64,
85    /// Nanosecond component of [`atime`](Self::atime).
86    pub(crate) atime_nsec: i64,
87    /// Last-modification time in seconds since the Unix epoch.
88    pub(crate) mtime: i64,
89    /// Nanosecond component of [`mtime`](Self::mtime).
90    pub(crate) mtime_nsec: i64,
91    /// Metadata-change time in seconds since the Unix epoch.
92    pub(crate) ctime: i64,
93    /// Nanosecond component of [`ctime`](Self::ctime).
94    pub(crate) ctime_nsec: i64,
95    /// Platform-specific metadata.
96    pub(crate) family: MetadataFamily,
97}
98
99/// Platform-specific metadata payload.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub(crate) enum MetadataFamily {
102    /// Unix metadata.
103    Unix(UnixMetadata),
104    /// Windows metadata.
105    Windows(WindowsMetadata),
106}
107
108/// Metadata specific to Unix targets.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct UnixMetadata {
111    /// File mode bits.
112    pub(crate) mode: Mode,
113    /// Device ID containing the file.
114    pub(crate) dev: u64,
115    /// File inode number.
116    pub(crate) ino: u64,
117    /// Number of hard links.
118    pub(crate) nlink: u64,
119    /// Owning user ID.
120    pub(crate) uid: u32,
121    /// Owning group ID.
122    pub(crate) gid: u32,
123    /// Device ID for special files.
124    pub(crate) rdev: u64,
125    /// Preferred I/O block size.
126    pub(crate) blksize: u64,
127    /// Number of allocated blocks.
128    pub(crate) blocks: u64,
129    pub(crate) platform: UnixMetadataPlatform,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub(crate) enum UnixMetadataPlatform {
134    FreeBsd { attrs: u32 },
135    Linux { attrs: Option<u32> },
136    Macos { attrs: u32 },
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140/// Windows-specific file metadata.
141pub struct WindowsMetadata {
142    pub(crate) attrs: u32,
143    pub(crate) user: Option<Sid>,
144    pub(crate) group: Option<Sid>,
145}
146
147impl Metadata {
148    /// Returns the file length in bytes.
149    pub const fn len(&self) -> u64 {
150        self.len
151    }
152    /// Returns whether the file is empty.
153    pub const fn is_empty(&self) -> bool {
154        self.len == 0
155    }
156    /// Returns the type of the filesystem object.
157    pub const fn file_type(&self) -> FileType {
158        self.file_type
159    }
160    /// Returns the access time in seconds since the Unix epoch.
161    pub const fn atime(&self) -> i64 {
162        self.atime
163    }
164    /// Returns the nanosecond component of the access time.
165    pub const fn atime_nsec(&self) -> i64 {
166        self.atime_nsec
167    }
168    /// Returns the modification time in seconds since the Unix epoch.
169    pub const fn mtime(&self) -> i64 {
170        self.mtime
171    }
172    /// Returns the nanosecond component of the modification time.
173    pub const fn mtime_nsec(&self) -> i64 {
174        self.mtime_nsec
175    }
176    /// Returns the metadata-change time in seconds since the Unix epoch.
177    pub const fn ctime(&self) -> i64 {
178        self.ctime
179    }
180    /// Returns the nanosecond component of the metadata-change time.
181    pub const fn ctime_nsec(&self) -> i64 {
182        self.ctime_nsec
183    }
184    /// Returns the Unix-specific metadata, if present.
185    pub fn unix(&self) -> Option<&UnixMetadata> {
186        if let MetadataFamily::Unix(metadata) = &self.family {
187            Some(metadata)
188        } else {
189            None
190        }
191    }
192    /// Returns the Windows-specific metadata, if present.
193    pub fn windows(&self) -> Option<&WindowsMetadata> {
194        if let MetadataFamily::Windows(metadata) = &self.family {
195            Some(metadata)
196        } else {
197            None
198        }
199    }
200    /// Returns the Linux inode attributes, if present and available.
201    pub const fn linux_attrs(&self) -> Option<u32> {
202        match &self.family {
203            MetadataFamily::Unix(UnixMetadata {
204                platform: UnixMetadataPlatform::Linux { attrs },
205                ..
206            }) => *attrs,
207            _ => None,
208        }
209    }
210    /// Returns the FreeBSD file flags, if present.
211    pub const fn freebsd_attrs(&self) -> Option<u32> {
212        match &self.family {
213            MetadataFamily::Unix(UnixMetadata {
214                platform: UnixMetadataPlatform::FreeBsd { attrs },
215                ..
216            }) => Some(*attrs),
217            _ => None,
218        }
219    }
220    /// Returns the macOS file flags, if present.
221    pub const fn macos_attrs(&self) -> Option<u32> {
222        match &self.family {
223            MetadataFamily::Unix(UnixMetadata {
224                platform: UnixMetadataPlatform::Macos { attrs },
225                ..
226            }) => Some(*attrs),
227            _ => None,
228        }
229    }
230    /// Returns the Windows file attributes, if present.
231    pub const fn win_attrs(&self) -> Option<u32> {
232        match &self.family {
233            MetadataFamily::Windows(metadata) => Some(metadata.attrs),
234            _ => None,
235        }
236    }
237}
238
239impl UnixMetadata {
240    /// Returns the Unix mode bits.
241    pub const fn mode(&self) -> Mode {
242        self.mode
243    }
244    /// Returns the device ID.
245    pub const fn dev(&self) -> u64 {
246        self.dev
247    }
248    /// Returns the inode number.
249    pub const fn ino(&self) -> u64 {
250        self.ino
251    }
252    /// Returns the number of hard links.
253    pub const fn nlink(&self) -> u64 {
254        self.nlink
255    }
256    /// Returns the owning user ID.
257    pub const fn uid(&self) -> u32 {
258        self.uid
259    }
260    /// Returns the owning group ID.
261    pub const fn gid(&self) -> u32 {
262        self.gid
263    }
264    /// Returns the device ID for a special file.
265    pub const fn rdev(&self) -> u64 {
266        self.rdev
267    }
268    /// Returns the preferred I/O block size.
269    pub const fn block_size(&self) -> u64 {
270        self.blksize
271    }
272    /// Returns the number of allocated blocks.
273    pub const fn blocks(&self) -> u64 {
274        self.blocks
275    }
276    /// Returns the Linux inode attributes, if present and available.
277    pub const fn linux_attrs(&self) -> Option<u32> {
278        match self.platform {
279            UnixMetadataPlatform::Linux { attrs } => attrs,
280            _ => None,
281        }
282    }
283    /// Returns the FreeBSD file flags, if present.
284    pub const fn freebsd_attrs(&self) -> Option<u32> {
285        match self.platform {
286            UnixMetadataPlatform::FreeBsd { attrs } => Some(attrs),
287            _ => None,
288        }
289    }
290    /// Returns the macOS file flags, if present.
291    pub const fn macos_attrs(&self) -> Option<u32> {
292        match self.platform {
293            UnixMetadataPlatform::Macos { attrs } => Some(attrs),
294            _ => None,
295        }
296    }
297}
298
299impl WindowsMetadata {
300    /// Returns the Windows file attributes.
301    pub const fn attrs(&self) -> u32 {
302        self.attrs
303    }
304    /// Returns the owner SID, if it was requested and available.
305    pub fn user(&self) -> Option<&Sid> {
306        self.user.as_ref()
307    }
308    /// Returns the group SID, if it was requested and available.
309    pub fn group(&self) -> Option<&Sid> {
310        self.group.as_ref()
311    }
312}
313
314/// Capacity and allocation information for a filesystem.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct FsMetadata {
317    pub(crate) capacity: u64,
318    pub(crate) free: u64,
319    pub(crate) available: u64,
320    pub(crate) block_size: u32,
321    pub(crate) family: FsMetadataFamily,
322}
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub(crate) enum FsMetadataFamily {
325    Unix(UnixFsMetadata),
326    Windows(WindowsFsMetadata),
327}
328#[derive(Debug, Clone, Serialize, Deserialize)]
329/// Unix-specific filesystem metadata.
330pub struct UnixFsMetadata {
331    pub(crate) blocks: u64,
332    pub(crate) blocks_free: u64,
333    pub(crate) blocks_available: u64,
334    pub(crate) files: u64,
335    pub(crate) files_free: u64,
336    pub(crate) files_available: u64,
337    pub(crate) fragment_size: u32,
338    pub(crate) fsid: Option<u64>,
339    pub(crate) name_max: u32,
340    pub(crate) platform: UnixFsMetadataPlatform,
341}
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub(crate) enum UnixFsMetadataPlatform {
344    Linux { flags: u64 },
345    Macos { flags: u64 },
346    FreeBsd { flags: u64 },
347}
348#[derive(Debug, Clone, Serialize, Deserialize)]
349/// Windows-specific filesystem metadata.
350pub struct WindowsFsMetadata {
351    pub(crate) flags: u32,
352    pub(crate) volume_serial_number: u32,
353    pub(crate) component_length_max: u32,
354}
355
356impl FsMetadata {
357    /// Returns the total filesystem capacity in bytes.
358    pub const fn capacity(&self) -> u64 {
359        self.capacity
360    }
361    /// Returns the total free space in bytes.
362    pub const fn free(&self) -> u64 {
363        self.free
364    }
365    /// Returns the space available to an unprivileged caller in bytes.
366    pub const fn available(&self) -> u64 {
367        self.available
368    }
369    /// Returns the fundamental filesystem block size in bytes.
370    pub const fn block_size(&self) -> u32 {
371        self.block_size
372    }
373    /// Returns the Unix-specific filesystem metadata, if present.
374    pub fn unix(&self) -> Option<&UnixFsMetadata> {
375        if let FsMetadataFamily::Unix(metadata) = &self.family {
376            Some(metadata)
377        } else {
378            None
379        }
380    }
381    /// Returns the Windows-specific filesystem metadata, if present.
382    pub fn windows(&self) -> Option<&WindowsFsMetadata> {
383        if let FsMetadataFamily::Windows(metadata) = &self.family {
384            Some(metadata)
385        } else {
386            None
387        }
388    }
389    /// Returns whether the filesystem is read-only.
390    #[allow(clippy::unnecessary_cast)]
391    pub fn read_only(&self) -> bool {
392        match &self.family {
393            FsMetadataFamily::Unix(metadata) => metadata.platform.flags() & 1 != 0,
394            FsMetadataFamily::Windows(metadata) => metadata.flags & 0x0008_0000 != 0,
395        }
396    }
397    /// Returns whether set-user-ID and set-group-ID bits are disabled, if known.
398    #[allow(clippy::unnecessary_cast)]
399    pub fn no_suid(&self) -> Option<bool> {
400        match &self.family {
401            FsMetadataFamily::Unix(metadata) => Some(metadata.platform.flags() & 2 != 0),
402            FsMetadataFamily::Windows(_) => None,
403        }
404    }
405    /// Returns whether execution is disabled, if known.
406    #[allow(clippy::unnecessary_cast)]
407    pub fn no_exec(&self) -> Option<bool> {
408        self.linux_flag(8)
409    }
410    /// Returns whether writes are synchronous, if known.
411    #[allow(clippy::unnecessary_cast)]
412    pub fn synchronous(&self) -> Option<bool> {
413        self.linux_flag(16)
414    }
415    /// Returns whether device files are disabled, if known.
416    #[allow(clippy::unnecessary_cast)]
417    pub fn no_dev(&self) -> Option<bool> {
418        self.linux_flag(4)
419    }
420    /// Returns whether access-time updates are disabled, if known.
421    #[allow(clippy::unnecessary_cast)]
422    pub fn no_atime(&self) -> Option<bool> {
423        self.linux_flag(1024)
424    }
425    /// Returns whether directory access-time updates are disabled, if known.
426    #[allow(clippy::unnecessary_cast)]
427    pub fn no_dir_atime(&self) -> Option<bool> {
428        self.linux_flag(2048)
429    }
430    /// Returns whether relative access-time updates are enabled, if known.
431    #[allow(clippy::unnecessary_cast)]
432    pub fn relatime(&self) -> Option<bool> {
433        self.linux_flag(1 << 21)
434    }
435    fn linux_flag(&self, flag: u64) -> Option<bool> {
436        match &self.family {
437            FsMetadataFamily::Unix(UnixFsMetadata {
438                platform: UnixFsMetadataPlatform::Linux { flags },
439                ..
440            }) => Some(flags & flag != 0),
441            _ => None,
442        }
443    }
444}
445impl UnixFsMetadata {
446    /// Returns the total number of filesystem blocks.
447    pub const fn blocks(&self) -> u64 {
448        self.blocks
449    }
450    /// Returns the number of free filesystem blocks.
451    pub const fn blocks_free(&self) -> u64 {
452        self.blocks_free
453    }
454    /// Returns the number of blocks available to an unprivileged caller.
455    pub const fn blocks_available(&self) -> u64 {
456        self.blocks_available
457    }
458    /// Returns the total number of file nodes.
459    pub const fn files(&self) -> u64 {
460        self.files
461    }
462    /// Returns the number of free file nodes.
463    pub const fn files_free(&self) -> u64 {
464        self.files_free
465    }
466    /// Returns the number of file nodes available to an unprivileged caller.
467    pub const fn files_available(&self) -> u64 {
468        self.files_available
469    }
470    /// Returns the fragment size in bytes.
471    pub const fn fragment_size(&self) -> u32 {
472        self.fragment_size
473    }
474    /// Returns the filesystem ID, if available.
475    pub const fn fsid(&self) -> Option<u64> {
476        self.fsid
477    }
478    /// Returns the maximum filename length.
479    pub const fn name_max(&self) -> u32 {
480        self.name_max
481    }
482    /// Returns the Linux mount flags, if present.
483    pub const fn linux_flags(&self) -> Option<u64> {
484        match self.platform {
485            UnixFsMetadataPlatform::Linux { flags } => Some(flags),
486            _ => None,
487        }
488    }
489    /// Returns the FreeBSD mount flags, if present.
490    pub const fn freebsd_flags(&self) -> Option<u64> {
491        match self.platform {
492            UnixFsMetadataPlatform::FreeBsd { flags } => Some(flags),
493            _ => None,
494        }
495    }
496    /// Returns the macOS mount flags, if present.
497    pub const fn macos_flags(&self) -> Option<u64> {
498        match self.platform {
499            UnixFsMetadataPlatform::Macos { flags } => Some(flags),
500            _ => None,
501        }
502    }
503}
504impl WindowsFsMetadata {
505    /// Returns the filesystem flags reported by Windows.
506    pub const fn flags(&self) -> u32 {
507        self.flags
508    }
509    /// Returns the volume serial number.
510    pub const fn volume_serial_number(&self) -> u32 {
511        self.volume_serial_number
512    }
513    /// Returns the maximum filesystem path-component length.
514    pub const fn component_length_max(&self) -> u32 {
515        self.component_length_max
516    }
517}
518impl UnixFsMetadataPlatform {
519    pub fn flags(&self) -> u64 {
520        match self {
521            Self::FreeBsd { flags } | Self::Linux { flags } | Self::Macos { flags } => *flags,
522        }
523    }
524}
525
526/// Portable filesystem attribute flags.
527#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
528#[serde(transparent)]
529pub struct AttrFlags(u64);
530impl AttrFlags {
531    /// Read-only.
532    pub const READONLY: Self = Self(1 << 0);
533    /// Hidden from ordinary directory listings.
534    pub const HIDDEN: Self = Self(1 << 1);
535    /// Used by the operating system.
536    pub const SYSTEM: Self = Self(1 << 2);
537    /// Marked for archival.
538    pub const ARCHIVE: Self = Self(1 << 3);
539    /// Stored in compressed form.
540    pub const COMPRESSED: Self = Self(1 << 4);
541    /// Intended for temporary storage.
542    pub const TEMPORARY: Self = Self(1 << 5);
543    /// Data is not immediately available.
544    pub const OFFLINE: Self = Self(1 << 6);
545    /// Excluded from content indexing.
546    pub const NOT_CONTENT_INDEXED: Self = Self(1 << 7);
547    /// Cannot be modified.
548    pub const IMMUTABLE: Self = Self(1 << 8);
549    /// May only be appended to.
550    pub const APPEND_ONLY: Self = Self(1 << 9);
551    /// Excluded from dump-style backups.
552    pub const NO_DUMP: Self = Self(1 << 10);
553    /// Does not update access time.
554    pub const NO_ATIME: Self = Self(1 << 11);
555    /// Disables copy-on-write behavior.
556    pub const NO_COPY_ON_WRITE: Self = Self(1 << 12);
557    /// Directory changes are written synchronously.
558    pub const DIR_SYNC: Self = Self(1 << 13);
559    /// Directory uses case-insensitive lookup.
560    pub const CASEFOLD: Self = Self(1 << 14);
561    /// File data is journaled.
562    pub const DATA_JOURNALING: Self = Self(1 << 15);
563    /// File must not be compressed.
564    pub const NO_COMPRESS: Self = Self(1 << 16);
565    /// New children inherit the project ID.
566    pub const PROJECT_INHERIT: Self = Self(1 << 17);
567    /// Requests secure deletion.
568    pub const SECURE_DELETE: Self = Self(1 << 18);
569    /// Changes are written synchronously.
570    pub const SYNC: Self = Self(1 << 19);
571    /// Disables tail merging.
572    pub const NO_TAIL_MERGE: Self = Self(1 << 20);
573    /// Directory is the top of a hierarchy.
574    pub const TOP_DIR: Self = Self(1 << 21);
575    /// File can be recovered after deletion.
576    pub const UNDELETE: Self = Self(1 << 22);
577    /// Supports direct-access storage.
578    pub const DIRECT_ACCESS: Self = Self(1 << 23);
579    /// Uses extent-based storage.
580    pub const EXTENT_FORMAT: Self = Self(1 << 24);
581    /// Directory is opaque to union mounts.
582    pub const OPAQUE: Self = Self(1 << 25);
583    /// File uses sparse allocation.
584    pub const SPARSE: Self = Self(1 << 26);
585    /// Returns an empty set of flags.
586    pub const fn empty() -> Self {
587        Self(0)
588    }
589    /// Returns whether all bits in `flag` are set.
590    pub const fn contains(self, flag: Self) -> bool {
591        self.0 & flag.0 != 0
592    }
593    /// Returns whether any bits in `other` are set.
594    pub const fn intersects(self, other: Self) -> bool {
595        self.0 & other.0 != 0
596    }
597    /// Returns the union of two flag sets.
598    pub const fn union(self, other: Self) -> Self {
599        Self(self.0 | other.0)
600    }
601    /// Returns these flags with the bits in `other` removed.
602    pub const fn difference(self, other: Self) -> Self {
603        Self(self.0 & !other.0)
604    }
605    /// Returns whether no flags are set.
606    pub const fn is_empty(self) -> bool {
607        self.0 == 0
608    }
609}
610#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
611pub(crate) struct AttrsPatch {
612    pub(crate) set: AttrFlags,
613    pub(crate) clear: AttrFlags,
614}
615impl AttrsPatch {
616    pub fn update(&mut self, flag: AttrFlags, value: Option<bool>) {
617        match value {
618            Some(true) => {
619                self.set = self.set.union(flag);
620                self.clear = self.clear.difference(flag);
621            }
622            Some(false) => {
623                self.clear = self.clear.union(flag);
624                self.set = self.set.difference(flag);
625            }
626            None => {}
627        }
628    }
629    pub const fn requested(self) -> AttrFlags {
630        self.set.union(self.clear)
631    }
632    pub const fn is_empty(self) -> bool {
633        self.set.is_empty() && self.clear.is_empty()
634    }
635}
636
637/// Requested changes to a filesystem object's metadata.
638#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
639pub struct MetadataPatch {
640    pub(crate) mode: Option<Mode>,
641    pub(crate) user: Option<OwnershipIdentity>,
642    pub(crate) group: Option<OwnershipIdentity>,
643    pub(crate) accessed: Option<i128>,
644    pub(crate) modified: Option<i128>,
645    pub(crate) created: Option<i128>,
646    pub(crate) attrs: AttrsPatch,
647    pub(crate) follow: bool,
648}
649impl Default for MetadataPatch {
650    fn default() -> Self {
651        Self {
652            mode: None,
653            user: None,
654            group: None,
655            accessed: None,
656            modified: None,
657            created: None,
658            attrs: AttrsPatch::default(),
659            follow: true,
660        }
661    }
662}
663impl MetadataPatch {
664    /// Creates an empty metadata patch that follows symbolic links.
665    pub fn new() -> Self {
666        Self::default()
667    }
668    /// Sets the replacement Unix mode.
669    pub fn mode(&mut self, mode: Mode) -> &mut Self {
670        self.mode = Some(mode);
671        self
672    }
673    /// Sets the replacement owner.
674    pub fn user(&mut self, user: OwnershipIdentity) -> &mut Self {
675        self.user = Some(user);
676        self
677    }
678    /// Sets the replacement group.
679    pub fn group(&mut self, group: OwnershipIdentity) -> &mut Self {
680        self.group = Some(group);
681        self
682    }
683    /// Sets the access time in nanoseconds since the Unix epoch.
684    pub fn accessed(&mut self, accessed: i128) -> &mut Self {
685        self.accessed = Some(accessed);
686        self
687    }
688    /// Sets the modification time in nanoseconds since the Unix epoch.
689    pub fn modified(&mut self, modified: i128) -> &mut Self {
690        self.modified = Some(modified);
691        self
692    }
693    /// Sets the creation time in nanoseconds since the Unix epoch.
694    pub fn created(&mut self, created: i128) -> &mut Self {
695        self.created = Some(created);
696        self
697    }
698    /// Requests that an attribute be set, cleared, or left unchanged.
699    pub fn attribute(&mut self, flag: AttrFlags, value: Option<bool>) -> &mut Self {
700        self.attrs.update(flag, value);
701        self
702    }
703    /// Selects whether the operation follows symbolic links.
704    pub fn follow_links(&mut self, follow: bool) -> &mut Self {
705        self.follow = follow;
706        self
707    }
708    /// Sets the replacement Unix mode and returns the patch.
709    pub fn with_mode(mut self, mode: Mode) -> Self {
710        self.mode(mode);
711        self
712    }
713    /// Sets the replacement owner and returns the patch.
714    pub fn with_user(mut self, user: OwnershipIdentity) -> Self {
715        self.user(user);
716        self
717    }
718    /// Sets the replacement group and returns the patch.
719    pub fn with_group(mut self, group: OwnershipIdentity) -> Self {
720        self.group(group);
721        self
722    }
723    /// Sets the access time and returns the patch.
724    pub fn with_accessed(mut self, accessed: i128) -> Self {
725        self.accessed(accessed);
726        self
727    }
728    /// Sets the modification time and returns the patch.
729    pub fn with_modified(mut self, modified: i128) -> Self {
730        self.modified(modified);
731        self
732    }
733    /// Sets the creation time and returns the patch.
734    pub fn with_created(mut self, created: i128) -> Self {
735        self.created(created);
736        self
737    }
738    /// Requests an attribute change and returns the patch.
739    pub fn with_attribute(mut self, flag: AttrFlags, value: Option<bool>) -> Self {
740        self.attribute(flag, value);
741        self
742    }
743    /// Selects symbolic-link following and returns the patch.
744    pub fn with_follow_links(mut self, follow: bool) -> Self {
745        self.follow_links(follow);
746        self
747    }
748    /// Returns whether the patch requests no metadata changes.
749    pub fn is_empty(&self) -> bool {
750        self.mode.is_none()
751            && self.user.is_none()
752            && self.group.is_none()
753            && self.accessed.is_none()
754            && self.modified.is_none()
755            && self.created.is_none()
756            && self.attrs.is_empty()
757    }
758}
759
760pub(crate) fn metadata_from_std(metadata: std::fs::Metadata) -> Metadata {
761    #[cfg(unix)]
762    {
763        use nix::sys::stat::{SFlag, mode_t};
764        #[cfg(target_os = "macos")]
765        use std::os::darwin::fs::MetadataExt as DarwinMetadataExt;
766        #[cfg(target_os = "freebsd")]
767        use std::os::freebsd::fs::MetadataExt as FreeBsdMetadataExt;
768        use std::os::unix::fs::MetadataExt;
769        let mode = metadata.mode();
770        let file_type = match SFlag::from_bits_truncate(mode as mode_t) & SFlag::S_IFMT {
771            SFlag::S_IFREG => FileType::File,
772            SFlag::S_IFDIR => FileType::Dir,
773            SFlag::S_IFLNK => FileType::Symlink,
774            SFlag::S_IFIFO => FileType::Fifo,
775            SFlag::S_IFCHR => FileType::CharacterDevice,
776            SFlag::S_IFBLK => FileType::BlockDevice,
777            SFlag::S_IFSOCK => FileType::Socket,
778            _ => FileType::Unknown,
779        };
780        Metadata {
781            len: metadata.len(),
782            file_type,
783            atime: metadata.atime(),
784            atime_nsec: metadata.atime_nsec(),
785            mtime: metadata.mtime(),
786            mtime_nsec: metadata.mtime_nsec(),
787            ctime: metadata.ctime(),
788            ctime_nsec: metadata.ctime_nsec(),
789            family: MetadataFamily::Unix(UnixMetadata {
790                mode: Mode::from_bits_retain(mode),
791                dev: metadata.dev(),
792                ino: metadata.ino(),
793                nlink: metadata.nlink(),
794                uid: metadata.uid(),
795                gid: metadata.gid(),
796                rdev: metadata.rdev(),
797                blksize: metadata.blksize(),
798                blocks: metadata.blocks(),
799                #[cfg(target_os = "linux")]
800                platform: UnixMetadataPlatform::Linux { attrs: None },
801                #[cfg(target_os = "freebsd")]
802                platform: UnixMetadataPlatform::FreeBsd {
803                    attrs: metadata.st_flags(),
804                },
805                #[cfg(target_os = "macos")]
806                platform: UnixMetadataPlatform::Macos {
807                    attrs: metadata.st_flags(),
808                },
809            }),
810        }
811    }
812    #[cfg(windows)]
813    {
814        use std::os::windows::fs::MetadataExt;
815        let file_type = if metadata.is_file() {
816            FileType::File
817        } else if metadata.is_dir() {
818            FileType::Dir
819        } else if metadata.file_type().is_symlink() {
820            FileType::Symlink
821        } else {
822            FileType::Unknown
823        };
824        Metadata {
825            len: metadata.len(),
826            file_type,
827            atime: system_time_to_parts(metadata.accessed().ok()).0,
828            atime_nsec: i64::from(system_time_to_parts(metadata.accessed().ok()).1),
829            mtime: system_time_to_parts(metadata.modified().ok()).0,
830            mtime_nsec: i64::from(system_time_to_parts(metadata.modified().ok()).1),
831            ctime: system_time_to_parts(metadata.created().ok()).0,
832            ctime_nsec: i64::from(system_time_to_parts(metadata.created().ok()).1),
833            family: MetadataFamily::Windows(WindowsMetadata {
834                attrs: metadata.file_attributes(),
835                user: None,
836                group: None,
837            }),
838        }
839    }
840}
841#[cfg(windows)]
842pub(crate) fn metadata_with_sids(
843    mut metadata: Metadata,
844    user: Option<Sid>,
845    group: Option<Sid>,
846) -> Metadata {
847    let MetadataFamily::Windows(windows) = &mut metadata.family else {
848        unreachable!()
849    };
850    windows.user = user;
851    windows.group = group;
852    metadata
853}
854#[cfg(windows)]
855fn system_time_to_parts(time: Option<std::time::SystemTime>) -> (i64, u32) {
856    use std::time::UNIX_EPOCH;
857    let Some(time) = time else { return (0, 0) };
858    match time.duration_since(UNIX_EPOCH) {
859        Ok(duration) => (
860            i64::try_from(duration.as_secs()).unwrap_or(i64::MAX),
861            duration.subsec_nanos(),
862        ),
863        Err(err) => {
864            let duration = err.duration();
865            let secs = i64::try_from(duration.as_secs()).unwrap_or(i64::MAX);
866            if duration.subsec_nanos() == 0 {
867                (-secs, 0)
868            } else {
869                (-secs - 1, 1_000_000_000 - duration.subsec_nanos())
870            }
871        }
872    }
873}
874
875#[cfg(test)]
876mod tests {
877    use super::{AttrFlags, MetadataPatch, Mode};
878    use crate::security::OwnershipIdentity;
879
880    #[test]
881    fn metadata_patch_builder_tracks_requested_changes() {
882        let patch = MetadataPatch::new()
883            .with_mode(Mode::OWNER_READ)
884            .with_user(OwnershipIdentity::Id(1))
885            .with_group(OwnershipIdentity::Name("staff".to_owned()))
886            .with_accessed(10)
887            .with_modified(20)
888            .with_created(30)
889            .with_attribute(AttrFlags::HIDDEN, Some(true))
890            .with_follow_links(false);
891
892        assert!(!patch.is_empty());
893        assert_eq!(patch.mode, Some(Mode::OWNER_READ));
894        assert!(patch.attrs.set.contains(AttrFlags::HIDDEN));
895        assert!(!patch.follow);
896        assert_eq!(
897            postcard::from_bytes::<MetadataPatch>(&postcard::to_stdvec(&patch).unwrap()).unwrap(),
898            patch
899        );
900    }
901
902    #[test]
903    fn metadata_patch_attribute_updates_are_disjoint() {
904        let mut patch = MetadataPatch::new();
905        patch.attribute(AttrFlags::READONLY, Some(true));
906        patch.attribute(AttrFlags::READONLY, Some(false));
907        assert!(!patch.attrs.set.contains(AttrFlags::READONLY));
908        assert!(patch.attrs.clear.contains(AttrFlags::READONLY));
909    }
910}