1#![forbid(unsafe_code)]
2use crate::error::{Ext4Error, Result};
3use crate::ondisk::superblock::EXT4_CRC32C;
4use bitflags::bitflags;
5use crc::Crc;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub struct Timestamp {
17 pub seconds: i64,
18 pub nanoseconds: u32,
19}
20
21impl PartialOrd for Timestamp {
22 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
23 Some(self.cmp(other))
24 }
25}
26
27impl Ord for Timestamp {
28 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
29 self.seconds
30 .cmp(&other.seconds)
31 .then(self.nanoseconds.cmp(&other.nanoseconds))
32 }
33}
34
35fn decode_timestamp(secs_raw: u32, extra: Option<u32>) -> Timestamp {
42 match extra {
43 None => Timestamp {
44 seconds: i64::from(secs_raw as i32),
45 nanoseconds: 0,
46 },
47 Some(ex) => {
48 let epoch_bits = i64::from(ex & 0x3);
49 let nanoseconds = ex >> 2;
50 let seconds = ((epoch_bits) << 32) | i64::from(secs_raw);
53 Timestamp {
54 seconds,
55 nanoseconds,
56 }
57 }
58 }
59}
60
61bitflags! {
66 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
68 pub struct InodeFlags: u32 {
69 const SYNC = 0x0000_0010;
70 const IMMUTABLE = 0x0000_0020;
71 const APPEND = 0x0000_0040;
72 const NODUMP = 0x0000_0080;
73 const NOATIME = 0x0000_0100;
74 const ENCRYPT = 0x0000_0800;
75 const INDEX = 0x0000_1000;
76 const HUGE_FILE = 0x0004_0000;
77 const EXTENTS = 0x0008_0000;
78 const EA_INODE = 0x0020_0000;
79 const INLINE_DATA = 0x1000_0000;
80 const CASEFOLD = 0x2000_0000;
81 const VERITY = 0x8000_0000;
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum FileType {
92 Unknown,
93 Fifo,
94 CharDevice,
95 Directory,
96 BlockDevice,
97 RegularFile,
98 Symlink,
99 Socket,
100}
101
102impl FileType {
103 fn from_mode(mode: u16) -> Self {
104 match mode >> 12 {
105 0x1 => FileType::Fifo,
106 0x2 => FileType::CharDevice,
107 0x4 => FileType::Directory,
108 0x6 => FileType::BlockDevice,
109 0x8 => FileType::RegularFile,
110 0xA => FileType::Symlink,
111 0xC => FileType::Socket,
112 _ => FileType::Unknown,
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
123pub struct Inode {
124 pub mode: u16,
126 pub uid: u32,
127 pub gid: u32,
128
129 pub size: u64,
132
133 pub atime: Timestamp,
135 pub ctime: Timestamp,
136 pub mtime: Timestamp,
137 pub dtime: u32,
139 pub crtime: Timestamp,
141
142 pub links_count: u16,
144 pub blocks_count: u64,
146
147 pub flags: InodeFlags,
149
150 pub i_block: [u8; 60],
152
153 pub generation: u32,
155 pub file_acl: u64,
156 pub extra_isize: u16,
157 pub checksum: u32,
159 pub projid: u32,
160}
161
162const INODE_BASE_SIZE: usize = 128;
164
165impl Inode {
166 pub fn parse(buf: &[u8], inode_size: u16) -> Result<Self> {
171 let inode_size = inode_size as usize;
172 let required = INODE_BASE_SIZE.min(inode_size);
173 if buf.len() < required || buf.len() < INODE_BASE_SIZE {
174 return Err(Ext4Error::TooShort {
175 structure: "inode",
176 expected: INODE_BASE_SIZE,
177 found: buf.len(),
178 });
179 }
180
181 let u16_at = |off: usize| -> u16 { u16::from_le_bytes([buf[off], buf[off + 1]]) };
183 let u32_at = |off: usize| -> u32 {
184 u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
185 };
186
187 let mode = u16_at(0x00);
189 let uid_lo = u32::from(u16_at(0x02));
190 let size_lo = u64::from(u32_at(0x04));
191 let atime_raw = u32_at(0x08);
192 let ctime_raw = u32_at(0x0C);
193 let mtime_raw = u32_at(0x10);
194 let dtime = u32_at(0x14);
195 let gid_lo = u32::from(u16_at(0x18));
196 let links_count = u16_at(0x1A);
197 let blocks_lo = u64::from(u32_at(0x1C));
198 let flags_raw = u32_at(0x20);
199
200 let mut i_block = [0u8; 60];
201 i_block.copy_from_slice(&buf[0x28..0x64]);
202
203 let generation = u32_at(0x64);
204 let file_acl_lo = u64::from(u32_at(0x68));
205 let size_hi = u64::from(u32_at(0x6C));
206 let blocks_hi = u64::from(u16_at(0x74));
207 let file_acl_hi = u64::from(u16_at(0x76));
208 let uid_hi = u32::from(u16_at(0x78));
209 let gid_hi = u32::from(u16_at(0x7A));
210 let checksum_lo = u32::from(u16_at(0x7C));
211
212 let (
214 extra_isize,
215 checksum_hi,
216 ctime_extra,
217 mtime_extra,
218 atime_extra,
219 crtime_raw,
220 crtime_extra,
221 projid,
222 ) = if buf.len() > INODE_BASE_SIZE {
223 let extra_isize = u16_at(0x80);
224 let checksum_hi = u32::from(u16_at(0x82));
225 let ext_end = 0x80 + (extra_isize as usize);
229 let (ctime_extra, mtime_extra, atime_extra, crtime_raw, crtime_extra) =
230 if extra_isize >= 28 && buf.len() >= ext_end {
231 (
232 Some(u32_at(0x84)),
233 Some(u32_at(0x88)),
234 Some(u32_at(0x8C)),
235 Some(u32_at(0x90)),
236 Some(u32_at(0x94)),
237 )
238 } else {
239 (None, None, None, None, None)
240 };
241 let projid = if extra_isize >= 32 && buf.len() >= 0xA0 {
243 u32_at(0x9C)
244 } else {
245 0
246 };
247 (
248 extra_isize,
249 checksum_hi,
250 ctime_extra,
251 mtime_extra,
252 atime_extra,
253 crtime_raw,
254 crtime_extra,
255 projid,
256 )
257 } else {
258 (0, 0, None, None, None, None, None, 0)
259 };
260
261 let uid = uid_lo | (uid_hi << 16);
263 let gid = gid_lo | (gid_hi << 16);
264 let size = size_lo | (size_hi << 32);
265 let blocks_count = blocks_lo | (blocks_hi << 32);
266 let file_acl = file_acl_lo | (file_acl_hi << 32);
267 let checksum = checksum_lo | (checksum_hi << 16);
268 let flags = InodeFlags::from_bits_truncate(flags_raw);
269
270 let atime = decode_timestamp(atime_raw, atime_extra);
272 let ctime = decode_timestamp(ctime_raw, ctime_extra);
273 let mtime = decode_timestamp(mtime_raw, mtime_extra);
274 let crtime = match (crtime_raw, crtime_extra) {
275 (Some(secs), extra) => decode_timestamp(secs, extra),
276 _ => Timestamp::default(),
277 };
278
279 Ok(Inode {
280 mode,
281 uid,
282 gid,
283 size,
284 atime,
285 ctime,
286 mtime,
287 dtime,
288 crtime,
289 links_count,
290 blocks_count,
291 flags,
292 i_block,
293 generation,
294 file_acl,
295 extra_isize,
296 checksum,
297 projid,
298 })
299 }
300
301 pub fn file_type(&self) -> FileType {
303 FileType::from_mode(self.mode)
304 }
305
306 pub fn mode_permissions(&self) -> u16 {
308 self.mode & 0x0FFF
309 }
310
311 pub fn uses_extents(&self) -> bool {
313 self.flags.contains(InodeFlags::EXTENTS)
314 }
315
316 pub fn has_inline_data(&self) -> bool {
318 self.flags.contains(InodeFlags::INLINE_DATA)
319 }
320
321 pub fn has_htree(&self) -> bool {
323 self.flags.contains(InodeFlags::INDEX)
324 }
325
326 pub fn is_deleted(&self) -> bool {
334 self.dtime != 0
335 }
336
337 pub fn verify_checksum(
345 &self,
346 raw_buf: &[u8],
347 uuid: &[u8; 16],
348 ino: u32,
349 generation: u32,
350 csum_seed: u32,
351 ) -> bool {
352 let crc32c = Crc::<u32>::new(&EXT4_CRC32C);
353
354 let seed = if csum_seed != 0 {
355 csum_seed
356 } else {
357 let mut d = crc32c.digest();
358 d.update(uuid);
359 d.finalize()
360 };
361
362 let mut digest = crc32c.digest_with_initial(seed.reverse_bits());
363 digest.update(&ino.to_le_bytes());
364 digest.update(&generation.to_le_bytes());
365
366 let mut buf = raw_buf.to_vec();
368 if buf.len() > 0x7E {
370 buf[0x7C] = 0;
371 buf[0x7D] = 0;
372 }
373 if buf.len() > 0x84 {
375 buf[0x82] = 0;
376 buf[0x83] = 0;
377 }
378 digest.update(&buf);
379
380 let computed = digest.finalize();
381 computed == self.checksum
382 }
383
384 pub fn is_orphan(&self) -> bool {
395 self.links_count == 0 && self.dtime == 0 && self.mode != 0
396 }
397}
398
399#[cfg(test)]
404mod tests {
405 use super::*;
406
407 fn make_inode_bytes(mode: u16, size: u32) -> Vec<u8> {
408 let mut buf = vec![0u8; 256];
409 buf[0x00] = (mode & 0xFF) as u8;
410 buf[0x01] = (mode >> 8) as u8;
411 buf[0x04] = (size & 0xFF) as u8;
412 buf[0x05] = ((size >> 8) & 0xFF) as u8;
413 buf[0x06] = ((size >> 16) & 0xFF) as u8;
414 buf[0x07] = ((size >> 24) & 0xFF) as u8;
415 let atime: u32 = 1_700_000_000;
416 buf[0x08..0x0C].copy_from_slice(&atime.to_le_bytes());
417 buf[0x1A] = 1; buf[0x20] = 0x00;
419 buf[0x21] = 0x00;
420 buf[0x22] = 0x08;
421 buf[0x23] = 0x00; buf[0x80] = 32; let crtime: u32 = 1_699_000_000;
424 buf[0x90..0x94].copy_from_slice(&crtime.to_le_bytes());
425 buf
426 }
427
428 #[test]
429 fn parse_regular_file_inode() {
430 let buf = make_inode_bytes(0x8180, 12);
431 let inode = Inode::parse(&buf, 256).unwrap();
432 assert_eq!(inode.file_type(), FileType::RegularFile);
433 assert_eq!(inode.mode_permissions(), 0o600);
434 assert_eq!(inode.size, 12);
435 assert_eq!(inode.links_count, 1);
436 assert!(inode.flags.contains(InodeFlags::EXTENTS));
437 assert_eq!(inode.atime.seconds, 1_700_000_000);
438 assert_eq!(inode.crtime.seconds, 1_699_000_000);
439 }
440
441 #[test]
442 fn parse_directory_inode() {
443 let buf = make_inode_bytes(0x4180, 4096);
444 let inode = Inode::parse(&buf, 256).unwrap();
445 assert_eq!(inode.file_type(), FileType::Directory);
446 }
447
448 #[test]
449 fn timestamp_nanoseconds() {
450 let mut buf = make_inode_bytes(0x8180, 100);
451 let extra = 500_000_000u32 << 2;
452 buf[0x8C..0x90].copy_from_slice(&extra.to_le_bytes());
453 let inode = Inode::parse(&buf, 256).unwrap();
454 assert_eq!(inode.atime.nanoseconds, 500_000_000);
455 }
456
457 #[test]
458 fn dtime_set_means_deleted() {
459 let mut buf = make_inode_bytes(0x8180, 100);
460 let dtime: u32 = 1_700_001_000;
461 buf[0x14..0x18].copy_from_slice(&dtime.to_le_bytes());
462 buf[0x1A] = 0;
463 let inode = Inode::parse(&buf, 256).unwrap();
464 assert_eq!(inode.dtime, 1_700_001_000);
465 assert_eq!(inode.links_count, 0);
466 }
467
468 #[test]
469 fn i_block_60_bytes() {
470 let mut buf = make_inode_bytes(0x8180, 100);
471 for i in 0..60 {
472 buf[0x28 + i] = i as u8;
473 }
474 let inode = Inode::parse(&buf, 256).unwrap();
475 assert_eq!(inode.i_block.len(), 60);
476 assert_eq!(inode.i_block[0], 0);
477 assert_eq!(inode.i_block[59], 59);
478 }
479
480 #[test]
481 fn verify_inode_checksum_on_forensic_img() {
482 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
483 let data = if let Ok(d) = std::fs::read(path) {
484 d
485 } else {
486 eprintln!("skip: forensic.img not found");
487 return;
488 };
489 use crate::ondisk::superblock::Superblock;
490 let sb = Superblock::parse(&data[1024..]).unwrap();
491 assert!(
492 sb.has_metadata_csum(),
493 "forensic.img should have metadata_csum"
494 );
495
496 let inode_size = sb.inode_size;
497 let inodes_per_group = sb.inodes_per_group;
498
499 let desc_size = sb.desc_size;
501 let gdt_offset = sb.block_size as usize;
502 use crate::ondisk::group_desc::GroupDescriptor;
503 let gd = GroupDescriptor::parse(
504 &data[gdt_offset..gdt_offset + desc_size as usize],
505 desc_size,
506 )
507 .unwrap();
508
509 let ino: u32 = 12;
511 let index = (ino - 1) % inodes_per_group;
512 let byte_offset =
513 gd.inode_table * u64::from(sb.block_size) + u64::from(index) * u64::from(inode_size);
514 let raw = &data[byte_offset as usize..(byte_offset + u64::from(inode_size)) as usize];
515 let inode = Inode::parse(raw, inode_size).unwrap();
516
517 assert!(
518 inode.verify_checksum(raw, &sb.uuid, ino, inode.generation, sb.checksum_seed),
519 "inode 12 checksum should verify"
520 );
521 }
522
523 #[test]
524 fn inode_predicate_uses_extents() {
525 let buf = make_inode_bytes(0x8180, 100);
526 let inode = Inode::parse(&buf, 256).unwrap();
527 assert!(inode.uses_extents());
528 }
529
530 #[test]
531 fn inode_predicate_has_inline_data() {
532 let mut buf = make_inode_bytes(0x8180, 100);
533 let flags = 0x1000_0000u32;
535 buf[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
536 let inode = Inode::parse(&buf, 256).unwrap();
537 assert!(inode.has_inline_data());
538 assert!(!inode.uses_extents());
539 }
540
541 #[test]
542 fn inode_predicate_has_htree() {
543 let mut buf = make_inode_bytes(0x4180, 4096); let flags = 0x1000u32;
546 buf[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
547 let inode = Inode::parse(&buf, 256).unwrap();
548 assert!(inode.has_htree());
549 }
550
551 #[test]
552 fn inode_predicate_is_deleted() {
553 let mut buf = make_inode_bytes(0x8180, 100);
554 buf[0x14..0x18].copy_from_slice(&1000u32.to_le_bytes()); let inode = Inode::parse(&buf, 256).unwrap();
556 assert!(inode.is_deleted());
557 assert!(!inode.is_orphan());
558 }
559
560 #[test]
561 fn inode_predicate_is_orphan() {
562 let mut buf = make_inode_bytes(0x8180, 100);
563 buf[0x1A..0x1C].copy_from_slice(&0u16.to_le_bytes()); let inode = Inode::parse(&buf, 256).unwrap();
566 assert!(inode.is_orphan());
567 assert!(!inode.is_deleted());
568 }
569
570 #[test]
571 fn reject_too_short() {
572 let buf = vec![0u8; 50];
573 let err = Inode::parse(&buf, 256).unwrap_err();
574 assert!(matches!(err, crate::error::Ext4Error::TooShort { .. }));
575 }
576}