use pdfrum_common::hex_digit;
fn is_space(ch: u8) -> bool {
matches!(ch, b'\r' | b'\n' | b' ' | b'\t')
}
fn is_a85_digit(ch: u8) -> bool {
(b'!'..=b'u').contains(&ch)
}
#[must_use]
pub fn decode_ascii_hex(input: &[u8]) -> (Vec<u8>, usize) {
let mut out = Vec::with_capacity(input.len() / 2 + 1);
let mut high: Option<u8> = None;
let mut consumed = input.len();
for (i, &ch) in input.iter().enumerate() {
if ch == b'>' {
consumed = i + 1;
break;
}
if is_space(ch) {
continue;
}
let Some(digit) = hex_digit(ch) else {
continue;
};
match high.take() {
None => high = Some(digit << 4),
Some(hi) => out.push(hi | digit),
}
}
if let Some(hi) = high {
out.push(hi);
}
(out, consumed)
}
pub fn decode_ascii85(input: &[u8]) -> Result<(Vec<u8>, usize), crate::Error> {
if input.is_empty() {
return Ok((Vec::new(), 0));
}
let mut zeroes = 0usize;
let mut legal = 0usize;
for &ch in input {
if ch == b'z' {
zeroes += 1;
} else if !is_a85_digit(ch) && !is_space(ch) {
break;
}
legal += 1;
}
if legal == 0 {
return Ok((Vec::new(), 0));
}
let capacity = zeroes
.checked_mul(4)
.and_then(|z| z.checked_add((legal - zeroes) / 5 * 4 + 4))
.ok_or(crate::Error::SizeOverflow)?;
let mut out = Vec::with_capacity(capacity);
let mut group = 0u32;
let mut in_group = 0usize;
let mut pos = 0usize;
for &ch in input {
pos += 1;
if is_space(ch) {
continue;
}
if ch == b'z' {
out.extend_from_slice(&[0; 4]);
group = 0;
in_group = 0;
continue;
}
if !is_a85_digit(ch) {
break;
}
group = group
.wrapping_mul(85)
.wrapping_add(u32::from(ch) - u32::from(b'!'));
in_group += 1;
if in_group == 5 {
out.extend_from_slice(&group.to_be_bytes());
group = 0;
in_group = 0;
}
}
if in_group > 0 {
for _ in in_group..5 {
group = group.wrapping_mul(85).wrapping_add(84);
}
let full = group.to_be_bytes();
out.extend_from_slice(full.get(..in_group - 1).unwrap_or(&full));
}
if input.get(pos) == Some(&b'>') {
pos += 1;
}
Ok((out, pos))
}
#[cfg(test)]
mod tests {
use super::{decode_ascii_hex, decode_ascii85};
#[test]
fn ascii85_reference_vectors() {
let cases: [(&[u8], &[u8], usize); 8] = [
(b"", b"", 0),
(b"~>", b"", 0),
(b"FCfN8~>", b"test", 7),
(b"FCfN8~>FCfN8", b"test", 7),
(b"\t F C\r\n \tf N 8 ~>", b"test", 17),
(b"@3B0)DJj_BF*)>@Gp#-s", b"a funny story :)", 20),
(b"12A", b"2k", 3),
(b"FCfN8FCfN8vw", b"testtest", 11),
];
for (input, expected, consumed) in cases {
assert_eq!(
decode_ascii85(input).expect("no overflow"),
(expected.to_vec(), consumed),
"{:?}",
String::from_utf8_lossy(input)
);
}
}
#[test]
fn ascii85_z_expands_to_four_zeroes_anywhere() {
assert_eq!(decode_ascii85(b"zzz").expect("ok").0, vec![0u8; 12]);
assert_eq!(decode_ascii85(b"FCz").expect("ok"), (vec![0, 0, 0, 0], 3));
}
#[test]
fn ascii85_single_character_tail_emits_nothing() {
assert_eq!(
decode_ascii85(b"FCfN8F").expect("ok"),
(b"test".to_vec(), 6)
);
}
#[test]
fn ascii85_illegal_first_byte_consumes_nothing() {
for input in [&b"\x00abc"[..], b"~>", b"\x7f"] {
assert_eq!(decode_ascii85(input).expect("ok"), (Vec::new(), 0));
}
}
#[test]
fn ascii85_long_run_of_the_lowest_digit_is_all_zeroes() {
let input = vec![b'!'; 5000];
let (out, consumed) = decode_ascii85(&input).expect("no overflow");
assert_eq!(consumed, 5000);
assert_eq!(out.len(), 4000);
assert!(out.iter().all(|&b| b == 0));
}
#[test]
fn ascii85_treats_a_bare_greater_than_as_a_code_character() {
assert_eq!(
decode_ascii85(b"FCfN8>").expect("ok"),
(b"test".to_vec(), 6)
);
assert_eq!(decode_ascii85(b">>").expect("ok"), (vec![91], 2));
}
#[test]
fn ascii_hex_reference_vectors() {
let cases: [(&[u8], &[u8], usize); 8] = [
(b"", b"", 0),
(b">", b"", 1),
(b"\t \r\n>", b"", 7),
(b"12Ac>zzz", b"\x12\xac", 5),
(b"12 Ac\t02\r\nBF>zzz>", b"\x12\xac\x02\xbf", 13),
(b"12A>zzz", b"\x12\xa0", 4),
(b"12tk \tAc>zzz", b"\x12\xac", 10),
(b"12AcED3c3456", b"\x12\xac\xed\x3c\x34\x56", 12),
];
for (input, expected, consumed) in cases {
assert_eq!(
decode_ascii_hex(input),
(expected.to_vec(), consumed),
"{:?}",
String::from_utf8_lossy(input)
);
}
}
#[test]
fn ascii_hex_skips_interleaved_garbage() {
assert_eq!(decode_ascii_hex(b"1g2h3i>"), (b"\x12\x30".to_vec(), 7));
}
#[test]
fn ascii_hex_whitespace_only_decodes_to_nothing() {
assert_eq!(decode_ascii_hex(b" \r\n\t "), (Vec::new(), 6));
}
#[test]
fn ascii_hex_accepts_both_letter_cases() {
assert_eq!(decode_ascii_hex(b"aAbBcC>").0, b"\xaa\xbb\xcc");
}
}