use std::io::Read;
use std::path::{Path, PathBuf};
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::ext::Ext;
use crate::fs::fat::Fat32;
use crate::fs::{DirEntry, Filesystem};
use crate::part::{Gpt, Mbr, Partition, PartitionTable, slice_partition};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsKind {
Ext,
Fat32,
}
pub fn detect_fs(dev: &mut dyn BlockDevice) -> Result<FsKind> {
let mut bs = [0u8; 512];
dev.read_at(0, &mut bs)?;
if bs[510] == 0x55 && bs[511] == 0xAA && &bs[82..87] == b"FAT32" {
return Ok(FsKind::Fat32);
}
let mut sb_magic = [0u8; 2];
dev.read_at(1024 + 56, &mut sb_magic)?;
if sb_magic == [0x53, 0xEF] {
return Ok(FsKind::Ext);
}
Err(crate::Error::InvalidImage(
"inspect: no recognised filesystem (ext2/3/4 or FAT32) on this image".into(),
))
}
pub enum AnyFs {
Ext(Box<Ext>),
Fat32(Box<Fat32>),
}
impl AnyFs {
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
match detect_fs(dev)? {
FsKind::Ext => Ok(Self::Ext(Box::new(Ext::open(dev)?))),
FsKind::Fat32 => Ok(Self::Fat32(Box::new(Fat32::open(dev)?))),
}
}
pub fn kind(&self) -> FsKind {
match self {
Self::Ext(_) => FsKind::Ext,
Self::Fat32(_) => FsKind::Fat32,
}
}
pub fn list(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<Vec<DirEntry>> {
match self {
Self::Ext(ext) => {
let ino = ext.path_to_inode(dev, path)?;
ext.list_inode(dev, ino)
}
Self::Fat32(fat) => fat.list_path(dev, path),
}
}
pub fn copy_file_to(
&self,
dev: &mut dyn BlockDevice,
path: &str,
out: &mut dyn std::io::Write,
) -> Result<u64> {
let mut buf = [0u8; 64 * 1024];
let mut total = 0u64;
match self {
Self::Ext(ext) => {
let ino = ext.path_to_inode(dev, path)?;
let mut reader = ext.open_file_reader(dev, ino)?;
loop {
let n = reader.read(&mut buf).map_err(crate::Error::from)?;
if n == 0 {
break;
}
out.write_all(&buf[..n]).map_err(crate::Error::from)?;
total += n as u64;
}
}
Self::Fat32(fat) => {
let mut reader = fat.open_file_reader(dev, path)?;
loop {
let n = reader.read(&mut buf).map_err(crate::Error::from)?;
if n == 0 {
break;
}
out.write_all(&buf[..n]).map_err(crate::Error::from)?;
total += n as u64;
}
}
}
Ok(total)
}
pub fn add_file(
&mut self,
dev: &mut dyn BlockDevice,
dest_path: &str,
host_src: &Path,
) -> Result<()> {
match self {
Self::Ext(ext) => {
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::symlink_metadata(host_src)?;
let fmeta = crate::fs::FileMeta {
mode: (meta.permissions().mode() & 0o7777) as u16,
..crate::fs::FileMeta::default()
};
let dest = std::path::Path::new(dest_path);
use crate::fs::FileSource;
ext.create_file(
dev,
dest,
FileSource::HostPath(host_src.to_path_buf()),
fmeta,
)
}
Self::Fat32(fat) => fat.add_file(dev, dest_path, host_src),
}
}
pub fn add_dir_tree(
&mut self,
dev: &mut dyn BlockDevice,
dest_path: &str,
host_src: &Path,
) -> Result<()> {
match self {
Self::Ext(ext) => {
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::symlink_metadata(host_src)?;
let fmeta = crate::fs::FileMeta {
mode: (meta.permissions().mode() & 0o7777) as u16,
..crate::fs::FileMeta::default()
};
let dest = std::path::Path::new(dest_path);
ext.create_dir(dev, dest, fmeta)?;
let dir_ino = ext.path_to_inode(dev, dest_path)?;
ext.populate_from_host_dir(dev, dir_ino, host_src)?;
Ok(())
}
Self::Fat32(fat) => {
fat.add_dir(dev, dest_path)?;
add_host_tree_into_fat32(fat, dev, dest_path, host_src)
}
}
}
pub fn mkdir(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<()> {
match self {
Self::Ext(ext) => {
let meta = crate::fs::FileMeta {
mode: 0o755,
..crate::fs::FileMeta::default()
};
ext.create_dir(dev, std::path::Path::new(path), meta)
}
Self::Fat32(fat) => fat.add_dir(dev, path),
}
}
pub fn remove(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<()> {
match self {
Self::Ext(ext) => ext.remove_path(dev, path),
Self::Fat32(fat) => fat.remove(dev, path),
}
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
match self {
Self::Ext(ext) => ext.flush(dev),
Self::Fat32(fat) => fat.flush(dev),
}
}
pub fn kind_string(&self) -> &'static str {
match self {
Self::Ext(ext) => match ext.kind {
crate::fs::ext::FsKind::Ext2 => "ext2",
crate::fs::ext::FsKind::Ext3 => "ext3",
crate::fs::ext::FsKind::Ext4 => "ext4",
},
Self::Fat32(_) => "fat32",
}
}
}
fn add_host_tree_into_fat32(
fat: &mut Fat32,
dev: &mut dyn BlockDevice,
dest_path: &str,
host_src: &Path,
) -> Result<()> {
let mut entries: Vec<std::fs::DirEntry> =
std::fs::read_dir(host_src)?.collect::<std::result::Result<_, _>>()?;
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
let meta = entry.metadata()?;
let name = entry
.file_name()
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 file name".into()))?
.to_string();
let child_dest = if dest_path.ends_with('/') {
format!("{dest_path}{name}")
} else {
format!("{dest_path}/{name}")
};
let ft = meta.file_type();
if ft.is_symlink() {
continue; }
if ft.is_file() {
fat.add_file(dev, &child_dest, &path)?;
} else if ft.is_dir() {
fat.add_dir(dev, &child_dest)?;
add_host_tree_into_fat32(fat, dev, &child_dest, &path)?;
}
}
Ok(())
}
pub fn open_image_file(path: &Path) -> Result<(Box<dyn BlockDevice>, AnyFs)> {
let mut dev = crate::block::open_image(path)?;
let fs = AnyFs::open(dev.as_mut())?;
Ok((dev, fs))
}
#[derive(Debug, Clone)]
pub struct Target {
pub path: PathBuf,
pub partition: Option<usize>,
}
impl Target {
pub fn parse(s: &str) -> Self {
if let Some((head, tail)) = s.rsplit_once(':')
&& let Ok(n) = tail.parse::<usize>()
&& n >= 1
{
return Self {
path: PathBuf::from(head),
partition: Some(n - 1),
};
}
Self {
path: PathBuf::from(s),
partition: None,
}
}
}
pub enum DetectedTable {
Gpt(Box<Gpt>),
Mbr(Box<Mbr>),
}
impl DetectedTable {
pub fn as_table(&self) -> &dyn PartitionTable {
match self {
Self::Gpt(g) => g.as_ref(),
Self::Mbr(m) => m.as_ref(),
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Gpt(_) => "gpt",
Self::Mbr(_) => "mbr",
}
}
pub fn partitions(&self) -> &[Partition] {
self.as_table().partitions()
}
}
pub fn detect_partition_table(dev: &mut dyn BlockDevice) -> Result<Option<DetectedTable>> {
if dev.total_size() < 512 {
return Ok(None);
}
let mut s0 = [0u8; 512];
dev.read_at(0, &mut s0)?;
let is_fat32 = s0[510] == 0x55 && s0[511] == 0xAA && &s0[82..87] == b"FAT32";
if is_fat32 {
return Ok(None);
}
let has_55aa = s0[510] == 0x55 && s0[511] == 0xAA;
if dev.total_size() >= 1024 {
let mut s1_head = [0u8; 8];
dev.read_at(512, &mut s1_head)?;
if &s1_head == b"EFI PART" {
let gpt = Gpt::read(dev)?;
return Ok(Some(DetectedTable::Gpt(Box::new(gpt))));
}
}
if has_55aa {
for i in 0..4 {
let entry_off = 446 + i * 16;
if s0[entry_off + 4] != 0 {
let mbr = Mbr::read(dev)?;
return Ok(Some(DetectedTable::Mbr(Box::new(mbr))));
}
}
}
Ok(None)
}
pub fn with_target_device<F, R>(target: &Target, op: F) -> Result<R>
where
F: FnOnce(&mut dyn BlockDevice) -> Result<R>,
{
let mut disk = crate::block::open_image(&target.path)?;
match target.partition {
None => op(disk.as_mut()),
Some(idx) => {
let table = detect_partition_table(disk.as_mut())?.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"{}: no partition table found, can't target partition {}",
target.path.display(),
idx + 1
))
})?;
let mut slice = slice_partition(table.as_table(), disk.as_mut(), idx)?;
op(&mut slice)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::{FileBackend, MemoryBackend};
use crate::fs::ext::{Ext, FormatOpts};
#[test]
fn detects_ext2_in_memory() {
let opts = FormatOpts::default();
let mut dev = MemoryBackend::new(opts.blocks_count as u64 * opts.block_size as u64);
Ext::format_with(&mut dev, &opts).unwrap();
assert_eq!(detect_fs(&mut dev).unwrap(), FsKind::Ext);
}
#[test]
fn detects_fat32_in_memory() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = crate::fs::fat::FatFormatOpts {
total_sectors: 64 * 1024 * 1024 / 512,
volume_id: 0xCAFE_F00D,
volume_label: *b"DETECTVOL ",
};
crate::fs::fat::Fat32::format(&mut dev, &opts).unwrap();
assert_eq!(detect_fs(&mut dev).unwrap(), FsKind::Fat32);
}
#[test]
fn rejects_random_garbage() {
let mut dev = MemoryBackend::new(64 * 1024);
dev.write_at(0, b"not a filesystem").unwrap();
assert!(detect_fs(&mut dev).is_err());
}
#[test]
fn anyfs_lists_an_ext_image() {
use tempfile::NamedTempFile;
let opts = FormatOpts::default();
let size = opts.blocks_count as u64 * opts.block_size as u64;
let tmp = NamedTempFile::new().unwrap();
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
ext.flush(&mut dev).unwrap();
dev.sync().unwrap();
drop(dev);
let (mut dev, fs) = open_image_file(tmp.path()).unwrap();
assert_eq!(fs.kind(), FsKind::Ext);
let entries = fs.list(dev.as_mut(), "/").unwrap();
assert!(entries.iter().any(|e| e.name == "lost+found"));
}
}