pub mod reader;
pub mod tree;
pub mod writer;
pub mod ar;
pub mod cpio;
pub mod zip;
pub mod arc;
pub mod cab;
pub mod lha;
pub mod lzx;
pub mod rar;
pub mod sevenz;
pub mod sit;
use std::collections::HashMap;
use std::io::Read;
use std::path::{Path, PathBuf};
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::{
DeviceKind, DirEntry, FileAttrs, FileMeta, FileReadHandle, FileSource, MutationCapability,
SetAttrs,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryKind {
Regular,
Dir,
Symlink,
HardLink,
Char,
Block,
Fifo,
Socket,
}
impl EntryKind {
pub fn to_fs(self) -> crate::fs::EntryKind {
match self {
Self::Regular | Self::HardLink => crate::fs::EntryKind::Regular,
Self::Dir => crate::fs::EntryKind::Dir,
Self::Symlink => crate::fs::EntryKind::Symlink,
Self::Char => crate::fs::EntryKind::Char,
Self::Block => crate::fs::EntryKind::Block,
Self::Fifo => crate::fs::EntryKind::Fifo,
Self::Socket => crate::fs::EntryKind::Socket,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
Stored,
Deflate,
Codec(crate::compression::Algo),
Unsupported(u16),
}
#[derive(Debug, Clone)]
pub struct DataLocator {
pub offset: u64,
pub compressed_len: u64,
pub uncompressed_len: u64,
pub method: Method,
}
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
pub path: String,
pub kind: EntryKind,
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub mtime: u64,
pub link_target: Option<String>,
pub device_major: u32,
pub device_minor: u32,
pub data: Option<DataLocator>,
}
impl ArchiveEntry {
pub fn regular(path: impl Into<String>, data: DataLocator) -> Self {
Self {
path: path.into(),
kind: EntryKind::Regular,
mode: 0o644,
uid: 0,
gid: 0,
mtime: 0,
link_target: None,
device_major: 0,
device_minor: 0,
data: Some(data),
}
}
pub fn dir(path: impl Into<String>) -> Self {
Self {
path: path.into(),
kind: EntryKind::Dir,
mode: 0o755,
uid: 0,
gid: 0,
mtime: 0,
link_target: None,
device_major: 0,
device_minor: 0,
data: None,
}
}
fn logical_size(&self) -> u64 {
match (&self.data, self.kind) {
(Some(loc), EntryKind::Regular | EntryKind::HardLink) => loc.uncompressed_len,
_ => 0,
}
}
}
pub struct ArchiveIndex {
pub kind: &'static str,
entries: Vec<ArchiveEntry>,
by_path: HashMap<String, usize>,
children: HashMap<String, Vec<String>>,
}
impl ArchiveIndex {
pub fn new(kind: &'static str) -> Self {
let mut children = HashMap::new();
children.insert("/".to_string(), Vec::new());
Self {
kind,
entries: Vec::new(),
by_path: HashMap::new(),
children,
}
}
pub fn push(&mut self, mut e: ArchiveEntry) {
e.path = tree::normalise_path(&e.path);
if e.path == "/" {
return;
}
let comps: Vec<&str> = e.path.trim_start_matches('/').split('/').collect();
let mut parent = String::from("/");
for (i, comp) in comps.iter().enumerate() {
let child_path = if parent == "/" {
format!("/{comp}")
} else {
format!("{parent}/{comp}")
};
let kids = self.children.entry(parent.clone()).or_default();
if !kids.iter().any(|k| k == comp) {
kids.push((*comp).to_string());
}
let is_leaf = i + 1 == comps.len();
if !is_leaf {
self.children.entry(child_path.clone()).or_default();
}
parent = child_path;
}
if matches!(e.kind, EntryKind::Dir) {
self.children.entry(e.path.clone()).or_default();
}
if let Some(&existing) = self.by_path.get(&e.path) {
self.entries[existing] = e;
} else {
let idx = self.entries.len();
self.by_path.insert(e.path.clone(), idx);
self.entries.push(e);
}
}
pub fn entries(&self) -> &[ArchiveEntry] {
&self.entries
}
pub fn lookup(&self, path: &str) -> Option<&ArchiveEntry> {
let p = tree::normalise_path(path);
self.by_path.get(&p).map(|&i| &self.entries[i])
}
pub fn list(&self, path: &str) -> Result<Vec<DirEntry>> {
let p = tree::normalise_path(path);
let names = self.children.get(&p).ok_or_else(|| {
crate::Error::InvalidArgument(format!("{}: no such directory {p:?}", self.kind))
})?;
let mut out = Vec::with_capacity(names.len());
for name in names {
let child = if p == "/" {
format!("/{name}")
} else {
format!("{p}/{name}")
};
let (kind, size, inode) = match self.by_path.get(&child) {
Some(&i) => {
let e = &self.entries[i];
(e.kind.to_fs(), e.logical_size(), i as u32 + 1)
}
None => (crate::fs::EntryKind::Dir, 0, 0),
};
out.push(DirEntry {
name: name.clone(),
inode,
kind,
size,
});
}
Ok(out)
}
}
pub trait ArchiveBuilder: Send {
fn add_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
src: FileSource,
meta: FileMeta,
) -> Result<()>;
fn add_dir(&mut self, dev: &mut dyn BlockDevice, path: &str, meta: FileMeta) -> Result<()>;
fn add_symlink(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
target: &str,
meta: FileMeta,
) -> Result<()>;
fn add_device(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
kind: DeviceKind,
major: u32,
minor: u32,
meta: FileMeta,
) -> Result<()>;
fn finish(&mut self, dev: &mut dyn BlockDevice) -> Result<()>;
fn position(&self) -> u64;
}
pub struct ArchiveFs {
index: ArchiveIndex,
builder: Option<Box<dyn ArchiveBuilder>>,
cap: MutationCapability,
scaffold: bool,
flushed_len: Option<u64>,
}
impl ArchiveFs {
pub fn from_index(index: ArchiveIndex) -> Self {
Self {
index,
builder: None,
cap: MutationCapability::Streaming,
scaffold: false,
flushed_len: None,
}
}
pub fn writer(kind: &'static str, builder: Box<dyn ArchiveBuilder>) -> Self {
Self {
index: ArchiveIndex::new(kind),
builder: Some(builder),
cap: MutationCapability::Streaming,
scaffold: false,
flushed_len: None,
}
}
pub fn scaffold(kind: &'static str) -> Self {
Self {
index: ArchiveIndex::new(kind),
builder: None,
cap: MutationCapability::Immutable,
scaffold: true,
flushed_len: None,
}
}
pub fn kind(&self) -> &'static str {
self.index.kind
}
fn guard_scaffold(&self, op: &str) -> Result<()> {
if self.scaffold {
return Err(crate::Error::Unsupported(format!(
"{}: {op} not implemented yet — this format is detection-only",
self.index.kind
)));
}
Ok(())
}
fn write_refused(&self, op: &'static str) -> crate::Error {
match self.cap {
MutationCapability::Immutable => crate::Error::Immutable {
kind: self.index.kind,
op,
},
_ => crate::Error::Streaming {
kind: self.index.kind,
op,
},
}
}
fn path_str<'p>(&self, path: &'p Path) -> Result<&'p str> {
path.to_str().ok_or_else(|| {
crate::Error::InvalidArgument(format!("{}: non-UTF-8 path", self.index.kind))
})
}
}
impl crate::fs::Filesystem for ArchiveFs {
fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
src: FileSource,
meta: FileMeta,
) -> Result<()> {
let s = self.path_str(path)?.to_string();
match self.builder.as_mut() {
Some(b) => b.add_file(dev, &s, src, meta),
None => Err(self.write_refused("write")),
}
}
fn streams_immediately(&self) -> bool {
true
}
fn create_dir(&mut self, dev: &mut dyn BlockDevice, path: &Path, meta: FileMeta) -> Result<()> {
let s = self.path_str(path)?.to_string();
match self.builder.as_mut() {
Some(b) => b.add_dir(dev, &s, meta),
None => Err(self.write_refused("write")),
}
}
fn create_symlink(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
target: &Path,
meta: FileMeta,
) -> Result<()> {
let s = self.path_str(path)?.to_string();
let t = target.to_str().ok_or_else(|| {
crate::Error::InvalidArgument(format!("{}: non-UTF-8 symlink target", self.index.kind))
})?;
match self.builder.as_mut() {
Some(b) => b.add_symlink(dev, &s, t, meta),
None => Err(self.write_refused("write")),
}
}
fn create_device(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
kind: DeviceKind,
major: u32,
minor: u32,
meta: FileMeta,
) -> Result<()> {
let s = self.path_str(path)?.to_string();
match self.builder.as_mut() {
Some(b) => b.add_device(dev, &s, kind, major, minor, meta),
None => Err(self.write_refused("write")),
}
}
fn remove(&mut self, _dev: &mut dyn BlockDevice, _path: &Path) -> Result<()> {
Err(self.write_refused("rm"))
}
fn list(&mut self, _dev: &mut dyn BlockDevice, path: &Path) -> Result<Vec<DirEntry>> {
self.guard_scaffold("list")?;
let s = self.path_str(path)?;
self.index.list(s)
}
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &Path,
) -> Result<Box<dyn Read + 'a>> {
self.guard_scaffold("read")?;
let s = self.path_str(path)?;
let e = self.index.lookup(s).ok_or_else(|| {
crate::Error::InvalidArgument(format!("{}: no entry at {s:?}", self.index.kind))
})?;
let loc = match (e.kind, &e.data) {
(EntryKind::Regular | EntryKind::HardLink, Some(loc)) => loc.clone(),
_ => {
return Err(crate::Error::InvalidArgument(format!(
"{}: {s:?} is not a regular file",
self.index.kind
)));
}
};
reader::open(dev, loc)
}
fn open_file_ro<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &Path,
) -> Result<Box<dyn FileReadHandle + 'a>> {
self.guard_scaffold("read")?;
let s = self.path_str(path)?;
let e = self.index.lookup(s).ok_or_else(|| {
crate::Error::InvalidArgument(format!("{}: no entry at {s:?}", self.index.kind))
})?;
let loc = match (e.kind, &e.data) {
(EntryKind::Regular | EntryKind::HardLink, Some(loc)) => loc.clone(),
_ => {
return Err(crate::Error::InvalidArgument(format!(
"{}: {s:?} is not a regular file",
self.index.kind
)));
}
};
reader::open_ro(dev, loc)
}
fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if let Some(b) = self.builder.as_mut() {
b.finish(dev)?;
self.flushed_len = Some(b.position());
}
Ok(())
}
fn image_len(&self) -> Option<u64> {
self.flushed_len
}
fn mutation_capability(&self) -> MutationCapability {
self.cap
}
fn read_symlink(&mut self, _dev: &mut dyn BlockDevice, path: &Path) -> Result<PathBuf> {
self.guard_scaffold("read")?;
let s = self.path_str(path)?;
let e = self.index.lookup(s).ok_or_else(|| {
crate::Error::InvalidArgument(format!("{}: no entry at {s:?}", self.index.kind))
})?;
if !matches!(e.kind, EntryKind::Symlink) {
return Err(crate::Error::InvalidArgument(format!(
"{}: {s:?} is not a symlink",
self.index.kind
)));
}
e.link_target.clone().map(PathBuf::from).ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"{}: symlink {s:?} has no target",
self.index.kind
))
})
}
fn getattr(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<FileAttrs> {
self.guard_scaffold("getattr").or_else(|e| {
if path == Path::new("/") || path.as_os_str().is_empty() {
Ok(())
} else {
Err(e)
}
})?;
if path == Path::new("/") || path.as_os_str().is_empty() {
return Ok(dir_attrs(1));
}
let s = self.path_str(path)?;
let (e, inode) = {
let idx = self.index.by_path.get(s).copied();
match idx {
Some(i) => (self.index.entries[i].clone(), i as u32 + 1),
None => {
if self.index.children.contains_key(s) {
return Ok(dir_attrs(0));
}
return default_getattr(self, dev, path);
}
}
};
let size = e.logical_size();
Ok(FileAttrs {
kind: e.kind.to_fs(),
mode: e.mode,
uid: e.uid,
gid: e.gid,
size,
blocks: size.div_ceil(512),
nlink: 1,
atime: e.mtime as u32,
mtime: e.mtime as u32,
ctime: e.mtime as u32,
rdev: 0,
inode,
})
}
fn set_attrs(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &Path,
_attrs: SetAttrs,
) -> Result<()> {
Err(self.write_refused("set_attrs"))
}
}
fn dir_attrs(inode: u32) -> FileAttrs {
FileAttrs {
kind: crate::fs::EntryKind::Dir,
mode: 0o755,
uid: 0,
gid: 0,
size: 0,
blocks: 0,
nlink: 2,
atime: 0,
mtime: 0,
ctime: 0,
rdev: 0,
inode,
}
}
fn default_getattr(
fs: &mut ArchiveFs,
dev: &mut dyn BlockDevice,
path: &Path,
) -> Result<FileAttrs> {
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 = crate::fs::Filesystem::list(fs, dev, parent)?;
let entry = entries
.into_iter()
.find(|e| e.name == name)
.ok_or_else(|| crate::Error::InvalidArgument(format!("getattr: {name} not found")))?;
let mode = match entry.kind {
crate::fs::EntryKind::Dir => 0o755,
crate::fs::EntryKind::Symlink => 0o777,
_ => 0o644,
};
Ok(FileAttrs {
kind: entry.kind,
mode,
uid: 0,
gid: 0,
size: entry.size,
blocks: entry.size.div_ceil(512),
nlink: 1,
atime: 0,
mtime: 0,
ctime: 0,
rdev: 0,
inode: entry.inode,
})
}
#[macro_export]
macro_rules! impl_archive_fs_filesystem {
($t:ty) => {
impl $crate::fs::Filesystem for $t {
fn create_file(
&mut self,
dev: &mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
src: $crate::fs::FileSource,
meta: $crate::fs::FileMeta,
) -> $crate::Result<()> {
self.0.create_file(dev, path, src, meta)
}
fn create_dir(
&mut self,
dev: &mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
meta: $crate::fs::FileMeta,
) -> $crate::Result<()> {
self.0.create_dir(dev, path, meta)
}
fn create_symlink(
&mut self,
dev: &mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
target: &std::path::Path,
meta: $crate::fs::FileMeta,
) -> $crate::Result<()> {
self.0.create_symlink(dev, path, target, meta)
}
fn create_device(
&mut self,
dev: &mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
kind: $crate::fs::DeviceKind,
major: u32,
minor: u32,
meta: $crate::fs::FileMeta,
) -> $crate::Result<()> {
self.0.create_device(dev, path, kind, major, minor, meta)
}
fn remove(
&mut self,
dev: &mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
) -> $crate::Result<()> {
self.0.remove(dev, path)
}
fn list(
&mut self,
dev: &mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
) -> $crate::Result<Vec<$crate::fs::DirEntry>> {
self.0.list(dev, path)
}
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
) -> $crate::Result<Box<dyn std::io::Read + 'a>> {
self.0.read_file(dev, path)
}
fn open_file_ro<'a>(
&'a mut self,
dev: &'a mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
) -> $crate::Result<Box<dyn $crate::fs::FileReadHandle + 'a>> {
self.0.open_file_ro(dev, path)
}
fn flush(&mut self, dev: &mut dyn $crate::block::BlockDevice) -> $crate::Result<()> {
self.0.flush(dev)
}
fn streams_immediately(&self) -> bool {
self.0.streams_immediately()
}
fn image_len(&self) -> Option<u64> {
self.0.image_len()
}
fn mutation_capability(&self) -> $crate::fs::MutationCapability {
self.0.mutation_capability()
}
fn read_symlink(
&mut self,
dev: &mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
) -> $crate::Result<std::path::PathBuf> {
self.0.read_symlink(dev, path)
}
fn getattr(
&mut self,
dev: &mut dyn $crate::block::BlockDevice,
path: &std::path::Path,
) -> $crate::Result<$crate::fs::FileAttrs> {
self.0.getattr(dev, path)
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
fn loc(len: u64) -> DataLocator {
DataLocator {
offset: 0,
compressed_len: len,
uncompressed_len: len,
method: Method::Stored,
}
}
#[test]
fn synthesises_intermediate_dirs() {
let mut idx = ArchiveIndex::new("test");
idx.push(ArchiveEntry::regular("a/b/c.txt", loc(10)));
let root = idx.list("/").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "a");
assert_eq!(root[0].kind, crate::fs::EntryKind::Dir);
let a = idx.list("/a").unwrap();
assert_eq!(a.len(), 1);
assert_eq!(a[0].name, "b");
let b = idx.list("/a/b").unwrap();
assert_eq!(b.len(), 1);
assert_eq!(b[0].name, "c.txt");
assert_eq!(b[0].kind, crate::fs::EntryKind::Regular);
assert_eq!(b[0].size, 10);
}
#[test]
fn last_write_wins_on_duplicate_path() {
let mut idx = ArchiveIndex::new("test");
idx.push(ArchiveEntry::regular("dup", loc(1)));
idx.push(ArchiveEntry::regular("dup", loc(99)));
assert_eq!(idx.lookup("/dup").unwrap().logical_size(), 99);
assert_eq!(idx.list("/").unwrap().len(), 1);
}
#[test]
fn explicit_dir_record_merges_with_synthesised() {
let mut idx = ArchiveIndex::new("test");
idx.push(ArchiveEntry::regular("d/f", loc(1)));
idx.push(ArchiveEntry::dir("d"));
let root = idx.list("/").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].kind, crate::fs::EntryKind::Dir);
assert_eq!(idx.list("/d").unwrap()[0].name, "f");
}
}