1use crate::error::{Error, Result};
12
13pub const INODE_BASE_SIZE: usize = 128;
15pub const GOOD_OLD_INODE_SIZE: usize = 128;
28
29pub const INODE_EXTRA_OFFSET: usize = 128;
31
32pub(crate) const OFF_MODE: usize = 0x00;
36pub(crate) const OFF_SIZE_LO: usize = 0x04;
37pub(crate) const OFF_ATIME: usize = 0x08;
38pub(crate) const OFF_CTIME: usize = 0x0C;
39pub(crate) const OFF_MTIME: usize = 0x10;
40pub(crate) const OFF_LINKS_COUNT: usize = 0x1A;
41pub(crate) const OFF_BLOCKS_LO: usize = 0x1C;
42pub(crate) const OFF_FLAGS: usize = 0x20;
43pub(crate) const OFF_BLOCK: usize = 0x28; pub(crate) const OFF_GENERATION: usize = 0x64;
45pub(crate) const OFF_SIZE_HI: usize = 0x6C;
46pub(crate) const OFF_BLOCKS_HI: usize = 0x74;
47pub(crate) const OFF_CHECKSUM_LO: usize = 0x7C;
48pub(crate) const OFF_EXTRA_ISIZE: usize = 0x80;
49pub(crate) const OFF_CHECKSUM_HI: usize = 0x82;
50pub(crate) const OFF_CRTIME: usize = 0x90;
51
52pub(crate) const EXTRA_ISIZE_DEFAULT: u16 = 32;
55#[cfg(test)]
57pub(crate) const INODE_SIZE_WITH_CRTIME: usize = 0x94;
58pub(crate) const INODE_SIZE_WITH_EXTRA: usize = 0x84;
60
61pub const S_IFMT: u16 = 0xF000;
63pub const S_IFREG: u16 = 0x8000;
64pub const S_IFDIR: u16 = 0x4000;
65pub const S_IFLNK: u16 = 0xA000;
66pub const S_IFBLK: u16 = 0x6000;
67pub const S_IFCHR: u16 = 0x2000;
68pub const S_IFIFO: u16 = 0x1000;
69pub const S_IFSOCK: u16 = 0xC000;
70
71bitflags::bitflags! {
72 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
75 pub struct InodeFlags: u32 {
76 const SECRM = 0x0000_0001;
78 const UNRM = 0x0000_0002;
80 const COMPR = 0x0000_0004;
82 const SYNC = 0x0000_0008;
84 const IMMUTABLE = 0x0000_0010;
86 const APPEND = 0x0000_0020;
88 const NODUMP = 0x0000_0040;
90 const NOATIME = 0x0000_0080;
92 const ENCRYPT = 0x0000_0800;
95 const INDEX = 0x0000_1000;
97 const EA_INODE = 0x0020_0000;
99 const EXTENTS = 0x0008_0000;
101 const HUGE_FILE = 0x0004_0000;
103 const INLINE_DATA = 0x1000_0000;
105 const EXTENT = 0x0008_0000;
107 const EXTRA_ATIME = 0x0000_0100;
109 }
110}
111
112pub const USER_MODIFIABLE_FLAGS: u32 = InodeFlags::SECRM.bits()
124 | InodeFlags::UNRM.bits()
125 | InodeFlags::COMPR.bits()
126 | InodeFlags::SYNC.bits()
127 | InodeFlags::IMMUTABLE.bits()
128 | InodeFlags::APPEND.bits()
129 | InodeFlags::NODUMP.bits()
130 | InodeFlags::NOATIME.bits()
131 | 0x0000_4000 | 0x0000_8000 | 0x0001_0000 | 0x0002_0000 | 0x2000_0000; #[derive(Debug, Clone)]
143pub struct Inode {
144 pub mode: u16,
145 pub uid: u32,
146 pub gid: u32,
147 pub size: u64,
148 pub atime: i64,
157 pub mtime: i64,
158 pub ctime: i64,
159 pub dtime: i64,
160 pub crtime: i64,
161 pub atime_nsec: u32,
162 pub mtime_nsec: u32,
163 pub ctime_nsec: u32,
164 pub crtime_nsec: u32,
165 pub links_count: u16,
166 pub blocks: u64, pub flags: u32,
168 pub block: [u8; 60],
171 pub generation: u32,
172 pub file_acl: u64,
173 pub checksum: u32,
174}
175
176const ERA_SECONDS: i64 = 1 << 32;
199
200const ERA_FIELD: u32 = 0b11;
202
203fn unpack_seconds(base: u32, extra: u32) -> i64 {
210 let signed_base = i64::from(base as i32);
211 let era = i64::from(extra & ERA_FIELD);
212 signed_base + era * ERA_SECONDS
213}
214
215pub(crate) fn pack_seconds(secs: i64) -> (u32, u32) {
234 let half_windows = secs >> 31;
235 let era = (((half_windows + 1) >> 1) as u32) & ERA_FIELD;
236 (secs as u32, era)
237}
238
239pub(crate) const MIN_ENCODABLE_TIME: i64 = i32::MIN as i64;
242pub(crate) const MAX_ENCODABLE_TIME: i64 = i32::MAX as i64 + 3 * ERA_SECONDS;
245
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub(crate) enum InodeTime {
250 Atime,
251 Ctime,
252 Mtime,
253 Crtime,
254}
255
256impl InodeTime {
257 fn base_offset(self) -> usize {
259 match self {
260 InodeTime::Atime => OFF_ATIME,
261 InodeTime::Ctime => OFF_CTIME,
262 InodeTime::Mtime => OFF_MTIME,
263 InodeTime::Crtime => OFF_CRTIME,
264 }
265 }
266
267 fn extra_offset(self) -> usize {
269 match self {
270 InodeTime::Ctime => 0x84,
271 InodeTime::Mtime => 0x88,
272 InodeTime::Atime => 0x8C,
273 InodeTime::Crtime => 0x94,
274 }
275 }
276}
277
278fn extra_field_fits(raw: &[u8], offset: usize) -> bool {
282 let Some(size_bytes) = raw.get(OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2) else {
283 return false;
284 };
285 let declared_end =
286 INODE_EXTRA_OFFSET + usize::from(u16::from_le_bytes([size_bytes[0], size_bytes[1]]));
287 let field_end = offset + 4;
288 field_end <= raw.len() && field_end <= declared_end
289}
290
291pub(crate) fn set_inode_time(raw: &mut [u8], field: InodeTime, secs: i64) {
303 let base_at = field.base_offset();
304 let extra_at = field.extra_offset();
305 let (base, extra) = if extra_field_fits(raw, extra_at) {
306 let (base, era) = pack_seconds(secs.clamp(MIN_ENCODABLE_TIME, MAX_ENCODABLE_TIME));
307 (base, Some(era))
308 } else if field != InodeTime::Crtime || extra_field_fits(raw, base_at) {
309 let clamped = secs.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
310 (clamped as u32, None)
311 } else {
312 return;
313 };
314 raw[base_at..base_at + 4].copy_from_slice(&base.to_le_bytes());
315 if let Some(era) = extra {
316 raw[extra_at..extra_at + 4].copy_from_slice(&era.to_le_bytes());
317 }
318}
319
320impl Inode {
321 pub fn parse(raw: &[u8]) -> Result<Self> {
325 if raw.len() < INODE_BASE_SIZE {
326 return Err(Error::Corrupt("inode buffer too small"));
327 }
328
329 let mode = u16::from_le_bytes(raw[OFF_MODE..OFF_MODE + 2].try_into().unwrap());
330 let uid_lo = u16::from_le_bytes(raw[0x02..0x04].try_into().unwrap());
331 let size_lo = u32::from_le_bytes(raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].try_into().unwrap());
332 let atime_base = u32::from_le_bytes(raw[OFF_ATIME..OFF_ATIME + 4].try_into().unwrap());
333 let ctime_base = u32::from_le_bytes(raw[OFF_CTIME..OFF_CTIME + 4].try_into().unwrap());
334 let mtime_base = u32::from_le_bytes(raw[OFF_MTIME..OFF_MTIME + 4].try_into().unwrap());
335 let dtime = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
336 let gid_lo = u16::from_le_bytes(raw[0x18..0x1A].try_into().unwrap());
337 let links_count = u16::from_le_bytes(
338 raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2]
339 .try_into()
340 .unwrap(),
341 );
342 let blocks_lo =
343 u32::from_le_bytes(raw[OFF_BLOCKS_LO..OFF_BLOCKS_LO + 4].try_into().unwrap());
344 let flags = u32::from_le_bytes(raw[OFF_FLAGS..OFF_FLAGS + 4].try_into().unwrap());
345 let mut block = [0u8; 60];
348 block.copy_from_slice(&raw[OFF_BLOCK..OFF_BLOCK + 60]);
349
350 let generation =
351 u32::from_le_bytes(raw[OFF_GENERATION..OFF_GENERATION + 4].try_into().unwrap());
352 let file_acl_lo = u32::from_le_bytes(raw[0x68..0x6C].try_into().unwrap());
353 let size_hi = u32::from_le_bytes(raw[OFF_SIZE_HI..OFF_SIZE_HI + 4].try_into().unwrap());
354 let blocks_hi =
356 u16::from_le_bytes(raw[OFF_BLOCKS_HI..OFF_BLOCKS_HI + 2].try_into().unwrap());
357 let file_acl_hi = u16::from_le_bytes(raw[0x76..0x78].try_into().unwrap());
358 let uid_hi = u16::from_le_bytes(raw[0x78..0x7A].try_into().unwrap());
359 let gid_hi = u16::from_le_bytes(raw[0x7A..0x7C].try_into().unwrap());
360 let checksum_lo = u16::from_le_bytes(
361 raw[OFF_CHECKSUM_LO..OFF_CHECKSUM_LO + 2]
362 .try_into()
363 .unwrap(),
364 );
365 let mut atime_nsec = 0u32;
369 let mut mtime_nsec = 0u32;
370 let mut ctime_nsec = 0u32;
371 let mut crtime_nsec = 0u32;
372 let mut crtime_base = 0u32;
373 let mut atime_extra = 0u32;
376 let mut mtime_extra = 0u32;
377 let mut ctime_extra = 0u32;
378 let mut crtime_extra = 0u32;
379 let mut checksum_hi = 0u16;
380
381 if raw.len() >= INODE_EXTRA_OFFSET + 4 {
385 let i_extra_isize = u16::from_le_bytes(
386 raw[OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2]
387 .try_into()
388 .unwrap(),
389 );
390 let extra_end = INODE_EXTRA_OFFSET + i_extra_isize as usize;
393 if extra_end > raw.len() {
394 return Err(Error::Corrupt("i_extra_isize exceeds inode size"));
395 }
396
397 if i_extra_isize >= 4 {
407 checksum_hi = u16::from_le_bytes(
408 raw[OFF_CHECKSUM_HI..OFF_CHECKSUM_HI + 2]
409 .try_into()
410 .unwrap(),
411 );
412 }
413 if i_extra_isize >= 8 {
414 let extra = u32::from_le_bytes(raw[0x84..0x88].try_into().unwrap());
415 ctime_nsec = extra >> 2;
416 ctime_extra = extra;
417 }
418 if i_extra_isize >= 12 {
419 let extra = u32::from_le_bytes(raw[0x88..0x8C].try_into().unwrap());
420 mtime_nsec = extra >> 2;
421 mtime_extra = extra;
422 }
423 if i_extra_isize >= 16 {
424 let extra = u32::from_le_bytes(raw[0x8C..0x90].try_into().unwrap());
425 atime_nsec = extra >> 2;
426 atime_extra = extra;
427 }
428 if i_extra_isize >= 20 {
429 crtime_base =
430 u32::from_le_bytes(raw[OFF_CRTIME..OFF_CRTIME + 4].try_into().unwrap());
431 }
432 if i_extra_isize >= 24 {
433 let extra = u32::from_le_bytes(raw[0x94..0x98].try_into().unwrap());
434 crtime_nsec = extra >> 2;
435 crtime_extra = extra;
436 }
437 }
438
439 Ok(Self {
440 mode,
441 uid: join16(uid_hi, uid_lo),
442 gid: join16(gid_hi, gid_lo),
443 size: join32(size_hi, size_lo),
444 atime: unpack_seconds(atime_base, atime_extra),
445 mtime: unpack_seconds(mtime_base, mtime_extra),
446 ctime: unpack_seconds(ctime_base, ctime_extra),
447 dtime: dtime as i32 as i64,
450 crtime: unpack_seconds(crtime_base, crtime_extra),
451 atime_nsec,
452 mtime_nsec,
453 ctime_nsec,
454 crtime_nsec,
455 links_count,
456 blocks: join32(blocks_hi, blocks_lo),
457 flags,
458 block,
459 generation,
460 file_acl: join32(file_acl_hi, file_acl_lo),
461 checksum: join16(checksum_hi, checksum_lo),
462 })
463 }
464
465 pub fn file_type(&self) -> u16 {
467 self.mode & S_IFMT
468 }
469
470 pub fn is_dir(&self) -> bool {
471 self.file_type() == S_IFDIR
472 }
473
474 pub fn is_file(&self) -> bool {
475 self.file_type() == S_IFREG
476 }
477
478 pub fn is_symlink(&self) -> bool {
479 self.file_type() == S_IFLNK
480 }
481
482 pub fn has_extents(&self) -> bool {
485 self.flags & InodeFlags::EXTENTS.bits() != 0
486 }
487
488 pub fn has_inline_data(&self) -> bool {
490 self.flags & InodeFlags::INLINE_DATA.bits() != 0
491 }
492
493 pub fn flag_set(&self) -> InodeFlags {
495 InodeFlags::from_bits_truncate(self.flags)
496 }
497}
498
499#[inline]
502fn join16(hi: u16, lo: u16) -> u32 {
503 ((hi as u32) << 16) | lo as u32
504}
505
506#[inline]
510fn join32<H: Into<u64>>(hi: H, lo: u32) -> u64 {
511 (hi.into() << 32) | lo as u64
512}
513
514#[cfg(test)]
515mod timestamp_tests {
516 use super::{pack_seconds, unpack_seconds, MAX_ENCODABLE_TIME, MIN_ENCODABLE_TIME};
517
518 const KERNEL_WORDS: &[(i64, u32, u32)] = &[
524 (-2_147_483_648, 0x8000_0000, 0), (-1_000, 0xFFFF_FC18, 0), (-1, 0xFFFF_FFFF, 0), (0, 0x0000_0000, 0), (1, 0x0000_0001, 0),
529 (2_147_483_647, 0x7FFF_FFFF, 0), (2_147_483_648, 0x8000_0000, 1), (4_294_967_295, 0xFFFF_FFFF, 1), (4_294_967_296, 0x0000_0000, 1), (6_442_450_943, 0x7FFF_FFFF, 1), (6_442_450_944, 0x8000_0000, 2), (8_589_934_592, 0x0000_0000, 2), (12_884_901_887, 0xFFFF_FFFF, 3), (12_884_901_888, 0x0000_0000, 3),
538 (15_032_385_535, 0x7FFF_FFFF, 3), ];
540
541 #[test]
542 fn every_kernel_written_word_pair_reads_back_as_its_seconds() {
543 for &(secs, base, era) in KERNEL_WORDS {
544 assert_eq!(unpack_seconds(base, era), secs, "{base:#x}:{era}");
545 }
546 }
547
548 #[test]
549 fn every_second_in_the_table_packs_to_the_kernels_words() {
550 for &(secs, base, era) in KERNEL_WORDS {
551 assert_eq!(pack_seconds(secs), (base, era), "{secs}");
552 }
553 }
554
555 #[test]
557 fn the_encodable_range_is_1901_to_2446() {
558 assert_eq!(MIN_ENCODABLE_TIME, KERNEL_WORDS[0].0);
559 assert_eq!(MAX_ENCODABLE_TIME, KERNEL_WORDS[KERNEL_WORDS.len() - 1].0);
560 }
561
562 #[test]
564 fn nanosecond_bits_are_not_seconds() {
565 let all_nanosecond_bits = !0b11u32;
566 assert_eq!(unpack_seconds(1_000, all_nanosecond_bits), 1_000);
567 assert_eq!(
568 unpack_seconds(1_000, all_nanosecond_bits | 0b10),
569 1_000 + (2 << 32)
570 );
571 }
572
573 #[test]
576 fn window_edges_survive_a_round_trip() {
577 for era in 0..4i64 {
578 let lowest = i64::from(i32::MIN) + (era << 32);
579 let highest = i64::from(i32::MAX) + (era << 32);
580 for secs in [lowest, lowest + 1, highest - 1, highest] {
581 let (base, bits) = pack_seconds(secs);
582 assert_eq!(bits as i64, era, "{secs}");
583 assert_eq!(unpack_seconds(base, bits), secs, "{secs}");
584 }
585 }
586 }
587
588 mod needs_host {
590 use super::KERNEL_WORDS;
591 use crate::block_io::FileDevice;
592 use crate::fs::Filesystem;
593 use std::sync::Arc;
594
595 #[test]
601 fn the_kernel_writes_and_reads_the_documented_words() {
602 let image =
603 fs_ext4_test_support::temp_path!("fs_ext4_inode_times_{}.img", std::process::id());
604 std::fs::File::create(&image)
605 .and_then(|f| f.set_len(8 << 20))
606 .unwrap();
607 let out = fs_ext4_test_support::oracle("mkfs.ext4")
608 .args(["-q", "-F", "-I", "256", &image])
609 .output();
610 assert!(
611 out.status.success(),
612 "{}",
613 String::from_utf8_lossy(&out.stderr)
614 );
615
616 let past_the_end = [15_032_385_536i64, 17_179_869_184, 99_999_999_999];
617 let mut all: Vec<i64> = KERNEL_WORDS.iter().map(|row| row.0).collect();
618 all.extend(past_the_end);
619 let mut script = String::from("set -e\n");
620 for secs in &all {
621 script.push_str(&format!(
622 "touch \"$MNT/t{secs}\"; touch -m -d @{secs} \"$MNT/t{secs}\"\n"
623 ));
624 }
625 for secs in &all {
626 script.push_str(&format!(
627 "printf '%s %s\\n' {secs} \"$(stat -c %Y \"$MNT/t{secs}\")\"\n"
628 ));
629 }
630 let out = fs_ext4_test_support::guest_kernel_write(&image, &script);
631 assert!(
632 out.status.success(),
633 "{}",
634 String::from_utf8_lossy(&out.stderr)
635 );
636 let kernel_reads: std::collections::BTreeMap<i64, i64> =
637 String::from_utf8_lossy(&out.stdout)
638 .lines()
639 .filter_map(|l| {
640 let (a, b) = l.split_once(' ')?;
641 Some((a.parse().ok()?, b.trim().parse().ok()?))
642 })
643 .collect();
644
645 let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
646 let last = KERNEL_WORDS[KERNEL_WORDS.len() - 1];
647 for &secs in &all {
648 let (want_secs, want_base, want_era) = KERNEL_WORDS
649 .iter()
650 .copied()
651 .find(|row| row.0 == secs)
652 .unwrap_or(last);
653 assert_eq!(
654 kernel_reads.get(&secs),
655 Some(&want_secs),
656 "kernel stat of @{secs}"
657 );
658
659 let out = fs_ext4_test_support::oracle("debugfs")
660 .args(["-R", &format!("stat /t{secs}"), &image])
661 .output();
662 let text = String::from_utf8_lossy(&out.stdout).into_owned();
663 let words = text
664 .lines()
665 .find_map(|l| l.trim().strip_prefix("mtime: 0x"))
666 .unwrap_or_else(|| panic!("no mtime line for @{secs}: {text}"));
667 let (base, extra) = words.split_once(":").unwrap();
668 let base = u32::from_str_radix(base, 16).unwrap();
669 let extra = u32::from_str_radix(&extra[..8], 16).unwrap();
670 assert_eq!(
671 (base, extra & 3),
672 (want_base, want_era),
673 "debugfs words for @{secs}"
674 );
675
676 let mut lookup = |ino: u32| fs.read_inode_verified(ino).map(|(inode, _)| inode);
677 let ino =
678 crate::path::lookup(fs.dev.as_ref(), &fs.sb, &mut lookup, &format!("/t{secs}"))
679 .unwrap();
680 let inode = fs.read_inode_verified(ino).unwrap().0;
681 assert_eq!(inode.mtime, want_secs, "this crate's reading of @{secs}");
682 }
683 drop(fs);
684 let _ = std::fs::remove_file(&image);
685 }
686
687 #[test]
690 fn the_kernel_reads_what_this_crate_stamps() {
691 let image = fs_ext4_test_support::temp_path!(
692 "fs_ext4_inode_times_w_{}.img",
693 std::process::id()
694 );
695 std::fs::File::create(&image)
696 .and_then(|f| f.set_len(8 << 20))
697 .unwrap();
698 let out = fs_ext4_test_support::oracle("mkfs.ext4")
699 .args(["-q", "-F", "-I", "256", &image])
700 .output();
701 assert!(
702 out.status.success(),
703 "{}",
704 String::from_utf8_lossy(&out.stderr)
705 );
706 {
707 let fs = Filesystem::mount(Arc::new(FileDevice::open_rw(&image).unwrap())).unwrap();
708 for &(secs, _, _) in KERNEL_WORDS {
709 let path = format!("/w{secs}");
710 fs.apply_create(&path, 0o644).unwrap();
711 fs.apply_utimens(&path, secs, 0, secs, 0).unwrap();
712 }
713 }
714 let mut script = String::from("set -e\n");
721 for &(secs, _, _) in KERNEL_WORDS {
722 script.push_str(&format!(
723 "printf '%s %s\\n' {secs} \"$(stat -c %Y \"$MNT/w{secs}\")\"\n"
724 ));
725 }
726 let out = fs_ext4_test_support::guest_kernel_write(&image, &script);
727 assert!(
728 out.status.success(),
729 "{}",
730 String::from_utf8_lossy(&out.stderr)
731 );
732 let stdout = String::from_utf8_lossy(&out.stdout);
733 for &(secs, _, _) in KERNEL_WORDS {
734 assert!(
735 stdout.lines().any(|l| l == format!("{secs} {secs}")),
736 "the kernel did not read @{secs} back:\n{stdout}"
737 );
738 }
739 let _ = std::fs::remove_file(&image);
740 }
741 }
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747
748 #[test]
749 fn join16_combines_halves() {
750 assert_eq!(join16(0x0001, 0x0002), 0x0001_0002);
751 assert_eq!(join16(0xFFFF, 0x0000), 0xFFFF_0000);
752 assert_eq!(join16(0x0000, 0xFFFF), 0x0000_FFFF);
753 assert_eq!(join16(0, 0), 0);
754 }
755
756 #[test]
757 fn join32_combines_halves_u16_hi() {
758 assert_eq!(join32(0x0001u16, 0x0000_0002), 0x0000_0001_0000_0002);
759 assert_eq!(join32(0xFFFFu16, 0x0000_0000), 0x0000_FFFF_0000_0000);
760 assert_eq!(join32(0x0000u16, 0xFFFF_FFFF), 0x0000_0000_FFFF_FFFF);
761 }
762
763 #[test]
764 fn join32_combines_halves_u32_hi() {
765 assert_eq!(join32(0x0000_0001u32, 0x0000_0002), 0x0000_0001_0000_0002);
766 assert_eq!(join32(0xFFFF_FFFFu32, 0x0000_0000), 0xFFFF_FFFF_0000_0000);
767 }
768
769 #[test]
770 fn parse_rejects_short_buffer() {
771 let short = vec![0u8; 64];
772 assert!(matches!(
773 Inode::parse(&short),
774 Err(crate::error::Error::Corrupt(_))
775 ));
776 }
777
778 #[test]
779 fn parse_rejects_invalid_extra_isize() {
780 let mut raw = vec![0u8; 160];
782 raw[0x80] = 200; raw[0x81] = 0;
784 assert!(matches!(
785 Inode::parse(&raw),
786 Err(crate::error::Error::Corrupt(_))
787 ));
788 }
789
790 #[test]
791 fn parse_mode_and_links_roundtrip() {
792 let mut raw = vec![0u8; 128];
793 raw[0x00..0x02].copy_from_slice(&0x81A4u16.to_le_bytes()); raw[0x1A..0x1C].copy_from_slice(&3u16.to_le_bytes()); let inode = Inode::parse(&raw).unwrap();
796 assert_eq!(inode.mode, 0x81A4);
797 assert_eq!(inode.links_count, 3);
798 assert!(inode.is_file());
799 }
800}
801
802#[cfg(test)]
805mod set_inode_time_tests {
806 use super::{set_inode_time, Inode, InodeTime, OFF_ATIME, OFF_CRTIME, OFF_EXTRA_ISIZE};
807
808 const PAST_2038: i64 = (1i64 << 31) + 10;
809
810 fn inode(len: usize, extra_isize: u16) -> Vec<u8> {
811 let mut raw = vec![0u8; len];
812 if len >= OFF_EXTRA_ISIZE + 2 {
813 raw[OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2].copy_from_slice(&extra_isize.to_le_bytes());
814 }
815 raw
816 }
817
818 fn le32(raw: &[u8], off: usize) -> u32 {
819 u32::from_le_bytes(raw[off..off + 4].try_into().unwrap())
820 }
821
822 #[test]
823 fn a_large_inode_keeps_the_epoch_bits() {
824 let mut raw = inode(256, 32);
825 for field in [
826 InodeTime::Atime,
827 InodeTime::Ctime,
828 InodeTime::Mtime,
829 InodeTime::Crtime,
830 ] {
831 set_inode_time(&mut raw, field, PAST_2038);
832 }
833 let parsed = Inode::parse(&raw).unwrap();
834 assert_eq!(parsed.atime, PAST_2038);
835 assert_eq!(parsed.ctime, PAST_2038);
836 assert_eq!(parsed.mtime, PAST_2038);
837 assert_eq!(parsed.crtime, PAST_2038);
838 assert_eq!(le32(&raw, OFF_ATIME), 0x8000_000A);
840 assert_eq!(le32(&raw, 0x8C), 1);
841 }
842
843 #[test]
844 fn a_new_stamp_clears_the_old_nanoseconds() {
845 let mut raw = inode(256, 32);
846 raw[0x88..0x8C].copy_from_slice(&(123u32 << 2).to_le_bytes());
847 set_inode_time(&mut raw, InodeTime::Mtime, 1_700_000_000);
848 let parsed = Inode::parse(&raw).unwrap();
849 assert_eq!(parsed.mtime, 1_700_000_000);
850 assert_eq!(parsed.mtime_nsec, 0);
851 }
852
853 #[test]
854 fn a_small_inode_clamps_instead_of_wrapping() {
855 let mut raw = inode(128, 0);
856 set_inode_time(&mut raw, InodeTime::Mtime, PAST_2038);
857 assert_eq!(Inode::parse(&raw).unwrap().mtime, i32::MAX as i64);
858 set_inode_time(&mut raw, InodeTime::Mtime, i32::MIN as i64 - 10);
859 assert_eq!(Inode::parse(&raw).unwrap().mtime, i32::MIN as i64);
860 }
861
862 #[test]
863 fn an_extra_isize_too_small_for_the_field_clamps_it() {
864 let mut raw = inode(256, 12);
866 set_inode_time(&mut raw, InodeTime::Mtime, PAST_2038);
867 set_inode_time(&mut raw, InodeTime::Atime, PAST_2038);
868 let parsed = Inode::parse(&raw).unwrap();
869 assert_eq!(parsed.mtime, PAST_2038);
870 assert_eq!(parsed.atime, i32::MAX as i64);
871 assert_eq!(le32(&raw, 0x8C), 0, "atime_extra is outside i_extra_isize");
872 }
873
874 #[test]
875 fn crtime_is_not_written_where_the_inode_has_no_room_for_it() {
876 let mut raw = inode(128, 0);
877 set_inode_time(&mut raw, InodeTime::Crtime, 1_700_000_000);
878 assert!(raw.iter().all(|&b| b == 0));
879 let mut raw = inode(256, 16);
880 set_inode_time(&mut raw, InodeTime::Crtime, 1_700_000_000);
881 assert_eq!(le32(&raw, OFF_CRTIME), 0);
882 }
883
884 #[test]
885 fn past_the_format_ceiling_is_clamped_to_it() {
886 let mut raw = inode(256, 32);
887 set_inode_time(&mut raw, InodeTime::Mtime, i64::MAX);
888 assert_eq!(Inode::parse(&raw).unwrap().mtime, super::MAX_ENCODABLE_TIME);
889 }
890}