use crate::buffer::Buffer;
use crate::header::{Encoding, Frame, Header, Zrinit};
use crate::receiver::MAX_ZRPOS_RETRIES;
use crate::wire::{BufferWriter, HeaderReader, SliceReader, SubpacketType};
use crate::{Action, Error, Event, FileInfo, Position, Receiver, Sender, ZDLE, ZPAD};
use rstest::rstest;
fn write_header(header: Header) -> Vec<u8> {
let mut buf = Buffer::<64>::new();
let mut writer = BufferWriter::new(&mut buf);
assert_eq!(header.write(&mut writer), Ok(Some(())));
buf.to_vec()
}
fn read_header(bytes: &[u8]) -> Header {
let mut framed = Vec::with_capacity(bytes.len() + 2);
framed.push(ZPAD);
framed.push(ZDLE);
framed.extend_from_slice(bytes);
let mut reader = SliceReader::new(&framed);
let mut header_reader = HeaderReader::new();
header_reader.read(&mut reader).unwrap().unwrap()
}
fn drain_wire_sender(sender: &mut Sender) {
while let Action::WriteWire(bytes) = sender.poll() {
let n = bytes.len();
sender.wire_written(n);
}
}
fn drain_wire_receiver(receiver: &mut Receiver) {
while let Action::WriteWire(bytes) = receiver.poll() {
let n = bytes.len();
receiver.wire_written(n);
}
}
#[rstest]
#[case(Encoding::ZBIN, Frame::ZRQINIT, [0; 4], &[ZPAD, ZDLE, Encoding::ZBIN as u8, 0, 0, 0, 0, 0, 0, 0])]
#[case(Encoding::ZBIN32, Frame::ZRQINIT, [0; 4], &[ZPAD, ZDLE, Encoding::ZBIN32 as u8, 0, 0, 0, 0, 0, 29, 247, 34, 198])]
#[case(Encoding::ZBIN, Frame::ZRQINIT, [1; 4], &[ZPAD, ZDLE, Encoding::ZBIN as u8, 0, 1, 1, 1, 1, 98, 148])]
#[case(Encoding::ZHEX, Frame::ZRQINIT, [1; 4], &[ZPAD, ZPAD, ZDLE, Encoding::ZHEX as u8, b'0', b'0', b'0', b'1', b'0', b'1', b'0', b'1', b'0', b'1', 54, 50, 57, 52, b'\r', b'\n', crate::XON])]
fn test_header_write(
#[case] encoding: Encoding,
#[case] frame: Frame,
#[case] flags: [u8; 4],
#[case] expected: &[u8],
) {
assert_eq!(write_header(Header::new(encoding, frame, flags)), expected);
}
#[rstest]
#[case(&[Encoding::ZHEX as u8, b'0', b'1', b'0', b'1', b'0', b'2', b'0', b'3', b'0', b'4', b'a', b'7', b'5', b'2'], Encoding::ZHEX, Frame::ZRINIT, [0x1, 0x2, 0x3, 0x4])]
#[case(&[Encoding::ZBIN as u8, Frame::ZRINIT as u8, 0xa, 0xb, 0xc, 0xd, 0xa6, 0xcb], Encoding::ZBIN, Frame::ZRINIT, [0xa, 0xb, 0xc, 0xd])]
#[case(&[Encoding::ZBIN32 as u8, Frame::ZRINIT as u8, 0xa, 0xb, 0xc, 0xd, 0x99, 0xe2, 0xae, 0x4a], Encoding::ZBIN32, Frame::ZRINIT, [0xa, 0xb, 0xc, 0xd])]
#[case(&[Encoding::ZBIN as u8, Frame::ZRINIT as u8, 0xa, ZDLE, b'l', 0xd, ZDLE, b'm', 0x5e, 0x6f], Encoding::ZBIN, Frame::ZRINIT, [0xa, 0x7f, 0xd, 0xff])]
fn test_header_read(
#[case] port: &[u8],
#[case] encoding: Encoding,
#[case] frame: Frame,
#[case] flags: [u8; 4],
) {
assert_eq!(read_header(port), Header::new(encoding, frame, flags));
}
#[test]
fn test_receive_malformed_header() {
let mut receiver = Receiver::new().unwrap();
drain_wire_receiver(&mut receiver);
let input = b"malformed data";
let consumed = receiver.submit_wire(input).unwrap();
assert_eq!(consumed, input.len());
assert_eq!(receiver.poll(), Action::Idle);
}
fn zfile_round_trip(info: FileInfo) -> (Vec<u8>, Option<u32>, Option<u32>, Option<u32>) {
let mut sender = Sender::new().unwrap();
drain_wire_sender(&mut sender);
sender.start_file(info).unwrap();
let zrinit = write_header(Header::new(Encoding::ZHEX, Frame::ZRINIT, [0; 4]));
assert!(sender.submit_wire(&zrinit).unwrap() > 0);
let mut wire = Vec::new();
while let Action::WriteWire(bytes) = sender.poll() {
wire.extend_from_slice(bytes);
let n = bytes.len();
sender.wire_written(n);
}
let mut receiver = Receiver::new().unwrap();
drain_wire_receiver(&mut receiver);
let mut offset = 0;
loop {
match receiver.poll() {
Action::WriteWire(bytes) => {
let n = bytes.len();
receiver.wire_written(n);
}
Action::Event(Event::FileStarted(got)) => {
return (
got.name.to_vec(),
got.size.map(Position::get),
got.modified,
got.mode,
);
}
Action::Event(event) => panic!("unexpected event: {event:?}"),
Action::Idle => {
assert!(offset < wire.len(), "ran out of input before FileStarted");
let consumed = receiver.submit_wire(&wire[offset..]).unwrap();
assert!(consumed > 0);
offset += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
}
}
#[test]
fn test_zfile_carries_time_and_mode() {
let info = FileInfo::new(b"stamped.bin", Some(Position::new(16)))
.with_modified(1_700_000_000)
.with_mode(0o100_644);
let (name, size, modified, mode) = zfile_round_trip(info);
assert_eq!(name, b"stamped.bin");
assert_eq!(size, Some(16));
assert_eq!(modified, Some(1_700_000_000));
assert_eq!(mode, Some(0o100_644));
let mut sender = Sender::new().unwrap();
drain_wire_sender(&mut sender);
sender.start_file(info).unwrap();
let zrinit = write_header(Header::new(Encoding::ZHEX, Frame::ZRINIT, [0; 4]));
assert!(sender.submit_wire(&zrinit).unwrap() > 0);
let mut wire = Vec::new();
while let Action::WriteWire(bytes) = sender.poll() {
wire.extend_from_slice(bytes);
let n = bytes.len();
sender.wire_written(n);
}
let field = b"stamped.bin\x0016 14524770400 100644\x00";
assert!(
wire.windows(field.len()).any(|w| w == field),
"the ZFILE fields are not as lrzsz writes them"
);
}
#[test]
fn test_zfile_without_time_and_mode() {
let (name, size, modified, mode) =
zfile_round_trip(FileInfo::new(b"plain.bin", Some(Position::new(16))));
assert_eq!(name, b"plain.bin");
assert_eq!(size, Some(16));
assert_eq!(modified, None);
assert_eq!(mode, None);
let info = FileInfo::new(b"moded.bin", Some(Position::new(16))).with_mode(0o100_600);
let (_, _, modified, mode) = zfile_round_trip(info);
assert_eq!(modified, None);
assert_eq!(mode, Some(0o100_600));
}
#[test]
fn test_receive_zfile_with_non_utf8_name() {
let file_name = b"bad\x80name";
let file_size = 123u32;
let mut sender = Sender::new().unwrap();
drain_wire_sender(&mut sender);
sender
.start_file(FileInfo::new(file_name, Some(Position::new(file_size))))
.unwrap();
let zrinit = write_header(Header::new(Encoding::ZHEX, Frame::ZRINIT, [0; 4]));
assert!(sender.submit_wire(&zrinit).unwrap() > 0);
let mut wire = Vec::new();
while let Action::WriteWire(bytes) = sender.poll() {
wire.extend_from_slice(bytes);
let n = bytes.len();
sender.wire_written(n);
}
assert_ne!(wire, []);
let mut receiver = Receiver::new().unwrap();
drain_wire_receiver(&mut receiver);
let mut offset = 0;
let mut started: Option<(Vec<u8>, Option<u32>)> = None;
while started.is_none() {
match receiver.poll() {
Action::WriteWire(bytes) => {
let n = bytes.len();
receiver.wire_written(n);
}
Action::Event(Event::FileStarted(info)) => {
started = Some((info.name.to_vec(), info.size.map(Position::get)));
}
Action::Event(event) => panic!("unexpected event: {event:?}"),
Action::Idle => {
assert!(offset < wire.len(), "ran out of input before FileStarted");
let consumed = receiver.submit_wire(&wire[offset..]).unwrap();
assert!(consumed > 0);
offset += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
}
let (name, size) = started.unwrap();
assert_eq!(name, file_name);
assert_eq!(size, Some(file_size));
}
fn test_crc32_iso_hdlc(data: &[u8]) -> u32 {
let mut crc = 0xffff_ffffu32;
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xedb8_8320;
} else {
crc >>= 1;
}
}
}
!crc
}
fn encode_zdle(byte: u8) -> u8 {
match byte {
0x0d => 0x4d,
0x10 => 0x50,
0x11 => 0x51,
0x13 => 0x53,
0x18 => 0x58,
0x7f => 0x6c,
0x8d => 0xcd,
0x90 => 0xd0,
0x91 => 0xd1,
0x93 => 0xd3,
0xff => 0x6d,
_ => byte,
}
}
fn escaped_zbin32_subpacket(kind: SubpacketType, data: &[u8]) -> Vec<u8> {
fn push_escaped(out: &mut Vec<u8>, byte: u8) {
let escaped = encode_zdle(byte);
if escaped != byte {
out.push(ZDLE);
}
out.push(escaped);
}
let mut out = Vec::new();
for &byte in data {
push_escaped(&mut out, byte);
}
out.push(ZDLE);
out.push(kind as u8);
let mut crc_input = Vec::from(data);
crc_input.push(kind as u8);
for byte in test_crc32_iso_hdlc(&crc_input).to_le_bytes() {
push_escaped(&mut out, byte);
}
out
}
fn feed_receiver_zfile(receiver: &mut Receiver) {
drain_wire_receiver(receiver);
let mut bytes = write_header(Header::new(Encoding::ZBIN32, Frame::ZFILE, [0; 4]));
bytes.extend(escaped_zbin32_subpacket(
SubpacketType::ZCRCW,
b"file.bin\x00123\x00",
));
let mut offset = 0;
let mut started = false;
while !started {
match receiver.poll() {
Action::WriteWire(b) => {
let n = b.len();
receiver.wire_written(n);
}
Action::Event(Event::FileStarted(_)) => started = true,
Action::Event(event) => panic!("unexpected event: {event:?}"),
Action::Idle => {
assert!(offset < bytes.len(), "ran out of input before FileStarted");
let consumed = receiver.submit_wire(&bytes[offset..]).unwrap();
assert!(consumed > 0);
offset += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
}
drain_wire_receiver(receiver);
}
fn parse_first_header(bytes: &[u8]) -> Header {
let mut reader = SliceReader::new(bytes);
let mut header_reader = HeaderReader::new();
header_reader.read(&mut reader).unwrap().unwrap()
}
#[test]
fn test_manual_accept_resumes_from_offset() {
let mut receiver = Receiver::new().unwrap();
receiver.set_manual_file_accept(true);
feed_receiver_zfile(&mut receiver);
assert_eq!(receiver.poll(), Action::Idle);
receiver.accept_file_at(64).unwrap();
let (header, n) = match receiver.poll() {
Action::WriteWire(bytes) => (parse_first_header(bytes), bytes.len()),
other => panic!("unexpected action: {other:?}"),
};
receiver.wire_written(n);
assert_eq!(header.frame(), Frame::ZRPOS);
assert_eq!(header.count(), 64);
let mut wire = write_header(Header::new(
Encoding::ZBIN32,
Frame::ZDATA,
64u32.to_le_bytes(),
));
wire.extend(escaped_zbin32_subpacket(SubpacketType::ZCRCE, b"hello"));
let mut offset = 0;
let mut got = Vec::new();
while got.is_empty() {
match receiver.poll() {
Action::WriteWire(bytes) => {
let n = bytes.len();
receiver.wire_written(n);
}
Action::WriteFile(bytes) => {
got = bytes.to_vec();
let n = bytes.len();
receiver.file_written(n).unwrap();
}
Action::Idle => {
assert!(offset < wire.len(), "ran out of input before file data");
let consumed = receiver.submit_wire(&wire[offset..]).unwrap();
assert!(consumed > 0);
offset += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
}
assert_eq!(got, b"hello");
}
#[test]
fn test_manual_accept_can_skip_a_file() {
let mut receiver = Receiver::new().unwrap();
receiver.set_manual_file_accept(true);
feed_receiver_zfile(&mut receiver);
receiver.skip_file().unwrap();
let (header, n) = match receiver.poll() {
Action::WriteWire(bytes) => (parse_first_header(bytes), bytes.len()),
other => panic!("unexpected action: {other:?}"),
};
receiver.wire_written(n);
assert_eq!(header.frame(), Frame::ZSKIP);
assert_eq!(receiver.accept_file_at(0), Err(Error::InvalidState));
assert_eq!(receiver.skip_file(), Err(Error::InvalidState));
}
#[test]
fn test_zsinit_is_acknowledged() {
let mut receiver = Receiver::new().unwrap();
drain_wire_receiver(&mut receiver);
let mut wire = write_header(Header::new(Encoding::ZBIN32, Frame::ZSINIT, [0; 4]));
wire.extend(escaped_zbin32_subpacket(SubpacketType::ZCRCW, b"\x00"));
let mut offset = 0;
let mut acked = false;
while !acked {
match receiver.poll() {
Action::WriteWire(bytes) => {
let header = parse_first_header(bytes);
let n = bytes.len();
receiver.wire_written(n);
if header.frame() == Frame::ZACK {
acked = true;
}
}
Action::Idle => {
assert!(offset < wire.len(), "input ran out before the ZACK");
let consumed = receiver.submit_wire(&wire[offset..]).unwrap();
assert!(consumed > 0);
offset += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
}
feed_receiver_zfile(&mut receiver);
}
#[test]
fn test_zsinit_hex_header_from_lrzsz_is_acknowledged() {
const LRZSZ_ZSINIT: &[u8] = &[
0x2a, 0x2a, 0x18, 0x42, 0x30, 0x32, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x34, 0x30, 0x30, 0x63, 0x34, 0x37, 0x0d, 0x8a, 0x11, 0x18, 0x40, 0x18, 0x6b, 0xdd, 0xcd, 0x11, ];
let mut receiver = Receiver::new().unwrap();
drain_wire_receiver(&mut receiver);
let mut offset = 0;
let mut acked = false;
while !acked {
match receiver.poll() {
Action::WriteWire(bytes) => {
let header = parse_first_header(bytes);
let n = bytes.len();
receiver.wire_written(n);
if header.frame() == Frame::ZACK {
acked = true;
}
}
Action::Idle => {
assert!(offset < LRZSZ_ZSINIT.len(), "input ran out before the ZACK");
let consumed = receiver.submit_wire(&LRZSZ_ZSINIT[offset..]).unwrap();
assert!(consumed > 0);
offset += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
}
feed_receiver_zfile(&mut receiver);
}
#[test]
fn test_sender_resumes_from_zrpos() {
let mut sender = Sender::new().unwrap();
drain_wire_sender(&mut sender);
sender
.start_file(FileInfo::new(b"resume.bin", Some(Position::new(16))))
.unwrap();
let zrinit = write_header(Header::new(Encoding::ZHEX, Frame::ZRINIT, [0; 4]));
assert!(sender.submit_wire(&zrinit).unwrap() > 0);
drain_wire_sender(&mut sender);
let zrpos = write_header(Header::new(
Encoding::ZHEX,
Frame::ZRPOS,
5u32.to_le_bytes(),
));
assert!(sender.submit_wire(&zrpos).unwrap() > 0);
match sender.poll() {
Action::ReadFile { offset, .. } => assert_eq!(offset, Position::new(5)),
other => panic!("unexpected action: {other:?}"),
}
}
#[test]
fn test_sender_skips_file_on_zskip() {
let mut sender = Sender::new().unwrap();
drain_wire_sender(&mut sender);
sender
.start_file(FileInfo::new(b"skip.bin", Some(Position::new(16))))
.unwrap();
let zrinit = write_header(Header::new(Encoding::ZHEX, Frame::ZRINIT, [0; 4]));
sender.submit_wire(&zrinit).unwrap();
drain_wire_sender(&mut sender);
let zskip = write_header(Header::new(Encoding::ZHEX, Frame::ZSKIP, [0; 4]));
sender.submit_wire(&zskip).unwrap();
assert_eq!(sender.poll(), Action::Event(Event::FileCompleted));
}
#[test]
fn test_local_abort_sends_cancel_first() {
let mut expected = [0x18u8; 20];
expected[10..].fill(0x08);
let mut sender = Sender::new().unwrap();
sender.abort();
match sender.poll() {
Action::WriteWire(bytes) => assert_eq!(bytes, &expected[..]),
other => panic!("expected the cancel sequence, got {other:?}"),
}
sender.wire_written(20);
assert_eq!(sender.poll(), Action::Event(Event::Aborted));
assert_eq!(sender.poll(), Action::Idle);
let mut receiver = Receiver::new().unwrap();
drain_wire_receiver(&mut receiver);
receiver.abort().unwrap();
match receiver.poll() {
Action::WriteWire(bytes) => assert_eq!(bytes, &expected[..]),
other => panic!("expected the cancel sequence, got {other:?}"),
}
receiver.wire_written(7);
match receiver.poll() {
Action::WriteWire(bytes) => assert_eq!(bytes, &expected[7..]),
other => panic!("expected the rest of the cancel sequence, got {other:?}"),
}
receiver.wire_written(13);
assert_eq!(receiver.poll(), Action::Event(Event::Aborted));
}
#[test]
fn test_abort_events() {
let zabort = write_header(Header::new(Encoding::ZHEX, Frame::ZABORT, [0; 4]));
let mut sender = Sender::new().unwrap();
drain_wire_sender(&mut sender);
assert!(sender.submit_wire(&zabort).unwrap() > 0);
assert_eq!(sender.poll(), Action::Event(Event::Aborted));
let mut receiver = Receiver::new().unwrap();
drain_wire_receiver(&mut receiver);
assert!(receiver.submit_wire(&zabort).unwrap() > 0);
assert_eq!(receiver.poll(), Action::Event(Event::Aborted));
}
#[test]
fn test_receiver_timeout_requeues_zrinit() {
let mut receiver = Receiver::new().unwrap();
drain_wire_receiver(&mut receiver);
receiver.timeout().unwrap();
match receiver.poll() {
Action::WriteWire(bytes) => assert_ne!(bytes, []),
other => panic!("unexpected action: {other:?}"),
}
}
#[test]
fn test_start_file_rejects_unknown_size() {
let mut sender = Sender::new().unwrap();
assert_eq!(
sender.start_file(FileInfo::new(b"unknown.bin", None)),
Err(Error::UnsupportedFeature)
);
}
fn feed_corrupt_subpacket(receiver: &mut Receiver, offset: u32) -> Result<u32, Error> {
let mut bytes = write_header(Header::new(
Encoding::ZBIN32,
Frame::ZDATA,
offset.to_le_bytes(),
));
let mut subpacket = escaped_zbin32_subpacket(SubpacketType::ZCRCG, b"corrupt");
subpacket[0] ^= 0x01; bytes.extend(subpacket);
bytes.extend_from_slice(&[ZPAD, ZDLE, encode_zdle(0x11), b'x', b'y']);
let mut consumed_total = 0;
let mut zrpos = None;
loop {
match receiver.poll() {
Action::WriteWire(b) => {
let header = parse_first_header(b);
if header.frame() == Frame::ZRPOS {
zrpos = Some(header.count());
}
let n = b.len();
receiver.wire_written(n);
}
Action::WriteFile(_) => panic!("corrupt data must never be written"),
Action::Idle => {
if consumed_total >= bytes.len() {
break;
}
let consumed = receiver.submit_wire(&bytes[consumed_total..])?;
assert!(consumed > 0, "receiver made no progress on input");
consumed_total += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
}
Ok(zrpos.expect("a corrupt subpacket must queue a ZRPOS"))
}
#[test]
fn test_corrupt_subpacket_triggers_zrpos_and_recovers() {
let mut receiver = Receiver::new().unwrap();
feed_receiver_zfile(&mut receiver);
let count = feed_corrupt_subpacket(&mut receiver, 0).unwrap();
assert_eq!(count, 0, "receiver should rewind to the last good offset");
let mut good = write_header(Header::new(Encoding::ZBIN32, Frame::ZDATA, [0; 4]));
good.extend(escaped_zbin32_subpacket(SubpacketType::ZCRCE, b"clean"));
consume_file_chunk(&mut receiver, &good, b"clean");
}
#[test]
fn test_repeated_corruption_eventually_fails() {
let mut receiver = Receiver::new().unwrap();
feed_receiver_zfile(&mut receiver);
let mut result = Ok(0);
for _ in 0..64 {
result = feed_corrupt_subpacket(&mut receiver, 0);
if result.is_err() {
break;
}
}
assert_eq!(result, Err(Error::UnexpectedCrc32));
assert_eq!(receiver.submit_wire(&[0]), Err(Error::UnexpectedCrc32));
assert_eq!(receiver.submit_wire(&[]), Err(Error::UnexpectedCrc32));
}
#[test]
fn test_recovery_budget_replenishes_on_progress() {
let mut receiver = Receiver::new().unwrap();
feed_receiver_zfile(&mut receiver);
let mut offset = 0u32;
for _ in 0..(u32::from(MAX_ZRPOS_RETRIES) * 3) {
let count = feed_corrupt_subpacket(&mut receiver, offset).unwrap();
assert_eq!(
count, offset,
"receiver should rewind to the last good offset"
);
let mut good = write_header(Header::new(
Encoding::ZBIN32,
Frame::ZDATA,
offset.to_le_bytes(),
));
good.extend(escaped_zbin32_subpacket(SubpacketType::ZCRCE, b"clean"));
consume_file_chunk(&mut receiver, &good, b"clean");
offset += 5;
}
}
#[test]
fn test_offset_overflow_is_refused() {
let mut receiver = Receiver::new().unwrap();
receiver.set_manual_file_accept(true);
feed_receiver_zfile(&mut receiver);
let near = u32::MAX - 4;
receiver.accept_file_at(near).unwrap();
loop {
match receiver.poll() {
Action::WriteWire(b) => {
let n = b.len();
receiver.wire_written(n);
}
Action::Idle => break,
other => panic!("unexpected action: {other:?}"),
}
}
let mut wire = write_header(Header::new(
Encoding::ZBIN32,
Frame::ZDATA,
near.to_le_bytes(),
));
wire.extend(escaped_zbin32_subpacket(SubpacketType::ZCRCE, b"overflow!"));
let mut offset = 0;
let err = loop {
match receiver.poll() {
Action::WriteWire(b) => {
let n = b.len();
receiver.wire_written(n);
}
Action::WriteFile(b) => {
let n = b.len();
if let Err(e) = receiver.file_written(n) {
break e;
}
}
Action::Idle => {
assert!(offset < wire.len(), "ran out of input before overflow");
let consumed = receiver.submit_wire(&wire[offset..]).unwrap();
offset += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
};
assert_eq!(err, Error::OutOfMemory);
}
#[test]
fn test_receiver_zcrcq_and_zcrce() {
let mut receiver = Receiver::new().unwrap();
feed_receiver_zfile(&mut receiver);
let mut first = write_header(Header::new(Encoding::ZBIN32, Frame::ZDATA, [0; 4]));
first.extend(escaped_zbin32_subpacket(SubpacketType::ZCRCQ, b"abc"));
consume_file_chunk(&mut receiver, &first, b"abc");
match receiver.poll() {
Action::WriteWire(bytes) => {
assert_ne!(bytes, []);
let n = bytes.len();
receiver.wire_written(n);
}
other => panic!("expected ZACK, got {other:?}"),
}
let second = escaped_zbin32_subpacket(SubpacketType::ZCRCE, b"def");
consume_file_chunk(&mut receiver, &second, b"def");
assert_eq!(receiver.poll(), Action::Idle);
let zeof = write_header(Header::new(
Encoding::ZBIN32,
Frame::ZEOF,
6u32.to_le_bytes(),
));
assert!(receiver.submit_wire(&zeof).unwrap() > 0);
assert_eq!(receiver.poll(), Action::Event(Event::FileCompleted));
}
fn consume_file_chunk(receiver: &mut Receiver, input: &[u8], expected: &[u8]) {
let mut offset = 0;
loop {
match receiver.poll() {
Action::WriteFile(bytes) => {
assert_eq!(bytes, expected);
let n = bytes.len();
receiver.file_written(n).unwrap();
break;
}
Action::WriteWire(bytes) => {
let n = bytes.len();
receiver.wire_written(n);
}
Action::Idle => {
assert!(offset < input.len(), "ran out of input before file data");
let consumed = receiver.submit_wire(&input[offset..]).unwrap();
offset += consumed;
}
other => panic!("unexpected action: {other:?}"),
}
}
}
#[test]
fn test_abort_event() {
let mut sender = Sender::new().unwrap();
drain_wire_sender(&mut sender);
sender.abort();
let n = match sender.poll() {
Action::WriteWire(bytes) => bytes.len(),
other => panic!("expected the cancel sequence, got {other:?}"),
};
sender.wire_written(n);
assert_eq!(sender.poll(), Action::Event(Event::Aborted));
}
#[test]
fn test_zrinit_advertises_configured_flow_control() {
let mut receiver = Receiver::new().unwrap();
let header = match receiver.poll() {
Action::WriteWire(bytes) => parse_first_header(bytes),
other => panic!("unexpected action: {other:?}"),
};
let flags = header.count().to_le_bytes();
assert_eq!(u16::from_le_bytes([flags[0], flags[1]]), 1024);
assert_eq!(flags[3] & Zrinit::CANOVIO.bits(), 0);
let mut receiver = Receiver::with_flow_control(0, true).unwrap();
let header = match receiver.poll() {
Action::WriteWire(bytes) => parse_first_header(bytes),
other => panic!("unexpected action: {other:?}"),
};
let flags = header.count().to_le_bytes();
assert_eq!(u16::from_le_bytes([flags[0], flags[1]]), 0);
assert_ne!(flags[3] & Zrinit::CANOVIO.bits(), 0);
}
fn count_zcrcw_waits(window: Option<usize>, window_after: Option<usize>, file_len: usize) -> usize {
let mut sender = Sender::new().unwrap();
if let Some(window) = window {
sender.set_streaming_window(window);
}
drain_wire_sender(&mut sender);
sender
.start_file(FileInfo::new(
b"stream.bin",
Some(Position::new(u32::try_from(file_len).unwrap())),
))
.unwrap();
let mut flags = [0u8; 4];
flags[3] = (Zrinit::CANFDX | Zrinit::CANFC32 | Zrinit::CANOVIO).bits();
let zrinit = write_header(Header::new(Encoding::ZHEX, Frame::ZRINIT, flags));
assert!(sender.submit_wire(&zrinit).unwrap() > 0);
if let Some(window) = window_after {
sender.set_streaming_window(window);
}
drain_wire_sender(&mut sender);
let zrpos = write_header(Header::new(
Encoding::ZHEX,
Frame::ZRPOS,
0u32.to_le_bytes(),
));
assert!(sender.submit_wire(&zrpos).unwrap() > 0);
let mut zcrcw = 0usize;
let mut sent = 0usize;
loop {
match sender.poll() {
Action::WriteWire(bytes) => {
zcrcw += bytes
.windows(2)
.filter(|pair| pair == &[ZDLE, SubpacketType::ZCRCW as u8])
.count();
let n = bytes.len();
sender.wire_written(n);
}
Action::ReadFile { offset, max_len } => {
let remaining = file_len - offset.get() as usize;
let n = remaining.min(max_len);
sender.submit_file(&vec![0u8; n]).unwrap();
sent += n;
}
Action::Idle => {
if sent >= file_len {
break;
}
let zack = write_header(Header::new(
Encoding::ZHEX,
Frame::ZACK,
u32::try_from(sent).unwrap().to_le_bytes(),
));
assert!(sender.submit_wire(&zack).unwrap() > 0);
}
Action::Event(_) => {}
other => panic!("unexpected action: {other:?}"),
}
}
zcrcw
}
#[test]
fn test_streaming_window_controls_ack_cadence() {
assert_eq!(count_zcrcw_waits(None, None, 32 * 1024), 4);
assert_eq!(count_zcrcw_waits(Some(usize::MAX), None, 32 * 1024), 1);
}
#[test]
fn test_streaming_window_applies_after_zrinit() {
assert_eq!(count_zcrcw_waits(Some(usize::MAX), None, 32 * 1024), 1);
assert_eq!(count_zcrcw_waits(None, Some(usize::MAX), 32 * 1024), 1);
}
fn run_full_transfer(contents: &[u8]) -> Vec<u8> {
let mut sender = Sender::new().unwrap();
let mut receiver = Receiver::new().unwrap();
sender
.start_file(FileInfo::new(
b"payload.bin",
Some(Position::new(u32::try_from(contents.len()).unwrap())),
))
.unwrap();
sender.finish().unwrap();
let mut downstream: Vec<u8> = Vec::new();
let mut upstream: Vec<u8> = Vec::new();
let mut persisted: Vec<u8> = Vec::new();
let mut file_done = false;
let mut session_done = false;
for _ in 0..100_000 {
let mut progressed = false;
match sender.poll() {
Action::WriteWire(bytes) => {
let n = bytes.len();
downstream.extend_from_slice(bytes);
sender.wire_written(n);
progressed = true;
}
Action::ReadFile { offset, max_len } => {
let start = offset.get() as usize;
let end = (start + max_len).min(contents.len());
sender.submit_file(&contents[start..end]).unwrap();
progressed = true;
}
Action::Event(Event::SessionCompleted) => {
session_done = true;
progressed = true;
}
Action::Event(_) => progressed = true,
Action::Idle => {
if !upstream.is_empty() {
let consumed = sender.submit_wire(&upstream).unwrap();
upstream.drain(..consumed);
progressed = true;
}
}
other => panic!("unexpected sender action: {other:?}"),
}
match receiver.poll() {
Action::WriteWire(bytes) => {
let n = bytes.len();
upstream.extend_from_slice(bytes);
receiver.wire_written(n);
progressed = true;
}
Action::WriteFile(bytes) => {
persisted.extend_from_slice(bytes);
let n = bytes.len();
receiver.file_written(n).unwrap();
progressed = true;
}
Action::Event(Event::FileCompleted) => {
file_done = true;
progressed = true;
}
Action::Event(_) => progressed = true,
Action::Idle => {
if !downstream.is_empty() {
let consumed = receiver.submit_wire(&downstream).unwrap();
downstream.drain(..consumed);
progressed = true;
}
}
other => panic!("unexpected receiver action: {other:?}"),
}
if session_done && downstream.is_empty() && upstream.is_empty() {
break;
}
if !progressed {
break;
}
}
assert!(file_done, "file transfer never completed (deadlock?)");
assert!(session_done, "session never completed");
persisted
}
#[test]
fn test_empty_file_transfer_completes() {
assert_eq!(run_full_transfer(b""), b"");
}
#[test]
fn test_nonempty_file_transfer_roundtrips() {
let contents: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
assert_eq!(run_full_transfer(&contents), contents);
}
#[test]
fn test_manual_accept_at_eof_completes() {
let mut receiver = Receiver::new().unwrap();
receiver.set_manual_file_accept(true);
feed_receiver_zfile(&mut receiver);
receiver.accept_file_at(123).unwrap();
drain_wire_receiver(&mut receiver);
let zeof = write_header(Header::new(
Encoding::ZBIN32,
Frame::ZEOF,
123u32.to_le_bytes(),
));
assert!(receiver.submit_wire(&zeof).unwrap() > 0);
assert_eq!(receiver.poll(), Action::Event(Event::FileCompleted));
}