use super::constants::DENT_DIR;
pub const DX_HASH_LEGACY: u8 = 0;
pub const DX_HASH_HALF_MD4: u8 = 1;
pub const DX_HASH_TEA: u8 = 2;
pub const DX_HASH_LEGACY_UNSIGNED: u8 = 3;
pub const DX_HASH_HALF_MD4_UNSIGNED: u8 = 4;
pub const DX_HASH_TEA_UNSIGNED: u8 = 5;
pub const DX_HASH_SIPHASH: u8 = 6;
pub const DX_TAIL_LEN: usize = 8;
pub const DX_ENTRY_LEN: usize = 8;
pub const DX_ROOT_HEADER_LEN: usize = 32;
pub const DX_NODE_HEADER_LEN: usize = 8;
#[derive(Debug, Clone, Copy)]
pub struct DxEntry {
pub hash: u32,
pub block: u32,
}
pub fn dx_root_limit(block_size: u32, csum_tail: bool) -> usize {
let mut entry_space = block_size as usize - DX_ROOT_HEADER_LEN;
if csum_tail {
entry_space -= DX_TAIL_LEN;
}
entry_space / DX_ENTRY_LEN
}
pub fn dx_node_limit(block_size: u32, csum_tail: bool) -> usize {
let mut entry_space = block_size as usize - DX_NODE_HEADER_LEN;
if csum_tail {
entry_space -= DX_TAIL_LEN;
}
entry_space / DX_ENTRY_LEN
}
pub fn leaf_max_entries(_block_size: u32) -> usize {
usize::MAX
}
pub fn half_md4_hash(name: &[u8]) -> (u32, u32) {
let mut buf: [u32; 4] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476];
let mut remaining = name;
let mut in_buf = [0u32; 8];
while !remaining.is_empty() {
str2hashbuf_unsigned(remaining, &mut in_buf, 8);
half_md4_transform(&mut buf, &in_buf);
if remaining.len() <= 32 {
break;
}
remaining = &remaining[32..];
}
let mut hash = buf[1] & !1; if hash == 0xFFFF_FFFE {
hash = 0xFFFF_FFFC;
}
(hash, buf[2])
}
fn str2hashbuf_unsigned(msg: &[u8], out: &mut [u32; 8], num: usize) {
let mut len = msg.len();
let len_byte = (len & 0xff) as u32;
let pad = len_byte | (len_byte << 8) | (len_byte << 16) | (len_byte << 24);
let mut val = pad;
let mut written = 0usize;
let mut num_remaining = num;
if len > num * 4 {
len = num * 4;
}
for (i, &b) in msg.iter().take(len).enumerate() {
val = (b as u32).wrapping_add(val.wrapping_shl(8));
if i % 4 == 3 {
out[written] = val;
written += 1;
val = pad;
num_remaining -= 1;
}
}
if num_remaining > 0 {
out[written] = val;
written += 1;
num_remaining -= 1;
}
while num_remaining > 0 {
out[written] = pad;
written += 1;
num_remaining -= 1;
}
}
fn half_md4_transform(buf: &mut [u32; 4], i: &[u32; 8]) {
let (mut a, mut b, mut c, mut d) = (buf[0], buf[1], buf[2], buf[3]);
a = round_f(a, b, c, d, i[0], 3);
d = round_f(d, a, b, c, i[1], 7);
c = round_f(c, d, a, b, i[2], 11);
b = round_f(b, c, d, a, i[3], 19);
a = round_f(a, b, c, d, i[4], 3);
d = round_f(d, a, b, c, i[5], 7);
c = round_f(c, d, a, b, i[6], 11);
b = round_f(b, c, d, a, i[7], 19);
a = round_g(a, b, c, d, i[1], 3);
d = round_g(d, a, b, c, i[3], 5);
c = round_g(c, d, a, b, i[5], 9);
b = round_g(b, c, d, a, i[7], 13);
a = round_g(a, b, c, d, i[0], 3);
d = round_g(d, a, b, c, i[2], 5);
c = round_g(c, d, a, b, i[4], 9);
b = round_g(b, c, d, a, i[6], 13);
a = round_h(a, b, c, d, i[3], 3);
d = round_h(d, a, b, c, i[7], 9);
c = round_h(c, d, a, b, i[2], 11);
b = round_h(b, c, d, a, i[6], 15);
a = round_h(a, b, c, d, i[1], 3);
d = round_h(d, a, b, c, i[5], 9);
c = round_h(c, d, a, b, i[0], 11);
b = round_h(b, c, d, a, i[4], 15);
buf[0] = buf[0].wrapping_add(a);
buf[1] = buf[1].wrapping_add(b);
buf[2] = buf[2].wrapping_add(c);
buf[3] = buf[3].wrapping_add(d);
}
#[inline]
fn round_f(a: u32, b: u32, c: u32, d: u32, x: u32, s: u32) -> u32 {
let f = (b & c) | ((!b) & d);
a.wrapping_add(f).wrapping_add(x).rotate_left(s)
}
#[inline]
fn round_g(a: u32, b: u32, c: u32, d: u32, x: u32, s: u32) -> u32 {
let g = (b & c) | (b & d) | (c & d);
const K2: u32 = 0x5a827999;
a.wrapping_add(g)
.wrapping_add(x)
.wrapping_add(K2)
.rotate_left(s)
}
#[inline]
fn round_h(a: u32, b: u32, c: u32, d: u32, x: u32, s: u32) -> u32 {
let h = b ^ c ^ d;
const K3: u32 = 0x6ed9eba1;
a.wrapping_add(h)
.wrapping_add(x)
.wrapping_add(K3)
.rotate_left(s)
}
#[allow(clippy::too_many_arguments)] pub fn make_dx_root_block(
self_inode: u32,
parent_inode: u32,
block_size: u32,
hash_version: u8,
indirect_levels: u8,
entries: &[DxEntry],
with_filetype: bool,
csum_tail: bool,
) -> Vec<u8> {
assert!(
!entries.is_empty(),
"dx_root must have at least the countlimit slot"
);
let mut buf = vec![0u8; block_size as usize];
write_fake_dirent(&mut buf[0..12], self_inode, b".", with_filetype);
let _ = csum_tail; let dotdot_rec_len = (block_size as usize - 12) as u16;
let mut dotdot = vec![0u8; 12];
dotdot[0..4].copy_from_slice(&parent_inode.to_le_bytes());
dotdot[4..6].copy_from_slice(&dotdot_rec_len.to_le_bytes());
if with_filetype {
dotdot[6] = 2; dotdot[7] = DENT_DIR;
} else {
dotdot[6..8].copy_from_slice(&2u16.to_le_bytes());
}
dotdot[8] = b'.';
dotdot[9] = b'.';
buf[12..24].copy_from_slice(&dotdot);
buf[24..28].copy_from_slice(&0u32.to_le_bytes());
buf[28] = hash_version;
buf[29] = 8;
buf[30] = indirect_levels;
buf[31] = 0;
for (i, e) in entries.iter().enumerate() {
let off = DX_ROOT_HEADER_LEN + i * DX_ENTRY_LEN;
buf[off..off + 4].copy_from_slice(&e.hash.to_le_bytes());
buf[off + 4..off + 8].copy_from_slice(&e.block.to_le_bytes());
}
if csum_tail {
let tail_off = block_size as usize - DX_TAIL_LEN;
for b in buf[tail_off..].iter_mut() {
*b = 0;
}
}
buf
}
pub fn make_dx_node_block(block_size: u32, entries: &[DxEntry], csum_tail: bool) -> Vec<u8> {
assert!(
!entries.is_empty(),
"dx_node must have at least the countlimit slot"
);
let mut buf = vec![0u8; block_size as usize];
buf[0..4].copy_from_slice(&0u32.to_le_bytes()); buf[4..6].copy_from_slice(&(block_size as u16).to_le_bytes()); buf[6] = 0; buf[7] = 0; for (i, e) in entries.iter().enumerate() {
let off = DX_NODE_HEADER_LEN + i * DX_ENTRY_LEN;
buf[off..off + 4].copy_from_slice(&e.hash.to_le_bytes());
buf[off + 4..off + 8].copy_from_slice(&e.block.to_le_bytes());
}
if csum_tail {
let tail_off = block_size as usize - DX_TAIL_LEN;
for b in buf[tail_off..].iter_mut() {
*b = 0;
}
}
buf
}
pub fn pack_countlimit(limit: u16, count: u16) -> u32 {
(limit as u32) | ((count as u32) << 16)
}
pub fn compute_dx_csum(
raw_update: impl Fn(u32, &[u8]) -> u32,
seed: u32,
inode_num: u32,
inode_generation: u32,
block: &[u8],
count_offset: usize,
count: usize,
) -> u32 {
let c = raw_update(seed, &inode_num.to_le_bytes());
let c = raw_update(c, &inode_generation.to_le_bytes());
let used_len = count_offset + count * DX_ENTRY_LEN;
let c = raw_update(c, &block[..used_len]);
let c = raw_update(c, &0u32.to_le_bytes());
raw_update(c, &0u32.to_le_bytes())
}
pub fn stamp_dx_csum(block: &mut [u8], csum: u32) {
let n = block.len();
block[n - 4..].copy_from_slice(&csum.to_le_bytes());
}
fn write_fake_dirent(dst: &mut [u8], inode: u32, name: &[u8], with_filetype: bool) {
assert!(dst.len() >= 12, "fake dirent slot must be 12 bytes");
dst[0..4].copy_from_slice(&inode.to_le_bytes());
dst[4..6].copy_from_slice(&12u16.to_le_bytes());
if with_filetype {
dst[6] = name.len() as u8;
dst[7] = DENT_DIR;
} else {
dst[6..8].copy_from_slice(&(name.len() as u16).to_le_bytes());
}
dst[8..8 + name.len()].copy_from_slice(name);
for b in dst[8 + name.len()..12].iter_mut() {
*b = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dx_root_header_layout() {
let entries = vec![DxEntry {
hash: pack_countlimit(508, 1),
block: 1,
}];
let buf = make_dx_root_block(
12,
2,
4096,
DX_HASH_HALF_MD4_UNSIGNED,
0,
&entries,
true,
false,
);
assert_eq!(u32::from_le_bytes(buf[0..4].try_into().unwrap()), 12);
assert_eq!(u16::from_le_bytes(buf[4..6].try_into().unwrap()), 12);
assert_eq!(buf[6], 1); assert_eq!(buf[7], DENT_DIR);
assert_eq!(buf[8], b'.');
assert_eq!(u32::from_le_bytes(buf[12..16].try_into().unwrap()), 2);
assert_eq!(
u16::from_le_bytes(buf[16..18].try_into().unwrap()),
(4096 - 12) as u16
);
assert_eq!(u32::from_le_bytes(buf[24..28].try_into().unwrap()), 0);
assert_eq!(buf[28], DX_HASH_HALF_MD4_UNSIGNED);
assert_eq!(buf[29], 8); assert_eq!(buf[30], 0); let cl_hash = u32::from_le_bytes(buf[32..36].try_into().unwrap());
assert_eq!(cl_hash & 0xffff, 508);
assert_eq!(cl_hash >> 16, 1);
assert_eq!(u32::from_le_bytes(buf[36..40].try_into().unwrap()), 1);
}
#[test]
fn dx_root_limit_metadata_csum() {
assert_eq!(dx_root_limit(4096, false), (4096 - 32) / 8);
assert_eq!(dx_root_limit(4096, true), (4096 - 32 - 8) / 8);
assert_eq!(dx_root_limit(1024, true), (1024 - 32 - 8) / 8);
}
#[test]
fn half_md4_empty_name() {
let (h, _) = half_md4_hash(b"");
assert_eq!(h & 1, 0);
}
#[test]
fn half_md4_different_names_different_hashes() {
let (h1, _) = half_md4_hash(b"foo");
let (h2, _) = half_md4_hash(b"bar");
let (h3, _) = half_md4_hash(b"foobar");
assert_ne!(h1, h2);
assert_ne!(h1, h3);
assert_ne!(h2, h3);
}
#[test]
fn half_md4_low_bit_always_clear() {
for n in 0u32..32 {
let name = format!("name{n:04}");
let (h, _) = half_md4_hash(name.as_bytes());
assert_eq!(h & 1, 0, "hash {h:#x} of {name:?} has low bit set");
}
}
#[test]
fn half_md4_eof_sentinel_remapped() {
for n in 0u32..2000 {
let name = format!("entry_{n}");
let (h, _) = half_md4_hash(name.as_bytes());
assert_ne!(h, 0xFFFF_FFFE);
}
}
#[test]
fn countlimit_packing() {
let v = pack_countlimit(508, 2);
assert_eq!(v & 0xffff, 508);
assert_eq!(v >> 16, 2);
}
#[test]
fn dx_node_limit_larger_than_root_limit() {
for &bs in &[1024u32, 2048, 4096] {
for &csum in &[false, true] {
let r = dx_root_limit(bs, csum);
let n = dx_node_limit(bs, csum);
assert!(
n > r,
"dx_node should fit more slots than dx_root at bs={bs} csum={csum}: \
root={r} node={n}"
);
}
}
}
#[test]
fn dx_node_routing_two_level_layout() {
let bs: u32 = 4096;
let root_entries = vec![
DxEntry {
hash: pack_countlimit(dx_root_limit(bs, true) as u16, 2),
block: 1,
},
DxEntry {
hash: 0x50000000,
block: 2,
},
];
let root = make_dx_root_block(2, 2, bs, DX_HASH_HALF_MD4, 1, &root_entries, true, true);
assert_eq!(root[30], 1);
let node0_entries = vec![
DxEntry {
hash: pack_countlimit(dx_node_limit(bs, true) as u16, 3),
block: 3,
},
DxEntry {
hash: 0x20000000,
block: 4,
},
DxEntry {
hash: 0x40000000,
block: 5,
},
];
let node0 = make_dx_node_block(bs, &node0_entries, true);
assert_eq!(u32::from_le_bytes(node0[0..4].try_into().unwrap()), 0);
assert_eq!(
u16::from_le_bytes(node0[4..6].try_into().unwrap()),
bs as u16
);
let node1_entries = vec![
DxEntry {
hash: pack_countlimit(dx_node_limit(bs, true) as u16, 3),
block: 6,
},
DxEntry {
hash: 0x80000000,
block: 7,
},
DxEntry {
hash: 0xC0000000,
block: 8,
},
];
let node1 = make_dx_node_block(bs, &node1_entries, true);
let route = |buf: &[u8], header_len: usize, target: u32| -> u32 {
let cl = u32::from_le_bytes(buf[header_len..header_len + 4].try_into().unwrap());
let count = (cl >> 16) as usize;
let mut chosen = 0usize;
for slot in 1..count {
let off = header_len + slot * 8;
let h = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
if h <= target {
chosen = slot;
} else {
break;
}
}
let block_off = header_len + chosen * 8 + 4;
u32::from_le_bytes(buf[block_off..block_off + 4].try_into().unwrap())
};
assert_eq!(route(&root, DX_ROOT_HEADER_LEN, 0x10000000), 1);
assert_eq!(route(&node0, DX_NODE_HEADER_LEN, 0x10000000), 3);
assert_eq!(route(&root, DX_ROOT_HEADER_LEN, 0x30000000), 1);
assert_eq!(route(&node0, DX_NODE_HEADER_LEN, 0x30000000), 4);
assert_eq!(route(&root, DX_ROOT_HEADER_LEN, 0x50000000), 2);
assert_eq!(route(&node1, DX_NODE_HEADER_LEN, 0x50000000), 6);
assert_eq!(route(&root, DX_ROOT_HEADER_LEN, 0xF0000000), 2);
assert_eq!(route(&node1, DX_NODE_HEADER_LEN, 0xF0000000), 8);
}
#[test]
fn half_md4_matches_libext2fs() {
let cases: &[(&[u8], u32, u32)] = &[
(b"entry_0000", 0x63df8060, 0x0cf0abb8),
(b"entry_0001", 0xb88bbf6c, 0x2dae5e85),
(b"entry_0002", 0xa4283f96, 0x870c4121),
(b"foo", 0x74c657ac, 0x85a8d812),
(b"bar", 0x4caaf2ba, 0x16c15fb9),
(b"baz", 0xee788c74, 0xc5a8743c),
];
for &(name, want_h, want_m) in cases {
let (got_h, got_m) = half_md4_hash(name);
assert_eq!(
got_h,
want_h,
"major hash mismatch for {:?}: got {got_h:#010x}, want {want_h:#010x}",
String::from_utf8_lossy(name)
);
assert_eq!(
got_m,
want_m,
"minor hash mismatch for {:?}: got {got_m:#010x}, want {want_m:#010x}",
String::from_utf8_lossy(name)
);
}
}
}