use crate::error::{Error, Result};
use crate::superblock::{le32, le64};
pub const INODE_NODATASUM: u64 = 1 << 0;
pub const INODE_NODATACOW: u64 = 1 << 1;
pub const INODE_ITEM_KEY: u8 = 1;
pub const INODE_REF_KEY: u8 = 12;
pub const INODE_ITEM_SIZE: usize = 160;
pub const TIMESPEC_SIZE: usize = 12;
pub const FIRST_FREE_OBJECTID: u64 = 256;
pub mod offsets {
pub const GENERATION: usize = 0;
pub const TRANSID: usize = 8;
pub const SIZE: usize = 16;
pub const NBYTES: usize = 24;
pub const BLOCK_GROUP: usize = 32;
pub const NLINK: usize = 40;
pub const UID: usize = 44;
pub const GID: usize = 48;
pub const MODE: usize = 52;
pub const RDEV: usize = 56;
pub const FLAGS: usize = 64;
pub const SEQUENCE: usize = 72;
pub const RESERVED: usize = 80;
pub const ATIME: usize = 112;
pub const CTIME: usize = 124;
pub const MTIME: usize = 136;
pub const OTIME: usize = 148;
}
const NSEC_PER_SEC: u32 = 1_000_000_000;
const S_IFMT: u32 = 0xF000;
const S_IPERM: u32 = 0o7777;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
Regular,
Directory,
Symlink,
CharDevice,
BlockDevice,
Fifo,
Socket,
}
impl FileType {
pub fn from_mode(mode: u32) -> Option<Self> {
Some(match mode & S_IFMT {
0x8000 => FileType::Regular,
0x4000 => FileType::Directory,
0xA000 => FileType::Symlink,
0x2000 => FileType::CharDevice,
0x6000 => FileType::BlockDevice,
0x1000 => FileType::Fifo,
0xC000 => FileType::Socket,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Timestamp {
pub sec: i64,
pub nsec: u32,
}
impl Timestamp {
fn parse(buf: &[u8], off: usize, what: &str) -> Result<Self> {
let nsec = le32(buf, off + 8);
if nsec >= NSEC_PER_SEC {
return Err(Error::BadSuperblock(format!(
"inode {what} has {nsec} nanoseconds, which is a second or more — \
the timestamp offsets are wrong"
)));
}
Ok(Timestamp {
sec: le64(buf, off) as i64,
nsec,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Inode {
pub ino: u64,
pub generation: u64,
pub transid: u64,
pub size: u64,
pub nbytes: u64,
pub block_group: u64,
pub nlink: u32,
pub uid: u32,
pub gid: u32,
pub mode: u32,
pub rdev: u64,
pub flags: u64,
pub sequence: u64,
pub atime: Timestamp,
pub ctime: Timestamp,
pub mtime: Timestamp,
pub otime: Timestamp,
}
impl Inode {
pub fn parse(data: &[u8], ino: u64) -> Result<Self> {
use offsets as o;
if data.len() < INODE_ITEM_SIZE {
return Err(Error::BadSuperblock(format!(
"inode {ino}: item is {} bytes, too short for the {INODE_ITEM_SIZE}-byte inode item",
data.len()
)));
}
Ok(Inode {
ino,
generation: le64(data, o::GENERATION),
transid: le64(data, o::TRANSID),
size: le64(data, o::SIZE),
nbytes: le64(data, o::NBYTES),
block_group: le64(data, o::BLOCK_GROUP),
nlink: le32(data, o::NLINK),
uid: le32(data, o::UID),
gid: le32(data, o::GID),
mode: le32(data, o::MODE),
rdev: le64(data, o::RDEV),
flags: le64(data, o::FLAGS),
sequence: le64(data, o::SEQUENCE),
atime: Timestamp::parse(data, o::ATIME, "atime")?,
ctime: Timestamp::parse(data, o::CTIME, "ctime")?,
mtime: Timestamp::parse(data, o::MTIME, "mtime")?,
otime: Timestamp::parse(data, o::OTIME, "otime")?,
})
}
pub fn file_type(&self) -> Option<FileType> {
FileType::from_mode(self.mode)
}
pub fn permissions(&self) -> u32 {
self.mode & S_IPERM
}
pub fn is_dir(&self) -> bool {
self.file_type() == Some(FileType::Directory)
}
pub fn is_regular_file(&self) -> bool {
self.file_type() == Some(FileType::Regular)
}
pub fn is_symlink(&self) -> bool {
self.file_type() == Some(FileType::Symlink)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn item(mode: u32) -> Vec<u8> {
let mut b = vec![0u8; INODE_ITEM_SIZE];
let put64 = |b: &mut Vec<u8>, at: usize, v: u64| {
b[at..at + 8].copy_from_slice(&v.to_le_bytes());
};
let put32 = |b: &mut Vec<u8>, at: usize, v: u32| {
b[at..at + 4].copy_from_slice(&v.to_le_bytes());
};
put64(&mut b, offsets::GENERATION, 11);
put64(&mut b, offsets::TRANSID, 22);
put64(&mut b, offsets::SIZE, 33);
put64(&mut b, offsets::NBYTES, 4096);
put64(&mut b, offsets::BLOCK_GROUP, 55);
put32(&mut b, offsets::NLINK, 3);
put32(&mut b, offsets::UID, 1000);
put32(&mut b, offsets::GID, 1001);
put32(&mut b, offsets::MODE, mode);
put64(&mut b, offsets::RDEV, 77);
put64(&mut b, offsets::FLAGS, 0x88);
put64(&mut b, offsets::SEQUENCE, 99);
for (i, off) in [
offsets::ATIME,
offsets::CTIME,
offsets::MTIME,
offsets::OTIME,
]
.into_iter()
.enumerate()
{
put64(&mut b, off, 1_700_000_000 + i as u64);
put32(&mut b, off + 8, 100 + i as u32);
}
b
}
#[test]
fn parses_every_field() {
let inode = Inode::parse(&item(0o100644), 300).unwrap();
assert_eq!(inode.ino, 300);
assert_eq!(inode.generation, 11);
assert_eq!(inode.transid, 22);
assert_eq!(inode.size, 33);
assert_eq!(inode.nbytes, 4096);
assert_eq!(inode.block_group, 55);
assert_eq!(inode.nlink, 3);
assert_eq!(inode.uid, 1000);
assert_eq!(inode.gid, 1001);
assert_eq!(inode.mode, 0o100644);
assert_eq!(inode.rdev, 77);
assert_eq!(inode.flags, 0x88);
assert_eq!(inode.sequence, 99);
}
#[test]
fn timestamps_are_read_from_distinct_slots() {
let inode = Inode::parse(&item(0o100644), 300).unwrap();
assert_eq!(
inode.atime,
Timestamp {
sec: 1_700_000_000,
nsec: 100
}
);
assert_eq!(
inode.ctime,
Timestamp {
sec: 1_700_000_001,
nsec: 101
}
);
assert_eq!(
inode.mtime,
Timestamp {
sec: 1_700_000_002,
nsec: 102
}
);
assert_eq!(
inode.otime,
Timestamp {
sec: 1_700_000_003,
nsec: 103
}
);
}
#[test]
fn decodes_the_seven_file_types() {
for (bits, want) in [
(0o100000u32, FileType::Regular),
(0o040000, FileType::Directory),
(0o120000, FileType::Symlink),
(0o020000, FileType::CharDevice),
(0o060000, FileType::BlockDevice),
(0o010000, FileType::Fifo),
(0o140000, FileType::Socket),
] {
let inode = Inode::parse(&item(bits | 0o644), 300).unwrap();
assert_eq!(inode.file_type(), Some(want), "mode {bits:o}");
assert_eq!(inode.permissions(), 0o644);
}
}
#[test]
fn an_undefined_type_nibble_decodes_to_nothing() {
let inode = Inode::parse(&item(0o030644), 300).unwrap();
assert_eq!(inode.file_type(), None);
assert!(!inode.is_dir() && !inode.is_regular_file() && !inode.is_symlink());
}
#[test]
fn type_predicates_agree_with_the_decoded_type() {
assert!(Inode::parse(&item(0o040755), 256).unwrap().is_dir());
assert!(Inode::parse(&item(0o100644), 257)
.unwrap()
.is_regular_file());
assert!(Inode::parse(&item(0o120777), 258).unwrap().is_symlink());
}
#[test]
fn permissions_keep_the_set_id_and_sticky_bits() {
let inode = Inode::parse(&item(0o104755), 300).unwrap();
assert_eq!(inode.permissions(), 0o4755);
assert_eq!(inode.file_type(), Some(FileType::Regular));
}
#[test]
fn rejects_an_item_too_short_to_be_an_inode() {
let short = vec![0u8; INODE_ITEM_SIZE - 1];
match Inode::parse(&short, 300) {
Err(Error::BadSuperblock(m)) => assert!(m.contains("300"), "message lost the ino: {m}"),
other => panic!("expected a refusal, got {other:?}"),
}
}
#[test]
fn accepts_an_item_longer_than_todays_struct() {
let mut long = item(0o100644);
long.extend_from_slice(&[0xAB; 32]);
assert_eq!(Inode::parse(&long, 300).unwrap().size, 33);
}
#[test]
fn rejects_a_nanosecond_count_of_a_second_or_more() {
for (off, name) in [
(offsets::ATIME, "atime"),
(offsets::CTIME, "ctime"),
(offsets::MTIME, "mtime"),
(offsets::OTIME, "otime"),
] {
let mut b = item(0o100644);
b[off + 8..off + 12].copy_from_slice(&NSEC_PER_SEC.to_le_bytes());
match Inode::parse(&b, 300) {
Err(Error::BadSuperblock(m)) => {
assert!(m.contains(name), "message did not name {name}: {m}")
}
other => panic!("expected a refusal for {name}, got {other:?}"),
}
}
}
#[test]
fn a_second_count_above_the_signed_range_reads_as_a_negative_date() {
let mut b = item(0o100644);
b[offsets::ATIME..offsets::ATIME + 8].copy_from_slice(&(-1i64).to_le_bytes());
assert_eq!(Inode::parse(&b, 300).unwrap().atime.sec, -1);
}
#[test]
fn the_timestamps_end_exactly_at_the_end_of_the_item() {
assert_eq!(offsets::OTIME + TIMESPEC_SIZE, INODE_ITEM_SIZE);
assert_eq!(offsets::CTIME - offsets::ATIME, TIMESPEC_SIZE);
assert_eq!(offsets::MTIME - offsets::CTIME, TIMESPEC_SIZE);
assert_eq!(offsets::OTIME - offsets::MTIME, TIMESPEC_SIZE);
assert_eq!(offsets::ATIME - offsets::RESERVED, 4 * 8);
}
}