use std::path::{Path, PathBuf};
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::DirEntry;
use crate::fs::apfs::Apfs;
use crate::fs::exfat::Exfat;
use crate::fs::ext::Ext;
use crate::fs::f2fs::F2fs;
use crate::fs::fat::Fat32;
use crate::fs::hfs_plus::HfsPlus;
use crate::fs::ntfs::Ntfs;
use crate::fs::squashfs::Squashfs;
use crate::fs::tar::Tar;
use crate::fs::xfs::Xfs;
use crate::part::{Gpt, Mbr, Partition, PartitionTable, slice_partition};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FsKind {
Ext,
Fat32,
Tar,
Xfs,
Exfat,
HfsPlus,
Apfs,
Ntfs,
F2fs,
Squashfs,
Iso9660,
Grf,
}
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);
}
if &bs[3..11] == b"EXFAT " {
return Ok(FsKind::Exfat);
}
if &bs[3..11] == b"NTFS " {
return Ok(FsKind::Ntfs);
}
if &bs[0..4] == b"XFSB" {
return Ok(FsKind::Xfs);
}
if &bs[0..4] == b"hsqs" {
return Ok(FsKind::Squashfs);
}
if &bs[0..16] == b"Master of Magic\0" {
return Ok(FsKind::Grf);
}
if &bs[257..262] == b"ustar" {
return Ok(FsKind::Tar);
}
if dev.total_size() >= 32768 + 7 {
let mut iso = [0u8; 7];
dev.read_at(32768, &mut iso)?;
if &iso[1..6] == b"CD001" {
return Ok(FsKind::Iso9660);
}
}
if &bs[32..36] == b"NXSB" {
return Ok(FsKind::Apfs);
}
let mut sb_magic = [0u8; 2];
dev.read_at(1024 + 56, &mut sb_magic)?;
if sb_magic == [0x53, 0xEF] {
return Ok(FsKind::Ext);
}
let mut hfs_sig = [0u8; 2];
if dev.total_size() >= 1024 + 2 {
dev.read_at(1024, &mut hfs_sig)?;
if &hfs_sig == b"H+" || &hfs_sig == b"HX" {
return Ok(FsKind::HfsPlus);
}
}
let mut f2_magic = [0u8; 4];
if dev.total_size() >= 1024 + 0x1000 + 4 {
dev.read_at(1024, &mut f2_magic)?;
if u32::from_le_bytes(f2_magic) == 0xF2F5_2010 {
return Ok(FsKind::F2fs);
}
dev.read_at(1024 + 0x1000, &mut f2_magic)?;
if u32::from_le_bytes(f2_magic) == 0xF2F5_2010 {
return Ok(FsKind::F2fs);
}
}
Err(crate::Error::InvalidImage(
"inspect: no recognised filesystem (ext2/3/4, FAT32, exFAT, XFS, HFS+, APFS, tar, NTFS, F2FS, SquashFS, ISO 9660, GRF) on this image".into(),
))
}
pub enum AnyFs {
Ext(Box<Ext>),
Fat32(Box<Fat32>),
Tar(Box<Tar>),
Xfs(Box<Xfs>),
Exfat(Box<Exfat>),
HfsPlus(Box<HfsPlus>),
Apfs(Box<Apfs>),
Ntfs(Box<Ntfs>),
F2fs(Box<F2fs>),
Squashfs(Box<Squashfs>),
Iso9660(Box<crate::fs::iso9660::Iso9660>),
Grf(Box<crate::fs::grf::Grf>),
}
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)?))),
FsKind::Tar => Ok(Self::Tar(Box::new(Tar::open(dev)?))),
FsKind::Xfs => Ok(Self::Xfs(Box::new(Xfs::open(dev)?))),
FsKind::Exfat => Ok(Self::Exfat(Box::new(Exfat::open(dev)?))),
FsKind::HfsPlus => Ok(Self::HfsPlus(Box::new(HfsPlus::open(dev)?))),
FsKind::Apfs => Ok(Self::Apfs(Box::new(Apfs::open(dev)?))),
FsKind::Ntfs => Ok(Self::Ntfs(Box::new(Ntfs::open(dev)?))),
FsKind::F2fs => Ok(Self::F2fs(Box::new(F2fs::open(dev)?))),
FsKind::Squashfs => Ok(Self::Squashfs(Box::new(Squashfs::open(dev)?))),
FsKind::Iso9660 => Ok(Self::Iso9660(Box::new(crate::fs::iso9660::Iso9660::open(
dev,
)?))),
FsKind::Grf => Ok(Self::Grf(Box::new(crate::fs::grf::Grf::open_dev(dev)?))),
}
}
pub fn kind(&self) -> FsKind {
match self {
Self::Ext(_) => FsKind::Ext,
Self::Fat32(_) => FsKind::Fat32,
Self::Tar(_) => FsKind::Tar,
Self::Xfs(_) => FsKind::Xfs,
Self::Exfat(_) => FsKind::Exfat,
Self::HfsPlus(_) => FsKind::HfsPlus,
Self::Apfs(_) => FsKind::Apfs,
Self::Ntfs(_) => FsKind::Ntfs,
Self::F2fs(_) => FsKind::F2fs,
Self::Squashfs(_) => FsKind::Squashfs,
Self::Iso9660(_) => FsKind::Iso9660,
Self::Grf(_) => FsKind::Grf,
}
}
pub fn list(&mut 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),
Self::Tar(tar) => tar.list_path(dev, path),
Self::Xfs(xfs) => xfs.list_path(dev, path),
Self::Exfat(exfat) => exfat.list_path(dev, path),
Self::HfsPlus(hfs) => hfs.list_path(dev, path),
Self::Apfs(apfs) => apfs.list_path(dev, path),
Self::Ntfs(ntfs) => ntfs.list_path(dev, path),
Self::F2fs(f2) => f2.list_path(dev, path),
Self::Squashfs(sq) => sq.list_path(dev, path),
Self::Iso9660(iso) => iso.list_path(dev, path),
Self::Grf(grf) => {
use crate::fs::Filesystem;
grf.list(dev, std::path::Path::new(path))
}
}
}
pub fn total_file_bytes(&mut self, dev: &mut dyn BlockDevice) -> Result<u64> {
self.as_filesystem_dyn(|fs| fs.total_file_bytes(dev))
}
pub fn read_symlink(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<String> {
let p = std::path::Path::new(path);
let target = self.as_filesystem_dyn(|fs| fs.read_symlink(dev, p))?;
Ok(target.to_string_lossy().into_owned())
}
pub fn copy_file_to(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
out: &mut dyn std::io::Write,
) -> Result<u64> {
let mut buf = [0u8; 64 * 1024];
match self {
Self::Ext(ext) => {
let ino = ext.path_to_inode(dev, path)?;
let mut r = ext.open_file_reader(dev, ino)?;
pump(&mut r, out, &mut buf)
}
Self::Fat32(fat) => {
let mut r = fat.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::Tar(tar) => {
let mut r = tar.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::Xfs(xfs) => {
let mut r = xfs.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::Exfat(exfat) => {
let mut r = exfat.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::HfsPlus(hfs) => {
let mut r = hfs.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::Apfs(apfs) => {
let mut r = apfs.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::Ntfs(ntfs) => {
let mut r = ntfs.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::F2fs(f2) => {
let mut r = f2.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::Squashfs(sq) => {
let mut r = sq.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::Iso9660(iso) => {
let mut r = iso.open_file_reader(dev, path)?;
pump(&mut r, out, &mut buf)
}
Self::Grf(grf) => {
let key = path.trim_start_matches('/').to_string();
let entry = grf.entries.get(&key).cloned().ok_or_else(|| {
crate::Error::InvalidArgument(format!("grf: no entry at {key:?}"))
})?;
let bytes = grf.read_entry(dev, &entry)?;
let mut r = std::io::Cursor::new(bytes);
pump(&mut r, out, &mut buf)
}
}
}
pub fn add_file(
&mut self,
dev: &mut dyn BlockDevice,
dest_path: &str,
host_src: &Path,
) -> Result<()> {
self.require_mutable("add")?;
let meta = std::fs::symlink_metadata(host_src)?;
let fmeta = crate::fs::FileMeta {
mode: host_mode_from_meta(&meta, false),
..crate::fs::FileMeta::default()
};
let dest = std::path::Path::new(dest_path);
let src = crate::fs::FileSource::HostPath(host_src.to_path_buf());
self.as_filesystem_dyn(move |fs| fs.create_file(dev, dest, src, fmeta))
}
pub fn add_dir_tree(
&mut self,
dev: &mut dyn BlockDevice,
dest_path: &str,
host_src: &Path,
) -> Result<()> {
self.require_mutable("add")?;
let meta = std::fs::symlink_metadata(host_src)?;
let fmeta = crate::fs::FileMeta {
mode: host_mode_from_meta(&meta, true),
..crate::fs::FileMeta::default()
};
let dest = std::path::Path::new(dest_path);
self.as_filesystem_dyn(|fs| fs.create_dir(dev, dest, fmeta))?;
let source = crate::repack::Source::HostDir(host_src.to_path_buf());
self.populate_from_source_at(dev, dest_path, &source)
}
pub fn mkdir(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<()> {
self.require_mutable("mkdir")?;
let fmeta = crate::fs::FileMeta {
mode: 0o755,
..crate::fs::FileMeta::default()
};
let p = std::path::Path::new(path);
self.as_filesystem_dyn(|fs| fs.create_dir(dev, p, fmeta))
}
pub fn remove(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<()> {
self.require_mutable("rm")?;
let p = std::path::Path::new(path);
self.as_filesystem_dyn(|fs| fs.remove(dev, p))
}
pub fn supports_mutation(&self) -> bool {
self.mutation_capability().supports_add_remove()
}
pub fn mutation_capability(&self) -> crate::fs::MutationCapability {
match self {
Self::Ext(ext) => crate::fs::Filesystem::mutation_capability(ext.as_ref()),
Self::Fat32(fat) => crate::fs::Filesystem::mutation_capability(fat.as_ref()),
Self::HfsPlus(h) => crate::fs::Filesystem::mutation_capability(h.as_ref()),
Self::Ntfs(n) => crate::fs::Filesystem::mutation_capability(n.as_ref()),
Self::F2fs(fs2) => crate::fs::Filesystem::mutation_capability(fs2.as_ref()),
Self::Squashfs(sq) => crate::fs::Filesystem::mutation_capability(sq.as_ref()),
Self::Xfs(x) => crate::fs::Filesystem::mutation_capability(x.as_ref()),
Self::Iso9660(iso) => crate::fs::Filesystem::mutation_capability(iso.as_ref()),
Self::Tar(t) => crate::fs::Filesystem::mutation_capability(t.as_ref()),
Self::Apfs(a) => crate::fs::Filesystem::mutation_capability(a.as_ref()),
Self::Exfat(e) => crate::fs::Filesystem::mutation_capability(e.as_ref()),
Self::Grf(g) => crate::fs::Filesystem::mutation_capability(g.as_ref()),
}
}
fn require_mutable(&self, op: &'static str) -> Result<()> {
use crate::fs::MutationCapability;
match self.mutation_capability() {
MutationCapability::Mutable | MutationCapability::WholeFileOnly => Ok(()),
MutationCapability::Streaming => Err(crate::Error::Streaming {
kind: self.kind_string(),
op,
}),
MutationCapability::Immutable => Err(crate::Error::Immutable {
kind: self.kind_string(),
op,
}),
}
}
pub(crate) fn as_filesystem_dyn<R>(
&mut self,
f: impl FnOnce(&mut dyn crate::fs::Filesystem) -> Result<R>,
) -> Result<R> {
match self {
Self::Ext(ext) => f(ext.as_mut()),
Self::Fat32(fat) => f(fat.as_mut()),
Self::HfsPlus(h) => f(h.as_mut()),
Self::Ntfs(n) => f(n.as_mut()),
Self::F2fs(fs2) => f(fs2.as_mut()),
Self::Squashfs(sq) => f(sq.as_mut()),
Self::Xfs(x) => f(x.as_mut()),
Self::Tar(t) => f(t.as_mut()),
Self::Apfs(a) => f(a.as_mut()),
Self::Exfat(e) => f(e.as_mut()),
Self::Iso9660(iso) => f(iso.as_mut()),
Self::Grf(g) => f(g.as_mut()),
}
}
fn populate_from_source_at(
&mut self,
dev: &mut dyn BlockDevice,
at_path: &str,
src: &crate::repack::Source,
) -> Result<()> {
let _ = at_path; self.as_filesystem_dyn(|fs| {
crate::repack::populate_fs_from_source_dyn(dev, fs, src)
})
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
match self {
Self::Ext(ext) => ext.flush(dev),
Self::Fat32(fat) => fat.flush(dev),
Self::Grf(g) => crate::fs::Filesystem::flush(g.as_mut(), dev),
Self::Tar(_)
| Self::Xfs(_)
| Self::Exfat(_)
| Self::HfsPlus(_)
| Self::Apfs(_)
| Self::Ntfs(_)
| Self::F2fs(_)
| Self::Squashfs(_)
| Self::Iso9660(_) => Ok(()),
}
}
pub fn kind_string(&self) -> &'static str {
if let Self::Xfs(_) = self {
return "xfs";
}
if let Self::Exfat(_) = self {
return "exfat";
}
if let Self::HfsPlus(_) = self {
return "hfs+";
}
if let Self::Apfs(_) = self {
return "apfs";
}
if let Self::Ntfs(_) = self {
return "ntfs";
}
if let Self::F2fs(_) = self {
return "f2fs";
}
if let Self::Squashfs(_) = self {
return "squashfs";
}
if let Self::Iso9660(_) = self {
return "iso9660";
}
if let Self::Grf(_) = self {
return "grf";
}
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",
Self::Tar(_) => "tar",
Self::Xfs(_)
| Self::Exfat(_)
| Self::HfsPlus(_)
| Self::Apfs(_)
| Self::Ntfs(_)
| Self::F2fs(_)
| Self::Squashfs(_)
| Self::Iso9660(_)
| Self::Grf(_) => unreachable!(),
}
}
}
fn host_mode_from_meta(meta: &std::fs::Metadata, is_dir: bool) -> u16 {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = is_dir;
(meta.permissions().mode() & 0o7777) as u16
}
#[cfg(not(unix))]
{
let _ = meta;
if is_dir { 0o755 } else { 0o644 }
}
}
fn pump<R: std::io::Read + ?Sized, W: std::io::Write + ?Sized>(
reader: &mut R,
out: &mut W,
buf: &mut [u8],
) -> Result<u64> {
let mut total = 0u64;
loop {
let n = reader.read(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 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))
}
pub fn open(dev: &mut dyn BlockDevice) -> Result<Box<dyn crate::fs::Filesystem>> {
Ok(AnyFs::open(dev)?.into_dyn_filesystem())
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Summary {
pub kind: &'static str,
pub supports_mutation: bool,
}
pub fn summary(dev: &mut dyn BlockDevice) -> Result<Summary> {
let fs = AnyFs::open(dev)?;
Ok(Summary {
kind: fs.kind_string(),
supports_mutation: fs.supports_mutation(),
})
}
impl AnyFs {
fn into_dyn_filesystem(self) -> Box<dyn crate::fs::Filesystem> {
match self {
Self::Ext(b) => b,
Self::Fat32(b) => b,
Self::Tar(b) => b,
Self::Xfs(b) => b,
Self::Exfat(b) => b,
Self::HfsPlus(b) => b,
Self::Apfs(b) => b,
Self::Ntfs(b) => b,
Self::F2fs(b) => b,
Self::Squashfs(b) => b,
Self::Iso9660(b) => b,
Self::Grf(b) => b,
}
}
}
#[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, _tmp) = crate::block::open_image_maybe_compressed(&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, mut 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"));
}
#[test]
fn open_returns_dyn_filesystem() {
let opts = FormatOpts::default();
let mut dev = MemoryBackend::new(opts.blocks_count as u64 * opts.block_size as u64);
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
ext.flush(&mut dev).unwrap();
drop(ext);
let mut fs: Box<dyn crate::fs::Filesystem> = open(&mut dev).unwrap();
assert!(fs.supports_mutation());
let entries = fs
.list(&mut dev, std::path::Path::new("/"))
.expect("list /");
assert!(entries.iter().any(|e| e.name == "lost+found"));
}
#[test]
fn summary_reports_kind_and_mutability() {
let opts = FormatOpts::default();
let mut dev = MemoryBackend::new(opts.blocks_count as u64 * opts.block_size as u64);
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
ext.flush(&mut dev).unwrap();
drop(ext);
let s = summary(&mut dev).unwrap();
assert_eq!(s.kind, "ext2");
assert!(s.supports_mutation);
}
#[test]
fn add_on_streaming_fs_returns_streaming_error() {
use crate::fs::tar::{TarEntryMeta, TarStreamWriter};
let mut buf = Vec::<u8>::new();
{
let mut w = TarStreamWriter::new(&mut buf);
w.add_dir("/etc", TarEntryMeta::default(), &[]).unwrap();
w.finish().unwrap();
}
let mut dev = MemoryBackend::new(buf.len() as u64);
dev.write_at(0, &buf).unwrap();
let mut fs = AnyFs::open(&mut dev).unwrap();
assert_eq!(
fs.mutation_capability(),
crate::fs::MutationCapability::Streaming
);
let err = fs
.add_file(&mut dev, "/etc/new", std::path::Path::new("/dev/null"))
.expect_err("add on tar must fail");
match err {
crate::Error::Streaming { kind, op } => {
assert_eq!(kind, "tar");
assert_eq!(op, "add");
}
other => panic!("expected Streaming, got {other:?}"),
}
}
}