use crate::error::YencError;
use crate::header::{parse_ybegin, parse_yend, parse_ypart};
use crate::{DecodedPart, YencMetadata};
pub fn decode(input: &[u8]) -> Result<DecodedPart, YencError> {
let mut lines = LineIter::new(input);
let ybegin_payload = loop {
let line = lines.next().ok_or(YencError::NoHeader)?;
if let Some(payload) = strip_keyword(line, b"=ybegin ") {
break payload;
}
};
let ybegin = parse_ybegin(lossy_str(ybegin_payload).trim())?;
let filename = ybegin.name.ok_or(YencError::InvalidHeader {
field: "name".to_string(),
})?;
let total_size = ybegin.size.ok_or(YencError::InvalidHeader {
field: "size".to_string(),
})?;
let mut part_begin: Option<u64> = None;
let mut part_end: Option<u64> = None;
let first_data_line = {
let peeked = lines.next().ok_or(YencError::UnexpectedEof)?;
if let Some(payload) = strip_keyword(peeked, b"=ypart ") {
let ypart = parse_ypart(lossy_str(payload).trim())?;
part_begin = ypart.begin;
part_end = ypart.end;
None } else {
Some(peeked) }
};
let mut decoded: Vec<u8> = Vec::new();
if let Some(line) = first_data_line {
if let Some(yend_payload) = strip_keyword(line, b"=yend ") {
return finish_decode(
decoded,
yend_payload,
filename,
total_size,
ybegin.line_length.unwrap_or(128),
ybegin.part,
ybegin.total,
part_begin,
part_end,
);
}
decode_line(line, &mut decoded);
}
loop {
let line = lines.next().ok_or(YencError::UnexpectedEof)?;
if let Some(yend_payload) = strip_keyword(line, b"=yend ") {
return finish_decode(
decoded,
yend_payload,
filename,
total_size,
ybegin.line_length.unwrap_or(128),
ybegin.part,
ybegin.total,
part_begin,
part_end,
);
}
decode_line(line, &mut decoded);
}
}
#[allow(clippy::too_many_arguments)]
fn finish_decode(
data: Vec<u8>,
yend_raw: &[u8],
filename: String,
total_size: u64,
line_length: u8,
part: Option<u32>,
total_parts: Option<u32>,
part_begin: Option<u64>,
part_end: Option<u64>,
) -> Result<DecodedPart, YencError> {
let yend = parse_yend(lossy_str(yend_raw).trim())?;
let crc32_verified;
if let Some(expected) = yend.pcrc32.or(yend.crc32) {
let actual = crc32fast::hash(&data);
if actual != expected {
return Err(YencError::CrcMismatch { expected, actual });
}
crc32_verified = true;
} else {
crc32_verified = false;
}
Ok(DecodedPart {
data,
metadata: YencMetadata {
filename,
size: total_size,
line_length,
total_parts,
},
part,
part_begin,
part_end,
crc32_verified,
})
}
pub(crate) fn decode_line(line: &[u8], out: &mut Vec<u8>) {
let line = if line.len() >= 2 && line[0] == b'.' && line[1] == b'.' {
&line[1..]
} else {
line
};
let mut i = 0;
let len = line.len();
while i < len {
let b = line[i];
match b {
b'\r' | b'\n' | b'\0' => {
i += 1;
}
b'=' if i + 1 < len => {
let next = line[i + 1];
out.push(next.wrapping_sub(106));
i += 2;
}
b'=' => {
i += 1;
}
_ => {
out.push(b.wrapping_sub(42));
i += 1;
}
}
}
}
struct LineIter<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> LineIter<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn next(&mut self) -> Option<&'a [u8]> {
if self.pos >= self.data.len() {
return None;
}
let start = self.pos;
let nl = self.data[start..]
.iter()
.position(|&b| b == b'\n')
.map(|r| start + r);
let end = match nl {
Some(nl_pos) => nl_pos + 1, None => self.data.len(), };
self.pos = end;
Some(&self.data[start..end])
}
}
fn strip_keyword<'a>(line: &'a [u8], keyword: &[u8]) -> Option<&'a [u8]> {
let line = line
.strip_suffix(b"\r\n")
.or_else(|| line.strip_suffix(b"\n"))
.or_else(|| line.strip_suffix(b"\r"))
.unwrap_or(line);
line.strip_prefix(keyword)
}
fn lossy_str(bytes: &[u8]) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_line_simple_bytes() {
let encoded = b"*+,-";
let mut out = Vec::new();
decode_line(encoded, &mut out);
assert_eq!(out, &[0, 1, 2, 3]);
}
#[test]
fn decode_line_escape_sequence() {
let encoded = b"=}";
let mut out = Vec::new();
decode_line(encoded, &mut out);
assert_eq!(out, &[19]);
}
#[test]
fn decode_line_discards_crlf() {
let encoded = b"*+\r\n";
let mut out = Vec::new();
decode_line(encoded, &mut out);
assert_eq!(out, &[0, 1]);
}
#[test]
fn decode_line_discards_nul() {
let encoded = b"*\x00+";
let mut out = Vec::new();
decode_line(encoded, &mut out);
assert_eq!(out, &[0, 1]);
}
#[test]
fn decode_line_dot_stuffing() {
let encoded = b"..+";
let mut out = Vec::new();
decode_line(encoded, &mut out);
assert_eq!(out, &[4, 1]);
}
#[test]
fn decode_line_no_dot_stuffing_single_dot() {
let encoded = b".+";
let mut out = Vec::new();
decode_line(encoded, &mut out);
assert_eq!(out, &[4, 1]);
}
#[test]
fn decode_line_all_256_bytes() {
let raw: Vec<u8> = (0u8..=255).collect();
let mut encoded = Vec::new();
for &b in &raw {
let v = b.wrapping_add(42);
if matches!(v, 0 | 10 | 13 | 61) {
encoded.push(b'=');
encoded.push(v.wrapping_add(64));
} else {
encoded.push(v);
}
}
let mut decoded = Vec::new();
decode_line(&encoded, &mut decoded);
assert_eq!(decoded, raw, "all-bytes round-trip failed");
}
#[test]
fn decode_line_trailing_lone_eq_discarded() {
let encoded = b"*=";
let mut out = Vec::new();
decode_line(encoded, &mut out); assert_eq!(out, &[0]); }
fn load_fixture(name: &str) -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name);
std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read fixture {name}: {e}"))
}
#[test]
fn decode_single_part_fixture() {
let input = load_fixture("single_part.yenc");
let part = decode(&input).expect("single_part.yenc should decode cleanly");
assert_eq!(part.data.len(), 64, "decoded length");
assert_eq!(part.data, (0u8..64).collect::<Vec<_>>(), "decoded payload");
assert_eq!(part.metadata.filename, "test.bin");
assert_eq!(part.metadata.size, 64);
assert!(part.crc32_verified, "CRC should be verified");
assert!(part.part.is_none(), "single-part has no part number");
assert!(part.part_begin.is_none());
assert!(part.part_end.is_none());
assert!(part.metadata.total_parts.is_none());
}
#[test]
fn decode_multi_part_1_fixture() {
let input = load_fixture("multi_part_1.yenc");
let part = decode(&input).expect("multi_part_1.yenc should decode");
assert_eq!(part.data, (0u8..64).collect::<Vec<_>>());
assert_eq!(part.metadata.filename, "test.bin");
assert_eq!(part.metadata.size, 128); assert_eq!(part.part, Some(1));
assert_eq!(part.metadata.total_parts, Some(2));
assert_eq!(part.part_begin, Some(1)); assert_eq!(part.part_end, Some(64));
assert!(part.crc32_verified);
}
#[test]
fn decode_multi_part_2_fixture() {
let input = load_fixture("multi_part_2.yenc");
let part = decode(&input).expect("multi_part_2.yenc should decode");
assert_eq!(part.data, (64u8..128).collect::<Vec<_>>());
assert_eq!(part.part, Some(2));
assert_eq!(part.metadata.total_parts, Some(2));
assert_eq!(part.part_begin, Some(65));
assert_eq!(part.part_end, Some(128));
assert!(part.crc32_verified);
}
#[test]
fn decode_prose_preamble_fixture() {
let input = load_fixture("prose_preamble.yenc");
let part = decode(&input).expect("prose_preamble.yenc should decode");
assert_eq!(part.data, (0u8..64).collect::<Vec<_>>());
assert!(part.crc32_verified);
}
#[test]
fn decode_crc_mismatch_fixture() {
let input = load_fixture("crc_mismatch.yenc");
let err = decode(&input).expect_err("crc_mismatch.yenc should return CrcMismatch");
assert!(
matches!(err, YencError::CrcMismatch { expected: 0, .. }),
"expected CrcMismatch with expected=0, got: {:?}",
err
);
}
#[test]
fn decode_truncated_fixture() {
let input = load_fixture("truncated.yenc");
let err = decode(&input).expect_err("truncated.yenc should return UnexpectedEof");
assert_eq!(err, YencError::UnexpectedEof);
}
#[test]
fn decode_empty_input_returns_no_header() {
let err = decode(b"").unwrap_err();
assert_eq!(err, YencError::NoHeader);
}
#[test]
fn decode_missing_name_field_is_error() {
let input = b"=ybegin line=128 size=10\n*+,-\n=yend size=4\n";
let err = decode(input).unwrap_err();
assert!(matches!(err, YencError::InvalidHeader { field } if field == "name"));
}
#[test]
fn decode_missing_size_field_is_error() {
let input = b"=ybegin line=128 name=f.bin\n*+,-\n=yend size=4\n";
let err = decode(input).unwrap_err();
assert!(matches!(err, YencError::InvalidHeader { field } if field == "size"));
}
#[test]
fn decode_missing_yend_is_unexpected_eof() {
let input = b"=ybegin line=128 size=4 name=f.bin\n*+,-\n";
let err = decode(input).unwrap_err();
assert_eq!(err, YencError::UnexpectedEof);
}
#[test]
fn decode_no_panic_on_arbitrary_input() {
let inputs: &[&[u8]] = &[
b"",
b"\x00\x01\x02",
b"=ybegin",
b"=ybegin \n",
b"=ybegin size=0 name=f\n=yend size=0 crc32=00000000\n",
b"=ybegin size=1 name=f\n*\n=yend size=1 crc32=00000000\n",
&[b'='; 256],
&[0xFF; 100],
];
for input in inputs {
let _ = decode(input); }
}
}