1use dolang_winterop::security::Sid;
4use serde::{Deserialize, Serialize};
5
6use crate::security::{OwnershipIdentity, Permission};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum FileType {
11 File,
13 Dir,
15 Symlink,
17 Fifo,
19 CharacterDevice,
21 BlockDevice,
23 Socket,
25 Unknown,
27}
28
29bitflags::bitflags! {
30 #[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 pub fn owner(self) -> Permission {
64 Permission::from_bits_truncate((self.bits() >> 6) as u8 & 0o7)
65 }
66 pub fn group(self) -> Permission {
68 Permission::from_bits_truncate((self.bits() >> 3) as u8 & 0o7)
69 }
70 pub fn other(self) -> Permission {
72 Permission::from_bits_truncate(self.bits() as u8 & 0o7)
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Metadata {
79 pub(crate) len: u64,
81 pub(crate) file_type: FileType,
83 pub(crate) atime: i64,
85 pub(crate) atime_nsec: i64,
87 pub(crate) mtime: i64,
89 pub(crate) mtime_nsec: i64,
91 pub(crate) ctime: i64,
93 pub(crate) ctime_nsec: i64,
95 pub(crate) family: MetadataFamily,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub(crate) enum MetadataFamily {
102 Unix(UnixMetadata),
104 Windows(WindowsMetadata),
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct UnixMetadata {
111 pub(crate) mode: Mode,
113 pub(crate) dev: u64,
115 pub(crate) ino: u64,
117 pub(crate) nlink: u64,
119 pub(crate) uid: u32,
121 pub(crate) gid: u32,
123 pub(crate) rdev: u64,
125 pub(crate) blksize: u64,
127 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)]
140pub struct WindowsMetadata {
142 pub(crate) attrs: u32,
143 pub(crate) user: Option<Sid>,
144 pub(crate) group: Option<Sid>,
145}
146
147impl Metadata {
148 pub const fn len(&self) -> u64 {
150 self.len
151 }
152 pub const fn is_empty(&self) -> bool {
154 self.len == 0
155 }
156 pub const fn file_type(&self) -> FileType {
158 self.file_type
159 }
160 pub const fn atime(&self) -> i64 {
162 self.atime
163 }
164 pub const fn atime_nsec(&self) -> i64 {
166 self.atime_nsec
167 }
168 pub const fn mtime(&self) -> i64 {
170 self.mtime
171 }
172 pub const fn mtime_nsec(&self) -> i64 {
174 self.mtime_nsec
175 }
176 pub const fn ctime(&self) -> i64 {
178 self.ctime
179 }
180 pub const fn ctime_nsec(&self) -> i64 {
182 self.ctime_nsec
183 }
184 pub fn unix(&self) -> Option<&UnixMetadata> {
186 if let MetadataFamily::Unix(metadata) = &self.family {
187 Some(metadata)
188 } else {
189 None
190 }
191 }
192 pub fn windows(&self) -> Option<&WindowsMetadata> {
194 if let MetadataFamily::Windows(metadata) = &self.family {
195 Some(metadata)
196 } else {
197 None
198 }
199 }
200 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 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 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 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 pub const fn mode(&self) -> Mode {
242 self.mode
243 }
244 pub const fn dev(&self) -> u64 {
246 self.dev
247 }
248 pub const fn ino(&self) -> u64 {
250 self.ino
251 }
252 pub const fn nlink(&self) -> u64 {
254 self.nlink
255 }
256 pub const fn uid(&self) -> u32 {
258 self.uid
259 }
260 pub const fn gid(&self) -> u32 {
262 self.gid
263 }
264 pub const fn rdev(&self) -> u64 {
266 self.rdev
267 }
268 pub const fn block_size(&self) -> u64 {
270 self.blksize
271 }
272 pub const fn blocks(&self) -> u64 {
274 self.blocks
275 }
276 pub const fn linux_attrs(&self) -> Option<u32> {
278 match self.platform {
279 UnixMetadataPlatform::Linux { attrs } => attrs,
280 _ => None,
281 }
282 }
283 pub const fn freebsd_attrs(&self) -> Option<u32> {
285 match self.platform {
286 UnixMetadataPlatform::FreeBsd { attrs } => Some(attrs),
287 _ => None,
288 }
289 }
290 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 pub const fn attrs(&self) -> u32 {
302 self.attrs
303 }
304 pub fn user(&self) -> Option<&Sid> {
306 self.user.as_ref()
307 }
308 pub fn group(&self) -> Option<&Sid> {
310 self.group.as_ref()
311 }
312}
313
314#[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)]
330pub 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)]
350pub 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 pub const fn capacity(&self) -> u64 {
360 self.capacity
361 }
362 pub const fn free(&self) -> u64 {
364 self.free
365 }
366 pub const fn available(&self) -> u64 {
368 self.available
369 }
370 pub const fn block_size(&self) -> u32 {
372 self.block_size
373 }
374 pub fn fs_type(&self) -> Option<&str> {
376 self.fs_type.as_deref()
377 }
378 pub fn unix(&self) -> Option<&UnixFsMetadata> {
380 if let FsMetadataFamily::Unix(metadata) = &self.family {
381 Some(metadata)
382 } else {
383 None
384 }
385 }
386 pub fn windows(&self) -> Option<&WindowsFsMetadata> {
388 if let FsMetadataFamily::Windows(metadata) = &self.family {
389 Some(metadata)
390 } else {
391 None
392 }
393 }
394 #[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 #[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 #[allow(clippy::unnecessary_cast)]
412 pub fn no_exec(&self) -> Option<bool> {
413 self.linux_flag(8)
414 }
415 #[allow(clippy::unnecessary_cast)]
417 pub fn synchronous(&self) -> Option<bool> {
418 self.linux_flag(16)
419 }
420 #[allow(clippy::unnecessary_cast)]
422 pub fn no_dev(&self) -> Option<bool> {
423 self.linux_flag(4)
424 }
425 #[allow(clippy::unnecessary_cast)]
427 pub fn no_atime(&self) -> Option<bool> {
428 self.linux_flag(1024)
429 }
430 #[allow(clippy::unnecessary_cast)]
432 pub fn no_dir_atime(&self) -> Option<bool> {
433 self.linux_flag(2048)
434 }
435 #[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 pub const fn blocks(&self) -> u64 {
453 self.blocks
454 }
455 pub const fn blocks_free(&self) -> u64 {
457 self.blocks_free
458 }
459 pub const fn blocks_available(&self) -> u64 {
461 self.blocks_available
462 }
463 pub const fn files(&self) -> u64 {
465 self.files
466 }
467 pub const fn files_free(&self) -> u64 {
469 self.files_free
470 }
471 pub const fn files_available(&self) -> u64 {
473 self.files_available
474 }
475 pub const fn fragment_size(&self) -> u32 {
477 self.fragment_size
478 }
479 pub const fn fsid(&self) -> Option<u64> {
481 self.fsid
482 }
483 pub const fn name_max(&self) -> u32 {
485 self.name_max
486 }
487 pub const fn linux_flags(&self) -> Option<u64> {
489 match self.platform {
490 UnixFsMetadataPlatform::Linux { flags } => Some(flags),
491 _ => None,
492 }
493 }
494 pub const fn freebsd_flags(&self) -> Option<u64> {
496 match self.platform {
497 UnixFsMetadataPlatform::FreeBsd { flags } => Some(flags),
498 _ => None,
499 }
500 }
501 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 pub const fn flags(&self) -> u32 {
512 self.flags
513 }
514 pub const fn volume_serial_number(&self) -> u32 {
516 self.volume_serial_number
517 }
518 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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
533#[serde(transparent)]
534pub struct AttrFlags(u64);
535impl AttrFlags {
536 pub const READONLY: Self = Self(1 << 0);
538 pub const HIDDEN: Self = Self(1 << 1);
540 pub const SYSTEM: Self = Self(1 << 2);
542 pub const ARCHIVE: Self = Self(1 << 3);
544 pub const COMPRESSED: Self = Self(1 << 4);
546 pub const TEMPORARY: Self = Self(1 << 5);
548 pub const OFFLINE: Self = Self(1 << 6);
550 pub const NOT_CONTENT_INDEXED: Self = Self(1 << 7);
552 pub const IMMUTABLE: Self = Self(1 << 8);
554 pub const APPEND_ONLY: Self = Self(1 << 9);
556 pub const NO_DUMP: Self = Self(1 << 10);
558 pub const NO_ATIME: Self = Self(1 << 11);
560 pub const NO_COPY_ON_WRITE: Self = Self(1 << 12);
562 pub const DIR_SYNC: Self = Self(1 << 13);
564 pub const CASEFOLD: Self = Self(1 << 14);
566 pub const DATA_JOURNALING: Self = Self(1 << 15);
568 pub const NO_COMPRESS: Self = Self(1 << 16);
570 pub const PROJECT_INHERIT: Self = Self(1 << 17);
572 pub const SECURE_DELETE: Self = Self(1 << 18);
574 pub const SYNC: Self = Self(1 << 19);
576 pub const NO_TAIL_MERGE: Self = Self(1 << 20);
578 pub const TOP_DIR: Self = Self(1 << 21);
580 pub const UNDELETE: Self = Self(1 << 22);
582 pub const DIRECT_ACCESS: Self = Self(1 << 23);
584 pub const EXTENT_FORMAT: Self = Self(1 << 24);
586 pub const OPAQUE: Self = Self(1 << 25);
588 pub const SPARSE: Self = Self(1 << 26);
590 pub const fn empty() -> Self {
592 Self(0)
593 }
594 pub const fn contains(self, flag: Self) -> bool {
596 self.0 & flag.0 != 0
597 }
598 pub const fn intersects(self, other: Self) -> bool {
600 self.0 & other.0 != 0
601 }
602 pub const fn union(self, other: Self) -> Self {
604 Self(self.0 | other.0)
605 }
606 pub const fn difference(self, other: Self) -> Self {
608 Self(self.0 & !other.0)
609 }
610 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#[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 pub fn new() -> Self {
671 Self::default()
672 }
673 pub fn mode(&mut self, mode: Mode) -> &mut Self {
675 self.mode = Some(mode);
676 self
677 }
678 pub fn user(&mut self, user: OwnershipIdentity) -> &mut Self {
680 self.user = Some(user);
681 self
682 }
683 pub fn group(&mut self, group: OwnershipIdentity) -> &mut Self {
685 self.group = Some(group);
686 self
687 }
688 pub fn accessed(&mut self, accessed: i128) -> &mut Self {
690 self.accessed = Some(accessed);
691 self
692 }
693 pub fn modified(&mut self, modified: i128) -> &mut Self {
695 self.modified = Some(modified);
696 self
697 }
698 pub fn created(&mut self, created: i128) -> &mut Self {
700 self.created = Some(created);
701 self
702 }
703 pub fn attribute(&mut self, flag: AttrFlags, value: Option<bool>) -> &mut Self {
705 self.attrs.update(flag, value);
706 self
707 }
708 pub fn follow_links(&mut self, follow: bool) -> &mut Self {
710 self.follow = follow;
711 self
712 }
713 pub fn with_mode(mut self, mode: Mode) -> Self {
715 self.mode(mode);
716 self
717 }
718 pub fn with_user(mut self, user: OwnershipIdentity) -> Self {
720 self.user(user);
721 self
722 }
723 pub fn with_group(mut self, group: OwnershipIdentity) -> Self {
725 self.group(group);
726 self
727 }
728 pub fn with_accessed(mut self, accessed: i128) -> Self {
730 self.accessed(accessed);
731 self
732 }
733 pub fn with_modified(mut self, modified: i128) -> Self {
735 self.modified(modified);
736 self
737 }
738 pub fn with_created(mut self, created: i128) -> Self {
740 self.created(created);
741 self
742 }
743 pub fn with_attribute(mut self, flag: AttrFlags, value: Option<bool>) -> Self {
745 self.attribute(flag, value);
746 self
747 }
748 pub fn with_follow_links(mut self, follow: bool) -> Self {
750 self.follow_links(follow);
751 self
752 }
753 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}