use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
pub mod affs;
pub mod apfs;
pub mod archive;
pub(crate) mod dir_batch;
pub mod exfat;
pub mod ext;
pub mod f2fs;
pub mod fat;
pub mod grf;
pub mod hfs;
pub mod hfs_plus;
pub mod iso9660;
pub mod ntfs;
pub mod rootdevs;
pub mod squashfs;
pub mod tar;
pub mod xfs;
pub use rootdevs::{DeviceEntry, RootDevs};
#[derive(Debug, Clone, Copy)]
pub struct FileMeta {
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub mtime: u32,
pub atime: u32,
pub ctime: u32,
}
impl Default for FileMeta {
fn default() -> Self {
Self {
mode: 0o644,
uid: 0,
gid: 0,
mtime: 0,
atime: 0,
ctime: 0,
}
}
}
impl FileMeta {
pub fn with_mode(mode: u16) -> Self {
Self {
mode,
..Self::default()
}
}
}
pub enum FileSource {
HostPath(PathBuf),
Reader {
reader: Box<dyn ReadSeek + Send>,
len: u64,
},
Zero(u64),
TempFile(tempfile::NamedTempFile),
}
impl FileSource {
pub fn len(&self) -> io::Result<u64> {
match self {
FileSource::HostPath(p) => Ok(std::fs::metadata(p)?.len()),
FileSource::Reader { len, .. } => Ok(*len),
FileSource::Zero(n) => Ok(*n),
FileSource::TempFile(t) => Ok(t.as_file().metadata()?.len()),
}
}
pub fn is_empty(&self) -> io::Result<bool> {
self.len().map(|n| n == 0)
}
pub fn open(self) -> io::Result<(Box<dyn ReadSeek + Send>, u64)> {
match self {
FileSource::HostPath(p) => {
let f = File::open(&p)?;
let len = f.metadata()?.len();
Ok((Box::new(f), len))
}
FileSource::Reader { reader, len } => Ok((reader, len)),
FileSource::Zero(n) => Ok((Box::new(ZeroReader { remaining: n }), n)),
FileSource::TempFile(t) => {
let len = t.as_file().metadata()?.len();
let f = File::open(t.path())?;
Ok((Box::new(f), len))
}
}
}
}
pub trait ReadSeek: Read + Seek {}
impl<T: Read + Seek> ReadSeek for T {}
#[derive(Debug, Clone, Copy, Default)]
pub struct OpenFlags {
pub create: bool,
pub truncate: bool,
pub append: bool,
}
pub trait FileHandle: Read + Write + Seek {
fn len(&self) -> u64;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn set_len(&mut self, new_len: u64) -> crate::Result<()>;
fn sync(&mut self) -> crate::Result<()>;
}
pub trait FileReadHandle: Read + Seek {
fn len(&self) -> u64;
fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceKind {
Char,
Block,
Fifo,
Socket,
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub name: String,
pub inode: u32,
pub kind: EntryKind,
pub size: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct FileAttrs {
pub kind: EntryKind,
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub blocks: u64,
pub nlink: u32,
pub atime: u32,
pub mtime: u32,
pub ctime: u32,
pub rdev: u32,
pub inode: u32,
}
impl FileAttrs {
fn defaults_for(kind: EntryKind, size: u64, inode: u32) -> Self {
let mode = match kind {
EntryKind::Dir => 0o755,
EntryKind::Symlink => 0o777,
_ => 0o644,
};
let nlink = match kind {
EntryKind::Dir => 2,
_ => 1,
};
Self {
kind,
mode,
uid: 0,
gid: 0,
size,
blocks: size.div_ceil(512),
nlink,
atime: 0,
mtime: 0,
ctime: 0,
rdev: 0,
inode,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SetAttrs {
pub mode: Option<u16>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub atime: Option<u32>,
pub mtime: Option<u32>,
pub ctime: Option<u32>,
}
#[derive(Debug, Clone, Copy)]
pub struct StatFs {
pub block_size: u32,
pub blocks: u64,
pub blocks_free: u64,
pub blocks_avail: u64,
pub inodes: u64,
pub inodes_free: u64,
pub name_max: u32,
}
impl Default for StatFs {
fn default() -> Self {
Self {
block_size: 4096,
blocks: 0,
blocks_free: 0,
blocks_avail: 0,
inodes: 0,
inodes_free: 0,
name_max: 255,
}
}
}
#[derive(Debug, Clone)]
pub struct XattrPair {
pub name: String,
pub value: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MutationCapability {
Mutable,
WholeFileOnly,
Streaming,
Immutable,
}
impl MutationCapability {
pub fn supports_add_remove(self) -> bool {
matches!(self, Self::Mutable | Self::WholeFileOnly)
}
pub fn supports_partial_writes(self) -> bool {
matches!(self, Self::Mutable)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CloneCapability {
None,
WholeFile,
Range,
}
impl CloneCapability {
pub fn shares_extents(self) -> bool {
!matches!(self, Self::None)
}
pub fn supports_range(self) -> bool {
matches!(self, Self::Range)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryKind {
Regular,
Dir,
Symlink,
Char,
Block,
Fifo,
Socket,
Unknown,
}
pub trait Filesystem {
fn create_file(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
path: &Path,
src: FileSource,
meta: FileMeta,
) -> crate::Result<()>;
fn streams_immediately(&self) -> bool {
false
}
fn create_file_streaming(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
path: &Path,
body: &mut dyn Read,
len: u64,
meta: FileMeta,
) -> crate::Result<()> {
const MEM_CAP: u64 = 8 * 1024 * 1024;
if self.streams_immediately() && len <= MEM_CAP {
let mut buf = Vec::with_capacity(len as usize);
body.take(len).read_to_end(&mut buf)?;
let actual = buf.len() as u64;
return self.create_file(
dev,
path,
FileSource::Reader {
reader: Box::new(io::Cursor::new(buf)),
len: actual,
},
meta,
);
}
let mut tmp = tempfile::NamedTempFile::new()?;
let mut limited = body.take(len);
io::copy(&mut limited, tmp.as_file_mut())?;
self.create_file(dev, path, FileSource::TempFile(tmp), meta)
}
fn create_dir(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
path: &Path,
meta: FileMeta,
) -> crate::Result<()>;
fn create_symlink(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
path: &Path,
target: &Path,
meta: FileMeta,
) -> crate::Result<()>;
fn create_device(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
path: &Path,
kind: DeviceKind,
major: u32,
minor: u32,
meta: FileMeta,
) -> crate::Result<()>;
fn remove(&mut self, dev: &mut dyn crate::block::BlockDevice, path: &Path)
-> crate::Result<()>;
fn list(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
path: &Path,
) -> crate::Result<Vec<DirEntry>>;
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn crate::block::BlockDevice,
path: &Path,
) -> crate::Result<Box<dyn Read + 'a>>;
fn open_file_ro<'a>(
&'a mut self,
_dev: &'a mut dyn crate::block::BlockDevice,
_path: &Path,
) -> crate::Result<Box<dyn FileReadHandle + 'a>> {
Err(crate::Error::Unsupported(
"this filesystem does not yet implement open_file_ro".into(),
))
}
fn open_file_rw<'a>(
&'a mut self,
_dev: &'a mut dyn crate::block::BlockDevice,
_path: &Path,
_flags: OpenFlags,
_meta: Option<FileMeta>,
) -> crate::Result<Box<dyn FileHandle + 'a>> {
Err(crate::Error::Unsupported(
"this filesystem does not yet implement open_file_rw".into(),
))
}
fn flush(&mut self, dev: &mut dyn crate::block::BlockDevice) -> crate::Result<()>;
fn image_len(&self) -> Option<u64> {
None
}
fn mutation_capability(&self) -> MutationCapability {
MutationCapability::Mutable
}
fn supports_mutation(&self) -> bool {
self.mutation_capability().supports_add_remove()
}
fn clone_capability(&self) -> CloneCapability {
CloneCapability::None
}
fn clone_file(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
src: &Path,
dst: &Path,
) -> crate::Result<()> {
let (meta, size) = match self.getattr(dev, src) {
Ok(a) => (
FileMeta {
mode: a.mode,
uid: a.uid,
gid: a.gid,
mtime: a.mtime,
atime: a.atime,
ctime: a.ctime,
},
a.size,
),
Err(_) => (FileMeta::default(), u64::MAX),
};
const MEM_CAP: u64 = 8 * 1024 * 1024;
if size <= MEM_CAP {
let mut buf = Vec::with_capacity(size as usize);
{
let mut reader = self.read_file(dev, src)?;
reader.read_to_end(&mut buf).map_err(crate::Error::from)?;
}
let actual = buf.len() as u64;
return self.create_file(
dev,
dst,
FileSource::Reader {
reader: Box::new(io::Cursor::new(buf)),
len: actual,
},
meta,
);
}
let mut tmp = tempfile::NamedTempFile::new().map_err(crate::Error::from)?;
{
let mut reader = self.read_file(dev, src)?;
io::copy(&mut reader, &mut tmp).map_err(crate::Error::from)?;
}
self.create_file(dev, dst, FileSource::TempFile(tmp), meta)
}
fn clone_range(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_src: &Path,
_src_off: u64,
_dst: &Path,
_dst_off: u64,
_len: u64,
) -> crate::Result<()> {
Err(crate::Error::Unsupported(
"this filesystem does not implement clone_range (no extent sharing)".into(),
))
}
fn read_symlink(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_path: &Path,
) -> crate::Result<std::path::PathBuf> {
Err(crate::Error::Unsupported(
"this filesystem does not implement read_symlink".into(),
))
}
fn getattr(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
path: &Path,
) -> crate::Result<FileAttrs> {
if path == Path::new("/") || path.as_os_str().is_empty() {
return Ok(FileAttrs::defaults_for(EntryKind::Dir, 0, 1));
}
let parent = path.parent().unwrap_or(Path::new("/"));
let name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| crate::Error::InvalidArgument("getattr: bad path".into()))?;
let entries = self.list(dev, parent)?;
let entry = entries
.into_iter()
.find(|e| e.name == name)
.ok_or_else(|| crate::Error::InvalidArgument(format!("getattr: {name} not found")))?;
Ok(FileAttrs::defaults_for(entry.kind, entry.size, entry.inode))
}
fn set_attrs(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_path: &Path,
_attrs: SetAttrs,
) -> crate::Result<()> {
Err(crate::Error::Unsupported(
"this filesystem does not implement set_attrs".into(),
))
}
fn truncate(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_path: &Path,
_new_size: u64,
) -> crate::Result<()> {
Err(crate::Error::Unsupported(
"this filesystem does not implement truncate".into(),
))
}
fn rename(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_old_path: &Path,
_new_path: &Path,
) -> crate::Result<()> {
Err(crate::Error::Unsupported(
"this filesystem does not implement rename".into(),
))
}
fn hardlink(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_target_path: &Path,
_new_path: &Path,
) -> crate::Result<()> {
Err(crate::Error::Unsupported(
"this filesystem does not implement hardlink".into(),
))
}
fn list_xattrs(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_path: &Path,
) -> crate::Result<Vec<XattrPair>> {
Ok(Vec::new())
}
fn set_xattr(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_path: &Path,
_name: &str,
_value: &[u8],
) -> crate::Result<()> {
Err(crate::Error::Unsupported(
"this filesystem does not implement set_xattr".into(),
))
}
fn set_xattrs(
&mut self,
dev: &mut dyn crate::block::BlockDevice,
path: &Path,
xattrs: &[XattrPair],
) -> crate::Result<()> {
for x in xattrs {
self.set_xattr(dev, path, &x.name, &x.value)?;
}
Ok(())
}
fn remove_xattr(
&mut self,
_dev: &mut dyn crate::block::BlockDevice,
_path: &Path,
_name: &str,
) -> crate::Result<()> {
Err(crate::Error::Unsupported(
"this filesystem does not implement remove_xattr".into(),
))
}
fn statfs(&mut self, _dev: &mut dyn crate::block::BlockDevice) -> crate::Result<StatFs> {
Ok(StatFs::default())
}
fn total_file_bytes(&mut self, dev: &mut dyn crate::block::BlockDevice) -> crate::Result<u64> {
let mut total = 0u64;
let mut stack: Vec<std::path::PathBuf> = vec![std::path::PathBuf::from("/")];
while let Some(dir) = stack.pop() {
let entries = self.list(dev, &dir)?;
for e in entries {
if e.name == "." || e.name == ".." || e.name == "lost+found" {
continue;
}
let child = dir.join(&e.name);
match e.kind {
EntryKind::Regular => total = total.saturating_add(e.size),
EntryKind::Dir => stack.push(child),
_ => {}
}
}
}
Ok(total)
}
}
pub trait FilesystemFactory: Filesystem + Sized {
type FormatOpts;
fn format(
dev: &mut dyn crate::block::BlockDevice,
opts: &Self::FormatOpts,
) -> crate::Result<Self>;
fn open(dev: &mut dyn crate::block::BlockDevice) -> crate::Result<Self>;
}
struct ZeroReader {
remaining: u64,
}
impl Read for ZeroReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.remaining == 0 {
return Ok(0);
}
let n = (buf.len() as u64).min(self.remaining) as usize;
buf[..n].fill(0);
self.remaining -= n as u64;
Ok(n)
}
}
impl Seek for ZeroReader {
fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"ZeroReader does not support seeking",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_source_streams_in_chunks() {
let src = FileSource::Zero(10_000);
let (mut reader, len) = src.open().unwrap();
assert_eq!(len, 10_000);
let mut total = 0;
let mut buf = [0u8; 4096];
loop {
let n = reader.read(&mut buf).unwrap();
if n == 0 {
break;
}
assert!(buf[..n].iter().all(|&b| b == 0));
total += n;
}
assert_eq!(total, 10_000);
}
#[test]
fn host_path_source_length_matches_file() {
use tempfile::NamedTempFile;
let mut f = NamedTempFile::new().unwrap();
std::io::Write::write_all(f.as_file_mut(), b"hello world").unwrap();
let src = FileSource::HostPath(f.path().to_path_buf());
assert_eq!(src.len().unwrap(), 11);
}
#[test]
fn file_attrs_defaults_set_kind_appropriate_mode() {
let d = FileAttrs::defaults_for(EntryKind::Dir, 0, 42);
assert_eq!(d.mode, 0o755);
assert_eq!(d.nlink, 2);
assert_eq!(d.inode, 42);
let f = FileAttrs::defaults_for(EntryKind::Regular, 1024, 7);
assert_eq!(f.mode, 0o644);
assert_eq!(f.nlink, 1);
assert_eq!(f.size, 1024);
assert_eq!(f.blocks, 2);
let s = FileAttrs::defaults_for(EntryKind::Symlink, 16, 9);
assert_eq!(s.mode, 0o777);
}
#[test]
fn default_getattr_walks_parent_listing() {
use crate::block::BlockDevice;
struct Tiny {
entries: Vec<DirEntry>,
}
impl Filesystem for Tiny {
fn create_file(
&mut self,
_: &mut dyn BlockDevice,
_: &Path,
_: FileSource,
_: FileMeta,
) -> crate::Result<()> {
unimplemented!()
}
fn create_dir(
&mut self,
_: &mut dyn BlockDevice,
_: &Path,
_: FileMeta,
) -> crate::Result<()> {
unimplemented!()
}
fn create_symlink(
&mut self,
_: &mut dyn BlockDevice,
_: &Path,
_: &Path,
_: FileMeta,
) -> crate::Result<()> {
unimplemented!()
}
fn create_device(
&mut self,
_: &mut dyn BlockDevice,
_: &Path,
_: DeviceKind,
_: u32,
_: u32,
_: FileMeta,
) -> crate::Result<()> {
unimplemented!()
}
fn remove(&mut self, _: &mut dyn BlockDevice, _: &Path) -> crate::Result<()> {
unimplemented!()
}
fn list(&mut self, _: &mut dyn BlockDevice, _: &Path) -> crate::Result<Vec<DirEntry>> {
Ok(self.entries.clone())
}
fn read_file<'a>(
&'a mut self,
_: &'a mut dyn BlockDevice,
_: &Path,
) -> crate::Result<Box<dyn Read + 'a>> {
unimplemented!()
}
fn flush(&mut self, _: &mut dyn BlockDevice) -> crate::Result<()> {
Ok(())
}
}
let mut t = Tiny {
entries: vec![DirEntry {
name: "hello.txt".into(),
inode: 17,
kind: EntryKind::Regular,
size: 11,
}],
};
let mut dev = crate::block::MemoryBackend::new(4096);
let attrs = t.getattr(&mut dev, Path::new("/hello.txt")).unwrap();
assert_eq!(attrs.kind, EntryKind::Regular);
assert_eq!(attrs.size, 11);
assert_eq!(attrs.inode, 17);
assert_eq!(attrs.mode, 0o644);
}
}