use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use thiserror::Error;
const DAR_MAGIC: [u8; 4] = [0x00, 0x00, 0x00, 0x7b];
const MAX_CATALOGUE_COMPRESSED: u64 = 512 * 1024 * 1024;
const MAX_CATALOGUE_INFLATED: u64 = 1024 * 1024 * 1024;
const SEQT_CATALOGUE: [u8; 6] = [0xAD, 0xFD, 0xEA, 0x77, 0x21, 0x43];
const FORMAT_11_1: u32 = 11 * 256 + 1;
#[derive(Debug, Error)]
pub enum DarError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("not a DAR archive")]
NotADar,
#[error("corrupt archive: {0}")]
Corrupt(String),
#[error("entry not found: '{0}'")]
EntryNotFound(String),
}
#[derive(Debug, Clone)]
pub struct DarEntry {
pub path: String,
pub size: u64,
}
#[derive(Debug, Clone)]
struct EntryRef {
path: String,
size: u64,
archive_offset: u64,
stored_size: u64,
compression: u8,
encrypted: bool,
}
pub struct DarReader<R: Read + Seek> {
inner: R,
archive_origin: u64,
entries: Vec<EntryRef>,
}
impl<R: Read + Seek> DarReader<R> {
pub fn open(mut reader: R) -> Result<Self, DarError> {
let mut magic = [0u8; 4];
reader
.read_exact(&mut magic)
.map_err(|_| DarError::NotADar)?;
if magic != DAR_MAGIC {
return Err(DarError::NotADar);
}
let mut label = [0u8; 10];
reader.read_exact(&mut label)?; let _flag = read_u8(&mut reader)?; let extension = read_u8(&mut reader)?;
let entries;
let archive_origin;
if extension == b'T' {
let tlv_count = read_infinint(&mut reader).map_err(|e| match e {
DarError::Io(_) => DarError::Corrupt("truncated TLV block".into()),
other => other,
})?;
for _ in 0..tlv_count {
skip(&mut reader, 2)?;
let len = read_infinint(&mut reader)?;
skip(&mut reader, len)?;
}
archive_origin = reader.stream_position()?;
let format_value = read_format_value(&mut reader);
let global_comp = read_u8(&mut reader).unwrap_or(b'n');
reader.seek(SeekFrom::Start(archive_origin))?;
let via_escape = find_catalogue(&mut reader, &label)?;
let format_major = format_value >> 8;
if via_escape && is_compressed(global_comp) {
let mut compressed = Vec::new();
reader
.by_ref()
.take(MAX_CATALOGUE_COMPRESSED)
.read_to_end(&mut compressed)?;
let inflated = decompress(&compressed, global_comp, MAX_CATALOGUE_INFLATED)?;
let mut cur = Cursor::new(inflated);
skip(&mut cur, 10)?; if format_value >= FORMAT_11_1 {
skip_nul_string(&mut cur)?;
}
entries = parse_catalog(&mut cur, format_major)?;
} else {
if via_escape {
skip(&mut reader, 10)?; if format_value >= FORMAT_11_1 {
skip_nul_string(&mut reader)?;
}
}
entries = parse_catalog(&mut reader, format_major)?;
}
} else if extension == b'N' || extension == b'S' {
if extension == b'S' {
read_infinint(&mut reader)?; }
archive_origin = reader.stream_position()?;
let format_value = read_format_value(&mut reader); let cat_offset = read_terminateur(&mut reader)?;
let cat_start = archive_origin
.checked_add(cat_offset)
.ok_or_else(|| DarError::Corrupt("catalogue offset overflows".into()))?;
let end = reader.seek(SeekFrom::End(0))?;
if cat_start >= end {
return Err(DarError::Corrupt(format!(
"catalogue start {cat_start} past archive end {end}"
)));
}
reader.seek(SeekFrom::Start(cat_start))?;
entries = parse_catalog(&mut reader, format_value >> 8)?;
} else {
return Err(DarError::Corrupt(format!(
"unknown slice-header extension {extension:#04x}"
)));
}
Ok(Self {
inner: reader,
archive_origin,
entries,
})
}
pub fn entries(&self) -> Vec<DarEntry> {
self.entries
.iter()
.map(|e| DarEntry {
path: e.path.clone(),
size: e.size,
})
.collect()
}
pub fn extract(&mut self, path: &str) -> Result<Vec<u8>, DarError> {
let entry = self
.entries
.iter()
.find(|e| e.path == path)
.ok_or_else(|| DarError::EntryNotFound(path.to_string()))?
.clone();
if entry.encrypted {
return Err(DarError::Corrupt(format!("'{path}' is encrypted")));
}
let start = self
.archive_origin
.checked_add(entry.archive_offset)
.ok_or_else(|| {
DarError::Corrupt(format!("'{path}' archive offset overflows file position"))
})?;
let end = self.inner.seek(SeekFrom::End(0))?;
if start > end {
return Err(DarError::Corrupt(format!(
"'{path}' starts at {start}, past archive end {end}"
)));
}
let available = end - start;
if entry.stored_size > available {
return Err(DarError::Corrupt(format!(
"'{path}' claims {} stored bytes but only {available} remain",
entry.stored_size
)));
}
self.inner.seek(SeekFrom::Start(start))?;
let mut data = vec![0u8; entry.stored_size as usize];
self.inner.read_exact(&mut data)?;
if !is_compressed(entry.compression) {
return Ok(data);
}
let out = decompress(&data, entry.compression, entry.size)?;
if out.len() as u64 != entry.size {
return Err(DarError::Corrupt(format!(
"'{path}' decompressed to {} bytes but catalog declares {}",
out.len(),
entry.size
)));
}
Ok(out)
}
}
const TAIL_SCAN: u64 = 256 * 1024 * 1024;
const CHUNK: usize = 4 * 1024 * 1024;
const OVERLAP: usize = 9;
fn scan_window<R: Read + Seek>(
r: &mut R,
label: &[u8; 10],
use_label: bool,
) -> Result<Option<bool>, DarError> {
let mut buf = vec![0u8; CHUNK + OVERLAP];
let mut overlap_len: usize = 0;
loop {
let chunk_file_pos = r.stream_position()?;
let n = r.read(&mut buf[overlap_len..overlap_len + CHUNK])?;
if n == 0 {
break;
}
let total = overlap_len + n;
let buf_base = chunk_file_pos - overlap_len as u64;
if let Some(i) = buf[..total]
.windows(SEQT_CATALOGUE.len())
.position(|w| w == SEQT_CATALOGUE)
{
r.seek(SeekFrom::Start(
buf_base + i as u64 + SEQT_CATALOGUE.len() as u64,
))?;
return Ok(Some(true));
}
if use_label {
if let Some(i) = buf[..total]
.windows(label.len())
.position(|w| w == label.as_ref())
{
r.seek(SeekFrom::Start(buf_base + i as u64 + label.len() as u64))?;
return Ok(Some(false));
}
}
let keep = OVERLAP.min(total);
buf.copy_within(total - keep..total, 0);
overlap_len = keep;
}
Ok(None)
}
fn find_catalogue<R: Read + Seek>(r: &mut R, label: &[u8; 10]) -> Result<bool, DarError> {
find_catalogue_within(r, label, TAIL_SCAN)
}
fn find_catalogue_within<R: Read + Seek>(
r: &mut R,
label: &[u8; 10],
tail_scan: u64,
) -> Result<bool, DarError> {
let use_label = !label.iter().all(|&b| b == 0);
let archive_origin = r.stream_position()?;
let file_end = r.seek(SeekFrom::End(0))?;
if file_end <= archive_origin {
return Err(DarError::Corrupt("archive body too short".into()));
}
let tail_start = archive_origin.max(file_end.saturating_sub(tail_scan));
r.seek(SeekFrom::Start(tail_start))?;
if let Some(result) = scan_window(r, label, use_label)? {
return Ok(result);
}
if tail_start > archive_origin {
r.seek(SeekFrom::Start(archive_origin))?;
if let Some(result) = scan_window(r, label, use_label)? {
return Ok(result);
}
}
Err(DarError::Corrupt("seqt_catalogue not found".into()))
}
fn read_format_value<R: Read>(r: &mut R) -> u32 {
let s = read_nul_string(r).unwrap_or_default();
let b = s.as_bytes();
if b.len() >= 2 {
let major = u32::from(b[0].saturating_sub(48)) * 256 + u32::from(b[1].saturating_sub(48));
let fix = if b.len() >= 3 {
u32::from(b[2].saturating_sub(48))
} else {
0
};
major * 256 + fix
} else {
u32::MAX
}
}
fn is_compressed(algo: u8) -> bool {
matches!(
algo.to_ascii_lowercase(),
b'z' | b'y' | b'x' | b'l' | b'j' | b'k' | b'd' | b'q'
)
}
fn decompress(data: &[u8], algo: u8, max_out: u64) -> Result<Vec<u8>, DarError> {
match algo.to_ascii_lowercase() {
b'z' => read_bounded(flate2::read::ZlibDecoder::new(data), max_out, "zlib"),
b'y' => read_bounded(bzip2_rs::DecoderReader::new(data), max_out, "bzip2"),
b'x' => {
let mut input: &[u8] = data;
let mut out = BoundedWriter {
buf: Vec::new(),
max: max_out,
};
match lzma_rs::xz_decompress(&mut input, &mut out) {
Ok(()) => Ok(out.buf),
Err(lzma_rs::error::Error::XzError(ref m))
if m == "Unexpected data after last XZ block" =>
{
Ok(out.buf)
}
Err(e) => Err(DarError::Corrupt(format!("xz decode failed: {e}"))),
}
}
other => Err(DarError::Corrupt(format!(
"unsupported compression '{}'",
other as char
))),
}
}
struct BoundedWriter {
buf: Vec<u8>,
max: u64,
}
impl Write for BoundedWriter {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
if self.buf.len() as u64 + data.len() as u64 > self.max {
return Err(std::io::Error::other("decompressed data exceeds bound"));
}
self.buf.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn read_bounded<R: Read>(decoder: R, max_out: u64, what: &str) -> Result<Vec<u8>, DarError> {
let mut out = Vec::new();
decoder
.take(max_out.saturating_add(1))
.read_to_end(&mut out)
.map_err(|e| DarError::Corrupt(format!("{what} decode failed: {e}")))?;
if out.len() as u64 > max_out {
return Err(DarError::Corrupt("decompressed data exceeds bound".into()));
}
Ok(out)
}
fn read_terminateur<R: Read + Seek>(r: &mut R) -> Result<u64, DarError> {
const BLOCK_SIZE: u64 = 4;
const MAX_BITS: u64 = 4096;
let mut pos = r.seek(SeekFrom::End(0))?;
let mut bits: u64 = 0;
let terminal = loop {
if pos == 0 {
return Err(DarError::Corrupt("terminator underflows archive".into()));
}
pos -= 1;
r.seek(SeekFrom::Start(pos))?;
let b = read_u8(r)?;
if b == 0xFF {
bits += 8;
if bits > MAX_BITS {
return Err(DarError::Corrupt("terminator padding too long".into()));
}
} else {
break b;
}
};
if terminal & 0x80 == 0 {
return Err(DarError::Corrupt(format!(
"invalid terminator byte {terminal:#04x}"
)));
}
let mut x = terminal;
while x != 0 {
if x & 0x80 == 0 {
return Err(DarError::Corrupt("malformed terminator bit run".into()));
}
bits += 1;
x <<= 1;
}
let byte_offset = bits * BLOCK_SIZE;
let infinint_start = pos
.checked_sub(byte_offset)
.ok_or_else(|| DarError::Corrupt("terminator offset underflows".into()))?;
r.seek(SeekFrom::Start(infinint_start))?;
read_infinint(r)
}
fn parse_catalog<R: Read + Seek>(r: &mut R, format_major: u32) -> Result<Vec<EntryRef>, DarError> {
let mut entries = Vec::new();
let mut dir_stack: Vec<String> = Vec::new();
let mut depth: u32 = 0;
loop {
let mut buf = [0u8; 1];
match r.read_exact(&mut buf) {
Ok(()) => {}
Err(_) => break,
}
let entry_type = ((buf[0] & 0x1f) | 0x60) as char;
match entry_type {
'z' => {
depth = depth.saturating_sub(1);
dir_stack.pop();
if depth == 0 {
break;
}
}
'd' => {
let name = read_nul_string(r)?;
let flags = read_inode_base(r, format_major)?;
if format_major >= 9 && (flags >> 4) & 1 != 0 {
skip_fsa(r)?;
}
depth += 1;
if name != "<ROOT>" {
dir_stack.push(name);
}
}
'f' => {
let name = read_nul_string(r)?;
let flags = read_inode_base(r, format_major)?;
if format_major >= 9 && (flags >> 4) & 1 != 0 {
skip_fsa(r)?;
}
let size = read_infinint(r)?;
let archive_offset = read_infinint(r)?;
let mut stored_size = read_infinint(r)?;
let (encryption_flag, compression) = if format_major >= 8 {
(read_u8(r)?, read_u8(r)?)
} else {
(0u8, b'n')
};
if format_major >= 8 {
let crc_size = read_infinint(r)?;
skip(r, crc_size)?;
} else {
skip(r, 2)?; }
if format_major <= 7 && stored_size == 0 {
stored_size = size;
}
let path = if dir_stack.is_empty() {
name
} else {
format!("{}/{}", dir_stack.join("/"), name)
};
entries.push(EntryRef {
path,
size,
archive_offset,
stored_size,
compression,
encrypted: encryption_flag != 0,
});
}
'l' => {
let _name = read_nul_string(r)?;
let flags = read_inode_base(r, format_major)?;
if format_major >= 9 && (flags >> 4) & 1 != 0 {
skip_fsa(r)?;
}
skip_nul_string(r)?; }
_ => break, }
}
Ok(entries)
}
fn read_infinint<R: Read>(r: &mut R) -> Result<u64, DarError> {
let terminal = read_u8(r)?;
if terminal == 0x00 {
return Err(DarError::Corrupt(
"infinint exceeds 64-bit range (multi-group encoding)".into(),
));
}
if terminal.count_ones() != 1 {
return Err(DarError::Corrupt(format!(
"invalid infinint terminal: {terminal:#04x}"
)));
}
let pos = terminal.leading_zeros(); if pos > 1 {
return Err(DarError::Corrupt(format!(
"infinint exceeds 64-bit range: terminal {terminal:#04x} implies {} bytes",
(pos + 1) * 4
)));
}
let data_bytes = (pos + 1) * 4; let mut val: u64 = 0;
for _ in 0..data_bytes {
val = (val << 8) | u64::from(read_u8(r)?);
}
Ok(val)
}
fn read_u8<R: Read>(r: &mut R) -> Result<u8, DarError> {
let mut b = [0u8; 1];
r.read_exact(&mut b)?;
Ok(b[0])
}
const MAX_NUL_STRING: usize = 64 * 1024;
fn read_nul_string<R: Read>(r: &mut R) -> Result<String, DarError> {
let mut bytes = Vec::new();
loop {
let b = read_u8(r)?;
if b == 0 {
break;
}
if bytes.len() >= MAX_NUL_STRING {
return Err(DarError::Corrupt(format!(
"NUL-terminated string exceeds {MAX_NUL_STRING} bytes"
)));
}
bytes.push(b);
}
String::from_utf8(bytes).map_err(|e| DarError::Corrupt(e.to_string()))
}
fn skip_nul_string<R: Read>(r: &mut R) -> Result<(), DarError> {
let mut len: usize = 0;
loop {
if read_u8(r)? == 0 {
return Ok(());
}
len += 1;
if len > MAX_NUL_STRING {
return Err(DarError::Corrupt(format!(
"NUL-terminated string exceeds {MAX_NUL_STRING} bytes"
)));
}
}
}
fn skip<R: Seek>(r: &mut R, n: u64) -> Result<(), DarError> {
if n > 0 {
let off = i64::try_from(n)
.map_err(|_| DarError::Corrupt(format!("skip length {n} exceeds seekable range")))?;
r.seek(SeekFrom::Current(off)).map_err(DarError::Io)?;
}
Ok(())
}
fn skip_timestamp<R: Read + Seek>(r: &mut R, format_major: u32) -> Result<(), DarError> {
if format_major < 9 {
read_infinint(r)?;
return Ok(());
}
let ts_type = read_u8(r)?;
read_infinint(r)?;
if ts_type == b'n' || ts_type == b'u' {
read_infinint(r)?;
}
Ok(())
}
fn read_inode_base<R: Read + Seek>(r: &mut R, format_major: u32) -> Result<u8, DarError> {
let flags = read_u8(r)?;
if format_major <= 7 {
skip(r, 4)?; } else {
read_infinint(r)?; read_infinint(r)?; }
skip(r, 2)?; skip_timestamp(r, format_major)?; skip_timestamp(r, format_major)?; if format_major >= 8 {
skip_timestamp(r, format_major)?;
}
if format_major >= 9 && (flags >> 4) & 1 != 0 {
read_infinint(r)?;
read_infinint(r)?;
}
Ok(flags)
}
fn skip_fsa<R: Read + Seek>(r: &mut R) -> Result<(), DarError> {
let _tag = read_infinint(r)?;
let size = read_infinint(r)?;
skip(r, size)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn infinint_decodes_value() {
let data = [0x80u8, 0x00, 0x00, 0x00, 0x0d];
assert_eq!(read_infinint(&mut Cursor::new(&data[..])).unwrap(), 13);
}
#[test]
fn infinint_bad_preamble_returns_corrupt() {
let data = [0x03u8, 0x00, 0x00, 0x00, 0x00];
let err = read_infinint(&mut Cursor::new(&data[..])).unwrap_err();
assert!(matches!(&err, DarError::Corrupt(_)));
}
#[test]
fn infinint_truncated_returns_io() {
let err = read_infinint(&mut Cursor::new(&[0x80u8, 0x00][..])).unwrap_err();
assert!(matches!(err, DarError::Io(_)));
}
#[test]
fn infinint_0x40_preamble_reads_8_data_bytes() {
let mut data = vec![0x40u8];
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x5d, 0x15, 0x93, 0x31]);
assert_eq!(
read_infinint(&mut Cursor::new(data)).unwrap(),
0x5d15_9331u64
);
}
#[test]
fn infinint_multi_bit_terminal_returns_corrupt() {
let data = [0x60u8, 0x00, 0x00, 0x00, 0x00];
let err = read_infinint(&mut Cursor::new(&data[..])).unwrap_err();
assert!(matches!(&err, DarError::Corrupt(_)));
}
#[test]
fn read_u8_reads_single_byte() {
assert_eq!(read_u8(&mut Cursor::new(&[0x42u8][..])).unwrap(), 0x42);
}
#[test]
fn read_u8_eof_returns_io() {
let err = read_u8(&mut Cursor::new(&[][..])).unwrap_err();
assert!(matches!(err, DarError::Io(_)));
}
#[test]
fn nul_string_reads_until_nul() {
let data = b"hello\x00world";
assert_eq!(
read_nul_string(&mut Cursor::new(&data[..])).unwrap(),
"hello"
);
}
#[test]
fn nul_string_invalid_utf8_returns_corrupt() {
let data = [0xFF, 0x80, 0x00];
let err = read_nul_string(&mut Cursor::new(&data[..])).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)));
}
#[test]
fn nul_string_eof_before_nul_returns_io() {
let err = read_nul_string(&mut Cursor::new(b"no-nul".to_vec())).unwrap_err();
assert!(matches!(err, DarError::Io(_)));
}
#[test]
fn skip_nul_string_advances_past_nul() {
let data = b"skip\x00rest";
let mut c = Cursor::new(data.to_vec());
skip_nul_string(&mut c).unwrap();
assert_eq!(c.position(), 5); }
#[test]
fn skip_nul_string_eof_returns_io() {
let err = skip_nul_string(&mut Cursor::new(b"no-nul".to_vec())).unwrap_err();
assert!(matches!(err, DarError::Io(_)));
}
#[test]
fn find_catalogue_body_too_short() {
let label = [0u8; 10];
let err = find_catalogue(&mut Cursor::new(&[0x01u8, 0x02, 0x03][..]), &label).unwrap_err();
assert!(
matches!(&err, DarError::Corrupt(s) if s == "archive body too short"
|| s == "seqt_catalogue not found")
);
}
#[test]
fn find_catalogue_escape_at_start() {
let mut data = [0xAD, 0xFD, 0xEA, 0x77, 0x21, 0x43, 0xFF];
let mut c = Cursor::new(&mut data[..]);
let via_escape = find_catalogue(&mut c, &[0u8; 10]).unwrap();
assert!(via_escape);
assert_eq!(c.position(), 6);
}
#[test]
fn find_catalogue_escape_not_found() {
let label = [0xFFu8; 10];
let err = find_catalogue(&mut Cursor::new(&[0u8; 10][..]), &label).unwrap_err();
assert!(matches!(&err, DarError::Corrupt(s) if s == "seqt_catalogue not found"));
}
#[test]
fn find_catalogue_label_fallback() {
let label: [u8; 10] = [0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18, 0x29, 0x3A];
let mut data = vec![0x00u8; 5];
data.extend_from_slice(&label);
let mut c = Cursor::new(data);
let via_escape = find_catalogue(&mut c, &label).unwrap();
assert!(!via_escape);
assert_eq!(c.position(), 15); }
#[test]
fn skip_zero_does_not_move_cursor() {
let mut c = Cursor::new(vec![0xFFu8; 10]);
skip(&mut c, 0).unwrap();
assert_eq!(c.position(), 0);
}
#[test]
fn skip_n_advances_cursor() {
let mut c = Cursor::new(vec![0xFFu8; 10]);
skip(&mut c, 7).unwrap();
assert_eq!(c.position(), 7);
}
#[test]
fn inode_base_bit4_clear_reads_31_bytes() {
let mut data = vec![0x00u8]; data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(&[0x00, 0x00]); for _ in 0..3 {
data.push(b's'); data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x00]); }
data.push(0xFF); let mut c = Cursor::new(data);
assert_eq!(read_inode_base(&mut c, 11).unwrap(), 0x00);
assert_eq!(c.position(), 31);
}
#[test]
fn inode_base_bit4_set_reads_41_bytes() {
let mut data = vec![0x10u8]; data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(&[0x00, 0x00]); for _ in 0..3 {
data.push(b's');
data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x00]);
}
data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x00]); data.push(0xFF); let mut c = Cursor::new(data);
assert_eq!(read_inode_base(&mut c, 11).unwrap(), 0x10);
assert_eq!(c.position(), 41);
}
#[test]
fn skip_fsa_consumes_tag_size_and_data() {
let mut data = Vec::new();
data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x05]); data.extend_from_slice(&[0x80, 0x00, 0x00, 0x00, 0x03]); data.extend_from_slice(&[0xAA, 0xBB, 0xCC]); data.push(0xFF); let mut c = Cursor::new(data);
skip_fsa(&mut c).unwrap();
assert_eq!(c.position(), 13); }
#[test]
fn infinint_leading_zero_byte_returns_corrupt() {
let data = [0x00u8, 0x80, 0x00, 0x00, 0x00, 0x00];
let err = read_infinint(&mut Cursor::new(&data[..])).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn infinint_12_byte_group_exceeds_u64_returns_corrupt() {
let mut data = vec![0x20u8];
data.extend_from_slice(&[0x11; 12]);
let err = read_infinint(&mut Cursor::new(data)).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn infinint_all_zero_run_returns_corrupt_without_hanging() {
let data = vec![0u8; 4096];
let err = read_infinint(&mut Cursor::new(data)).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn nul_string_without_terminator_is_length_bounded() {
let data = vec![b'A'; 200_000];
let err = read_nul_string(&mut Cursor::new(data)).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn skip_nul_string_without_terminator_is_length_bounded() {
let data = vec![b'A'; 200_000];
let err = skip_nul_string(&mut Cursor::new(data)).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn skip_value_above_i64_max_returns_corrupt() {
let mut c = Cursor::new(vec![0u8; 64]);
c.set_position(32);
let err = skip(&mut c, 0x8000_0000_0000_0000).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
assert_eq!(c.position(), 32); }
#[test]
fn terminateur_reads_catalogue_offset() {
let data = vec![0x80u8, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xc0];
assert_eq!(read_terminateur(&mut Cursor::new(data)).unwrap(), 24);
}
#[test]
fn terminateur_all_ff_underflows_returns_corrupt() {
let err = read_terminateur(&mut Cursor::new(vec![0xFFu8; 4])).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn terminateur_excessive_ff_padding_returns_corrupt() {
let err = read_terminateur(&mut Cursor::new(vec![0xFFu8; 600])).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn terminateur_low_terminator_byte_returns_corrupt() {
let data = vec![0x80u8, 0x00, 0x00, 0x00, 0x18, 0x01];
let err = read_terminateur(&mut Cursor::new(data)).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn terminateur_noncontiguous_high_bits_returns_corrupt() {
let data = vec![0x80u8, 0x00, 0x00, 0x00, 0x18, 0xA0];
let err = read_terminateur(&mut Cursor::new(data)).unwrap_err();
assert!(matches!(err, DarError::Corrupt(_)), "got {err:?}");
}
#[test]
fn find_catalogue_falls_back_to_full_scan() {
let mut data = vec![0x11u8, 0x22]; data.extend_from_slice(&SEQT_CATALOGUE);
data.extend_from_slice(&[0x33u8; 12]); let mut c = Cursor::new(data);
let via_escape = find_catalogue_within(&mut c, &[0u8; 10], 4).unwrap();
assert!(via_escape);
assert_eq!(c.position(), 2 + SEQT_CATALOGUE.len() as u64);
}
#[test]
fn find_catalogue_full_scan_miss_returns_not_found() {
let mut c = Cursor::new(vec![0x11u8; 16]);
let err = find_catalogue_within(&mut c, &[0xABu8; 10], 4).unwrap_err();
assert!(matches!(&err, DarError::Corrupt(s) if s == "seqt_catalogue not found"));
}
#[test]
fn find_catalogue_body_too_short_when_origin_at_eof() {
let mut c = Cursor::new(vec![0u8; 6]);
c.seek(SeekFrom::Start(6)).unwrap();
let err = find_catalogue(&mut c, &[0u8; 10]).unwrap_err();
assert!(matches!(&err, DarError::Corrupt(s) if s == "archive body too short"));
}
#[test]
fn decompress_rejects_decompression_bomb() {
use flate2::{write::ZlibEncoder, Compression};
use std::io::Write;
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(&[0u8; 4096]).unwrap();
let blob = enc.finish().unwrap();
let err = decompress(&blob, b'z', 16).unwrap_err();
assert!(matches!(&err, DarError::Corrupt(s) if s.contains("exceeds bound")));
}
#[test]
fn decompress_rejects_malformed_zlib() {
let err = decompress(b"not a zlib stream at all", b'z', 1024).unwrap_err();
assert!(matches!(&err, DarError::Corrupt(s) if s.contains("zlib decode failed")));
}
#[test]
fn decompress_rejects_malformed_xz() {
let err = decompress(b"this is not an xz stream", b'x', 1024).unwrap_err();
assert!(matches!(&err, DarError::Corrupt(s) if s.contains("xz decode failed")));
}
#[test]
fn bounded_writer_caps_output_and_flushes() {
let mut w = BoundedWriter {
buf: Vec::new(),
max: 4,
};
assert_eq!(w.write(b"ab").unwrap(), 2); w.flush().unwrap();
let err = w.write(b"cde").unwrap_err(); assert_eq!(err.to_string(), "decompressed data exceeds bound");
assert_eq!(w.buf, b"ab");
}
}