#![forbid(unsafe_code)]
use crate::error::{Ext4Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirEntryType {
Unknown,
RegularFile,
Directory,
CharDevice,
BlockDevice,
Fifo,
Socket,
Symlink,
}
impl From<u8> for DirEntryType {
fn from(v: u8) -> Self {
match v {
1 => Self::RegularFile,
2 => Self::Directory,
3 => Self::CharDevice,
4 => Self::BlockDevice,
5 => Self::Fifo,
6 => Self::Socket,
7 => Self::Symlink,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub inode: u32,
pub rec_len: u16,
pub name: Vec<u8>,
pub file_type: DirEntryType,
}
const HEADER_LEN: usize = 8;
impl DirEntry {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < HEADER_LEN {
return Err(Ext4Error::TooShort {
structure: "DirEntry",
expected: HEADER_LEN,
found: buf.len(),
});
}
let inode = u32::from_le_bytes(buf[0..4].try_into().unwrap());
let rec_len = u16::from_le_bytes(buf[4..6].try_into().unwrap());
let name_len = buf[6] as usize;
let file_type = DirEntryType::from(buf[7]);
let needed = HEADER_LEN + name_len;
if buf.len() < needed {
return Err(Ext4Error::TooShort {
structure: "DirEntry name",
expected: needed,
found: buf.len(),
});
}
let name = buf[HEADER_LEN..HEADER_LEN + name_len].to_vec();
Ok(Self {
inode,
rec_len,
name,
file_type,
})
}
pub fn name_str(&self) -> String {
String::from_utf8_lossy(&self.name).into_owned()
}
pub fn is_deleted(&self) -> bool {
self.inode == 0
}
}
pub fn parse_dir_block(block: &[u8]) -> Vec<DirEntry> {
let mut entries = Vec::new();
let mut offset = 0usize;
while offset < block.len() {
let remaining = &block[offset..];
match DirEntry::parse(remaining) {
Ok(entry) => {
let rec_len = entry.rec_len as usize;
entries.push(entry);
if rec_len == 0 {
break; }
offset += rec_len;
}
Err(_) => break,
}
}
entries
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_dir_entry() {
let mut buf = vec![0u8; 20];
buf[0] = 12; buf[4] = 20; buf[6] = 5; buf[7] = 1; buf[8..13].copy_from_slice(b"hello");
let entry = DirEntry::parse(&buf).unwrap();
assert_eq!(entry.inode, 12);
assert_eq!(entry.rec_len, 20);
assert_eq!(entry.name, b"hello");
assert_eq!(entry.file_type, DirEntryType::RegularFile);
}
#[test]
fn parse_dot_entries() {
let mut buf = vec![0u8; 12];
buf[0] = 2;
buf[4] = 12;
buf[6] = 1;
buf[7] = 2;
buf[8] = b'.';
let entry = DirEntry::parse(&buf).unwrap();
assert_eq!(entry.name, b".");
assert_eq!(entry.file_type, DirEntryType::Directory);
}
#[test]
fn skip_deleted_entry() {
let mut buf = vec![0u8; 12];
buf[0] = 0;
buf[4] = 12;
buf[6] = 3;
buf[7] = 1;
buf[8..11].copy_from_slice(b"foo");
let entry = DirEntry::parse(&buf).unwrap();
assert_eq!(entry.inode, 0);
}
#[test]
fn reject_too_short() {
let buf = vec![0u8; 4];
let err = DirEntry::parse(&buf).unwrap_err();
assert!(matches!(err, crate::error::Ext4Error::TooShort { .. }));
}
#[test]
fn is_deleted_flag() {
let mut buf = vec![0u8; 12];
buf[0] = 0;
buf[4] = 12;
buf[6] = 3;
buf[7] = 1;
buf[8..11].copy_from_slice(b"del");
let entry = DirEntry::parse(&buf).unwrap();
assert!(entry.is_deleted());
}
#[test]
fn name_str_utf8() {
let mut buf = vec![0u8; 13];
buf[0] = 5;
buf[4] = 13;
buf[6] = 5;
buf[7] = 1;
buf[8..13].copy_from_slice(b"world");
let entry = DirEntry::parse(&buf).unwrap();
assert_eq!(entry.name_str(), "world");
}
#[test]
fn parse_dir_block_multiple_entries() {
let mut block = vec![0u8; 24];
block[0] = 1;
block[4] = 12;
block[6] = 1;
block[7] = 2;
block[8] = b'.';
block[12] = 2;
block[16] = 12;
block[18] = 2;
block[19] = 2;
block[20] = b'.';
block[21] = b'.';
let entries = parse_dir_block(&block);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, b".");
assert_eq!(entries[1].name, b"..");
}
#[test]
fn dir_entry_type_from_u8() {
assert_eq!(DirEntryType::from(0), DirEntryType::Unknown);
assert_eq!(DirEntryType::from(1), DirEntryType::RegularFile);
assert_eq!(DirEntryType::from(2), DirEntryType::Directory);
assert_eq!(DirEntryType::from(3), DirEntryType::CharDevice);
assert_eq!(DirEntryType::from(4), DirEntryType::BlockDevice);
assert_eq!(DirEntryType::from(5), DirEntryType::Fifo);
assert_eq!(DirEntryType::from(6), DirEntryType::Socket);
assert_eq!(DirEntryType::from(7), DirEntryType::Symlink);
assert_eq!(DirEntryType::from(255), DirEntryType::Unknown);
}
}