1use crate::bytes::Endian;
22use crate::error::UfsError;
23use crate::superblock::{Superblock, UfsVersion};
24
25pub const UFS_NDADDR: usize = 12;
27
28pub const UFS_NIADDR: usize = 3;
31
32pub const UFS2_DINODE_SIZE: usize = 256;
34
35pub const UFS1_DINODE_SIZE: usize = 128;
37
38const U2_MODE: usize = 0;
40const U2_NLINK: usize = 2;
41const U2_UID: usize = 4;
42const U2_GID: usize = 8;
43const U2_SIZE: usize = 16;
44const U2_BLOCKS: usize = 24;
45const U2_ATIME: usize = 32;
46const U2_MTIME: usize = 40;
47const U2_CTIME: usize = 48;
48const U2_BIRTHTIME: usize = 56;
49const U2_MTIMENSEC: usize = 64;
50const U2_ATIMENSEC: usize = 68;
51const U2_CTIMENSEC: usize = 72;
52const U2_BIRTHNSEC: usize = 76;
53const U2_DB: usize = 112;
54const U2_IB: usize = 208;
55
56const U1_MODE: usize = 0;
58const U1_NLINK: usize = 2;
59const U1_SIZE: usize = 8;
60const U1_ATIME: usize = 16;
61const U1_ATIMENSEC: usize = 20;
62const U1_MTIME: usize = 24;
63const U1_MTIMENSEC: usize = 28;
64const U1_CTIME: usize = 32;
65const U1_CTIMENSEC: usize = 36;
66const U1_DB: usize = 40;
67const U1_IB: usize = 88;
68const U1_BLOCKS: usize = 104;
69const U1_UID: usize = 112;
70const U1_GID: usize = 116;
71
72const IFMT: u16 = 0o170_000;
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
79pub struct Timespec {
80 pub sec: i64,
82 pub nsec: i32,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92#[non_exhaustive]
93pub enum FileType {
94 Fifo,
96 CharDevice,
98 Directory,
100 BlockDevice,
102 Regular,
104 Symlink,
106 Socket,
108 Whiteout,
110 Other(u16),
113}
114
115impl FileType {
116 #[must_use]
118 pub fn from_mode(mode: u16) -> Self {
119 match mode & IFMT {
120 0o010_000 => FileType::Fifo,
121 0o020_000 => FileType::CharDevice,
122 0o040_000 => FileType::Directory,
123 0o060_000 => FileType::BlockDevice,
124 0o100_000 => FileType::Regular,
125 0o120_000 => FileType::Symlink,
126 0o140_000 => FileType::Socket,
127 0o160_000 => FileType::Whiteout,
128 other => FileType::Other(other),
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
138#[non_exhaustive]
139pub struct Inode {
140 pub version: UfsVersion,
142 pub mode: u16,
144 pub file_type: FileType,
146 pub nlink: u16,
148 pub uid: u32,
150 pub gid: u32,
152 pub size: u64,
154 pub blocks: u64,
156 pub atime: Timespec,
158 pub mtime: Timespec,
160 pub ctime: Timespec,
162 pub birthtime: Option<Timespec>,
164 pub direct: [u64; UFS_NDADDR],
168 pub indirect: [u64; UFS_NIADDR],
171 fast_symlink: Option<Vec<u8>>,
177}
178
179impl Inode {
180 pub fn parse(data: &[u8], version: UfsVersion, endian: Endian) -> Result<Self, UfsError> {
197 Self::parse_with_maxsymlink(data, version, endian, DEFAULT_MAXSYMLINKLEN)
198 }
199
200 pub fn parse_with_maxsymlink(
208 data: &[u8],
209 version: UfsVersion,
210 endian: Endian,
211 maxsymlinklen: i32,
212 ) -> Result<Self, UfsError> {
213 let need = match version {
214 UfsVersion::Ufs1 => UFS1_DINODE_SIZE,
215 UfsVersion::Ufs2 => UFS2_DINODE_SIZE,
216 };
217 if data.len() < need {
218 return Err(UfsError::Truncated {
219 structure: "dinode",
220 need,
221 have: data.len(),
222 });
223 }
224
225 let (mode, nlink, uid, gid, size, blocks, atime, mtime, ctime, birthtime, db_off, ib_off) =
226 match version {
227 UfsVersion::Ufs2 => (
228 endian.u16(data, U2_MODE),
229 endian.u16(data, U2_NLINK),
230 endian.u32(data, U2_UID),
231 endian.u32(data, U2_GID),
232 endian.u64(data, U2_SIZE),
233 endian.u64(data, U2_BLOCKS),
234 Timespec {
235 sec: endian.i64(data, U2_ATIME),
236 nsec: endian.i32(data, U2_ATIMENSEC),
237 },
238 Timespec {
239 sec: endian.i64(data, U2_MTIME),
240 nsec: endian.i32(data, U2_MTIMENSEC),
241 },
242 Timespec {
243 sec: endian.i64(data, U2_CTIME),
244 nsec: endian.i32(data, U2_CTIMENSEC),
245 },
246 Some(Timespec {
247 sec: endian.i64(data, U2_BIRTHTIME),
248 nsec: endian.i32(data, U2_BIRTHNSEC),
249 }),
250 U2_DB,
251 U2_IB,
252 ),
253 UfsVersion::Ufs1 => (
254 endian.u16(data, U1_MODE),
255 endian.u16(data, U1_NLINK),
256 endian.u32(data, U1_UID),
257 endian.u32(data, U1_GID),
258 endian.u64(data, U1_SIZE),
259 u64::from(endian.u32(data, U1_BLOCKS)),
260 Timespec {
261 sec: i64::from(endian.i32(data, U1_ATIME)),
262 nsec: endian.i32(data, U1_ATIMENSEC),
263 },
264 Timespec {
265 sec: i64::from(endian.i32(data, U1_MTIME)),
266 nsec: endian.i32(data, U1_MTIMENSEC),
267 },
268 Timespec {
269 sec: i64::from(endian.i32(data, U1_CTIME)),
270 nsec: endian.i32(data, U1_CTIMENSEC),
271 },
272 None,
273 U1_DB,
274 U1_IB,
275 ),
276 };
277
278 let ptr_size = match version {
279 UfsVersion::Ufs1 => 4usize,
280 UfsVersion::Ufs2 => 8usize,
281 };
282 let read_ptr = |off: usize| -> u64 {
283 match version {
284 UfsVersion::Ufs1 => u64::from(endian.u32(data, off)),
285 UfsVersion::Ufs2 => endian.u64(data, off),
286 }
287 };
288
289 let mut direct = [0u64; UFS_NDADDR];
290 for (i, slot) in direct.iter_mut().enumerate() {
291 *slot = read_ptr(db_off + i * ptr_size);
292 }
293 let mut indirect = [0u64; UFS_NIADDR];
294 for (i, slot) in indirect.iter_mut().enumerate() {
295 *slot = read_ptr(ib_off + i * ptr_size);
296 }
297
298 let file_type = FileType::from_mode(mode);
299
300 let fast_symlink = if file_type == FileType::Symlink
306 && maxsymlinklen > 0
307 && size <= maxsymlinklen as u64
308 {
309 let region_len = (UFS_NDADDR + UFS_NIADDR) * ptr_size;
310 let take = (size as usize).min(region_len);
311 data.get(db_off..db_off + take).map(<[u8]>::to_vec)
312 } else {
313 None
314 };
315
316 Ok(Self {
317 version,
318 mode,
319 file_type,
320 nlink,
321 uid,
322 gid,
323 size,
324 blocks,
325 atime,
326 mtime,
327 ctime,
328 birthtime,
329 direct,
330 indirect,
331 fast_symlink,
332 })
333 }
334
335 #[must_use]
337 pub fn is_dir(&self) -> bool {
338 self.file_type == FileType::Directory
339 }
340
341 #[must_use]
343 pub fn is_regular(&self) -> bool {
344 self.file_type == FileType::Regular
345 }
346
347 #[must_use]
349 pub fn is_symlink(&self) -> bool {
350 self.file_type == FileType::Symlink
351 }
352
353 #[must_use]
359 pub fn symlink_target(&self) -> Option<&[u8]> {
360 self.fast_symlink.as_deref()
361 }
362}
363
364const DEFAULT_MAXSYMLINKLEN: i32 = ((UFS_NDADDR + UFS_NIADDR) * 8) as i32;
368
369pub fn read_inode(partition: &[u8], sb: &Superblock, ino: u64) -> Result<Inode, UfsError> {
387 if sb.ipg <= 0 {
390 return Err(UfsError::ImpossibleGeometry {
391 field: "fs_ipg",
392 value: sb.ipg as u64,
393 limit: i64::MAX as u64,
394 });
395 }
396 if sb.fpg <= 0 {
397 return Err(UfsError::ImpossibleGeometry {
398 field: "fs_fpg",
399 value: sb.fpg as u64,
400 limit: i64::MAX as u64,
401 });
402 }
403 if sb.fsize <= 0 {
404 return Err(UfsError::ImpossibleGeometry {
405 field: "fs_fsize",
406 value: sb.fsize as u64,
407 limit: i64::MAX as u64,
408 });
409 }
410 if sb.iblkno < 0 {
411 return Err(UfsError::ImpossibleGeometry {
412 field: "fs_iblkno",
413 value: sb.iblkno as u64,
414 limit: i64::MAX as u64,
415 });
416 }
417
418 let ipg = sb.ipg as u64;
419 let fpg = sb.fpg as u64;
420 let fsize = sb.fsize as u64;
421 let iblkno = sb.iblkno as u64;
422 let inode_size = u64::from(sb.inode_size());
423
424 let count = ipg.saturating_mul(u64::from(sb.ncg));
426 if ino >= count {
427 return Err(UfsError::InodeOutOfRange { ino, count });
428 }
429
430 let cg = ino / ipg;
435 let within = ino % ipg;
436 let cgimin = cg.saturating_mul(fpg).saturating_add(iblkno);
437 let byte = cgimin
438 .saturating_mul(fsize)
439 .saturating_add(within.saturating_mul(inode_size));
440
441 let start = usize::try_from(byte).unwrap_or(usize::MAX);
442 let end = start.saturating_add(inode_size as usize);
443 let Some(slice) = partition.get(start..end) else {
444 return Err(UfsError::Truncated {
445 structure: "dinode (located)",
446 need: end,
447 have: partition.len(),
448 });
449 };
450
451 Inode::parse_with_maxsymlink(slice, sb.version, sb.endian, sb.maxsymlinklen)
452}
453
454#[cfg(test)]
455#[allow(clippy::unreadable_literal)]
459mod tests {
460 use super::*;
461
462 fn ufs2_dinode(mode: u16, size: u64, db0: u64) -> Vec<u8> {
466 let mut d = vec![0u8; UFS2_DINODE_SIZE];
467 d[U2_MODE..U2_MODE + 2].copy_from_slice(&mode.to_le_bytes());
468 d[U2_NLINK..U2_NLINK + 2].copy_from_slice(&1u16.to_le_bytes());
469 d[U2_UID..U2_UID + 4].copy_from_slice(&1000u32.to_le_bytes());
470 d[U2_GID..U2_GID + 4].copy_from_slice(&1000u32.to_le_bytes());
471 d[U2_SIZE..U2_SIZE + 8].copy_from_slice(&size.to_le_bytes());
472 d[U2_BLOCKS..U2_BLOCKS + 8].copy_from_slice(&8u64.to_le_bytes());
473 d[U2_MTIME..U2_MTIME + 8].copy_from_slice(&0x1122_3344i64.to_le_bytes());
474 d[U2_MTIMENSEC..U2_MTIMENSEC + 4].copy_from_slice(&500i32.to_le_bytes());
475 d[U2_BIRTHTIME..U2_BIRTHTIME + 8].copy_from_slice(&0x2233i64.to_le_bytes());
476 d[U2_DB..U2_DB + 8].copy_from_slice(&db0.to_le_bytes());
477 d
478 }
479
480 fn ufs1_dinode(mode: u16, size: u64, db0: u32) -> Vec<u8> {
482 let mut d = vec![0u8; UFS1_DINODE_SIZE];
483 d[U1_MODE..U1_MODE + 2].copy_from_slice(&mode.to_le_bytes());
484 d[U1_NLINK..U1_NLINK + 2].copy_from_slice(&2u16.to_le_bytes());
485 d[U1_SIZE..U1_SIZE + 8].copy_from_slice(&size.to_le_bytes());
486 d[U1_MTIME..U1_MTIME + 4].copy_from_slice(&0x0055_6677u32.to_le_bytes());
487 d[U1_MTIMENSEC..U1_MTIMENSEC + 4].copy_from_slice(&7i32.to_le_bytes());
488 d[U1_BLOCKS..U1_BLOCKS + 4].copy_from_slice(&4u32.to_le_bytes());
489 d[U1_UID..U1_UID + 4].copy_from_slice(&501u32.to_le_bytes());
490 d[U1_GID..U1_GID + 4].copy_from_slice(&20u32.to_le_bytes());
491 d[U1_DB..U1_DB + 4].copy_from_slice(&db0.to_le_bytes());
492 d
493 }
494
495 #[test]
496 fn file_type_from_mode_classifies_all_ifmt() {
497 assert_eq!(FileType::from_mode(0o040755), FileType::Directory);
498 assert_eq!(FileType::from_mode(0o100644), FileType::Regular);
499 assert_eq!(FileType::from_mode(0o120777), FileType::Symlink);
500 assert_eq!(FileType::from_mode(0o010000), FileType::Fifo);
501 assert_eq!(FileType::from_mode(0o020000), FileType::CharDevice);
502 assert_eq!(FileType::from_mode(0o060000), FileType::BlockDevice);
503 assert_eq!(FileType::from_mode(0o140000), FileType::Socket);
504 assert_eq!(FileType::from_mode(0o160000), FileType::Whiteout);
505 assert_eq!(FileType::from_mode(0o050000), FileType::Other(0o050000));
507 }
508
509 #[test]
510 fn decodes_ufs2_regular_file() {
511 let d = ufs2_dinode(0o100644, 116, 57);
512 let ino = Inode::parse(&d, UfsVersion::Ufs2, Endian::Little).unwrap();
513 assert_eq!(ino.version, UfsVersion::Ufs2);
514 assert_eq!(ino.file_type, FileType::Regular);
515 assert!(ino.is_regular());
516 assert!(!ino.is_dir());
517 assert_eq!(ino.mode & 0o7777, 0o644);
518 assert_eq!(ino.nlink, 1);
519 assert_eq!(ino.uid, 1000);
520 assert_eq!(ino.gid, 1000);
521 assert_eq!(ino.size, 116);
522 assert_eq!(ino.blocks, 8);
523 assert_eq!(ino.mtime.sec, 0x1122_3344);
524 assert_eq!(ino.mtime.nsec, 500);
525 assert_eq!(
526 ino.birthtime,
527 Some(Timespec {
528 sec: 0x2233,
529 nsec: 0
530 })
531 );
532 assert_eq!(ino.direct[0], 57);
533 assert!(ino.direct[1..].iter().all(|&b| b == 0));
534 assert!(ino.symlink_target().is_none());
535 }
536
537 #[test]
538 fn decodes_ufs2_directory() {
539 let d = ufs2_dinode(0o040755, 512, 56);
540 let ino = Inode::parse(&d, UfsVersion::Ufs2, Endian::Little).unwrap();
541 assert!(ino.is_dir());
542 assert_eq!(ino.direct[0], 56);
543 }
544
545 #[test]
546 fn decodes_ufs2_fast_symlink_inline_target() {
547 let target = b"a_directory/another_file";
548 let mut d = ufs2_dinode(0o120755, target.len() as u64, 0);
549 d[U2_DB..U2_DB + target.len()].copy_from_slice(target);
550 let ino = Inode::parse(&d, UfsVersion::Ufs2, Endian::Little).unwrap();
551 assert_eq!(ino.file_type, FileType::Symlink);
552 assert!(ino.is_symlink());
553 assert_eq!(ino.symlink_target(), Some(&target[..]));
554 }
555
556 #[test]
557 fn slow_symlink_over_threshold_has_no_inline_target() {
558 let d = ufs2_dinode(0o120755, 200, 57);
562 let ino = Inode::parse_with_maxsymlink(&d, UfsVersion::Ufs2, Endian::Little, 120).unwrap();
563 assert_eq!(ino.file_type, FileType::Symlink);
564 assert!(ino.symlink_target().is_none());
565 assert_eq!(ino.direct[0], 57);
567 }
568
569 #[test]
570 fn decodes_ufs2_big_endian() {
571 let mut d = vec![0u8; UFS2_DINODE_SIZE];
572 d[U2_MODE..U2_MODE + 2].copy_from_slice(&0o100644u16.to_be_bytes());
573 d[U2_SIZE..U2_SIZE + 8].copy_from_slice(&999u64.to_be_bytes());
574 d[U2_DB..U2_DB + 8].copy_from_slice(&123u64.to_be_bytes());
575 let ino = Inode::parse(&d, UfsVersion::Ufs2, Endian::Big).unwrap();
576 assert_eq!(ino.file_type, FileType::Regular);
577 assert_eq!(ino.size, 999);
578 assert_eq!(ino.direct[0], 123);
579 }
580
581 #[test]
582 fn decodes_ufs1_dinode_32bit_layout() {
583 let d = ufs1_dinode(0o100600, 4096, 0xdead);
584 let ino = Inode::parse(&d, UfsVersion::Ufs1, Endian::Little).unwrap();
585 assert_eq!(ino.version, UfsVersion::Ufs1);
586 assert_eq!(ino.file_type, FileType::Regular);
587 assert_eq!(ino.mode & 0o7777, 0o600);
588 assert_eq!(ino.nlink, 2);
589 assert_eq!(ino.uid, 501);
590 assert_eq!(ino.gid, 20);
591 assert_eq!(ino.size, 4096);
592 assert_eq!(ino.blocks, 4);
593 assert_eq!(ino.mtime.sec, 0x0055_6677);
594 assert_eq!(ino.mtime.nsec, 7);
595 assert_eq!(ino.birthtime, None, "UFS1 has no birthtime");
596 assert_eq!(ino.direct[0], 0xdead);
597 }
598
599 #[test]
600 fn decodes_ufs1_fast_symlink() {
601 let target = b"../elsewhere";
602 let mut d = ufs1_dinode(0o120777, target.len() as u64, 0);
603 d[U1_DB..U1_DB + target.len()].copy_from_slice(target);
604 let ino = Inode::parse_with_maxsymlink(&d, UfsVersion::Ufs1, Endian::Little, 60).unwrap();
606 assert_eq!(ino.symlink_target(), Some(&target[..]));
607 }
608
609 #[test]
610 fn truncated_dinode_fails_loud_not_panic() {
611 let d = vec![0u8; UFS2_DINODE_SIZE - 1];
612 let err = Inode::parse(&d, UfsVersion::Ufs2, Endian::Little).unwrap_err();
613 assert!(matches!(
614 err,
615 UfsError::Truncated {
616 structure: "dinode",
617 need: UFS2_DINODE_SIZE,
618 ..
619 }
620 ));
621 }
622
623 #[test]
624 fn empty_dinode_buffer_does_not_panic() {
625 assert!(matches!(
626 Inode::parse(&[], UfsVersion::Ufs2, Endian::Little),
627 Err(UfsError::Truncated { .. })
628 ));
629 }
630
631 fn synthetic_partition(ino_to_place: u64, dinode: &[u8]) -> (Vec<u8>, Superblock) {
636 use crate::superblock::{FS_UFS2_MAGIC, SBLOCK_UFS2};
637 let iblkno = 40u64;
640 let fsize = 4096u64;
641 let ipg = 128u64;
642 let fpg = 256u64;
643 let ncg = 4u32;
644 let inode_size = 256u64;
645
646 let cg = ino_to_place / ipg;
647 let within = ino_to_place % ipg;
648 let cgimin = cg * fpg + iblkno;
649 let byte = (cgimin * fsize + within * inode_size) as usize;
650
651 let sboff = SBLOCK_UFS2;
654 let total = (byte + dinode.len()).max(sboff + 1376) + 16;
655 let mut part = vec![0u8; total];
656 part[byte..byte + dinode.len()].copy_from_slice(dinode);
657 let wr32 = |p: &mut [u8], off: usize, v: i32| {
658 p[off..off + 4].copy_from_slice(&v.to_le_bytes());
659 };
660 let wr64 = |p: &mut [u8], off: usize, v: i64| {
661 p[off..off + 8].copy_from_slice(&v.to_le_bytes());
662 };
663 wr32(&mut part, sboff + 8, 24); wr32(&mut part, sboff + 12, 32); wr32(&mut part, sboff + 16, iblkno as i32); wr32(&mut part, sboff + 20, 48); wr32(&mut part, sboff + 44, ncg as i32); wr32(&mut part, sboff + 48, 32768); wr32(&mut part, sboff + 52, fsize as i32); wr32(&mut part, sboff + 56, 8); wr32(&mut part, sboff + 80, 15); wr32(&mut part, sboff + 84, 12); wr32(&mut part, sboff + 120, 128); wr32(&mut part, sboff + 184, ipg as i32); wr32(&mut part, sboff + 188, fpg as i32); wr32(&mut part, sboff + 1320, 120); wr64(&mut part, sboff + 1080, 1022); wr64(&mut part, sboff + 1088, 901); wr64(&mut part, sboff + 1000, SBLOCK_UFS2 as i64); part[sboff + 1372..sboff + 1376].copy_from_slice(&FS_UFS2_MAGIC.to_le_bytes());
681
682 let sb = Superblock::parse(&part[sboff..]).unwrap();
683 (part, sb)
684 }
685
686 #[test]
687 fn read_inode_locates_and_decodes() {
688 let dinode = ufs2_dinode(0o100644, 116, 57);
689 let (part, sb) = synthetic_partition(4, &dinode);
690 let ino = read_inode(&part, &sb, 4).unwrap();
691 assert_eq!(ino.file_type, FileType::Regular);
692 assert_eq!(ino.size, 116);
693 assert_eq!(ino.direct[0], 57);
694 }
695
696 #[test]
697 fn read_inode_rejects_out_of_range() {
698 let (part, sb) = synthetic_partition(4, &ufs2_dinode(0o100644, 1, 1));
699 let err = read_inode(&part, &sb, 512).unwrap_err();
701 assert!(matches!(
702 err,
703 UfsError::InodeOutOfRange {
704 ino: 512,
705 count: 512
706 }
707 ));
708 }
709
710 #[test]
711 fn read_inode_truncated_partition_fails_loud() {
712 let dinode = ufs2_dinode(0o100644, 1, 1);
713 let (mut part, sb) = synthetic_partition(4, &dinode);
714 part.truncate(180_000);
717 let err = read_inode(&part, &sb, 200).unwrap_err();
719 assert!(matches!(err, UfsError::Truncated { .. }));
720 }
721
722 #[test]
723 fn read_inode_rejects_zero_ipg_geometry() {
724 let dinode = ufs2_dinode(0o100644, 1, 1);
725 let (part, mut sb) = synthetic_partition(4, &dinode);
726 sb.ipg = 0;
729 let err = read_inode(&part, &sb, 4).unwrap_err();
730 assert!(matches!(
731 err,
732 UfsError::ImpossibleGeometry {
733 field: "fs_ipg",
734 ..
735 }
736 ));
737 }
738
739 #[test]
740 fn read_inode_rejects_zero_fpg_geometry() {
741 let (part, mut sb) = synthetic_partition(4, &ufs2_dinode(0o100644, 1, 1));
742 sb.fpg = 0;
743 let err = read_inode(&part, &sb, 4).unwrap_err();
744 assert!(matches!(
745 err,
746 UfsError::ImpossibleGeometry {
747 field: "fs_fpg",
748 ..
749 }
750 ));
751 }
752
753 #[test]
754 fn read_inode_rejects_zero_fsize_geometry() {
755 let (part, mut sb) = synthetic_partition(4, &ufs2_dinode(0o100644, 1, 1));
756 sb.fsize = 0;
757 let err = read_inode(&part, &sb, 4).unwrap_err();
758 assert!(matches!(
759 err,
760 UfsError::ImpossibleGeometry {
761 field: "fs_fsize",
762 ..
763 }
764 ));
765 }
766
767 #[test]
768 fn read_inode_rejects_negative_iblkno_geometry() {
769 let (part, mut sb) = synthetic_partition(4, &ufs2_dinode(0o100644, 1, 1));
770 sb.iblkno = -1;
771 let err = read_inode(&part, &sb, 4).unwrap_err();
772 assert!(matches!(
773 err,
774 UfsError::ImpossibleGeometry {
775 field: "fs_iblkno",
776 ..
777 }
778 ));
779 }
780}