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) fs_type: Option<String>,
322    pub(crate) family: FsMetadataFamily,
323}
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub(crate) enum FsMetadataFamily {
326    Unix(UnixFsMetadata),
327    Windows(WindowsFsMetadata),
328}
329#[derive(Debug, Clone, Serialize, Deserialize)]
330/// Unix-specific filesystem metadata.
331pub struct UnixFsMetadata {
332    pub(crate) blocks: u64,
333    pub(crate) blocks_free: u64,
334    pub(crate) blocks_available: u64,
335    pub(crate) files: u64,
336    pub(crate) files_free: u64,
337    pub(crate) files_available: u64,
338    pub(crate) fragment_size: u32,
339    pub(crate) fsid: Option<u64>,
340    pub(crate) name_max: u32,
341    pub(crate) platform: UnixFsMetadataPlatform,
342}
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub(crate) enum UnixFsMetadataPlatform {
345    Linux { flags: u64 },
346    Macos { flags: u64 },
347    FreeBsd { flags: u64 },
348}
349#[derive(Debug, Clone, Serialize, Deserialize)]
350/// Windows-specific filesystem metadata.
351pub struct WindowsFsMetadata {
352    pub(crate) flags: u32,
353    pub(crate) volume_serial_number: u32,
354    pub(crate) component_length_max: u32,
355}
356
357impl FsMetadata {
358    /// Returns the total filesystem capacity in bytes.
359    pub const fn capacity(&self) -> u64 {
360        self.capacity
361    }
362    /// Returns the total free space in bytes.
363    pub const fn free(&self) -> u64 {
364        self.free
365    }
366    /// Returns the space available to an unprivileged caller in bytes.
367    pub const fn available(&self) -> u64 {
368        self.available
369    }
370    /// Returns the fundamental filesystem block size in bytes.
371    pub const fn block_size(&self) -> u32 {
372        self.block_size
373    }
374    /// Returns the filesystem type name as reported by the platform, if known.
375    pub fn fs_type(&self) -> Option<&str> {
376        self.fs_type.as_deref()
377    }
378    /// Returns the Unix-specific filesystem metadata, if present.
379    pub fn unix(&self) -> Option<&UnixFsMetadata> {
380        if let FsMetadataFamily::Unix(metadata) = &self.family {
381            Some(metadata)
382        } else {
383            None
384        }
385    }
386    /// Returns the Windows-specific filesystem metadata, if present.
387    pub fn windows(&self) -> Option<&WindowsFsMetadata> {
388        if let FsMetadataFamily::Windows(metadata) = &self.family {
389            Some(metadata)
390        } else {
391            None
392        }
393    }
394    /// Returns whether the filesystem is read-only.
395    #[allow(clippy::unnecessary_cast)]
396    pub fn read_only(&self) -> bool {
397        match &self.family {
398            FsMetadataFamily::Unix(metadata) => metadata.platform.flags() & 1 != 0,
399            FsMetadataFamily::Windows(metadata) => metadata.flags & 0x0008_0000 != 0,
400        }
401    }
402    /// Returns whether set-user-ID and set-group-ID bits are disabled, if known.
403    #[allow(clippy::unnecessary_cast)]
404    pub fn no_suid(&self) -> Option<bool> {
405        match &self.family {
406            FsMetadataFamily::Unix(metadata) => Some(metadata.platform.flags() & 2 != 0),
407            FsMetadataFamily::Windows(_) => None,
408        }
409    }
410    /// Returns whether execution is disabled, if known.
411    #[allow(clippy::unnecessary_cast)]
412    pub fn no_exec(&self) -> Option<bool> {
413        self.linux_flag(8)
414    }
415    /// Returns whether writes are synchronous, if known.
416    #[allow(clippy::unnecessary_cast)]
417    pub fn synchronous(&self) -> Option<bool> {
418        self.linux_flag(16)
419    }
420    /// Returns whether device files are disabled, if known.
421    #[allow(clippy::unnecessary_cast)]
422    pub fn no_dev(&self) -> Option<bool> {
423        self.linux_flag(4)
424    }
425    /// Returns whether access-time updates are disabled, if known.
426    #[allow(clippy::unnecessary_cast)]
427    pub fn no_atime(&self) -> Option<bool> {
428        self.linux_flag(1024)
429    }
430    /// Returns whether directory access-time updates are disabled, if known.
431    #[allow(clippy::unnecessary_cast)]
432    pub fn no_dir_atime(&self) -> Option<bool> {
433        self.linux_flag(2048)
434    }
435    /// Returns whether relative access-time updates are enabled, if known.
436    #[allow(clippy::unnecessary_cast)]
437    pub fn relatime(&self) -> Option<bool> {
438        self.linux_flag(1 << 21)
439    }
440    fn linux_flag(&self, flag: u64) -> Option<bool> {
441        match &self.family {
442            FsMetadataFamily::Unix(UnixFsMetadata {
443                platform: UnixFsMetadataPlatform::Linux { flags },
444                ..
445            }) => Some(flags & flag != 0),
446            _ => None,
447        }
448    }
449}
450impl UnixFsMetadata {
451    /// Returns the total number of filesystem blocks.
452    pub const fn blocks(&self) -> u64 {
453        self.blocks
454    }
455    /// Returns the number of free filesystem blocks.
456    pub const fn blocks_free(&self) -> u64 {
457        self.blocks_free
458    }
459    /// Returns the number of blocks available to an unprivileged caller.
460    pub const fn blocks_available(&self) -> u64 {
461        self.blocks_available
462    }
463    /// Returns the total number of file nodes.
464    pub const fn files(&self) -> u64 {
465        self.files
466    }
467    /// Returns the number of free file nodes.
468    pub const fn files_free(&self) -> u64 {
469        self.files_free
470    }
471    /// Returns the number of file nodes available to an unprivileged caller.
472    pub const fn files_available(&self) -> u64 {
473        self.files_available
474    }
475    /// Returns the fragment size in bytes.
476    pub const fn fragment_size(&self) -> u32 {
477        self.fragment_size
478    }
479    /// Returns the filesystem ID, if available.
480    pub const fn fsid(&self) -> Option<u64> {
481        self.fsid
482    }
483    /// Returns the maximum filename length.
484    pub const fn name_max(&self) -> u32 {
485        self.name_max
486    }
487    /// Returns the Linux mount flags, if present.
488    pub const fn linux_flags(&self) -> Option<u64> {
489        match self.platform {
490            UnixFsMetadataPlatform::Linux { flags } => Some(flags),
491            _ => None,
492        }
493    }
494    /// Returns the FreeBSD mount flags, if present.
495    pub const fn freebsd_flags(&self) -> Option<u64> {
496        match self.platform {
497            UnixFsMetadataPlatform::FreeBsd { flags } => Some(flags),
498            _ => None,
499        }
500    }
501    /// Returns the macOS mount flags, if present.
502    pub const fn macos_flags(&self) -> Option<u64> {
503        match self.platform {
504            UnixFsMetadataPlatform::Macos { flags } => Some(flags),
505            _ => None,
506        }
507    }
508}
509impl WindowsFsMetadata {
510    /// Returns the filesystem flags reported by Windows.
511    pub const fn flags(&self) -> u32 {
512        self.flags
513    }
514    /// Returns the volume serial number.
515    pub const fn volume_serial_number(&self) -> u32 {
516        self.volume_serial_number
517    }
518    /// Returns the maximum filesystem path-component length.
519    pub const fn component_length_max(&self) -> u32 {
520        self.component_length_max
521    }
522}
523impl UnixFsMetadataPlatform {
524    pub fn flags(&self) -> u64 {
525        match self {
526            Self::FreeBsd { flags } | Self::Linux { flags } | Self::Macos { flags } => *flags,
527        }
528    }
529}
530
531/// Portable filesystem attribute flags.
532#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
533#[serde(transparent)]
534pub struct AttrFlags(u64);
535impl AttrFlags {
536    /// Read-only.
537    pub const READONLY: Self = Self(1 << 0);
538    /// Hidden from ordinary directory listings.
539    pub const HIDDEN: Self = Self(1 << 1);
540    /// Used by the operating system.
541    pub const SYSTEM: Self = Self(1 << 2);
542    /// Marked for archival.
543    pub const ARCHIVE: Self = Self(1 << 3);
544    /// Stored in compressed form.
545    pub const COMPRESSED: Self = Self(1 << 4);
546    /// Intended for temporary storage.
547    pub const TEMPORARY: Self = Self(1 << 5);
548    /// Data is not immediately available.
549    pub const OFFLINE: Self = Self(1 << 6);
550    /// Excluded from content indexing.
551    pub const NOT_CONTENT_INDEXED: Self = Self(1 << 7);
552    /// Cannot be modified.
553    pub const IMMUTABLE: Self = Self(1 << 8);
554    /// May only be appended to.
555    pub const APPEND_ONLY: Self = Self(1 << 9);
556    /// Excluded from dump-style backups.
557    pub const NO_DUMP: Self = Self(1 << 10);
558    /// Does not update access time.
559    pub const NO_ATIME: Self = Self(1 << 11);
560    /// Disables copy-on-write behavior.
561    pub const NO_COPY_ON_WRITE: Self = Self(1 << 12);
562    /// Directory changes are written synchronously.
563    pub const DIR_SYNC: Self = Self(1 << 13);
564    /// Directory uses case-insensitive lookup.
565    pub const CASEFOLD: Self = Self(1 << 14);
566    /// File data is journaled.
567    pub const DATA_JOURNALING: Self = Self(1 << 15);
568    /// File must not be compressed.
569    pub const NO_COMPRESS: Self = Self(1 << 16);
570    /// New children inherit the project ID.
571    pub const PROJECT_INHERIT: Self = Self(1 << 17);
572    /// Requests secure deletion.
573    pub const SECURE_DELETE: Self = Self(1 << 18);
574    /// Changes are written synchronously.
575    pub const SYNC: Self = Self(1 << 19);
576    /// Disables tail merging.
577    pub const NO_TAIL_MERGE: Self = Self(1 << 20);
578    /// Directory is the top of a hierarchy.
579    pub const TOP_DIR: Self = Self(1 << 21);
580    /// File can be recovered after deletion.
581    pub const UNDELETE: Self = Self(1 << 22);
582    /// Supports direct-access storage.
583    pub const DIRECT_ACCESS: Self = Self(1 << 23);
584    /// Uses extent-based storage.
585    pub const EXTENT_FORMAT: Self = Self(1 << 24);
586    /// Directory is opaque to union mounts.
587    pub const OPAQUE: Self = Self(1 << 25);
588    /// File uses sparse allocation.
589    pub const SPARSE: Self = Self(1 << 26);
590    /// Returns an empty set of flags.
591    pub const fn empty() -> Self {
592        Self(0)
593    }
594    /// Returns whether all bits in `flag` are set.
595    pub const fn contains(self, flag: Self) -> bool {
596        self.0 & flag.0 != 0
597    }
598    /// Returns whether any bits in `other` are set.
599    pub const fn intersects(self, other: Self) -> bool {
600        self.0 & other.0 != 0
601    }
602    /// Returns the union of two flag sets.
603    pub const fn union(self, other: Self) -> Self {
604        Self(self.0 | other.0)
605    }
606    /// Returns these flags with the bits in `other` removed.
607    pub const fn difference(self, other: Self) -> Self {
608        Self(self.0 & !other.0)
609    }
610    /// Returns whether no flags are set.
611    pub const fn is_empty(self) -> bool {
612        self.0 == 0
613    }
614}
615#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
616pub(crate) struct AttrsPatch {
617    pub(crate) set: AttrFlags,
618    pub(crate) clear: AttrFlags,
619}
620impl AttrsPatch {
621    pub fn update(&mut self, flag: AttrFlags, value: Option<bool>) {
622        match value {
623            Some(true) => {
624                self.set = self.set.union(flag);
625                self.clear = self.clear.difference(flag);
626            }
627            Some(false) => {
628                self.clear = self.clear.union(flag);
629                self.set = self.set.difference(flag);
630            }
631            None => {}
632        }
633    }
634    pub const fn requested(self) -> AttrFlags {
635        self.set.union(self.clear)
636    }
637    pub const fn is_empty(self) -> bool {
638        self.set.is_empty() && self.clear.is_empty()
639    }
640}
641
642/// Requested changes to a filesystem object's metadata.
643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
644pub struct MetadataPatch {
645    pub(crate) mode: Option<Mode>,
646    pub(crate) user: Option<OwnershipIdentity>,
647    pub(crate) group: Option<OwnershipIdentity>,
648    pub(crate) accessed: Option<i128>,
649    pub(crate) modified: Option<i128>,
650    pub(crate) created: Option<i128>,
651    pub(crate) attrs: AttrsPatch,
652    pub(crate) follow: bool,
653}
654impl Default for MetadataPatch {
655    fn default() -> Self {
656        Self {
657            mode: None,
658            user: None,
659            group: None,
660            accessed: None,
661            modified: None,
662            created: None,
663            attrs: AttrsPatch::default(),
664            follow: true,
665        }
666    }
667}
668impl MetadataPatch {
669    /// Creates an empty metadata patch that follows symbolic links.
670    pub fn new() -> Self {
671        Self::default()
672    }
673    /// Sets the replacement Unix mode.
674    pub fn mode(&mut self, mode: Mode) -> &mut Self {
675        self.mode = Some(mode);
676        self
677    }
678    /// Sets the replacement owner.
679    pub fn user(&mut self, user: OwnershipIdentity) -> &mut Self {
680        self.user = Some(user);
681        self
682    }
683    /// Sets the replacement group.
684    pub fn group(&mut self, group: OwnershipIdentity) -> &mut Self {
685        self.group = Some(group);
686        self
687    }
688    /// Sets the access time in nanoseconds since the Unix epoch.
689    pub fn accessed(&mut self, accessed: i128) -> &mut Self {
690        self.accessed = Some(accessed);
691        self
692    }
693    /// Sets the modification time in nanoseconds since the Unix epoch.
694    pub fn modified(&mut self, modified: i128) -> &mut Self {
695        self.modified = Some(modified);
696        self
697    }
698    /// Sets the creation time in nanoseconds since the Unix epoch.
699    pub fn created(&mut self, created: i128) -> &mut Self {
700        self.created = Some(created);
701        self
702    }
703    /// Requests that an attribute be set, cleared, or left unchanged.
704    pub fn attribute(&mut self, flag: AttrFlags, value: Option<bool>) -> &mut Self {
705        self.attrs.update(flag, value);
706        self
707    }
708    /// Selects whether the operation follows symbolic links.
709    pub fn follow_links(&mut self, follow: bool) -> &mut Self {
710        self.follow = follow;
711        self
712    }
713    /// Sets the replacement Unix mode and returns the patch.
714    pub fn with_mode(mut self, mode: Mode) -> Self {
715        self.mode(mode);
716        self
717    }
718    /// Sets the replacement owner and returns the patch.
719    pub fn with_user(mut self, user: OwnershipIdentity) -> Self {
720        self.user(user);
721        self
722    }
723    /// Sets the replacement group and returns the patch.
724    pub fn with_group(mut self, group: OwnershipIdentity) -> Self {
725        self.group(group);
726        self
727    }
728    /// Sets the access time and returns the patch.
729    pub fn with_accessed(mut self, accessed: i128) -> Self {
730        self.accessed(accessed);
731        self
732    }
733    /// Sets the modification time and returns the patch.
734    pub fn with_modified(mut self, modified: i128) -> Self {
735        self.modified(modified);
736        self
737    }
738    /// Sets the creation time and returns the patch.
739    pub fn with_created(mut self, created: i128) -> Self {
740        self.created(created);
741        self
742    }
743    /// Requests an attribute change and returns the patch.
744    pub fn with_attribute(mut self, flag: AttrFlags, value: Option<bool>) -> Self {
745        self.attribute(flag, value);
746        self
747    }
748    /// Selects symbolic-link following and returns the patch.
749    pub fn with_follow_links(mut self, follow: bool) -> Self {
750        self.follow_links(follow);
751        self
752    }
753    /// Returns whether the patch requests no metadata changes.
754    pub fn is_empty(&self) -> bool {
755        self.mode.is_none()
756            && self.user.is_none()
757            && self.group.is_none()
758            && self.accessed.is_none()
759            && self.modified.is_none()
760            && self.created.is_none()
761            && self.attrs.is_empty()
762    }
763}
764
765pub(crate) fn metadata_from_std(metadata: std::fs::Metadata) -> Metadata {
766    #[cfg(unix)]
767    {
768        use nix::sys::stat::{SFlag, mode_t};
769        #[cfg(target_os = "macos")]
770        use std::os::darwin::fs::MetadataExt as DarwinMetadataExt;
771        #[cfg(target_os = "freebsd")]
772        use std::os::freebsd::fs::MetadataExt as FreeBsdMetadataExt;
773        use std::os::unix::fs::MetadataExt;
774        let mode = metadata.mode();
775        let file_type = match SFlag::from_bits_truncate(mode as mode_t) & SFlag::S_IFMT {
776            SFlag::S_IFREG => FileType::File,
777            SFlag::S_IFDIR => FileType::Dir,
778            SFlag::S_IFLNK => FileType::Symlink,
779            SFlag::S_IFIFO => FileType::Fifo,
780            SFlag::S_IFCHR => FileType::CharacterDevice,
781            SFlag::S_IFBLK => FileType::BlockDevice,
782            SFlag::S_IFSOCK => FileType::Socket,
783            _ => FileType::Unknown,
784        };
785        Metadata {
786            len: metadata.len(),
787            file_type,
788            atime: metadata.atime(),
789            atime_nsec: metadata.atime_nsec(),
790            mtime: metadata.mtime(),
791            mtime_nsec: metadata.mtime_nsec(),
792            ctime: metadata.ctime(),
793            ctime_nsec: metadata.ctime_nsec(),
794            family: MetadataFamily::Unix(UnixMetadata {
795                mode: Mode::from_bits_retain(mode),
796                dev: metadata.dev(),
797                ino: metadata.ino(),
798                nlink: metadata.nlink(),
799                uid: metadata.uid(),
800                gid: metadata.gid(),
801                rdev: metadata.rdev(),
802                blksize: metadata.blksize(),
803                blocks: metadata.blocks(),
804                #[cfg(target_os = "linux")]
805                platform: UnixMetadataPlatform::Linux { attrs: None },
806                #[cfg(target_os = "freebsd")]
807                platform: UnixMetadataPlatform::FreeBsd {
808                    attrs: metadata.st_flags(),
809                },
810                #[cfg(target_os = "macos")]
811                platform: UnixMetadataPlatform::Macos {
812                    attrs: metadata.st_flags(),
813                },
814            }),
815        }
816    }
817    #[cfg(windows)]
818    {
819        use std::os::windows::fs::MetadataExt;
820        let file_type = if metadata.is_file() {
821            FileType::File
822        } else if metadata.is_dir() {
823            FileType::Dir
824        } else if metadata.file_type().is_symlink() {
825            FileType::Symlink
826        } else {
827            FileType::Unknown
828        };
829        Metadata {
830            len: metadata.len(),
831            file_type,
832            atime: system_time_to_parts(metadata.accessed().ok()).0,
833            atime_nsec: i64::from(system_time_to_parts(metadata.accessed().ok()).1),
834            mtime: system_time_to_parts(metadata.modified().ok()).0,
835            mtime_nsec: i64::from(system_time_to_parts(metadata.modified().ok()).1),
836            ctime: system_time_to_parts(metadata.created().ok()).0,
837            ctime_nsec: i64::from(system_time_to_parts(metadata.created().ok()).1),
838            family: MetadataFamily::Windows(WindowsMetadata {
839                attrs: metadata.file_attributes(),
840                user: None,
841                group: None,
842            }),
843        }
844    }
845}
846#[cfg(windows)]
847pub(crate) fn metadata_with_sids(
848    mut metadata: Metadata,
849    user: Option<Sid>,
850    group: Option<Sid>,
851) -> Metadata {
852    let MetadataFamily::Windows(windows) = &mut metadata.family else {
853        unreachable!()
854    };
855    windows.user = user;
856    windows.group = group;
857    metadata
858}
859#[cfg(windows)]
860fn system_time_to_parts(time: Option<std::time::SystemTime>) -> (i64, u32) {
861    use std::time::UNIX_EPOCH;
862    let Some(time) = time else { return (0, 0) };
863    match time.duration_since(UNIX_EPOCH) {
864        Ok(duration) => (
865            i64::try_from(duration.as_secs()).unwrap_or(i64::MAX),
866            duration.subsec_nanos(),
867        ),
868        Err(err) => {
869            let duration = err.duration();
870            let secs = i64::try_from(duration.as_secs()).unwrap_or(i64::MAX);
871            if duration.subsec_nanos() == 0 {
872                (-secs, 0)
873            } else {
874                (-secs - 1, 1_000_000_000 - duration.subsec_nanos())
875            }
876        }
877    }
878}
879
880#[cfg(test)]
881mod tests {
882    use super::{AttrFlags, MetadataPatch, Mode};
883    use crate::security::OwnershipIdentity;
884
885    #[test]
886    fn metadata_patch_builder_tracks_requested_changes() {
887        let patch = MetadataPatch::new()
888            .with_mode(Mode::OWNER_READ)
889            .with_user(OwnershipIdentity::Id(1))
890            .with_group(OwnershipIdentity::Name("staff".to_owned()))
891            .with_accessed(10)
892            .with_modified(20)
893            .with_created(30)
894            .with_attribute(AttrFlags::HIDDEN, Some(true))
895            .with_follow_links(false);
896
897        assert!(!patch.is_empty());
898        assert_eq!(patch.mode, Some(Mode::OWNER_READ));
899        assert!(patch.attrs.set.contains(AttrFlags::HIDDEN));
900        assert!(!patch.follow);
901        assert_eq!(
902            postcard::from_bytes::<MetadataPatch>(&postcard::to_stdvec(&patch).unwrap()).unwrap(),
903            patch
904        );
905    }
906
907    #[test]
908    fn metadata_patch_attribute_updates_are_disjoint() {
909        let mut patch = MetadataPatch::new();
910        patch.attribute(AttrFlags::READONLY, Some(true));
911        patch.attribute(AttrFlags::READONLY, Some(false));
912        assert!(!patch.attrs.set.contains(AttrFlags::READONLY));
913        assert!(patch.attrs.clear.contains(AttrFlags::READONLY));
914    }
915}