use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use forensic_vfs::{
Allocation, ByteRun, DirEntry as VfsDirEntry, DirStream, DynFs, DynSource, ExtentStream,
FileId, FileSystem, FsKind, FsMeta, MacbTimes, NodeKind, NodeStream, ResidencyKind, RunAlloc,
RunFlags, RunInfo, SectorSizes, SmallHex, StreamId, TimeZonePolicy, VfsError, VfsResult,
};
const ARCHIVE_BLOCK: u32 = 512;
trait Members: Send {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>>;
}
struct Flat {
name: String,
size: u64,
is_dir: bool,
index: usize,
}
struct Node {
entry_idx: Option<usize>,
name: Vec<u8>,
kind: NodeKind,
size: u64,
children: Vec<u64>,
}
struct Inner<R: Members> {
reader: R,
cache: HashMap<u64, Arc<Vec<u8>>>,
}
struct ContainerFs<R: Members> {
inner: Mutex<Inner<R>>,
nodes: Vec<Node>,
kind: FsKind,
}
impl<R: Members> ContainerFs<R> {
fn new(reader: R, nodes: Vec<Node>, kind: FsKind) -> Self {
Self {
inner: Mutex::new(Inner {
reader,
cache: HashMap::new(),
}),
nodes,
kind,
}
}
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: "container 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 = inner.reader.read_member(entry_idx)?;
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: "container file-id",
scheme: format!("{other:?}"),
}),
}
}
fn require_default_stream(stream: StreamId) -> VfsResult<()> {
match stream {
StreamId::Default => Ok(()),
other => Err(VfsError::Unsupported {
layer: "container stream",
scheme: format!("{other:?}"),
}),
}
}
fn build_tree(members: &[Flat]) -> Vec<Node> {
let mut nodes: Vec<Node> = Vec::with_capacity(members.len() + 1);
nodes.push(Node {
entry_idx: None,
name: Vec::new(),
kind: NodeKind::Dir,
size: 0,
children: Vec::new(),
});
let mut by_path: HashMap<String, u64> = HashMap::new();
by_path.insert(String::new(), 0);
for m in members {
let comps: Vec<&str> = m
.name
.split(['/', '\\'])
.filter(|c| !c.is_empty() && *c != ".")
.collect();
let Some(last) = comps.len().checked_sub(1) else {
continue; };
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) {
let Some(n) = nodes.get_mut(usize::try_from(existing).unwrap_or(usize::MAX))
else {
continue; };
if n.entry_idx.is_none() {
n.entry_idx = Some(m.index);
}
} else {
let id = nodes.len() as u64;
nodes.push(Node {
entry_idx: Some(m.index),
name: comp.as_bytes().to_vec(),
kind: if m.is_dir {
NodeKind::Dir
} else {
NodeKind::File
},
size: if m.is_dir { 0 } else { m.size },
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,
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: Members> FileSystem for ContainerFs<R> {
fn kind(&self) -> FsKind {
self.kind
}
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::LocalUnknown
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let node = self.node_of(ino)?;
if node.kind != NodeKind::Dir {
return Err(not_a_dir(ino)?);
}
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(not_a_dir(parent)?);
}
for &child in &node.children {
let Some(c) = self.nodes.get(usize::try_from(child).unwrap_or(usize::MAX)) else {
continue; };
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::default(),
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())
}
}
fn not_a_dir(id: FileId) -> VfsResult<VfsError> {
Ok(VfsError::Decode {
layer: "container",
offset: 0,
detail: format!("node {:?} is not a directory", index_of(id)?),
bytes: SmallHex::new(&[]),
})
}
struct ArchiveBackend(archive_core::Archive);
impl Members for ArchiveBackend {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>> {
self.0.read(index).map_err(|e| VfsError::Decode {
layer: "archive",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})
}
}
fn archive_kind(format: archive_core::Format) -> FsKind {
match format {
archive_core::Format::Zip => FsKind::ZIP,
archive_core::Format::SevenZip => FsKind::from_name("7z"),
_ => FsKind::from_name("tar"),
}
}
pub(crate) fn open_archive(base: &DynSource, name: Option<&str>) -> VfsResult<Option<DynFs>> {
let len = base.len();
let mut bytes = vec![0u8; usize::try_from(len).unwrap_or(usize::MAX)];
let n = base.read_at(0, &mut bytes)?;
bytes.truncate(n);
let Some(archive) =
archive_core::Archive::open(&bytes, name).map_err(|e| VfsError::Decode {
layer: "archive",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})?
else {
return Ok(None);
};
let kind = archive_kind(archive.format());
let members: Vec<Flat> = archive
.entries()
.iter()
.enumerate()
.map(|(index, e)| Flat {
name: e.name.clone(),
size: e.size,
is_dir: e.is_dir,
index,
})
.collect();
let nodes = build_tree(&members);
Ok(Some(Arc::new(ContainerFs::new(
ArchiveBackend(archive),
nodes,
kind,
))))
}
pub(crate) fn open_ad1(path: &Path) -> VfsResult<Option<DynFs>> {
match ad1::Ad1Vfs::open(path) {
Ok(fs) => Ok(Some(Arc::new(fs))),
Err(VfsError::Bootstrap {
stage: "ad1 mount", ..
}) => Ok(None),
Err(e) => Err(e),
}
}
struct DarBackend {
reader: dar::DarReader<std::fs::File>,
paths: Vec<Vec<u8>>,
}
impl Members for DarBackend {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>> {
let key = self
.paths
.get(index)
.ok_or(VfsError::Unsupported {
layer: "dar member",
scheme: format!("index {index} out of range"),
})?
.clone();
self.reader.extract(&key).map_err(|e| VfsError::Decode {
layer: "dar",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})
}
}
pub(crate) fn open_dar(path: &Path) -> VfsResult<Option<DynFs>> {
let file = std::fs::File::open(path).map_err(|source| VfsError::Io {
op: "dar open",
source,
})?;
let reader = match dar::DarReader::open(file) {
Ok(r) => r,
Err(dar::DarError::NotADar) => return Ok(None),
Err(e) => {
return Err(VfsError::Decode {
layer: "dar",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})
}
};
let entries = reader.entries();
let mut paths: Vec<Vec<u8>> = Vec::with_capacity(entries.len());
let members: Vec<Flat> = entries
.iter()
.enumerate()
.map(|(index, e)| {
paths.push(e.path.clone());
Flat {
name: String::from_utf8_lossy(&e.path).into_owned(),
size: e.size,
is_dir: matches!(e.kind, dar::EntryKind::Directory),
index,
}
})
.collect();
let nodes = build_tree(&members);
Ok(Some(Arc::new(ContainerFs::new(
DarBackend { reader, paths },
nodes,
FsKind::DAR,
))))
}
struct Aff4LogicalBackend(aff4::LogicalContainer);
impl Members for Aff4LogicalBackend {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>> {
let entry = self
.0
.files()
.get(index)
.ok_or(VfsError::Unsupported {
layer: "aff4-logical member",
scheme: format!("index {index} out of range"),
})?
.clone();
self.0.read_file(&entry).map_err(|e| VfsError::Decode {
layer: "aff4-logical",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})
}
}
pub(crate) fn open_aff4_logical(path: &Path) -> VfsResult<Option<DynFs>> {
match aff4::container_kind(path) {
Ok(aff4::ContainerKind::Logical) => {}
Ok(_) | Err(_) => return Ok(None),
}
let container = aff4::LogicalContainer::open(path).map_err(|e| VfsError::Decode {
layer: "aff4-logical",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})?;
let members: Vec<Flat> = container
.files()
.iter()
.enumerate()
.map(|(index, e)| Flat {
name: e.original_file_name.clone(),
size: e.size,
is_dir: false,
index,
})
.collect();
let nodes = build_tree(&members);
Ok(Some(Arc::new(ContainerFs::new(
Aff4LogicalBackend(container),
nodes,
FsKind::from_name("aff4"),
))))
}
#[cfg(test)]
mod tests {
use super::*;
use forensic_vfs::{FileId, ImageSource};
use std::io::Write;
const DAR_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/loose.dar");
struct Mem(Vec<u8>);
impl ImageSource for Mem {
fn len(&self) -> u64 {
self.0.len() as u64
}
fn read_at(&self, off: u64, buf: &mut [u8]) -> VfsResult<usize> {
let o = usize::try_from(off).unwrap_or(usize::MAX).min(self.0.len());
let s = &self.0[o..];
let n = s.len().min(buf.len());
buf[..n].copy_from_slice(&s[..n]);
Ok(n)
}
}
fn mem(b: Vec<u8>) -> DynSource {
Arc::new(Mem(b))
}
fn plain_zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut c = std::io::Cursor::new(Vec::new());
{
let opts = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
let mut zw = zip::ZipWriter::new(&mut c);
for (name, data) in entries {
zw.start_file(*name, opts).unwrap();
zw.write_all(data).unwrap();
}
zw.finish().unwrap();
}
c.into_inner()
}
struct MockMembers {
data: Vec<Vec<u8>>,
fail_index: Option<usize>,
}
impl Members for MockMembers {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>> {
if Some(index) == self.fail_index {
return Err(VfsError::Io {
op: "mock read",
source: std::io::Error::other("boom"),
});
}
self.data
.get(index)
.cloned()
.ok_or_else(|| VfsError::Unsupported {
layer: "mock",
scheme: format!("index {index} out of range"),
})
}
}
fn sample_members() -> Vec<Flat> {
vec![
Flat {
name: "/".into(),
size: 0,
is_dir: false,
index: 0,
},
Flat {
name: "top.txt".into(),
size: 14,
is_dir: false,
index: 1,
},
Flat {
name: "onlydir/".into(),
size: 0,
is_dir: true,
index: 2,
},
Flat {
name: "onlydir/inside.txt".into(),
size: 6,
is_dir: false,
index: 3,
},
Flat {
name: "imp/leaf.txt".into(),
size: 4,
is_dir: false,
index: 4,
},
Flat {
name: "imp".into(),
size: 0,
is_dir: true,
index: 5,
},
Flat {
name: "empty.dat".into(),
size: 0,
is_dir: false,
index: 6,
},
]
}
fn sample_fs(fail_index: Option<usize>) -> ContainerFs<MockMembers> {
let members = sample_members();
let nodes = build_tree(&members);
let data = vec![
Vec::new(), b"hello from mock".to_vec(), Vec::new(), b"inside".to_vec(), b"leaf".to_vec(), Vec::new(), Vec::new(), ];
ContainerFs::new(
MockMembers { data, fail_index },
nodes,
FsKind::from_name("mock"),
)
}
fn child(fs: &ContainerFs<MockMembers>, parent: FileId, name: &[u8]) -> FileId {
fs.lookup(parent, name).unwrap().unwrap()
}
#[test]
fn container_fs_surface_and_arms() {
let fs = sample_fs(None);
let root = fs.root();
assert_eq!(fs.kind(), FsKind::from_name("mock"));
assert_eq!(root, FileId::Opaque(0));
assert_eq!(fs.sector_sizes().logical, ARCHIVE_BLOCK);
assert!(matches!(
fs.timestamp_zone(),
forensic_vfs::TimeZonePolicy::LocalUnknown
));
let root_names: Vec<Vec<u8>> = fs
.read_dir(root)
.unwrap()
.map(|e| e.unwrap().name)
.collect();
assert!(root_names.iter().any(|n| n == b"top.txt"));
assert!(root_names.iter().any(|n| n == b"onlydir"));
assert!(root_names.iter().any(|n| n == b"imp"));
assert!(root_names.iter().any(|n| n == b"empty.dat"));
let onlydir = child(&fs, root, b"onlydir");
assert_eq!(fs.meta(onlydir).unwrap().kind, NodeKind::Dir);
let inside = child(&fs, onlydir, b"inside.txt");
assert_eq!(fs.meta(inside).unwrap().kind, NodeKind::File);
let imp = child(&fs, root, b"imp");
let _leaf = child(&fs, imp, b"leaf.txt");
let top = child(&fs, root, b"top.txt");
let runs: Vec<_> = fs
.extents(top, StreamId::Default)
.unwrap()
.map(|r| r.unwrap())
.collect();
assert_eq!(runs.len(), 1);
let empty = child(&fs, root, b"empty.dat");
assert_eq!(fs.extents(empty, StreamId::Default).unwrap().count(), 0);
let mut buf = vec![0u8; 32];
let n = fs.read_at(top, StreamId::Default, 0, &mut buf).unwrap();
assert_eq!(&buf[..n], b"hello from mock");
let n2 = fs.read_at(top, StreamId::Default, 6, &mut buf).unwrap();
assert_eq!(&buf[..n2], b"from mock");
assert_eq!(
fs.read_at(top, StreamId::Default, 9999, &mut buf).unwrap(),
0
);
assert_eq!(
fs.read_at(onlydir, StreamId::Default, 0, &mut buf).unwrap(),
0
);
assert!(fs.read_link(top, 0).unwrap().is_empty());
assert_eq!(fs.deleted().unwrap().count(), 0);
assert_eq!(fs.unallocated().unwrap().count(), 0);
assert!(fs.meta(FileId::IsoExtent { block: 7 }).is_err());
assert!(fs.read_at(top, StreamId::Slack, 0, &mut buf).is_err());
assert!(fs.extents(top, StreamId::Named(1)).is_err());
assert!(fs.read_dir(top).is_err());
assert!(fs.lookup(top, b"x").is_err());
assert!(fs.lookup(root, b"nope").unwrap().is_none());
}
#[test]
fn content_error_propagates() {
let fs = sample_fs(Some(1));
let top = child(&fs, fs.root(), b"top.txt");
assert!(fs
.read_at(top, StreamId::Default, 0, &mut [0u8; 8])
.is_err());
let mut empty = MockMembers {
data: Vec::new(),
fail_index: None,
};
assert!(empty.read_member(5).is_err());
}
#[test]
fn archive_kind_maps_each_format() {
assert_eq!(archive_kind(archive_core::Format::Zip), FsKind::ZIP);
assert_eq!(
archive_kind(archive_core::Format::SevenZip),
FsKind::from_name("7z")
);
assert_eq!(
archive_kind(archive_core::Format::Tar),
FsKind::from_name("tar")
);
}
#[test]
fn archive_backend_read_and_error() {
let bytes = plain_zip(&[("a.txt", b"hi")]);
let archive = archive_core::Archive::open(&bytes, Some("a.zip"))
.unwrap()
.unwrap();
let mut backend = ArchiveBackend(archive);
assert_eq!(backend.read_member(0).unwrap(), b"hi");
assert!(backend.read_member(9999).is_err());
}
#[test]
fn open_archive_rejects_corrupt_zip() {
let corrupt = mem(b"PK\x03\x04corrupt-not-a-real-zip".to_vec());
assert!(open_archive(&corrupt, Some("x.zip")).is_err());
}
#[test]
fn open_aff4_logical_declines_a_plain_zip() {
let mut f = tempfile::Builder::new().suffix(".zip").tempfile().unwrap();
f.write_all(&plain_zip(&[("hello.txt", b"hi")])).unwrap();
f.flush().unwrap();
assert!(open_aff4_logical(f.path()).unwrap().is_none());
}
#[test]
fn dar_backend_read_and_oob() {
let file = std::fs::File::open(DAR_FIXTURE).unwrap();
let reader = dar::DarReader::open(file).unwrap();
let entries = reader.entries();
let paths: Vec<Vec<u8>> = entries.iter().map(|e| e.path.clone()).collect();
let file_idx = entries
.iter()
.position(|e| !matches!(e.kind, dar::EntryKind::Directory))
.unwrap();
let mut backend = DarBackend { reader, paths };
assert!(backend.read_member(file_idx).is_ok());
let reader2 = dar::DarReader::open(std::fs::File::open(DAR_FIXTURE).unwrap()).unwrap();
let mut oob = DarBackend {
reader: reader2,
paths: Vec::new(),
};
assert!(oob.read_member(0).is_err());
}
#[test]
fn open_dar_missing_file_is_io_error() {
assert!(open_dar(Path::new("/nonexistent/definitely-not-here.dar")).is_err());
}
#[test]
fn dar_backend_surfaces_an_extract_error() {
let reader = dar::DarReader::open(std::fs::File::open(DAR_FIXTURE).unwrap()).unwrap();
let mut backend = DarBackend {
reader,
paths: vec![b"no/such/member".to_vec()],
};
assert!(backend.read_member(0).is_err());
}
#[test]
fn open_dar_surfaces_a_parse_error() {
let ok = std::fs::read(DAR_FIXTURE).unwrap();
let mut f = tempfile::Builder::new().suffix(".dar").tempfile().unwrap();
f.write_all(&ok[..64]).unwrap();
f.flush().unwrap();
assert!(open_dar(f.path()).is_err());
}
#[test]
fn open_ad1_surfaces_a_parse_error() {
let built = ad1::testfix::build(ad1::testfix::sample_tree());
let mut f = tempfile::Builder::new().suffix(".ad1").tempfile().unwrap();
f.write_all(&built.bytes[..64]).unwrap();
f.flush().unwrap();
assert!(open_ad1(f.path()).is_err());
}
#[test]
fn aff4_logical_read_error_propagates() {
let ok = aff4::testutil::test_aff4_logical(
"hello.txt",
b"HELLO_AFF4_PAYLOAD_UNIQUE_MARKER",
"00000000000000000000000000000000",
);
let marker = b"HELLO_AFF4_PAYLOAD_UNIQUE_MARKER";
let pos = ok.windows(marker.len()).position(|w| w == marker).unwrap();
let mut bytes = ok.clone();
bytes[pos + 2] ^= 0xff;
let mut f = tempfile::Builder::new().suffix(".aff4").tempfile().unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
let fs = open_aff4_logical(f.path()).unwrap().unwrap();
let hello = fs.lookup(fs.root(), b"hello.txt").unwrap().unwrap();
assert!(fs
.read_at(hello, StreamId::Default, 0, &mut [0u8; 64])
.is_err());
}
#[test]
fn open_aff4_logical_surfaces_an_open_error() {
let ok = aff4::testutil::test_aff4_logical(
"hello.txt",
b"payload",
"00000000000000000000000000000000",
);
let turtle = {
let mut za = zip::ZipArchive::new(std::io::Cursor::new(ok)).unwrap();
let mut e = za.by_name("information.turtle").unwrap();
let mut s = Vec::new();
std::io::Read::read_to_end(&mut e, &mut s).unwrap();
s
};
let mut c = std::io::Cursor::new(Vec::new());
{
let opts = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
let mut zw = zip::ZipWriter::new(&mut c);
zw.start_file("information.turtle", opts).unwrap();
zw.write_all(&turtle).unwrap();
zw.finish().unwrap();
}
let mut f = tempfile::Builder::new().suffix(".aff4").tempfile().unwrap();
f.write_all(&c.into_inner()).unwrap();
f.flush().unwrap();
assert!(open_aff4_logical(f.path()).is_err());
}
}