use crate::chunk::{DiskKey, DISK_KEY_SIZE};
use crate::error::{Error, Result};
use crate::inode::{FileType, INODE_ITEM_KEY};
use crate::superblock::{le16, le64};
pub const DIR_ITEM_KEY: u8 = 84;
pub const DIR_INDEX_KEY: u8 = 96;
pub const XATTR_ITEM_KEY: u8 = 24;
pub const DIR_ITEM_HEADER_SIZE: usize = DISK_KEY_SIZE + 8 + 2 + 2 + 1;
pub const MAX_NAME_LEN: usize = 255;
pub mod offsets {
pub const LOCATION: usize = 0;
pub const TRANSID: usize = 17;
pub const DATA_LEN: usize = 25;
pub const NAME_LEN: usize = 27;
pub const TYPE: usize = 29;
pub const NAME: usize = 30;
}
pub mod ftype {
pub const UNKNOWN: u8 = 0;
pub const REG_FILE: u8 = 1;
pub const DIR: u8 = 2;
pub const CHRDEV: u8 = 3;
pub const BLKDEV: u8 = 4;
pub const FIFO: u8 = 5;
pub const SOCK: u8 = 6;
pub const SYMLINK: u8 = 7;
pub const XATTR: u8 = 8;
}
pub fn ftype_from_raw(raw: u8) -> Result<Option<FileType>> {
Ok(Some(match raw {
ftype::UNKNOWN | ftype::XATTR => return Ok(None),
ftype::REG_FILE => FileType::Regular,
ftype::DIR => FileType::Directory,
ftype::CHRDEV => FileType::CharDevice,
ftype::BLKDEV => FileType::BlockDevice,
ftype::FIFO => FileType::Fifo,
ftype::SOCK => FileType::Socket,
ftype::SYMLINK => FileType::Symlink,
other => {
return Err(Error::BadSuperblock(format!(
"directory entry type {other} is not a defined value"
)))
}
}))
}
const NAME_HASH_SEED: u32 = !1u32;
pub fn name_hash(name: &[u8]) -> u64 {
u64::from(!crc32c::crc32c_append(!NAME_HASH_SEED, name))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirEntry {
pub name: Vec<u8>,
pub ino: u64,
pub ftype: Option<FileType>,
pub location_type: u8,
pub transid: u64,
}
impl DirEntry {
pub fn is_inode(&self) -> bool {
self.location_type == INODE_ITEM_KEY
}
}
pub(crate) struct RawItem<'a> {
pub(crate) location: DiskKey,
pub(crate) transid: u64,
pub(crate) ftype: u8,
pub(crate) name: &'a [u8],
pub(crate) value: &'a [u8],
}
pub(crate) fn parse_items<'a>(data: &'a [u8], what: &str) -> Result<Vec<RawItem<'a>>> {
use offsets as o;
let mut out: Vec<RawItem<'a>> = Vec::new();
let mut pos = 0usize;
while pos < data.len() {
let rest = &data[pos..];
if rest.len() < DIR_ITEM_HEADER_SIZE {
return Err(Error::BadSuperblock(format!(
"{what} {} is truncated: {} bytes left, need {DIR_ITEM_HEADER_SIZE} \
for a header",
out.len(),
rest.len()
)));
}
let location = DiskKey::parse(&rest[o::LOCATION..])?;
let data_len = usize::from(le16(rest, o::DATA_LEN));
let name_len = usize::from(le16(rest, o::NAME_LEN));
if name_len == 0 {
return Err(Error::BadSuperblock(format!(
"{what} {} has an empty name",
out.len()
)));
}
if name_len > MAX_NAME_LEN {
return Err(Error::BadSuperblock(format!(
"{what} {} has a {name_len}-byte name, past the {MAX_NAME_LEN}-byte limit",
out.len()
)));
}
let name_end = o::NAME + name_len;
let end = name_end.checked_add(data_len).ok_or_else(|| {
Error::BadSuperblock(format!("{what} {} has lengths that overflow", out.len()))
})?;
if end > rest.len() {
return Err(Error::BadSuperblock(format!(
"{what} {} needs {end} bytes but only {} remain",
out.len(),
rest.len()
)));
}
out.push(RawItem {
location,
transid: le64(rest, o::TRANSID),
ftype: rest[o::TYPE],
name: &rest[o::NAME..name_end],
value: &rest[name_end..end],
});
pos += end;
}
debug_assert_eq!(pos, data.len(), "every record is checked to fit");
Ok(out)
}
pub fn parse_dir_items(data: &[u8]) -> Result<Vec<DirEntry>> {
parse_items(data, "directory item")?
.into_iter()
.map(|r| {
Ok(DirEntry {
name: r.name.to_vec(),
ino: r.location.objectid,
ftype: ftype_from_raw(r.ftype)?,
location_type: r.location.key_type,
transid: r.transid,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn encode(name: &[u8], ino: u64, ft: u8, loc_type: u8, value: &[u8]) -> Vec<u8> {
let mut b = vec![0u8; DIR_ITEM_HEADER_SIZE];
b[0..8].copy_from_slice(&ino.to_le_bytes());
b[8] = loc_type;
b[9..17].copy_from_slice(&0u64.to_le_bytes());
b[offsets::TRANSID..offsets::TRANSID + 8].copy_from_slice(&42u64.to_le_bytes());
b[offsets::DATA_LEN..offsets::DATA_LEN + 2]
.copy_from_slice(&(value.len() as u16).to_le_bytes());
b[offsets::NAME_LEN..offsets::NAME_LEN + 2]
.copy_from_slice(&(name.len() as u16).to_le_bytes());
b[offsets::TYPE] = ft;
b.extend_from_slice(name);
b.extend_from_slice(value);
b
}
fn one(name: &[u8], ino: u64, ft: u8) -> Vec<u8> {
encode(name, ino, ft, INODE_ITEM_KEY, &[])
}
#[test]
fn parses_a_single_entry() {
let entries = parse_dir_items(&one(b"hello.txt", 257, ftype::REG_FILE)).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, b"hello.txt");
assert_eq!(entries[0].ino, 257);
assert_eq!(entries[0].ftype, Some(FileType::Regular));
assert_eq!(entries[0].transid, 42);
assert!(entries[0].is_inode());
}
#[test]
fn parses_several_entries_packed_into_one_item() {
let mut data = one(b"a", 300, ftype::REG_FILE);
data.extend(one(b"bb", 301, ftype::DIR));
data.extend(one(b"ccc", 302, ftype::SYMLINK));
let entries = parse_dir_items(&data).unwrap();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].name, b"a");
assert_eq!(entries[1].name, b"bb");
assert_eq!(entries[2].name, b"ccc");
assert_eq!(entries[1].ftype, Some(FileType::Directory));
assert_eq!(entries[2].ino, 302);
}
#[test]
fn leftover_bytes_after_the_records_are_refused() {
let good = one(b"hello.txt", 257, ftype::REG_FILE);
for leftover in 1..=DIR_ITEM_HEADER_SIZE + 12 {
let mut data = good.clone();
data.extend(std::iter::repeat_n(0xA5u8, leftover));
assert!(
parse_items(&data, "dir item").is_err(),
"{leftover} leftover bytes were accepted"
);
let mut zeros = good.clone();
zeros.extend(std::iter::repeat_n(0u8, leftover));
assert!(
parse_items(&zeros, "dir item").is_err(),
"{leftover} leftover zero bytes were accepted"
);
}
}
#[test]
fn a_trailing_value_is_skipped_by_exactly_its_length() {
let mut data = encode(b"user.thing", 0, ftype::XATTR, 0, b"the value");
data.extend(one(b"after", 400, ftype::REG_FILE));
let entries = parse_dir_items(&data).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, b"user.thing");
assert_eq!(entries[0].ftype, None, "an xattr is not a file type");
assert_eq!(entries[1].name, b"after");
}
#[test]
fn an_entry_naming_a_subvolume_is_flagged_as_such() {
let entries = parse_dir_items(&encode(b"sub", 260, ftype::DIR, 132, &[])).unwrap();
assert!(!entries[0].is_inode());
assert_eq!(entries[0].location_type, 132);
}
#[test]
fn empty_data_is_an_empty_list() {
assert!(parse_dir_items(&[]).unwrap().is_empty());
}
#[test]
fn rejects_a_truncated_header() {
let data = vec![0u8; DIR_ITEM_HEADER_SIZE - 1];
assert!(matches!(
parse_dir_items(&data),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_a_name_running_past_the_end() {
let mut data = one(b"name", 300, ftype::REG_FILE);
data[offsets::NAME_LEN..offsets::NAME_LEN + 2].copy_from_slice(&200u16.to_le_bytes());
assert!(matches!(
parse_dir_items(&data),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_an_empty_name() {
let mut data = one(b"name", 300, ftype::REG_FILE);
data[offsets::NAME_LEN..offsets::NAME_LEN + 2].copy_from_slice(&0u16.to_le_bytes());
assert!(matches!(
parse_dir_items(&data),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_a_name_past_the_format_limit() {
let mut data = one(b"name", 300, ftype::REG_FILE);
data[offsets::NAME_LEN..offsets::NAME_LEN + 2]
.copy_from_slice(&((MAX_NAME_LEN + 1) as u16).to_le_bytes());
data.resize(DIR_ITEM_HEADER_SIZE + MAX_NAME_LEN + 1, b'x');
match parse_dir_items(&data) {
Err(Error::BadSuperblock(m)) => assert!(m.contains("255"), "message: {m}"),
other => panic!("expected a refusal, got {other:?}"),
}
}
#[test]
fn rejects_data_the_entries_do_not_exactly_consume() {
let mut data = one(b"name", 300, ftype::REG_FILE);
data.push(0);
match parse_dir_items(&data) {
Err(Error::BadSuperblock(m)) => {
assert!(
m.contains('1') && m.contains(&DIR_ITEM_HEADER_SIZE.to_string()),
"message should name the leftover byte count and the header size: {m}"
);
}
other => panic!("expected a refusal, got {other:?}"),
}
}
#[test]
fn rejects_an_undefined_type_byte() {
let data = one(b"name", 300, 99);
assert!(matches!(
parse_dir_items(&data),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn decodes_every_defined_type_byte() {
for (raw, want) in [
(ftype::REG_FILE, Some(FileType::Regular)),
(ftype::DIR, Some(FileType::Directory)),
(ftype::CHRDEV, Some(FileType::CharDevice)),
(ftype::BLKDEV, Some(FileType::BlockDevice)),
(ftype::FIFO, Some(FileType::Fifo)),
(ftype::SOCK, Some(FileType::Socket)),
(ftype::SYMLINK, Some(FileType::Symlink)),
(ftype::UNKNOWN, None),
(ftype::XATTR, None),
] {
assert_eq!(ftype_from_raw(raw).unwrap(), want, "raw type {raw}");
}
}
#[test]
fn name_hash_matches_an_independently_computed_vector() {
assert_eq!(name_hash(b"hello"), 0x5d9f_2b1f);
assert_eq!(name_hash(b"f1.txt"), 0xb0c1_9af9);
assert_eq!(name_hash(b"many"), 0xb870_7cbf);
assert_eq!(name_hash(b"."), 0xd32c_1e54);
assert_eq!(name_hash(b".."), 0x1aa9_502b);
assert_eq!(name_hash(b""), u64::from(NAME_HASH_SEED));
}
#[test]
fn the_header_arithmetic_closes() {
assert_eq!(DIR_ITEM_HEADER_SIZE, 30);
assert_eq!(offsets::NAME, DIR_ITEM_HEADER_SIZE);
assert_eq!(offsets::TRANSID, DISK_KEY_SIZE);
assert_eq!(offsets::TYPE + 1, offsets::NAME);
}
}