use std::fmt;
use std::io::{self, Read, Seek, SeekFrom, Write};
use crate::zbm;
pub const HEADER_LEN: usize = 16;
pub const BLOCK_SIZE: u64 = 0x10000;
pub const LZBITMAP_ATTR: u32 = 13;
pub const LZBITMAP_RSRC: u32 = 14;
#[derive(Debug)]
pub enum Error {
Io(io::Error),
BadHeader(&'static str),
Unsupported(u32),
BadTable(String),
Block { index: u64, reason: String },
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io(e) => write!(f, "I/O error: {e}"),
Error::BadHeader(why) => write!(f, "invalid decmpfs header: {why}"),
Error::Unsupported(t) => write!(f, "unsupported decmpfs type {t} (only 13 and 14)"),
Error::BadTable(why) => write!(f, "invalid block table: {why}"),
Error::Block { index, reason } => write!(f, "block {index}: {reason}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Header {
pub algo: u32,
pub size: u64,
}
pub fn parse_header(b: &[u8]) -> Result<Header, Error> {
if b.len() < HEADER_LEN {
return Err(Error::BadHeader("shorter than 16 bytes"));
}
if &b[0..4] != b"fpmc" {
return Err(Error::BadHeader("magic is not fpmc"));
}
Ok(Header {
algo: u32::from_le_bytes(b[4..8].try_into().unwrap()),
size: u64::from_le_bytes(b[8..16].try_into().unwrap()),
})
}
fn decode_block(index: u64, cdata: &[u8], expected: usize, out: &mut Vec<u8>) -> Result<(), Error> {
let err = |reason: String| Error::Block { index, reason };
out.clear();
match cdata.first() {
None => return Err(err("empty block".into())),
Some(0x5a) => {
zbm::decompress(cdata, out, expected).map_err(|e| err(e.to_string()))?;
}
Some(b) if b & 0x0f == 0x0f => out.extend_from_slice(&cdata[1..]),
Some(b) => return Err(err(format!("unknown block marker {b:#04x}"))),
}
if out.len() != expected {
return Err(err(format!(
"produced {} bytes, expected {expected}",
out.len()
)));
}
Ok(())
}
pub fn decode_attr<W: Write>(xattr: &[u8], mut w: W) -> Result<u64, Error> {
let hdr = parse_header(xattr)?;
if hdr.algo != LZBITMAP_ATTR {
return Err(Error::Unsupported(hdr.algo));
}
if hdr.size > BLOCK_SIZE {
return Err(Error::BadHeader("type 13 larger than one block"));
}
let mut out = Vec::new();
decode_block(0, &xattr[HEADER_LEN..], hdr.size as usize, &mut out)?;
w.write_all(&out)?;
Ok(hdr.size)
}
pub fn decode_rsrc<R: Read + Seek, W: Write>(
hdr: Header,
mut rsrc: R,
mut w: W,
) -> Result<u64, Error> {
if hdr.algo != LZBITMAP_RSRC {
return Err(Error::Unsupported(hdr.algo));
}
let rsrc_len = rsrc.seek(SeekFrom::End(0))?;
let n = hdr.size.div_ceil(BLOCK_SIZE);
let table_len = (n + 1) * 4;
if table_len > rsrc_len {
return Err(Error::BadTable(format!(
"{n} blocks do not fit in {rsrc_len} bytes"
)));
}
rsrc.seek(SeekFrom::Start(0))?;
let mut raw = vec![0u8; table_len as usize];
rsrc.read_exact(&mut raw)?;
let table: Vec<u64> = raw
.chunks_exact(4)
.map(|c| u32::from_le_bytes(c.try_into().unwrap()) as u64)
.collect();
if table[0] != table_len {
return Err(Error::BadTable(format!(
"first offset {} != table size {table_len}",
table[0]
)));
}
if table.windows(2).any(|p| p[1] < p[0]) || table[n as usize] > rsrc_len {
return Err(Error::BadTable(
"offsets not monotonic or past end of fork".into(),
));
}
let mut cdata = Vec::new();
let mut out = Vec::with_capacity(BLOCK_SIZE as usize);
let mut total = 0u64;
for i in 0..n {
let (start, end) = (table[i as usize], table[i as usize + 1]);
cdata.resize((end - start) as usize, 0);
rsrc.seek(SeekFrom::Start(start))?;
rsrc.read_exact(&mut cdata)?;
let expected = (hdr.size - i * BLOCK_SIZE).min(BLOCK_SIZE) as usize;
decode_block(i, &cdata, expected, &mut out)?;
w.write_all(&out)?;
total += out.len() as u64;
}
w.flush()?;
Ok(total)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn header(algo: u32, size: u64) -> Vec<u8> {
let mut h = b"fpmc".to_vec();
h.extend_from_slice(&algo.to_le_bytes());
h.extend_from_slice(&size.to_le_bytes());
h
}
#[test]
fn parses_header() {
let h = parse_header(&header(14, 123)).unwrap();
assert_eq!(
h,
Header {
algo: 14,
size: 123
}
);
assert!(parse_header(b"xpmc000000000000").is_err());
}
#[test]
fn rsrc_with_raw_blocks() {
let size = BLOCK_SIZE + 10;
let b0: Vec<u8> = (0..BLOCK_SIZE).map(|i| i as u8).collect();
let b1 = vec![7u8; 10];
let table_len = 3 * 4u32;
let o1 = table_len + 1 + b0.len() as u32;
let o2 = o1 + 1 + b1.len() as u32;
let mut fork = Vec::new();
for o in [table_len, o1, o2] {
fork.extend_from_slice(&o.to_le_bytes());
}
fork.push(0xff);
fork.extend(&b0);
fork.push(0xff);
fork.extend(&b1);
let mut out = Vec::new();
let n = decode_rsrc(Header { algo: 14, size }, Cursor::new(fork), &mut out).unwrap();
assert_eq!(n, size);
assert_eq!(&out[..BLOCK_SIZE as usize], &b0[..]);
assert_eq!(&out[BLOCK_SIZE as usize..], &b1[..]);
}
#[test]
fn attr_inline_raw() {
let mut x = header(13, 3);
x.extend_from_slice(&[0xff, b'a', b'b', b'c']);
let mut out = Vec::new();
assert_eq!(decode_attr(&x, &mut out).unwrap(), 3);
assert_eq!(out, b"abc");
}
#[test]
fn rejects_wrong_block_size() {
let mut fork = [8u32.to_le_bytes(), 12u32.to_le_bytes()].concat();
fork.extend_from_slice(&[0xff, 1, 2, 3]);
let r = decode_rsrc(Header { algo: 14, size: 5 }, Cursor::new(fork), Vec::new());
assert!(matches!(r, Err(Error::Block { index: 0, .. })));
}
#[test]
fn rejects_other_types() {
let r = decode_rsrc(Header { algo: 8, size: 1 }, Cursor::new(vec![]), Vec::new());
assert!(matches!(r, Err(Error::Unsupported(8))));
}
}