use std::collections::HashMap;
use std::io::{Read, Seek};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use forensic_vfs::{
Allocation, ByteRun, DirEntry as VfsDirEntry, DirStream, ExtentStream, FileId, FileSystem,
FsKind, FsMeta, MacbTimes, NodeKind, NodeStream, ResidencyKind, RunAlloc, RunFlags, RunInfo,
SectorSizes, StreamId, TimeResolution, TimeSource, TimeStamp, TimeZonePolicy, VfsError,
VfsResult,
};
use crate::{EntryLayout, ExtraFields, FormatError, ZipArchive, ZipCoreError};
const ARCHIVE_BLOCK: u32 = 512;
struct Node {
entry_idx: Option<usize>,
name: Vec<u8>,
kind: NodeKind,
size: u64,
modified: Option<TimeStamp>,
accessed: Option<TimeStamp>,
born: Option<TimeStamp>,
children: Vec<u64>,
}
struct Inner<R: Read + Seek> {
archive: ZipArchive<R>,
cache: HashMap<u64, Arc<Vec<u8>>>,
}
pub struct ZipVfs<R: Read + Seek> {
inner: Mutex<Inner<R>>,
nodes: Vec<Node>,
}
impl<R: Read + Seek + Send> ZipVfs<R> {
pub fn open(reader: R) -> VfsResult<Self> {
let mut archive = ZipArchive::new(reader).map_err(map_err)?;
let layouts = archive.structural_view().map_err(map_err)?;
let nodes = build_tree(&layouts);
Ok(Self {
inner: Mutex::new(Inner {
archive,
cache: HashMap::new(),
}),
nodes,
})
}
fn lock(&self) -> MutexGuard<'_, Inner<R>> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
fn node_of(&self, id: FileId) -> VfsResult<&Node> {
let idx = index_of(id)?;
self.nodes
.get(usize::try_from(idx).unwrap_or(usize::MAX))
.ok_or(VfsError::Unsupported {
layer: "zip file-id",
scheme: format!("Opaque({idx}) out of range"),
})
}
fn content(&self, node_id: u64, entry_idx: usize) -> VfsResult<Arc<Vec<u8>>> {
let mut inner = self.lock();
if let Some(data) = inner.cache.get(&node_id) {
return Ok(Arc::clone(data));
}
let bytes = {
let mut file = inner.archive.by_index(entry_idx).map_err(map_err)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|source| VfsError::Io {
op: "zip read",
source,
})?;
bytes
};
let arc = Arc::new(bytes);
inner.cache.insert(node_id, Arc::clone(&arc));
Ok(arc)
}
}
fn index_of(id: FileId) -> VfsResult<u64> {
match id {
FileId::Opaque(n) => Ok(n),
other => Err(VfsError::Unsupported {
layer: "zip file-id",
scheme: format!("{other:?}"),
}),
}
}
fn require_default_stream(stream: StreamId) -> VfsResult<()> {
match stream {
StreamId::Default => Ok(()),
other => Err(VfsError::Unsupported {
layer: "zip stream",
scheme: format!("{other:?}"),
}),
}
}
fn map_err(e: ZipCoreError) -> VfsError {
match e {
ZipCoreError::Io(source) => VfsError::Io {
op: "zip read",
source,
},
ZipCoreError::Format(FormatError::NoEocd) => VfsError::Bootstrap {
stage: "zip mount",
detail: "no End Of Central Directory record (not a ZIP archive)".to_string(),
},
ZipCoreError::UnsupportedMethod(_)
| ZipCoreError::UnsupportedEncryption { .. }
| ZipCoreError::EncryptedNoPassword(_)
| ZipCoreError::WrongPassword(_)
| ZipCoreError::SpannedArchive { .. } => VfsError::Unsupported {
layer: "zip",
scheme: e.to_string(),
},
other => VfsError::Decode {
layer: "zip",
offset: 0,
detail: other.to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
},
}
}
fn filetime_ts(ft: u64) -> TimeStamp {
const EPOCH_DIFF_100NS: i128 = 116_444_736_000_000_000;
TimeStamp {
unix_nanos: (i128::from(ft) - EPOCH_DIFF_100NS) * 100,
source: TimeSource::Unspecified,
resolution: TimeResolution::WinFileTime,
}
}
fn unix_secs_ts(secs: i32) -> TimeStamp {
TimeStamp {
unix_nanos: i128::from(secs) * 1_000_000_000,
source: TimeSource::Unspecified,
resolution: TimeResolution::Seconds,
}
}
fn extra_times(extra: &ExtraFields) -> (Option<TimeStamp>, Option<TimeStamp>, Option<TimeStamp>) {
let modified = extra
.ntfs_mtime
.map(filetime_ts)
.or_else(|| extra.unix_mtime.map(unix_secs_ts));
let accessed = extra
.ntfs_atime
.map(filetime_ts)
.or_else(|| extra.unix_atime.map(unix_secs_ts));
let born = extra
.ntfs_ctime
.map(filetime_ts)
.or_else(|| extra.unix_ctime.map(unix_secs_ts));
(modified, accessed, born)
}
fn build_tree(layouts: &[EntryLayout]) -> Vec<Node> {
let mut nodes: Vec<Node> = Vec::with_capacity(layouts.len() + 1);
nodes.push(Node {
entry_idx: None,
name: Vec::new(),
kind: NodeKind::Dir,
size: 0,
modified: None,
accessed: None,
born: None,
children: Vec::new(),
});
let mut by_path: HashMap<String, u64> = HashMap::new();
by_path.insert(String::new(), 0);
for layout in layouts {
let raw = layout.central.name.as_str();
let is_dir = raw.ends_with('/') || raw.ends_with('\\');
let comps: Vec<&str> = raw
.split(['/', '\\'])
.filter(|c| !c.is_empty() && *c != ".")
.collect();
let Some(last) = comps.len().checked_sub(1) else {
continue; };
let (modified, accessed, born) = extra_times(&layout.extra);
let mut parent_id = 0u64;
let mut acc = String::new();
for (ci, comp) in comps.iter().enumerate() {
if !acc.is_empty() {
acc.push('/');
}
acc.push_str(comp);
if ci == last {
if let Some(&existing) = by_path.get(&acc) {
if let Some(n) = nodes.get_mut(usize::try_from(existing).unwrap_or(usize::MAX))
{
if n.entry_idx.is_none() {
n.entry_idx = Some(layout.index);
n.modified = modified;
n.accessed = accessed;
n.born = born;
}
}
} else {
let id = nodes.len() as u64;
nodes.push(Node {
entry_idx: Some(layout.index),
name: comp.as_bytes().to_vec(),
kind: if is_dir {
NodeKind::Dir
} else {
NodeKind::File
},
size: if is_dir {
0
} else {
layout.central.uncompressed_size
},
modified,
accessed,
born,
children: Vec::new(),
});
by_path.insert(acc.clone(), id);
push_child(&mut nodes, parent_id, id);
}
} else if let Some(&existing) = by_path.get(&acc) {
parent_id = existing;
} else {
let id = nodes.len() as u64;
nodes.push(Node {
entry_idx: None,
name: comp.as_bytes().to_vec(),
kind: NodeKind::Dir,
size: 0,
modified: None,
accessed: None,
born: None,
children: Vec::new(),
});
by_path.insert(acc.clone(), id);
push_child(&mut nodes, parent_id, id);
parent_id = id;
}
}
}
nodes
}
fn push_child(nodes: &mut [Node], parent_id: u64, child: u64) {
if let Some(parent) = nodes.get_mut(usize::try_from(parent_id).unwrap_or(usize::MAX)) {
parent.children.push(child);
}
}
impl<R: Read + Seek + Send> FileSystem for ZipVfs<R> {
fn kind(&self) -> FsKind {
FsKind::Other
}
fn root(&self) -> FileId {
FileId::Opaque(0)
}
fn sector_sizes(&self) -> SectorSizes {
SectorSizes {
logical: ARCHIVE_BLOCK,
physical: ARCHIVE_BLOCK,
cluster_or_block: ARCHIVE_BLOCK,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let node = self.node_of(ino)?;
if node.kind != NodeKind::Dir {
return Err(VfsError::Decode {
layer: "zip",
offset: 0,
detail: format!("node {:?} is not a directory", index_of(ino)?),
bytes: forensic_vfs::SmallHex::new(&[]),
});
}
let mut out: Vec<VfsResult<VfsDirEntry>> = Vec::with_capacity(node.children.len());
for &child in &node.children {
let Some(c) = self.nodes.get(usize::try_from(child).unwrap_or(usize::MAX)) else {
continue; };
out.push(Ok(VfsDirEntry {
name: c.name.clone(),
id: FileId::Opaque(child),
kind: c.kind,
}));
}
Ok(DirStream::new(out.into_iter()))
}
fn extents(&self, ino: FileId, stream: StreamId) -> VfsResult<ExtentStream> {
let node = self.node_of(ino)?;
require_default_stream(stream)?;
if node.size == 0 {
return Ok(ExtentStream::empty());
}
let run = RunInfo {
run: ByteRun {
image_offset: 0,
len: node.size,
flags: RunFlags::default(),
},
alloc: RunAlloc::Allocated,
};
Ok(ExtentStream::new(std::iter::once(Ok(run))))
}
fn lookup(&self, parent: FileId, name: &[u8]) -> VfsResult<Option<FileId>> {
let node = self.node_of(parent)?;
if node.kind != NodeKind::Dir {
return Err(VfsError::Decode {
layer: "zip",
offset: 0,
detail: format!("node {:?} is not a directory", index_of(parent)?),
bytes: forensic_vfs::SmallHex::new(&[]),
});
}
for &child in &node.children {
if let Some(c) = self.nodes.get(usize::try_from(child).unwrap_or(usize::MAX)) {
if c.name == name {
return Ok(Some(FileId::Opaque(child)));
}
}
}
Ok(None)
}
fn meta(&self, ino: FileId) -> VfsResult<FsMeta> {
let idx = index_of(ino)?;
let node = self.node_of(ino)?;
Ok(FsMeta {
ino: idx,
kind: node.kind,
allocated: Allocation::Allocated,
size: node.size,
nlink: 1,
uid: None,
gid: None,
mode: None,
times: MacbTimes {
modified: node.modified,
accessed: node.accessed,
changed: None,
born: node.born,
},
streams: Vec::new(),
residency: ResidencyKind::NonResident,
link_target: None,
})
}
fn read_at(&self, ino: FileId, stream: StreamId, off: u64, buf: &mut [u8]) -> VfsResult<usize> {
let idx = index_of(ino)?;
require_default_stream(stream)?;
let (kind, entry_idx) = {
let node = self.node_of(ino)?;
(node.kind, node.entry_idx)
};
if kind != NodeKind::File {
return Ok(0);
}
let Some(entry_idx) = entry_idx else {
return Ok(0); };
let data = self.content(idx, entry_idx)?;
let Ok(start) = usize::try_from(off) else {
return Ok(0); };
if start >= data.len() {
return Ok(0);
}
let n = (data.len() - start).min(buf.len());
if let (Some(dst), Some(src)) = (buf.get_mut(..n), data.get(start..start + n)) {
dst.copy_from_slice(src);
}
Ok(n)
}
fn read_link(&self, ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
self.node_of(ino)?;
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<NodeStream> {
Ok(NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<ExtentStream> {
Ok(ExtentStream::empty())
}
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Read, Seek, SeekFrom};
use forensic_vfs::{
Allocation, FileId, FileSystem, FsKind, NodeKind, RunAlloc, StreamId, TimeResolution,
TimeZonePolicy, VfsError,
};
use super::ZipVfs;
fn big_payload() -> Vec<u8> {
(0..8192u32).map(|i| (i % 251) as u8).collect()
}
fn oracle_zip() -> Vec<u8> {
include_bytes!("../../tests/data/oracle-infozip.zip").to_vec()
}
fn open() -> ZipVfs<Cursor<Vec<u8>>> {
ZipVfs::open(Cursor::new(oracle_zip())).expect("open oracle zip")
}
fn resolve(fs: &ZipVfs<Cursor<Vec<u8>>>, parts: &[&[u8]]) -> FileId {
let mut id = fs.root();
for p in parts {
id = fs.lookup(id, p).expect("lookup").expect("present");
}
id
}
fn read_all(fs: &ZipVfs<Cursor<Vec<u8>>>, id: FileId) -> Vec<u8> {
let mut out = Vec::new();
let mut off = 0u64;
loop {
let mut buf = [0u8; 8];
let n = fs
.read_at(id, StreamId::Default, off, &mut buf)
.expect("read_at");
if n == 0 {
break;
}
out.extend_from_slice(&buf[..n]);
off += n as u64;
}
out
}
struct Ent<'a> {
name: &'a str,
flags: u16,
method: u16,
data: &'a [u8],
extra: &'a [u8],
}
impl<'a> Ent<'a> {
fn stored(name: &'a str, data: &'a [u8]) -> Self {
Self {
name,
flags: 0,
method: 0,
data,
extra: &[],
}
}
}
fn mint_zip(entries: &[Ent<'_>]) -> Vec<u8> {
let mut o: Vec<u8> = Vec::new();
let mut cd: Vec<u8> = Vec::new();
for e in entries {
let n = e.name.as_bytes();
let mut h = crc32fast::Hasher::new();
h.update(e.data);
let crc = h.finalize();
let lfh_offset = o.len() as u32;
let sz = e.data.len() as u32;
o.extend_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
o.extend_from_slice(&20u16.to_le_bytes()); o.extend_from_slice(&e.flags.to_le_bytes());
o.extend_from_slice(&e.method.to_le_bytes());
o.extend_from_slice(&0u32.to_le_bytes()); o.extend_from_slice(&crc.to_le_bytes());
o.extend_from_slice(&sz.to_le_bytes()); o.extend_from_slice(&sz.to_le_bytes()); o.extend_from_slice(&(n.len() as u16).to_le_bytes());
o.extend_from_slice(&0u16.to_le_bytes()); o.extend_from_slice(n);
o.extend_from_slice(e.data);
cd.extend_from_slice(&[0x50, 0x4b, 0x01, 0x02]);
cd.extend_from_slice(&20u16.to_le_bytes()); cd.extend_from_slice(&20u16.to_le_bytes()); cd.extend_from_slice(&e.flags.to_le_bytes());
cd.extend_from_slice(&e.method.to_le_bytes());
cd.extend_from_slice(&0u32.to_le_bytes()); cd.extend_from_slice(&crc.to_le_bytes());
cd.extend_from_slice(&sz.to_le_bytes());
cd.extend_from_slice(&sz.to_le_bytes());
cd.extend_from_slice(&(n.len() as u16).to_le_bytes());
cd.extend_from_slice(&(e.extra.len() as u16).to_le_bytes());
cd.extend_from_slice(&0u16.to_le_bytes()); cd.extend_from_slice(&0u16.to_le_bytes()); cd.extend_from_slice(&0u16.to_le_bytes()); cd.extend_from_slice(&0u32.to_le_bytes()); cd.extend_from_slice(&lfh_offset.to_le_bytes());
cd.extend_from_slice(n);
cd.extend_from_slice(e.extra);
}
let cd_offset = o.len() as u32;
let cd_size = cd.len() as u32;
let count = entries.len() as u16;
o.extend_from_slice(&cd);
o.extend_from_slice(&[0x50, 0x4b, 0x05, 0x06]);
o.extend_from_slice(&0u16.to_le_bytes()); o.extend_from_slice(&0u16.to_le_bytes()); o.extend_from_slice(&count.to_le_bytes());
o.extend_from_slice(&count.to_le_bytes());
o.extend_from_slice(&cd_size.to_le_bytes());
o.extend_from_slice(&cd_offset.to_le_bytes());
o.extend_from_slice(&0u16.to_le_bytes()); o
}
fn extra_record(id: u16, data: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&id.to_le_bytes());
v.extend_from_slice(&(data.len() as u16).to_le_bytes());
v.extend_from_slice(data);
v
}
fn ntfs_extra(mtime: u64, atime: u64, ctime: u64) -> Vec<u8> {
let mut p = Vec::new();
p.extend_from_slice(&0u32.to_le_bytes()); p.extend_from_slice(&0x0001u16.to_le_bytes()); p.extend_from_slice(&0x0018u16.to_le_bytes()); p.extend_from_slice(&mtime.to_le_bytes());
p.extend_from_slice(&atime.to_le_bytes());
p.extend_from_slice(&ctime.to_le_bytes());
extra_record(0x000a, &p)
}
fn mount(bytes: Vec<u8>) -> ZipVfs<Cursor<Vec<u8>>> {
ZipVfs::open(Cursor::new(bytes)).expect("mount minted zip")
}
fn mount_err<R: Read + Seek + Send>(reader: R) -> VfsError {
ZipVfs::open(reader).err().expect("the mount must fail")
}
struct FailingReader;
impl Read for FailingReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("synthetic read failure"))
}
}
impl Seek for FailingReader {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
match pos {
SeekFrom::End(_) => Ok(4096),
_ => Ok(0),
}
}
}
#[test]
fn kind_root_zone_and_sectors() {
let fs = open();
assert_eq!(fs.kind(), FsKind::Other);
assert!(matches!(fs.root(), FileId::Opaque(0)));
assert_eq!(fs.timestamp_zone(), TimeZonePolicy::Utc);
let ss = fs.sector_sizes();
assert_eq!(ss.logical, 512);
assert_eq!(ss.cluster_or_block, 512);
assert!(ss.physical >= 512);
assert_eq!(fs.meta(fs.root()).expect("root meta").kind, NodeKind::Dir);
}
#[test]
fn lists_root_entries() {
let fs = open();
let names: Vec<Vec<u8>> = fs
.read_dir(fs.root())
.expect("read_dir root")
.map(|e| e.expect("entry").name)
.collect();
assert!(names.iter().any(|n| n == b"hello.txt"), "got {names:?}");
assert!(names.iter().any(|n| n == b"sub"), "got {names:?}");
}
#[test]
fn resolves_and_reads_hello() {
let fs = open();
let id = resolve(&fs, &[b"hello.txt"]);
let m = fs.meta(id).expect("meta");
assert_eq!(m.kind, NodeKind::File);
assert_eq!(m.size, b"hello zip\n".len() as u64);
assert_eq!(m.allocated, Allocation::Allocated);
assert!(m.times.changed.is_none());
assert!(m.times.modified.is_some(), "Info-ZIP stamps mtime");
assert_eq!(read_all(&fs, id), b"hello zip\n");
}
#[test]
fn reads_big_file_spanning_and_offset() {
let fs = open();
let id = resolve(&fs, &[b"sub", b"big.bin"]);
let want = big_payload();
assert_eq!(fs.meta(id).expect("meta").size, want.len() as u64);
assert_eq!(read_all(&fs, id), want);
let mut buf = [0u8; 16];
let n = fs
.read_at(id, StreamId::Default, 4096, &mut buf)
.expect("read");
assert_eq!(&buf[..n], &want[4096..4096 + n]);
}
#[test]
fn directory_reports_dir_kind() {
let fs = open();
let id = resolve(&fs, &[b"sub"]);
assert_eq!(fs.meta(id).expect("meta").kind, NodeKind::Dir);
assert!(fs.read_dir(id).is_ok());
}
#[test]
fn extents_hello_single_run_and_root() {
let fs = open();
let id = resolve(&fs, &[b"hello.txt"]);
let runs: Vec<_> = fs
.extents(id, StreamId::Default)
.expect("extents")
.map(|r| r.expect("run"))
.collect();
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].run.len, b"hello zip\n".len() as u64);
assert_eq!(runs[0].alloc, RunAlloc::Allocated);
let root_runs: Vec<_> = fs
.extents(fs.root(), StreamId::Default)
.expect("root extents")
.map(|r| r.expect("run"))
.collect();
assert!(root_runs.len() <= 1);
}
#[test]
fn wrong_file_id_and_stream_are_loud() {
let fs = open();
let bad = FileId::NtfsRef { entry: 5, seq: 1 };
assert!(fs.meta(bad).is_err());
assert!(fs.read_dir(bad).is_err());
assert!(fs.lookup(bad, b"x").is_err());
assert!(fs.read_link(bad, 8).is_err());
assert!(fs.meta(FileId::Opaque(9_999_999)).is_err());
let id = resolve(&fs, &[b"hello.txt"]);
assert!(fs
.read_at(id, StreamId::Named(1), 0, &mut [0u8; 4])
.is_err());
assert!(fs.extents(id, StreamId::Named(1)).is_err());
assert!(fs.read_dir(id).is_err());
}
#[test]
fn lookup_missing_is_none() {
let fs = open();
assert!(fs
.lookup(fs.root(), b"NOPE.NOTPRESENT")
.expect("lookup")
.is_none());
}
#[test]
fn empty_forensic_surfaces() {
let fs = open();
assert_eq!(fs.deleted().expect("deleted").count(), 0);
assert_eq!(fs.unallocated().expect("unallocated").count(), 0);
let id = resolve(&fs, &[b"hello.txt"]);
assert!(fs.read_link(id, 4096).expect("read_link").is_empty());
}
#[test]
fn read_fault_maps_to_vfs_io() {
let err = mount_err(FailingReader);
assert!(
matches!(err, VfsError::Io { op: "zip read", .. }),
"got {err:?}"
);
}
#[test]
fn not_a_zip_is_a_loud_bootstrap_failure() {
let err = mount_err(Cursor::new(vec![0x41u8; 512]));
assert!(
matches!(
&err,
VfsError::Bootstrap { stage, detail }
if *stage == "zip mount" && detail.contains("End Of Central Directory")
),
"got {err:?}"
);
}
#[test]
fn bad_local_header_signature_is_a_decode_failure() {
let mut bytes = mint_zip(&[Ent::stored("f.txt", b"payload")]);
bytes[3] = 0xFF;
let err = mount_err(Cursor::new(bytes));
assert!(
matches!(err, VfsError::Decode { layer: "zip", .. }),
"got {err:?}"
);
}
#[test]
fn encrypted_entry_is_navigable_but_reading_it_is_refused() {
let fs = mount(mint_zip(&[Ent {
name: "secret.bin",
flags: 0x0001,
method: 0,
data: b"ciphertext",
extra: &[],
}]));
let id = resolve(&fs, &[b"secret.bin"]);
assert_eq!(fs.meta(id).expect("meta").size, b"ciphertext".len() as u64);
let err = fs
.read_at(id, StreamId::Default, 0, &mut [0u8; 4])
.expect_err("no password was supplied");
assert!(
matches!(err, VfsError::Unsupported { layer: "zip", .. }),
"got {err:?}"
);
}
#[test]
fn corrupt_compressed_stream_surfaces_the_decode_io_error() {
let fs = mount(mint_zip(&[Ent {
name: "broken.bin",
flags: 0,
method: 8,
data: &[0xFF, 0xFF, 0xFF, 0xFF],
extra: &[],
}]));
let id = resolve(&fs, &[b"broken.bin"]);
let err = fs
.read_at(id, StreamId::Default, 0, &mut [0u8; 8])
.expect_err("garbage is not a deflate stream");
assert!(
matches!(err, VfsError::Io { op: "zip read", .. }),
"got {err:?}"
);
}
#[test]
fn ntfs_filetime_extras_convert_and_outrank_unix_seconds() {
const EPOCH_DIFF: u64 = 116_444_736_000_000_000;
let mtime = EPOCH_DIFF + 1_600_000_000 * 10_000_000;
let atime = EPOCH_DIFF + 1_600_000_001 * 10_000_000;
let ctime = EPOCH_DIFF + 1_600_000_002 * 10_000_000;
let mut uxt = vec![0x07u8]; uxt.extend_from_slice(&1i32.to_le_bytes());
uxt.extend_from_slice(&2i32.to_le_bytes());
uxt.extend_from_slice(&3i32.to_le_bytes());
let mut extra = ntfs_extra(mtime, atime, ctime);
extra.extend_from_slice(&extra_record(0x5455, &uxt));
let fs = mount(mint_zip(&[Ent {
name: "stamped.txt",
flags: 0,
method: 0,
data: b"x",
extra: &extra,
}]));
let times = fs
.meta(resolve(&fs, &[b"stamped.txt"]))
.expect("meta")
.times;
let m = times.modified.expect("modified");
assert_eq!(m.unix_nanos, 1_600_000_000_000_000_000);
assert_eq!(m.resolution, TimeResolution::WinFileTime);
assert_eq!(
times.accessed.expect("accessed").unix_nanos,
1_600_000_001_000_000_000
);
assert_eq!(
times.born.expect("born").unix_nanos,
1_600_000_002_000_000_000
);
assert!(times.changed.is_none());
}
#[test]
fn implied_parent_directories_are_synthesized() {
let fs = mount(mint_zip(&[Ent::stored("a/b/c.txt", b"deep")]));
let a = resolve(&fs, &[b"a"]);
let a_meta = fs.meta(a).expect("meta a");
assert_eq!(a_meta.kind, NodeKind::Dir);
assert_eq!(a_meta.size, 0);
assert!(a_meta.times.modified.is_none(), "synthesized, not stamped");
let b = resolve(&fs, &[b"a", b"b"]);
assert_eq!(fs.meta(b).expect("meta b").kind, NodeKind::Dir);
let names: Vec<Vec<u8>> = fs
.read_dir(b)
.expect("read_dir a/b")
.map(|e| e.expect("entry").name)
.collect();
assert_eq!(names, vec![b"c.txt".to_vec()]);
assert_eq!(
read_all(&fs, resolve(&fs, &[b"a", b"b", b"c.txt"])),
b"deep"
);
}
#[test]
fn an_explicit_record_adopts_the_directory_implied_before_it() {
const EPOCH_DIFF: u64 = 116_444_736_000_000_000;
let stamp = ntfs_extra(
EPOCH_DIFF + 1_700_000_000 * 10_000_000,
EPOCH_DIFF,
EPOCH_DIFF,
);
let fs = mount(mint_zip(&[
Ent::stored("d/f.txt", b"leaf"),
Ent {
name: "d/",
flags: 0,
method: 0,
data: b"",
extra: &stamp,
},
]));
let root_names: Vec<Vec<u8>> = fs
.read_dir(fs.root())
.expect("read_dir root")
.map(|e| e.expect("entry").name)
.collect();
assert_eq!(root_names, vec![b"d".to_vec()], "no duplicate `d` node");
let d = resolve(&fs, &[b"d"]);
assert_eq!(fs.meta(d).expect("meta d").kind, NodeKind::Dir);
assert_eq!(
fs.meta(d)
.expect("meta d")
.times
.modified
.expect("adopted mtime")
.unix_nanos,
1_700_000_000_000_000_000
);
assert_eq!(read_all(&fs, resolve(&fs, &[b"d", b"f.txt"])), b"leaf");
}
#[test]
fn a_bare_separator_entry_names_no_node() {
let fs = mount(mint_zip(&[
Ent::stored("/", b""),
Ent::stored("ok.txt", b"fine"),
]));
let names: Vec<Vec<u8>> = fs
.read_dir(fs.root())
.expect("read_dir root")
.map(|e| e.expect("entry").name)
.collect();
assert_eq!(names, vec![b"ok.txt".to_vec()]);
}
#[test]
fn lookup_under_a_file_is_loud() {
let fs = open();
let file = resolve(&fs, &[b"hello.txt"]);
let err = fs
.lookup(file, b"anything")
.expect_err("a file has no children");
assert!(
matches!(
&err,
VfsError::Decode { layer: "zip", detail, .. }
if detail.contains("not a directory")
),
"got {err:?}"
);
}
#[test]
fn reading_a_directory_yields_no_bytes() {
let fs = open();
let mut buf = [0xAAu8; 16];
assert_eq!(
fs.read_at(fs.root(), StreamId::Default, 0, &mut buf)
.expect("root read"),
0
);
assert_eq!(
fs.read_at(resolve(&fs, &[b"sub"]), StreamId::Default, 0, &mut buf)
.expect("dir read"),
0
);
assert_eq!(buf, [0xAAu8; 16], "the buffer is left untouched");
}
}