use crate::error::{Error, Result};
pub const INODE_BASE_SIZE: usize = 128;
pub const GOOD_OLD_INODE_SIZE: usize = 128;
pub const INODE_EXTRA_OFFSET: usize = 128;
pub(crate) const OFF_MODE: usize = 0x00;
pub(crate) const OFF_SIZE_LO: usize = 0x04;
pub(crate) const OFF_ATIME: usize = 0x08;
pub(crate) const OFF_CTIME: usize = 0x0C;
pub(crate) const OFF_MTIME: usize = 0x10;
pub(crate) const OFF_LINKS_COUNT: usize = 0x1A;
pub(crate) const OFF_BLOCKS_LO: usize = 0x1C;
pub(crate) const OFF_FLAGS: usize = 0x20;
pub(crate) const OFF_BLOCK: usize = 0x28; pub(crate) const OFF_GENERATION: usize = 0x64;
pub(crate) const OFF_SIZE_HI: usize = 0x6C;
pub(crate) const OFF_BLOCKS_HI: usize = 0x74;
pub(crate) const OFF_CHECKSUM_LO: usize = 0x7C;
pub(crate) const OFF_EXTRA_ISIZE: usize = 0x80;
pub(crate) const OFF_CHECKSUM_HI: usize = 0x82;
pub(crate) const OFF_CRTIME: usize = 0x90;
pub(crate) const EXTRA_ISIZE_DEFAULT: u16 = 32;
pub(crate) const INODE_SIZE_WITH_CRTIME: usize = 0x94;
pub(crate) const INODE_SIZE_WITH_EXTRA: usize = 0x84;
pub const S_IFMT: u16 = 0xF000;
pub const S_IFREG: u16 = 0x8000;
pub const S_IFDIR: u16 = 0x4000;
pub const S_IFLNK: u16 = 0xA000;
pub const S_IFBLK: u16 = 0x6000;
pub const S_IFCHR: u16 = 0x2000;
pub const S_IFIFO: u16 = 0x1000;
pub const S_IFSOCK: u16 = 0xC000;
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InodeFlags: u32 {
const SECRM = 0x0000_0001;
const UNRM = 0x0000_0002;
const COMPR = 0x0000_0004;
const SYNC = 0x0000_0008;
const IMMUTABLE = 0x0000_0010;
const APPEND = 0x0000_0020;
const NODUMP = 0x0000_0040;
const NOATIME = 0x0000_0080;
const INDEX = 0x0000_1000;
const EA_INODE = 0x0020_0000;
const EXTENTS = 0x0008_0000;
const HUGE_FILE = 0x0004_0000;
const INLINE_DATA = 0x1000_0000;
const EXTENT = 0x0008_0000;
const EXTRA_ATIME = 0x0000_0100;
}
}
#[derive(Debug, Clone)]
pub struct Inode {
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub atime: i64,
pub mtime: i64,
pub ctime: i64,
pub dtime: i64,
pub crtime: i64,
pub atime_nsec: u32,
pub mtime_nsec: u32,
pub ctime_nsec: u32,
pub crtime_nsec: u32,
pub links_count: u16,
pub blocks: u64, pub flags: u32,
pub block: [u8; 60],
pub generation: u32,
pub file_acl: u64,
pub checksum: u32,
}
fn decode_extra_time(base: u32, extra: u32) -> i64 {
let secs = base as i32 as i64;
let epoch_bits = (extra & EXT4_EPOCH_MASK) as i64;
secs + (epoch_bits << 32)
}
const EXT4_EPOCH_MASK: u32 = 0x3;
pub(crate) fn encode_extra_time(secs: i64) -> (u32, u32) {
let base = secs as u32;
let epoch = (((secs - (secs as i32 as i64)) >> 32) as u32) & EXT4_EPOCH_MASK;
(base, epoch)
}
pub(crate) const MIN_ENCODABLE_TIME: i64 = i32::MIN as i64;
pub(crate) const MAX_ENCODABLE_TIME: i64 = i32::MAX as i64 + (3i64 << 32);
impl Inode {
pub fn parse(raw: &[u8]) -> Result<Self> {
if raw.len() < INODE_BASE_SIZE {
return Err(Error::Corrupt("inode buffer too small"));
}
let mode = u16::from_le_bytes(raw[OFF_MODE..OFF_MODE + 2].try_into().unwrap());
let uid_lo = u16::from_le_bytes(raw[0x02..0x04].try_into().unwrap());
let size_lo = u32::from_le_bytes(raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].try_into().unwrap());
let atime_base = u32::from_le_bytes(raw[OFF_ATIME..OFF_ATIME + 4].try_into().unwrap());
let ctime_base = u32::from_le_bytes(raw[OFF_CTIME..OFF_CTIME + 4].try_into().unwrap());
let mtime_base = u32::from_le_bytes(raw[OFF_MTIME..OFF_MTIME + 4].try_into().unwrap());
let dtime = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
let gid_lo = u16::from_le_bytes(raw[0x18..0x1A].try_into().unwrap());
let links_count = u16::from_le_bytes(
raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2]
.try_into()
.unwrap(),
);
let blocks_lo =
u32::from_le_bytes(raw[OFF_BLOCKS_LO..OFF_BLOCKS_LO + 4].try_into().unwrap());
let flags = u32::from_le_bytes(raw[OFF_FLAGS..OFF_FLAGS + 4].try_into().unwrap());
let mut block = [0u8; 60];
block.copy_from_slice(&raw[OFF_BLOCK..OFF_BLOCK + 60]);
let generation =
u32::from_le_bytes(raw[OFF_GENERATION..OFF_GENERATION + 4].try_into().unwrap());
let file_acl_lo = u32::from_le_bytes(raw[0x68..0x6C].try_into().unwrap());
let size_hi = u32::from_le_bytes(raw[OFF_SIZE_HI..OFF_SIZE_HI + 4].try_into().unwrap());
let blocks_hi =
u16::from_le_bytes(raw[OFF_BLOCKS_HI..OFF_BLOCKS_HI + 2].try_into().unwrap());
let file_acl_hi = u16::from_le_bytes(raw[0x76..0x78].try_into().unwrap());
let uid_hi = u16::from_le_bytes(raw[0x78..0x7A].try_into().unwrap());
let gid_hi = u16::from_le_bytes(raw[0x7A..0x7C].try_into().unwrap());
let checksum_lo = u16::from_le_bytes(
raw[OFF_CHECKSUM_LO..OFF_CHECKSUM_LO + 2]
.try_into()
.unwrap(),
);
let mut atime_nsec = 0u32;
let mut mtime_nsec = 0u32;
let mut ctime_nsec = 0u32;
let mut crtime_nsec = 0u32;
let mut crtime_base = 0u32;
let mut atime_extra = 0u32;
let mut mtime_extra = 0u32;
let mut ctime_extra = 0u32;
let mut crtime_extra = 0u32;
let mut checksum_hi = 0u16;
if raw.len() >= INODE_EXTRA_OFFSET + 4 {
let i_extra_isize = u16::from_le_bytes(
raw[OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2]
.try_into()
.unwrap(),
);
let extra_end = INODE_EXTRA_OFFSET + i_extra_isize as usize;
if extra_end > raw.len() {
return Err(Error::Corrupt("i_extra_isize exceeds inode size"));
}
if i_extra_isize >= 4 {
checksum_hi = u16::from_le_bytes(
raw[OFF_CHECKSUM_HI..OFF_CHECKSUM_HI + 2]
.try_into()
.unwrap(),
);
}
if i_extra_isize >= 8 {
let extra = u32::from_le_bytes(raw[0x84..0x88].try_into().unwrap());
ctime_nsec = extra >> 2;
ctime_extra = extra;
}
if i_extra_isize >= 12 {
let extra = u32::from_le_bytes(raw[0x88..0x8C].try_into().unwrap());
mtime_nsec = extra >> 2;
mtime_extra = extra;
}
if i_extra_isize >= 16 {
let extra = u32::from_le_bytes(raw[0x8C..0x90].try_into().unwrap());
atime_nsec = extra >> 2;
atime_extra = extra;
}
if i_extra_isize >= 20 {
crtime_base =
u32::from_le_bytes(raw[OFF_CRTIME..OFF_CRTIME + 4].try_into().unwrap());
}
if i_extra_isize >= 24 {
let extra = u32::from_le_bytes(raw[0x94..0x98].try_into().unwrap());
crtime_nsec = extra >> 2;
crtime_extra = extra;
}
}
Ok(Self {
mode,
uid: join16(uid_hi, uid_lo),
gid: join16(gid_hi, gid_lo),
size: join32(size_hi, size_lo),
atime: decode_extra_time(atime_base, atime_extra),
mtime: decode_extra_time(mtime_base, mtime_extra),
ctime: decode_extra_time(ctime_base, ctime_extra),
dtime: dtime as i32 as i64,
crtime: decode_extra_time(crtime_base, crtime_extra),
atime_nsec,
mtime_nsec,
ctime_nsec,
crtime_nsec,
links_count,
blocks: join32(blocks_hi, blocks_lo),
flags,
block,
generation,
file_acl: join32(file_acl_hi, file_acl_lo),
checksum: join16(checksum_hi, checksum_lo),
})
}
pub fn file_type(&self) -> u16 {
self.mode & S_IFMT
}
pub fn is_dir(&self) -> bool {
self.file_type() == S_IFDIR
}
pub fn is_file(&self) -> bool {
self.file_type() == S_IFREG
}
pub fn is_symlink(&self) -> bool {
self.file_type() == S_IFLNK
}
pub fn has_extents(&self) -> bool {
self.flags & InodeFlags::EXTENTS.bits() != 0
}
pub fn has_inline_data(&self) -> bool {
self.flags & InodeFlags::INLINE_DATA.bits() != 0
}
pub fn flag_set(&self) -> InodeFlags {
InodeFlags::from_bits_truncate(self.flags)
}
}
#[inline]
fn join16(hi: u16, lo: u16) -> u32 {
((hi as u32) << 16) | lo as u32
}
#[inline]
fn join32<H: Into<u64>>(hi: H, lo: u32) -> u64 {
(hi.into() << 32) | lo as u64
}
#[cfg(test)]
mod timestamp_tests {
use super::decode_extra_time;
#[test]
fn without_an_extra_field_the_base_is_used_as_is() {
assert_eq!(decode_extra_time(0, 0), 0);
assert_eq!(decode_extra_time(946_684_800, 0), 946_684_800);
}
#[test]
fn a_pre_1970_timestamp_stays_negative() {
assert_eq!(decode_extra_time(0xFFFF_FFFF, 0), -1);
assert_eq!(decode_extra_time(0x8000_0000, 0), i32::MIN as i64);
}
#[test]
fn the_epoch_bits_extend_the_range_past_2038() {
assert_eq!(decode_extra_time(0, 0b01), 1i64 << 32);
assert_eq!(decode_extra_time(0, 0b10), 2i64 << 32);
assert_eq!(decode_extra_time(0, 0b11), 3i64 << 32);
}
#[test]
fn the_nanosecond_bits_do_not_affect_the_seconds() {
let nsec_only = 0xFFFF_FFFCu32;
assert_eq!(
decode_extra_time(1_000, nsec_only),
1_000,
"nanoseconds must not be added to the seconds"
);
}
use super::encode_extra_time as encode;
#[test]
fn a_date_in_2100_decodes_correctly() {
const SECS_2100: i64 = 4_102_444_800;
let (base, epoch) = encode(SECS_2100);
assert_eq!(epoch, 1, "2100 needs the epoch extension");
assert_eq!(
decode_extra_time(base, epoch),
SECS_2100,
"a date in 2100 must not come back 136 years early"
);
}
#[test]
fn timestamps_round_trip_across_the_2038_boundary() {
for secs in [
i32::MIN as i64, -1, 0, 946_684_800, i32::MAX as i64, i32::MAX as i64 + 1, 4_102_444_800, (1i64 << 33) + 12_345, ] {
let (base, epoch) = encode(secs);
assert_eq!(
decode_extra_time(base, epoch),
secs,
"round trip for {secs}"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join16_combines_halves() {
assert_eq!(join16(0x0001, 0x0002), 0x0001_0002);
assert_eq!(join16(0xFFFF, 0x0000), 0xFFFF_0000);
assert_eq!(join16(0x0000, 0xFFFF), 0x0000_FFFF);
assert_eq!(join16(0, 0), 0);
}
#[test]
fn join32_combines_halves_u16_hi() {
assert_eq!(join32(0x0001u16, 0x0000_0002), 0x0000_0001_0000_0002);
assert_eq!(join32(0xFFFFu16, 0x0000_0000), 0x0000_FFFF_0000_0000);
assert_eq!(join32(0x0000u16, 0xFFFF_FFFF), 0x0000_0000_FFFF_FFFF);
}
#[test]
fn join32_combines_halves_u32_hi() {
assert_eq!(join32(0x0000_0001u32, 0x0000_0002), 0x0000_0001_0000_0002);
assert_eq!(join32(0xFFFF_FFFFu32, 0x0000_0000), 0xFFFF_FFFF_0000_0000);
}
#[test]
fn parse_rejects_short_buffer() {
let short = vec![0u8; 64];
assert!(matches!(
Inode::parse(&short),
Err(crate::error::Error::Corrupt(_))
));
}
#[test]
fn parse_rejects_invalid_extra_isize() {
let mut raw = vec![0u8; 160];
raw[0x80] = 200; raw[0x81] = 0;
assert!(matches!(
Inode::parse(&raw),
Err(crate::error::Error::Corrupt(_))
));
}
#[test]
fn parse_mode_and_links_roundtrip() {
let mut raw = vec![0u8; 128];
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();
assert_eq!(inode.mode, 0x81A4);
assert_eq!(inode.links_count, 3);
assert!(inode.is_file());
}
}