pub const DEFAULT_LINE_LENGTH: u8 = 128;
pub fn encode(data: &[u8], filename: &str, line_length: u8) -> Vec<u8> {
let line_length = line_length.max(1) as usize; let mut out = Vec::with_capacity(data.len() * 11 / 10 + 128);
out.extend_from_slice(
format!(
"=ybegin line={line_length} size={} name={filename}\r\n",
data.len()
)
.as_bytes(),
);
let crc = encode_body(data, line_length, &mut out);
out.extend_from_slice(format!("=yend size={} crc32={crc:08x}\r\n", data.len()).as_bytes());
out
}
#[allow(clippy::too_many_arguments)]
pub fn encode_part(
data: &[u8],
filename: &str,
total_size: u64,
total_parts: u32,
part: u32,
begin: u64,
end: u64,
whole_file_crc32: u32,
line_length: u8,
) -> Vec<u8> {
let line_length = line_length.max(1) as usize;
let mut out = Vec::with_capacity(data.len() * 11 / 10 + 256);
out.extend_from_slice(
format!(
"=ybegin part={part} total={total_parts} line={line_length} \
size={total_size} name={filename}\r\n"
)
.as_bytes(),
);
out.extend_from_slice(format!("=ypart begin={begin} end={end}\r\n").as_bytes());
let pcrc = encode_body(data, line_length, &mut out);
out.extend_from_slice(
format!(
"=yend size={} part={part} pcrc32={pcrc:08x} crc32={whole_file_crc32:08x}\r\n",
data.len()
)
.as_bytes(),
);
out
}
fn encode_body(data: &[u8], line_length: usize, out: &mut Vec<u8>) -> u32 {
let mut hasher = crc32fast::Hasher::new();
hasher.update(data);
let crc = hasher.finalize();
let mut col: usize = 0;
for &byte in data {
let encoded = byte.wrapping_add(42);
let must_escape = matches!(encoded, 0x00 | 0x0A | 0x0D | 0x3D)
|| (col == 0 && matches!(encoded, 0x2E | 0x09));
if must_escape {
out.push(b'=');
out.push(encoded.wrapping_add(64));
col += 2;
} else {
out.push(encoded);
col += 1;
}
if col >= line_length {
out.extend_from_slice(b"\r\n");
col = 0;
}
}
if col > 0 {
out.extend_from_slice(b"\r\n");
}
crc
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decode::decode;
#[test]
fn encode_simple_bytes() {
let mut out = Vec::new();
let crc = encode_body(&[0, 1, 2, 3], 128, &mut out);
assert_eq!(&out[..4], b"*+,-");
assert_eq!(crc, 0x8bb9_8613);
}
#[test]
fn encode_escapes_nul() {
let mut out = Vec::new();
encode_body(&[214], 128, &mut out);
assert_eq!(&out[..2], b"=@");
}
#[test]
fn encode_escapes_lf() {
let mut out = Vec::new();
encode_body(&[224], 128, &mut out);
assert_eq!(&out[..2], b"=J");
}
#[test]
fn encode_escapes_eq() {
let mut out = Vec::new();
encode_body(&[19], 128, &mut out);
assert_eq!(&out[..2], b"=}");
}
#[test]
fn encode_escapes_dot_at_line_start() {
let mut out = Vec::new();
encode_body(&[4], 128, &mut out);
assert_eq!(&out[..2], b"=n");
}
#[test]
fn encode_dot_not_escaped_mid_line() {
let mut out = Vec::new();
encode_body(&[1, 4], 128, &mut out);
assert_eq!(out[0], b'+');
assert_eq!(out[1], b'.'); }
#[test]
fn encode_line_wrapping() {
let data = vec![0u8; 8]; let mut out = Vec::new();
encode_body(&data, 4, &mut out);
assert_eq!(&out[..4], b"****");
assert_eq!(&out[4..6], b"\r\n");
assert_eq!(&out[6..10], b"****");
assert_eq!(&out[10..12], b"\r\n");
}
#[test]
fn encode_all_bytes_round_trip() {
let raw: Vec<u8> = (0u8..=255).collect();
let mut expected_encoded = Vec::new();
for &b in &raw {
let v = b.wrapping_add(42);
if matches!(v, 0 | 10 | 13 | 61) {
expected_encoded.push(b'=');
expected_encoded.push(v.wrapping_add(64));
} else if v == b'.' || v == 0x09 {
expected_encoded.push(v);
} else {
expected_encoded.push(v);
}
}
let encoded = encode(&raw, "all.bin", 128);
let decoded = decode(&encoded).expect("round-trip decode failed");
assert_eq!(decoded.data, raw, "all-bytes round-trip failed");
}
#[test]
fn encode_single_part_header_footer() {
let data = b"Cat";
let out = encode(data, "cat.bin", 128);
let s = String::from_utf8_lossy(&out);
assert!(s.starts_with("=ybegin line=128 size=3 name=cat.bin\r\n"));
assert!(s.contains("=yend size=3 crc32="));
assert!(s.ends_with("\r\n"));
}
#[test]
fn encode_empty_data() {
let out = encode(b"", "empty.bin", 128);
let s = String::from_utf8_lossy(&out);
assert!(s.starts_with("=ybegin line=128 size=0 name=empty.bin\r\n"));
assert!(s.contains("=yend size=0 crc32="));
let parts: Vec<&str> = s.lines().collect();
assert_eq!(parts[0], "=ybegin line=128 size=0 name=empty.bin");
assert_eq!(parts[1], "=yend size=0 crc32=00000000");
}
#[test]
fn encode_single_part_crc_correct() {
let data: Vec<u8> = (0..64).collect();
let out = encode(&data, "test.bin", 128);
assert!(
String::from_utf8_lossy(&out).contains("crc32=100ece8c"),
"CRC32 mismatch in encoded output"
);
let decoded = decode(&out).unwrap();
assert_eq!(decoded.data, data);
assert!(decoded.crc32_verified);
}
#[test]
fn encode_part_header_fields() {
let data: Vec<u8> = (0..64).collect();
let out = encode_part(&data, "test.bin", 128, 2, 1, 1, 64, 0xdeadbeef, 128);
let s = String::from_utf8_lossy(&out);
assert!(s.starts_with("=ybegin part=1 total=2 line=128 size=128 name=test.bin\r\n"));
assert!(s.contains("=ypart begin=1 end=64\r\n"));
assert!(s.contains("pcrc32="));
assert!(s.contains("crc32=deadbeef"));
}
#[test]
fn encode_part_pcrc_is_part_crc() {
let data: Vec<u8> = (0..64).collect();
let out = encode_part(&data, "test.bin", 128, 2, 1, 1, 64, 0x24650d57, 128);
let s = String::from_utf8_lossy(&out);
assert!(s.contains("pcrc32=100ece8c"), "per-part CRC wrong: {s}");
assert!(s.contains("crc32=24650d57"), "whole-file CRC wrong: {s}");
}
}