use crate::io::Reader;
use crate::{Error, Result};
const FOOTER_MAGIC: u32 = 0xDEC0_0621;
const COMPRESSED_END: usize = 0x14;
const PARAMS_MAGIC: usize = 0x1C;
pub(super) fn arm9(mut code: Vec<u8>, address: u32, footer: &[u8]) -> Result<Vec<u8>> {
let footer = Reader::new("DS ARM9 footer", footer);
let params = match footer.u32_at(0) {
Ok(FOOTER_MAGIC) => footer.u32_at(4)? as usize,
_ => {
let magic = [
FOOTER_MAGIC.to_le_bytes(),
FOOTER_MAGIC.swap_bytes().to_le_bytes(),
];
let found = code
.windows(8)
.position(|window| window == magic.as_flattened());
match found.and_then(|at| at.checked_sub(PARAMS_MAGIC)) {
Some(params) => params,
None => return Ok(code),
}
}
};
let end = match Reader::new("DS ARM9", &code).u32_at(params.saturating_add(COMPRESSED_END))? {
0 => return Ok(code),
end => end.wrapping_sub(address) as usize,
};
if end > code.len() {
return Err(Error::malformed(
"DS ARM9",
params as u64,
"compressed part exceeds the binary",
));
}
let rest = code.split_off(end);
let mut out = decompress_backward(&code)?;
out.extend(rest);
Ok(out)
}
fn decompress_backward(bytes: &[u8]) -> Result<Vec<u8>> {
const FORMAT: &str = "backward LZ77";
const MAX_LEN: usize = 4 << 20;
let input = Reader::new(FORMAT, bytes);
let footer_at = bytes
.len()
.checked_sub(8)
.ok_or_else(|| input.error_at(0, "too short for its footer"))?;
let lengths = input.u32_at(footer_at)?;
let added = input.u32_at(footer_at + 4)? as usize;
let start = bytes
.len()
.checked_sub(lengths as usize & 0xFF_FFFF)
.ok_or_else(|| input.error_at(footer_at, "compressed part exceeds the data"))?;
let mut source = bytes
.len()
.checked_sub((lengths >> 24) as usize)
.filter(|&source| source >= start)
.ok_or_else(|| input.error_at(footer_at, "footer exceeds the compressed part"))?;
if bytes.len().saturating_add(added) > MAX_LEN {
return Err(input.error_at(footer_at + 4, "decompressed size is too large"));
}
let mut out = bytes.to_vec();
out.resize(bytes.len() + added, 0);
let mut target = out.len();
let corrupt = |at: usize| Error::malformed(FORMAT, at as u64, "reference out of range");
'blocks: while source > start {
source -= 1;
let flags = bytes[source];
for bit in (0..8).rev() {
if source <= start {
break 'blocks;
}
if flags >> bit & 1 == 0 {
source -= 1;
target = target.checked_sub(1).ok_or_else(|| corrupt(source))?;
out[target] = bytes[source];
continue;
}
source = source.checked_sub(2).ok_or_else(|| corrupt(source))?;
let [low, high] = input.array_at(source)?;
let length = (high >> 4) as usize + 3;
let distance = ((high as usize & 0xF) << 8 | low as usize) + 3;
for _ in 0..length {
target = target.checked_sub(1).ok_or_else(|| corrupt(source))?;
out[target] = *out.get(target + distance).ok_or_else(|| corrupt(source))?;
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn compressed() -> Vec<u8> {
let mut bytes = b"ab".to_vec();
bytes.extend([0x00, 0xF0, b'x', b'y', b'z', 0b0001_0000]);
bytes.extend((14u32 | 8 << 24).to_le_bytes());
bytes.extend(7u32.to_le_bytes());
bytes
}
#[test]
fn decompresses_backward() {
let expected = [&b"ab"[..], &b"xyz".repeat(7)].concat();
assert_eq!(decompress_backward(&compressed()).unwrap(), expected);
assert!(decompress_backward(&[0; 4]).is_err());
let mut corrupt = compressed();
corrupt[3] = 0xFF; assert!(decompress_backward(&corrupt).is_err());
}
#[test]
fn decompresses_the_part_the_module_parameters_name() {
let mut code = vec![0; 0x40];
code[COMPRESSED_END..COMPRESSED_END + 4].copy_from_slice(&0x2000_0050u32.to_le_bytes());
code.extend(compressed());
code.extend(b"tail");
let footer = [FOOTER_MAGIC.to_le_bytes(), 0u32.to_le_bytes(), [0; 4]].concat();
let out = arm9(code.clone(), 0x2000_0000, &footer).unwrap();
assert_eq!(&out[0x40..0x42], b"ab");
assert_eq!(out.len(), code.len() + 7);
assert!(out.ends_with(b"xyztail"));
assert_eq!(arm9(code.clone(), 0x2000_0000, &[]).unwrap(), code);
code[PARAMS_MAGIC..PARAMS_MAGIC + 4].copy_from_slice(&FOOTER_MAGIC.to_le_bytes());
code[PARAMS_MAGIC + 4..PARAMS_MAGIC + 8].copy_from_slice(&0x2106_C0DEu32.to_le_bytes());
assert_eq!(arm9(code, 0x2000_0000, &[]).unwrap()[0x40..], out[0x40..]);
}
}