use super::error::{DiskError, DiskResult};
use super::{BlockDevice, PartitionInfo};
#[cfg(feature = "disk-image")]
use super::{BlockDeviceSync, PartitionReader};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
Regular,
Directory,
Symlink,
Hardlink,
CharDevice,
BlockDevice,
Fifo,
Socket,
}
#[derive(Debug, Clone)]
pub struct FileMetadata {
pub file_type: FileType,
pub size: u64,
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub atime: i64,
pub mtime: i64,
pub ctime: i64,
pub nlink: u32,
pub inode: u64,
pub device_id: Option<u64>,
pub symlink_target: Option<String>,
}
impl FileMetadata {
pub fn is_file(&self) -> bool {
self.file_type == FileType::Regular
}
pub fn is_dir(&self) -> bool {
self.file_type == FileType::Directory
}
pub fn is_symlink(&self) -> bool {
self.file_type == FileType::Symlink
}
pub fn device_major(&self) -> Option<u32> {
self.device_id.map(|d| (d >> 8) as u32)
}
pub fn device_minor(&self) -> Option<u32> {
self.device_id.map(|d| (d & 0xFF) as u32)
}
}
#[derive(Debug)]
pub struct FilesystemEntry {
pub path: PathBuf,
pub metadata: FileMetadata,
pub data_offset: Option<u64>,
}
pub struct FilesystemTraverser {
fs_type: FilesystemType,
partition: PartitionInfo,
root_inode: u64,
block_size: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilesystemType {
Ext4,
Xfs,
Btrfs,
Fat32,
Unknown,
}
impl FilesystemTraverser {
pub async fn open(device: &dyn BlockDevice, partition: &PartitionInfo) -> DiskResult<Self> {
let fs_type = Self::detect_filesystem(device, partition).await?;
match fs_type {
FilesystemType::Ext4 => Self::open_ext4(device, partition).await,
FilesystemType::Unknown => Err(DiskError::FilesystemError {
partition: partition.number,
reason: "Unknown filesystem type".to_string(),
}),
_ => Err(DiskError::Unsupported {
feature: format!("{:?} filesystem", fs_type),
}),
}
}
async fn detect_filesystem(
device: &dyn BlockDevice,
partition: &PartitionInfo,
) -> DiskResult<FilesystemType> {
let base = partition.start_offset;
let mut superblock = [0u8; 2];
device.read_at(&mut superblock, base + 1024 + 56).await?;
if superblock == [0x53, 0xEF] {
return Ok(FilesystemType::Ext4);
}
let mut xfs_magic = [0u8; 4];
device.read_at(&mut xfs_magic, base).await?;
if &xfs_magic == b"XFSB" {
return Ok(FilesystemType::Xfs);
}
let mut btrfs_magic = [0u8; 8];
device.read_at(&mut btrfs_magic, base + 64).await?;
if &btrfs_magic == b"_BHRfS_M" {
return Ok(FilesystemType::Btrfs);
}
let mut fat_sig = [0u8; 2];
device.read_at(&mut fat_sig, base + 510).await?;
if fat_sig == [0x55, 0xAA] {
let mut fat32_marker = [0u8; 8];
device.read_at(&mut fat32_marker, base + 82).await?;
if &fat32_marker == b"FAT32 " {
return Ok(FilesystemType::Fat32);
}
}
Ok(FilesystemType::Unknown)
}
async fn open_ext4(device: &dyn BlockDevice, partition: &PartitionInfo) -> DiskResult<Self> {
let base = partition.start_offset;
let mut sb = [0u8; 256];
device.read_at(&mut sb, base + 1024).await?;
let block_size = 1024u32 << u32::from_le_bytes(sb[24..28].try_into().unwrap());
let _blocks_count = u32::from_le_bytes(sb[4..8].try_into().unwrap());
let _inodes_count = u32::from_le_bytes(sb[0..4].try_into().unwrap());
let feature_incompat = u32::from_le_bytes(sb[96..100].try_into().unwrap());
let _is_64bit = (feature_incompat & 0x80) != 0;
Ok(Self {
fs_type: FilesystemType::Ext4,
partition: partition.clone(),
root_inode: 2, block_size,
})
}
pub async fn walk<F, Fut>(&self, mut callback: F) -> DiskResult<()>
where
F: FnMut(FilesystemEntry) -> Fut,
Fut: std::future::Future<Output = DiskResult<()>>,
{
match self.fs_type {
FilesystemType::Ext4 => {
let root_entry = FilesystemEntry {
path: PathBuf::from("/"),
metadata: FileMetadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
uid: 0,
gid: 0,
atime: 0,
mtime: 0,
ctime: 0,
nlink: 2,
inode: self.root_inode,
device_id: None,
symlink_target: None,
},
data_offset: None,
};
callback(root_entry).await?;
}
_ => {
return Err(DiskError::Unsupported {
feature: format!("{:?} filesystem traversal", self.fs_type),
});
}
}
Ok(())
}
#[cfg(feature = "disk-image")]
pub fn walk_sync<F>(&self, device: &dyn BlockDeviceSync, mut callback: F) -> DiskResult<()>
where
F: FnMut(FilesystemEntry) -> DiskResult<()>,
{
use std::io::{Read, Seek};
match self.fs_type {
FilesystemType::Ext4 => {
let partition_reader = PartitionReader::new(device, &self.partition);
let superblock = ext4::SuperBlock::new(partition_reader).map_err(|e| {
DiskError::FilesystemError {
partition: self.partition.number,
reason: format!("Failed to open ext4: {}", e),
}
})?;
let root = superblock.root().map_err(|e| DiskError::FilesystemError {
partition: self.partition.number,
reason: format!("Failed to load root inode: {}", e),
})?;
superblock
.walk(&root, "", &mut |fs, path, inode, enhanced| {
let entry = self.inode_to_entry(path, inode, enhanced)?;
callback(entry).map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(true) })
.map_err(|e| DiskError::FilesystemError {
partition: self.partition.number,
reason: format!("Walk failed: {}", e),
})?;
Ok(())
}
_ => Err(DiskError::Unsupported {
feature: format!("{:?} filesystem", self.fs_type),
}),
}
}
#[cfg(feature = "disk-image")]
fn inode_to_entry(
&self,
path: &str,
inode: &ext4::Inode,
enhanced: &ext4::Enhanced,
) -> Result<FilesystemEntry, anyhow::Error> {
let file_type = match inode.stat.extracted_type {
ext4::FileType::RegularFile => FileType::Regular,
ext4::FileType::Directory => FileType::Directory,
ext4::FileType::SymbolicLink => FileType::Symlink,
ext4::FileType::CharacterDevice => FileType::CharDevice,
ext4::FileType::BlockDevice => FileType::BlockDevice,
ext4::FileType::Fifo => FileType::Fifo,
ext4::FileType::Socket => FileType::Socket,
};
let symlink_target = match enhanced {
ext4::Enhanced::SymbolicLink(target) => Some(target.clone()),
_ => None,
};
let device_id = match enhanced {
ext4::Enhanced::CharacterDevice(major, minor) => {
Some(((*major as u64) << 8) | (*minor as u64))
}
ext4::Enhanced::BlockDevice(major, minor) => {
Some(((*major as u64) << 8) | (*minor as u64))
}
_ => None,
};
Ok(FilesystemEntry {
path: PathBuf::from(if path.is_empty() { "/" } else { path }),
metadata: FileMetadata {
file_type,
size: inode.stat.size,
mode: inode.stat.mode,
uid: inode.stat.uid,
gid: inode.stat.gid,
atime: inode.stat.atime.epoch_secs,
mtime: inode.stat.mtime.epoch_secs,
ctime: inode.stat.ctime.epoch_secs,
nlink: inode.stat.nlink,
inode: inode.number as u64,
device_id,
symlink_target,
},
data_offset: None, })
}
pub fn stats(&self) -> FilesystemStats {
FilesystemStats {
fs_type: self.fs_type,
block_size: self.block_size,
partition_size: self.partition.size,
}
}
}
#[derive(Debug)]
pub struct FilesystemStats {
pub fs_type: FilesystemType,
pub block_size: u32,
pub partition_size: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_types() {
let meta = FileMetadata {
file_type: FileType::Regular,
size: 1024,
mode: 0o644,
uid: 0,
gid: 0,
atime: 0,
mtime: 0,
ctime: 0,
nlink: 1,
inode: 1,
device_id: None,
symlink_target: None,
};
assert!(meta.is_file());
assert!(!meta.is_dir());
assert!(!meta.is_symlink());
}
#[test]
fn test_device_numbers() {
let meta = FileMetadata {
file_type: FileType::CharDevice,
size: 0,
mode: 0o666,
uid: 0,
gid: 0,
atime: 0,
mtime: 0,
ctime: 0,
nlink: 1,
inode: 1,
device_id: Some((1 << 8) | 3), symlink_target: None,
};
assert_eq!(meta.device_major(), Some(1));
assert_eq!(meta.device_minor(), Some(3));
}
}