mod crypto;
mod error;
mod logical;
mod map;
mod meta;
#[cfg(any(test, feature = "test-helpers"))]
pub mod testutil;
pub use crypto::{decrypt_encrypted_stream, decrypt_reader};
pub use error::Aff4Error;
pub use logical::{LogicalContainer, LogicalEntry};
use map::{parse_idx, parse_map_entries, resolve, LoadedMap, TargetKind};
use meta::{parse_logical_files, parse_turtle, Compression};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use zip_core::ZipArchive;
pub trait ReadSeekSend: Read + Seek + Send + Sync {}
impl<T: Read + Seek + Send + Sync> ReadSeekSend for T {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredHash {
pub algorithm: String,
pub hex: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContainerKind {
Disk,
Logical,
Encrypted,
}
pub fn container_kind(path: &Path) -> Result<ContainerKind, Aff4Error> {
let mut archive = ZipArchive::new(Box::new(File::open(path)?) as Box<dyn ReadSeekSend>)?;
let turtle = {
let mut entry = archive.by_name("information.turtle")?;
let mut content = String::new();
entry.read_to_string(&mut content)?;
content
};
if !parse_logical_files(&turtle)?.is_empty() {
return Ok(ContainerKind::Logical);
}
match parse_turtle(&turtle) {
Ok(_) => Ok(ContainerKind::Disk),
Err(Aff4Error::Encrypted(_)) => Ok(ContainerKind::Encrypted),
Err(e) => Err(e),
}
}
pub struct Aff4Reader {
archive: ZipArchive<Box<dyn ReadSeekSend>>,
zip_base: String,
virtual_size: u64,
image_stream_size: u64,
chunk_size: u64,
chunks_per_segment: u64,
compression: Compression,
image_hashes: Vec<StoredHash>,
pos: u64,
loaded_map: Option<LoadedMap>,
}
impl std::fmt::Debug for Aff4Reader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Aff4Reader")
.field("virtual_size", &self.virtual_size)
.field("chunk_size", &self.chunk_size)
.finish()
}
}
impl Aff4Reader {
pub fn open(path: &Path) -> Result<Self, Aff4Error> {
Self::open_reader(Box::new(File::open(path)?))
}
pub fn open_reader(backing: Box<dyn ReadSeekSend>) -> Result<Self, Aff4Error> {
let mut archive = ZipArchive::new(backing)?;
let turtle = {
let mut entry = archive.by_name("information.turtle")?;
let mut content = String::new();
entry.read_to_string(&mut content)?;
content
};
let meta = parse_turtle(&turtle)?;
let zip_base = detect_zip_base(&archive, &meta.stream_arn);
let loaded_map = if let Some(mm) = meta.map_meta {
let map_zip_base = detect_zip_base(&archive, &mm.map_arn);
let map_data = {
let map_entry_name = format!("{map_zip_base}/map");
let mut entry = archive.by_name(&map_entry_name)?;
let mut data = Vec::new();
entry.read_to_end(&mut data)?;
data
};
let idx_data = {
let idx_entry_name = format!("{map_zip_base}/idx");
let mut entry = archive.by_name(&idx_entry_name)?;
let mut content = String::new();
entry.read_to_string(&mut content)?;
content
};
let entries = parse_map_entries(&map_data);
let targets = parse_idx(&idx_data, &mm.image_stream_arn);
let gap_default = if mm.gap_is_symbolic_ff {
TargetKind::Fill(0xFF)
} else {
TargetKind::Fill(0x00)
};
Some(LoadedMap {
entries,
targets,
gap_default,
})
} else {
None
};
Ok(Self {
archive,
zip_base,
virtual_size: meta.virtual_size,
image_stream_size: meta.image_stream_size,
chunk_size: meta.chunk_size,
chunks_per_segment: meta.chunks_per_segment,
compression: meta.compression,
image_hashes: meta.image_hashes,
pos: 0,
loaded_map,
})
}
pub fn virtual_disk_size(&self) -> u64 {
self.virtual_size
}
pub fn image_stream_size(&self) -> u64 {
self.image_stream_size
}
pub fn stored_image_hashes(&self) -> &[StoredHash] {
&self.image_hashes
}
pub fn unreadable_regions(&self) -> Vec<(u64, u64)> {
self.loaded_map
.as_ref()
.map(LoadedMap::unreadable_regions)
.unwrap_or_default()
}
pub fn read_image_stream_content(
&mut self,
mut sink: impl FnMut(&[u8]),
) -> Result<(), Aff4Error> {
if self.chunk_size == 0 {
return Err(Aff4Error::BadFormat("aff4:chunkSize must be > 0".into()));
}
let total = self.image_stream_size;
let n_chunks = total.div_ceil(self.chunk_size);
let mut produced = 0u64;
for idx in 0..n_chunks {
let chunk = self.read_chunk(idx)?;
let remaining = total - produced;
let take = (chunk.len() as u64).min(remaining) as usize;
sink(&chunk[..take]);
produced += take as u64;
}
Ok(())
}
fn read_chunk(&mut self, chunk_idx: u64) -> Result<Vec<u8>, Aff4Error> {
let segment_idx = chunk_idx / self.chunks_per_segment;
let chunk_in_seg = chunk_idx % self.chunks_per_segment;
let segment_name = format!("{}/{:08x}", self.zip_base, segment_idx);
let index_name = format!("{segment_name}.index");
let index_data = self.read_zip_entry_bytes(&index_name)?;
let (chunk_start, chunk_end) = chunk_bounds_from_index(&index_data, chunk_in_seg)?;
if chunk_start == chunk_end {
return Ok(vec![0u8; self.chunk_size as usize]);
}
let bevy_data = self.read_zip_entry_bytes(&segment_name)?;
if chunk_end > bevy_data.len() {
return Err(Aff4Error::BadFormat(format!(
"chunk bounds ({chunk_start}..{chunk_end}) exceed bevy size ({})",
bevy_data.len()
)));
}
let compressed = &bevy_data[chunk_start..chunk_end];
if compressed.len() == self.chunk_size as usize {
return Ok(compressed.to_vec());
}
match &self.compression {
Compression::Null => Ok(compressed.to_vec()),
Compression::Deflate => {
let mut dec = flate2::read::ZlibDecoder::new(compressed);
let mut out = Vec::with_capacity(self.chunk_size as usize);
dec.read_to_end(&mut out)
.map_err(|e| Aff4Error::BadFormat(format!("deflate decode: {e}")))?;
Ok(out)
}
Compression::Snappy => {
let mut dec = snap::raw::Decoder::new();
dec.decompress_vec(compressed)
.map_err(|e| Aff4Error::BadFormat(format!("snappy decode: {e}")))
}
Compression::Lz4 => {
let mut dec = lz4_flex::frame::FrameDecoder::new(compressed);
let mut out = Vec::with_capacity(self.chunk_size as usize);
dec.read_to_end(&mut out)
.map_err(|e| Aff4Error::BadFormat(format!("lz4 decode: {e}")))?;
Ok(out)
}
}
}
fn read_zip_entry_bytes(&mut self, name: &str) -> Result<Vec<u8>, Aff4Error> {
let mut entry = self.archive.by_name(name)?;
let mut data = Vec::new();
entry.read_to_end(&mut data)?;
Ok(data)
}
}
pub(crate) fn detect_zip_base(archive: &ZipArchive<Box<dyn ReadSeekSend>>, arn: &str) -> String {
let stripped = arn.strip_prefix("aff4://").unwrap_or(arn);
let encoded = format!("aff4%3A%2F%2F{stripped}");
for cand in [encoded.as_str(), arn, stripped] {
if archive
.file_names()
.any(|n| n.starts_with(cand) && n[cand.len()..].starts_with('/'))
{
return cand.to_string();
}
}
stripped.to_string()
}
impl Read for Aff4Reader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if buf.is_empty() || self.pos >= self.virtual_size {
return Ok(0);
}
let remaining = (self.virtual_size - self.pos) as usize;
let to_read = buf.len().min(remaining);
let (target_kind, target_offset, bytes_in_region) = if let Some(ref lm) = self.loaded_map {
let r = resolve(lm, self.pos, self.virtual_size);
(r.kind, r.target_offset, r.bytes_in_region)
} else {
(TargetKind::ImageStream, self.pos, u64::MAX)
};
match target_kind {
TargetKind::Unknown => {
let n = to_read.min(bytes_in_region as usize);
buf[..n].fill(0);
self.pos += n as u64;
Ok(n)
}
TargetKind::Fill(byte) => {
let n = to_read.min(bytes_in_region as usize);
buf[..n].fill(byte);
self.pos += n as u64;
Ok(n)
}
TargetKind::Tile(tile) => {
let n = to_read.min(bytes_in_region as usize);
for (i, slot) in buf[..n].iter_mut().enumerate() {
*slot = tile.byte_at(target_offset + i as u64);
}
self.pos += n as u64;
Ok(n)
}
TargetKind::ImageStream => {
let region_limit = bytes_in_region as usize;
let chunk_idx = target_offset / self.chunk_size;
let offset_in_chunk = (target_offset % self.chunk_size) as usize;
let chunk = self
.read_chunk(chunk_idx)
.map_err(|e| std::io::Error::other(e.to_string()))?;
let available = chunk
.len()
.saturating_sub(offset_in_chunk)
.min(region_limit);
let n = to_read.min(available);
if n == 0 {
return Ok(0);
}
buf[..n].copy_from_slice(&chunk[offset_in_chunk..offset_in_chunk + n]);
self.pos += n as u64;
Ok(n)
}
}
}
}
impl Seek for Aff4Reader {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new_pos = match pos {
SeekFrom::Start(n) => n as i64,
SeekFrom::End(n) => self.virtual_size as i64 + n,
SeekFrom::Current(n) => self.pos as i64 + n,
};
if new_pos < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"seek before start of stream",
));
}
self.pos = new_pos as u64;
Ok(self.pos)
}
}
pub(crate) fn chunk_bounds_from_index(
index: &[u8],
chunk_in_seg: u64,
) -> Result<(usize, usize), Aff4Error> {
const ENTRY_SIZE: usize = 12;
let base = (chunk_in_seg as usize)
.checked_mul(ENTRY_SIZE)
.ok_or_else(|| Aff4Error::BadFormat("bevy index offset overflow".into()))?;
let entry = index.get(base..base + ENTRY_SIZE).ok_or_else(|| {
Aff4Error::BadFormat(format!("bevy index too small for chunk {chunk_in_seg}"))
})?;
let offset = u64::from_le_bytes(
entry[0..8]
.try_into()
.map_err(|_| Aff4Error::BadFormat("bevy index entry truncated".into()))?,
) as usize;
let length = u32::from_le_bytes(
entry[8..12]
.try_into()
.map_err(|_| Aff4Error::BadFormat("bevy index entry truncated".into()))?,
) as usize;
let end = offset
.checked_add(length)
.ok_or_else(|| Aff4Error::BadFormat("bevy chunk bounds overflow".into()))?;
Ok((offset, end))
}
#[cfg(test)]
mod tests {
use super::*;
use md5::Digest as _;
use std::io::Cursor;
use std::io::Write as _;
use zip::write::{SimpleFileOptions, ZipWriter};
fn write_tmp(data: &[u8]) -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().expect("tempfile");
f.write_all(data).expect("write");
f
}
#[test]
fn container_kind_classifies_disk_image() {
let f = write_tmp(&testutil::test_aff4(&[0u8; 512]));
assert_eq!(container_kind(f.path()).unwrap(), ContainerKind::Disk);
}
#[test]
fn container_kind_classifies_logical() {
let content = b"logical file body\n";
let md5 = format!("{:x}", md5::Md5::digest(content));
let f = write_tmp(&testutil::test_aff4_logical("dir/a.txt", content, &md5));
assert_eq!(container_kind(f.path()).unwrap(), ContainerKind::Logical);
}
#[test]
fn container_kind_classifies_encrypted() {
let f = write_tmp(&testutil::test_aff4_encrypted());
assert_eq!(container_kind(f.path()).unwrap(), ContainerKind::Encrypted);
}
#[test]
fn container_kind_rejects_non_aff4() {
let mut buf = Vec::new();
{
let mut zw = ZipWriter::new(std::io::Cursor::new(&mut buf));
zw.start_file("random.txt", SimpleFileOptions::default())
.unwrap();
zw.write_all(b"nope").unwrap();
zw.finish().unwrap();
}
let f = write_tmp(&buf);
assert!(container_kind(f.path()).is_err());
}
#[test]
fn chunk_size_zero_rejected() {
let img = testutil::test_aff4_with_geometry(0, 1);
let f = write_tmp(&img);
assert!(Aff4Reader::open(f.path()).is_err());
}
#[test]
fn chunks_per_segment_zero_rejected() {
let img = testutil::test_aff4_with_geometry(512, 0);
let f = write_tmp(&img);
assert!(Aff4Reader::open(f.path()).is_err());
}
#[test]
fn lz4_compressed_chunk_reads_decompressed_data() {
let img = testutil::test_aff4_lz4(&[0xCCu8; 512]);
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open lz4 aff4");
let mut buf = [0u8; 512];
reader.read_exact(&mut buf).expect("read");
assert_eq!(
buf, [0xCCu8; 512],
"LZ4-compressed chunk must be decompressed; without LZ4 support, \
raw frame bytes are returned instead of [0xCC; 512]"
);
}
#[test]
fn open_nonexistent_returns_err() {
assert!(Aff4Reader::open(Path::new("/tmp/nope_aff4_issen.aff4")).is_err());
}
#[test]
fn open_non_zip_returns_err() {
let f = write_tmp(&[0u8; 1024]);
assert!(Aff4Reader::open(f.path()).is_err());
}
#[test]
fn open_zip_without_turtle_returns_err() {
let cursor = Cursor::new(Vec::<u8>::new());
let mut zw = ZipWriter::new(cursor);
zw.start_file("dummy.txt", SimpleFileOptions::default())
.expect("start");
let data = zw.finish().expect("finish").into_inner();
let f = write_tmp(&data);
assert!(Aff4Reader::open(f.path()).is_err());
}
#[test]
fn encrypted_stream_is_detected_and_refused() {
let img = testutil::test_aff4_encrypted();
let f = write_tmp(&img);
let err = Aff4Reader::open(f.path()).expect_err("encrypted image must be refused");
assert!(
matches!(err, Aff4Error::Encrypted(_)),
"must be a named Aff4Error::Encrypted, got {err:?}"
);
let msg = err.to_string().to_ascii_lowercase();
assert!(
msg.contains("encrypt"),
"the refusal must name encryption as the cause; got: {err}"
);
}
#[test]
fn logical_container_lists_and_reads_files() {
let content = b"I have a Dream, delivered 1963.\n";
let md5 = format!("{:x}", md5::Md5::digest(content));
let img = testutil::test_aff4_logical("dir/dream.txt", content, &md5);
let f = write_tmp(&img);
let mut container = LogicalContainer::open(f.path()).expect("open AFF4-L container");
let files = container.files().to_vec();
assert_eq!(files.len(), 1, "one logical file expected");
let entry = &files[0];
assert_eq!(entry.original_file_name, "./dir/dream.txt");
assert_eq!(entry.size, content.len() as u64);
assert!(entry
.hashes
.iter()
.any(|h| h.algorithm.eq_ignore_ascii_case("MD5") && h.hex == md5));
let got = container.read_file(entry).expect("read logical file");
assert_eq!(
got, content,
"logical file bytes must match the stored segment"
);
}
#[test]
fn virtual_disk_size_matches_metadata() {
let img = testutil::test_aff4(&[0u8; 512]);
let f = write_tmp(&img);
let reader = Aff4Reader::open(f.path()).expect("open");
assert_eq!(reader.virtual_disk_size(), testutil::CHUNK_SIZE as u64);
}
#[test]
fn open_reader_over_cursor_matches_open_path() {
let mut sector = [0u8; 512];
sector[10] = 0xCA;
sector[11] = 0xFE;
let img = testutil::test_aff4(§or);
let tmp = write_tmp(&img);
let mut via_path = Aff4Reader::open(tmp.path()).expect("open path");
let mut want = Vec::new();
via_path.read_to_end(&mut want).expect("read path");
let mut via_reader =
Aff4Reader::open_reader(Box::new(Cursor::new(img.clone()))).expect("open_reader");
let mut got = Vec::new();
via_reader.read_to_end(&mut got).expect("read reader");
assert_eq!(
got, want,
"open_reader must read byte-identically to open(path)"
);
assert_eq!(via_reader.virtual_disk_size(), via_path.virtual_disk_size());
}
#[test]
fn read_returns_correct_bytes() {
let mut data = [0u8; 512];
data[10] = 0xCA;
data[11] = 0xFE;
let img = testutil::test_aff4(&data);
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open");
reader.seek(SeekFrom::Start(10)).expect("seek");
let mut buf = [0u8; 2];
reader.read_exact(&mut buf).expect("read");
assert_eq!(buf, [0xCA, 0xFE]);
}
#[test]
fn seek_from_end_works() {
let img = testutil::test_aff4(&[0xAB; 512]);
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open");
let pos = reader.seek(SeekFrom::End(-1)).expect("seek end");
assert_eq!(pos, 511);
let mut buf = [0u8; 1];
reader.read_exact(&mut buf).expect("read");
assert_eq!(buf[0], 0xAB);
}
#[test]
fn read_past_end_returns_zero_bytes() {
let img = testutil::test_aff4(&[0u8; 512]);
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open");
reader.seek(SeekFrom::Start(512)).expect("seek");
let mut buf = [0u8; 4];
let n = reader.read(&mut buf).expect("read");
assert_eq!(n, 0);
}
#[test]
fn aff4_reader_is_send() {
fn assert_send<T: Send>() {}
assert_send::<Aff4Reader>();
}
fn build_index(entries: &[(u64, u32)]) -> Vec<u8> {
let mut v = Vec::with_capacity(entries.len() * 12);
for &(off, len) in entries {
v.extend_from_slice(&off.to_le_bytes());
v.extend_from_slice(&len.to_le_bytes());
}
v
}
#[test]
fn chunk_bounds_from_index_single_chunk() {
let index = build_index(&[(0, 512)]);
let (start, end) = chunk_bounds_from_index(&index, 0).expect("bounds");
assert_eq!((start, end), (0, 512));
}
#[test]
fn chunk_bounds_from_index_second_chunk() {
let index = build_index(&[(0, 100), (100, 120)]);
let (start, end) = chunk_bounds_from_index(&index, 1).expect("bounds");
assert_eq!((start, end), (100, 220));
}
#[test]
fn chunk_bounds_from_index_out_of_range_errs() {
let index = build_index(&[(0, 512)]);
assert!(chunk_bounds_from_index(&index, 5).is_err());
}
#[test]
fn map_virtual_size_from_map_block_not_image_stream() {
let img = testutil::test_aff4_map(&[0u8; 512]);
let f = write_tmp(&img);
let reader = Aff4Reader::open(f.path()).expect("open map aff4");
assert_eq!(
reader.virtual_disk_size(),
1024,
"virtual_disk_size() must come from the aff4:Map block, not the ImageStream block"
);
}
#[test]
fn map_stream_gap_reads_zeros() {
let img = testutil::test_aff4_map(&[0xDDu8; 512]);
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open map aff4");
let mut buf = [0xFFu8; 512]; reader.read_exact(&mut buf).expect("read gap region");
assert_eq!(
buf, [0u8; 512],
"virtual bytes 0..511 are an unmapped gap and must read as zeros"
);
}
#[test]
fn map_stream_image_region_reads_correct_data() {
let img = testutil::test_aff4_map(&[0xDDu8; 512]);
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open map aff4");
reader
.seek(SeekFrom::Start(512))
.expect("seek to mapped region");
let mut buf = [0u8; 512];
reader.read_exact(&mut buf).expect("read mapped region");
assert_eq!(
buf, [0xDDu8; 512],
"virtual bytes 512..1023 map to the ImageStream and must return ImageStream data"
);
}
fn build_image(turtle: &str, base: &str, bevy: &[u8], index: &[u8]) -> Vec<u8> {
use zip::CompressionMethod;
let cursor = Cursor::new(Vec::<u8>::new());
let mut zw = ZipWriter::new(cursor);
let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
zw.start_file("information.turtle", opts).expect("turtle");
zw.write_all(turtle.as_bytes()).expect("write turtle");
zw.start_file(format!("{base}/00000000").as_str(), opts)
.expect("bevy");
zw.write_all(bevy).expect("write bevy");
zw.start_file(format!("{base}/00000000.index").as_str(), opts)
.expect("index");
zw.write_all(index).expect("write index");
zw.finish().expect("finish").into_inner()
}
fn index12(offset: u64, length: u32) -> Vec<u8> {
let mut v = offset.to_le_bytes().to_vec();
v.extend_from_slice(&length.to_le_bytes());
v
}
#[test]
fn debug_impl_renders() {
let img = testutil::test_aff4(&[0u8; 512]);
let f = write_tmp(&img);
let reader = Aff4Reader::open(f.path()).expect("open");
assert!(format!("{reader:?}").contains("Aff4Reader"));
}
#[test]
fn seek_before_start_is_err() {
let img = testutil::test_aff4(&[0u8; 512]);
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open");
assert!(reader.seek(SeekFrom::Current(-1)).is_err());
}
#[test]
fn deflate_chunk_reads_decompressed() {
let chunk = [0x7Au8; 512];
let mut compressed = Vec::new();
{
let mut enc =
flate2::write::ZlibEncoder::new(&mut compressed, flate2::Compression::default());
enc.write_all(&chunk).expect("zlib");
enc.finish().expect("finish");
}
let turtle = "@prefix aff4: <http://aff4.org/Schema#> .\n\
<aff4://s> rdf:type aff4:ImageStream ; aff4:size 512 ; aff4:chunkSize 512 ; \
aff4:chunksInSegment 1 ; aff4:compressionMethod aff4:DeflateCompressor .\n";
let img = build_image(
turtle,
"s",
&compressed,
&index12(0, compressed.len() as u32),
);
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open deflate");
let mut buf = [0u8; 512];
reader.read_exact(&mut buf).expect("read");
assert_eq!(buf, [0x7Au8; 512]);
}
#[test]
fn map_gap_symbolic_ff_reads_0xff() {
let turtle = "@prefix aff4: <http://aff4.org/Schema#> .\n\
<aff4://img> rdf:type aff4:ImageStream ; aff4:size 512 ; aff4:chunkSize 512 ; \
aff4:chunksInSegment 1 ; aff4:compressionMethod aff4:NullCompressor .\n\
<aff4://map> rdf:type aff4:Map ; aff4:size 1024 ; \
aff4:dependentStream <aff4://img> ; \
aff4:mapGapDefaultStream aff4:SymbolicStreamFF .\n";
use zip::CompressionMethod;
let cursor = Cursor::new(Vec::<u8>::new());
let mut zw = ZipWriter::new(cursor);
let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
zw.start_file("information.turtle", opts).expect("turtle");
zw.write_all(turtle.as_bytes()).expect("w");
zw.start_file("img/00000000", opts).expect("bevy");
zw.write_all(&[0xDDu8; 512]).expect("w");
zw.start_file("img/00000000.index", opts).expect("idx");
zw.write_all(&index12(0, 512)).expect("w");
let mut map_bin = 512u64.to_le_bytes().to_vec();
map_bin.extend_from_slice(&512u64.to_le_bytes());
map_bin.extend_from_slice(&0u64.to_le_bytes());
map_bin.extend_from_slice(&0u32.to_le_bytes());
zw.start_file("map/map", opts).expect("map");
zw.write_all(&map_bin).expect("w");
zw.start_file("map/idx", opts).expect("idxf");
zw.write_all(b"aff4://img\n").expect("w");
let img = zw.finish().expect("finish").into_inner();
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open map-ff");
let mut buf = [0u8; 512];
reader.read_exact(&mut buf).expect("read gap");
assert_eq!(buf, [0xFFu8; 512], "SymbolicStreamFF gap must read 0xFF");
}
#[test]
fn chunk_bounds_exceeding_bevy_is_err() {
let turtle = "@prefix aff4: <http://aff4.org/Schema#> .\n\
<aff4://s> rdf:type aff4:ImageStream ; aff4:size 512 ; aff4:chunkSize 512 ; \
aff4:chunksInSegment 1 ; aff4:compressionMethod aff4:NullCompressor .\n";
let img = build_image(turtle, "s", &[0u8; 512], &index12(0, 999_999));
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open");
let mut buf = [0u8; 512];
assert!(reader.read_exact(&mut buf).is_err());
}
#[test]
fn sparse_chunk_reads_zeros() {
let turtle = "@prefix aff4: <http://aff4.org/Schema#> .\n\
<aff4://s> rdf:type aff4:ImageStream ; aff4:size 512 ; aff4:chunkSize 512 ; \
aff4:chunksInSegment 1 ; aff4:compressionMethod aff4:NullCompressor .\n";
let img = build_image(turtle, "s", &[], &index12(0, 0));
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open");
let mut buf = [0xABu8; 512];
reader.read_exact(&mut buf).expect("read");
assert_eq!(buf, [0u8; 512]);
}
#[test]
fn null_partial_chunk_reads_stored_bytes() {
let turtle = "@prefix aff4: <http://aff4.org/Schema#> .\n\
<aff4://s> rdf:type aff4:ImageStream ; aff4:size 100 ; aff4:chunkSize 512 ; \
aff4:chunksInSegment 1 ; aff4:compressionMethod aff4:NullCompressor .\n";
let img = build_image(turtle, "s", &[0x5Au8; 100], &index12(0, 100));
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open");
let mut buf = [0u8; 100];
reader.read_exact(&mut buf).expect("read");
assert_eq!(buf, [0x5Au8; 100]);
}
#[test]
fn unknown_map_target_reads_zeros() {
let turtle = "@prefix aff4: <http://aff4.org/Schema#> .\n\
<aff4://img> rdf:type aff4:ImageStream ; aff4:size 512 ; aff4:chunkSize 512 ; \
aff4:chunksInSegment 1 ; aff4:compressionMethod aff4:NullCompressor .\n\
<aff4://map> rdf:type aff4:Map ; aff4:size 512 ; \
aff4:dependentStream <aff4://img> ; aff4:mapGapDefaultStream aff4:Zero .\n";
use zip::CompressionMethod;
let cursor = Cursor::new(Vec::<u8>::new());
let mut zw = ZipWriter::new(cursor);
let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
zw.start_file("information.turtle", opts).expect("t");
zw.write_all(turtle.as_bytes()).expect("w");
zw.start_file("img/00000000", opts).expect("b");
zw.write_all(&[0xDDu8; 512]).expect("w");
zw.start_file("img/00000000.index", opts).expect("i");
zw.write_all(&index12(0, 512)).expect("w");
let mut map_bin = 0u64.to_le_bytes().to_vec();
map_bin.extend_from_slice(&512u64.to_le_bytes());
map_bin.extend_from_slice(&0u64.to_le_bytes());
map_bin.extend_from_slice(&0u32.to_le_bytes());
zw.start_file("map/map", opts).expect("m");
zw.write_all(&map_bin).expect("w");
zw.start_file("map/idx", opts).expect("x");
zw.write_all(b"aff4://an-unknown-stream\n").expect("w");
let img = zw.finish().expect("finish").into_inner();
let f = write_tmp(&img);
let mut reader = Aff4Reader::open(f.path()).expect("open");
let mut buf = [0xFFu8; 512];
reader.read_exact(&mut buf).expect("read");
assert_eq!(buf, [0u8; 512], "unknown map target must read as zeros");
}
proptest::proptest! {
#[test]
fn open_never_panics_on_arbitrary_bytes(
bytes in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..8192)
) {
let f = write_tmp(&bytes);
let _ = Aff4Reader::open(f.path());
}
}
}