use std::io::{Read, Seek};
use forensic_vfs::{
Allocation, ByteRun, DirEntry as VfsDirEntry, DirStream, ExtentStream, FileId, FileSystem,
FsKind, FsMeta, MacbTimes, NodeKind, NodeStream, ResidencyKind, RunAlloc, RunFlags, RunInfo,
SectorSizes, SmallHex, StreamId, TimeResolution, TimeSource, TimeStamp, TimeZonePolicy,
VfsError, VfsResult,
};
use crate::error::Ext4Error;
use crate::ondisk::{DirEntryType, FileType, Timestamp};
use crate::Ext4Fs;
const ROOT_INO: u64 = 2;
fn ino_of(id: FileId) -> VfsResult<u64> {
match id {
FileId::ExtInode { ino, .. } => Ok(ino),
other => Err(VfsError::Unsupported {
layer: "ext4 file-id",
scheme: format!("{other:?}"),
}),
}
}
fn require_default_stream(stream: StreamId) -> VfsResult<()> {
match stream {
StreamId::Default => Ok(()),
other => Err(VfsError::Unsupported {
layer: "ext4 stream",
scheme: format!("{other:?}"),
}),
}
}
fn map_err(e: Ext4Error) -> VfsError {
match e {
Ext4Error::Io(source) => VfsError::Io {
op: "ext4 read",
source,
},
Ext4Error::InodeOutOfRange { ino, max } => VfsError::OutOfRange {
what: "ext4 inode",
offset: ino,
len: 1,
bound: max,
},
Ext4Error::BlockOutOfRange { block, max } => VfsError::OutOfRange {
what: "ext4 block",
offset: block,
len: 1,
bound: max,
},
other => VfsError::Decode {
layer: "ext4",
offset: 0,
detail: other.to_string(),
bytes: SmallHex::new(&[]),
},
}
}
fn node_kind(ft: FileType) -> NodeKind {
match ft {
FileType::RegularFile => NodeKind::File,
FileType::Directory => NodeKind::Dir,
FileType::Symlink => NodeKind::Symlink,
FileType::CharDevice | FileType::BlockDevice => NodeKind::Device,
FileType::Fifo | FileType::Socket | FileType::Unknown => NodeKind::Other,
}
}
fn dirent_kind(dt: DirEntryType) -> NodeKind {
match dt {
DirEntryType::RegularFile => NodeKind::File,
DirEntryType::Directory => NodeKind::Dir,
DirEntryType::Symlink => NodeKind::Symlink,
DirEntryType::CharDevice | DirEntryType::BlockDevice => NodeKind::Device,
DirEntryType::Fifo | DirEntryType::Socket | DirEntryType::Unknown => NodeKind::Other,
}
}
impl<R: Read + Seek + Send> FileSystem for Ext4Fs<R> {
fn kind(&self) -> FsKind {
FsKind::EXT
}
fn root(&self) -> FileId {
let generation = self
.dir_reader
.inode_reader()
.read_inode(ROOT_INO)
.map_or(0, |i| i.generation);
FileId::ExtInode {
ino: ROOT_INO,
gen: generation,
}
}
fn sector_sizes(&self) -> SectorSizes {
let block_size = self.superblock().block_size;
SectorSizes {
logical: 512,
physical: 512,
cluster_or_block: block_size,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let dir_ino = ino_of(ino)?;
let entries = self.dir_reader.read_dir(dir_ino).map_err(map_err)?;
let out: Vec<VfsResult<VfsDirEntry>> = entries
.into_iter()
.map(|e| {
Ok(VfsDirEntry {
name: e.name,
id: FileId::ExtInode {
ino: u64::from(e.inode),
gen: 0,
},
kind: dirent_kind(e.file_type),
})
})
.collect();
Ok(DirStream::new(out.into_iter()))
}
fn extents(&self, ino: FileId, stream: StreamId) -> VfsResult<ExtentStream> {
let inode = ino_of(ino)?;
require_default_stream(stream)?;
let block_size = u64::from(self.superblock().block_size);
let map = self
.dir_reader
.inode_reader()
.inode_block_map(inode)
.map_err(map_err)?;
let out: Vec<VfsResult<RunInfo>> = map
.into_iter()
.map(|m| {
Ok(RunInfo {
run: ByteRun {
image_offset: m.physical_block.saturating_mul(block_size),
len: m.length.saturating_mul(block_size),
flags: RunFlags::default(),
},
alloc: RunAlloc::Allocated,
})
})
.collect();
Ok(ExtentStream::new(out.into_iter()))
}
fn lookup(&self, parent: FileId, name: &[u8]) -> VfsResult<Option<FileId>> {
let dir_ino = ino_of(parent)?;
let found = self.dir_reader.lookup(dir_ino, name).map_err(map_err)?;
Ok(found.map(|ino| FileId::ExtInode { ino, gen: 0 }))
}
fn meta(&self, ino: FileId) -> VfsResult<FsMeta> {
let inode_no = ino_of(ino)?;
let ir = self.dir_reader.inode_reader();
let inode = ir.read_inode(inode_no).map_err(map_err)?;
let allocated = if ir.is_inode_allocated(inode_no).map_err(map_err)? {
Allocation::Allocated
} else {
Allocation::Deleted
};
let resolution = if inode.extra_isize >= 28 {
TimeResolution::Nanos
} else {
TimeResolution::Seconds
};
let ts = |t: &Timestamp| TimeStamp {
unix_nanos: i128::from(t.seconds) * 1_000_000_000 + i128::from(t.nanoseconds),
source: TimeSource::InodeTable,
resolution,
};
let born = if inode.crtime.seconds != 0 {
Some(ts(&inode.crtime))
} else {
None
};
let residency = if inode.has_inline_data() {
ResidencyKind::Resident {
inline_len: inode.size.min(60) as u32,
}
} else {
ResidencyKind::NonResident
};
Ok(FsMeta {
ino: inode_no,
kind: node_kind(inode.file_type()),
allocated,
size: inode.size,
nlink: u32::from(inode.links_count),
uid: Some(inode.uid),
gid: Some(inode.gid),
mode: Some(u32::from(inode.mode)),
times: MacbTimes {
modified: Some(ts(&inode.mtime)),
accessed: Some(ts(&inode.atime)),
changed: Some(ts(&inode.ctime)),
born,
},
streams: Vec::new(),
residency,
link_target: None,
})
}
fn read_at(&self, ino: FileId, stream: StreamId, off: u64, buf: &mut [u8]) -> VfsResult<usize> {
let inode = ino_of(ino)?;
require_default_stream(stream)?;
let data = self
.dir_reader
.inode_reader()
.read_inode_data_range(inode, off, buf.len())
.map_err(map_err)?;
let n = data.len().min(buf.len());
buf[..n].copy_from_slice(&data[..n]);
Ok(n)
}
fn read_link(&self, ino: FileId, cap: usize) -> VfsResult<Vec<u8>> {
let inode = ino_of(ino)?;
match self.dir_reader.read_link(inode) {
Ok(mut target) => {
target.truncate(cap);
Ok(target)
}
Err(Ext4Error::NotASymlink(_)) => Ok(Vec::new()),
Err(e) => Err(map_err(e)),
}
}
fn deleted(&self) -> VfsResult<NodeStream> {
Ok(NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<ExtentStream> {
Ok(ExtentStream::empty())
}
}
#[cfg(test)]
mod tests {
use super::{dirent_kind, map_err, node_kind};
use crate::error::Ext4Error;
use crate::ondisk::{dir_entry::DirEntryType, inode::FileType};
use forensic_vfs::{NodeKind, VfsError};
#[test]
fn map_err_io_is_io() {
let e = Ext4Error::Io(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe"));
assert!(matches!(
map_err(e),
VfsError::Io {
op: "ext4 read",
..
}
));
}
fn out_of_range(e: &VfsError) -> Option<(&'static str, u64, u64)> {
if let VfsError::OutOfRange {
what,
offset,
bound,
..
} = e
{
Some((what, *offset, *bound))
} else {
None
}
}
#[test]
fn map_err_inode_range_is_out_of_range() {
let e = Ext4Error::InodeOutOfRange { ino: 999, max: 100 };
assert_eq!(out_of_range(&map_err(e)), Some(("ext4 inode", 999, 100)));
}
#[test]
fn map_err_block_range_is_out_of_range() {
let e = Ext4Error::BlockOutOfRange { block: 50, max: 10 };
assert_eq!(out_of_range(&map_err(e)), Some(("ext4 block", 50, 10)));
}
#[test]
fn out_of_range_helper_rejects_non_range_errors() {
let io = map_err(Ext4Error::Io(std::io::Error::other("x")));
assert_eq!(out_of_range(&io), None);
}
#[test]
fn map_err_other_is_decode() {
let e = Ext4Error::CorruptMetadata {
structure: "inode",
detail: "bad".into(),
};
assert!(matches!(map_err(e), VfsError::Decode { layer: "ext4", .. }));
}
#[test]
fn node_kind_covers_every_file_type() {
assert_eq!(node_kind(FileType::RegularFile), NodeKind::File);
assert_eq!(node_kind(FileType::Directory), NodeKind::Dir);
assert_eq!(node_kind(FileType::Symlink), NodeKind::Symlink);
assert_eq!(node_kind(FileType::CharDevice), NodeKind::Device);
assert_eq!(node_kind(FileType::BlockDevice), NodeKind::Device);
assert_eq!(node_kind(FileType::Fifo), NodeKind::Other);
assert_eq!(node_kind(FileType::Socket), NodeKind::Other);
assert_eq!(node_kind(FileType::Unknown), NodeKind::Other);
}
#[test]
fn dirent_kind_covers_every_dir_entry_type() {
assert_eq!(dirent_kind(DirEntryType::RegularFile), NodeKind::File);
assert_eq!(dirent_kind(DirEntryType::Directory), NodeKind::Dir);
assert_eq!(dirent_kind(DirEntryType::Symlink), NodeKind::Symlink);
assert_eq!(dirent_kind(DirEntryType::CharDevice), NodeKind::Device);
assert_eq!(dirent_kind(DirEntryType::BlockDevice), NodeKind::Device);
assert_eq!(dirent_kind(DirEntryType::Fifo), NodeKind::Other);
assert_eq!(dirent_kind(DirEntryType::Socket), NodeKind::Other);
assert_eq!(dirent_kind(DirEntryType::Unknown), NodeKind::Other);
}
}