crates/gunnar-pktline/tests/decode.rs
//! Decoder tests: literal input bytes in, packets out; and the two properties
//! that keep a real connection alive: **every proper prefix is incomplete**, and
//! **every impossible header fails immediately**.
use gunnar_pktline::*;
fn one(buf: &[u8]) -> (Packet, usize) {
match decode(buf).expect("decode failed") {
Decoded::Packet(p, n) => (p, n),
Decoded::Incomplete => panic!("unexpected Incomplete for {buf:?}"),
}
}
#[test]
fn decodes_a_data_packet_and_reports_bytes_consumed_including_the_header() {
let (p, n) = one(b"0006hi");
assert_eq!(p, Packet::Data(b"hi".to_vec()));
assert_eq!(n, 6, "consumed count must include the 4 header bytes");
}
#[test]
fn decodes_the_three_control_packets() {
assert_eq!(one(b"0000"), (Packet::Flush, 4));
assert_eq!(one(b"0001"), (Packet::Delim, 4));
assert_eq!(one(b"0002"), (Packet::ResponseEnd, 4));
}
#[test]
fn an_empty_data_packet_is_not_a_flush() {
assert_eq!(one(b"0004"), (Packet::Data(Vec::new()), 4));
assert_ne!(one(b"0004").0, Packet::Flush);
}
#[test]
fn trailing_bytes_are_left_alone() {
// The next packet's bytes, or a raw packfile, routinely follow in the same
// buffer. Consuming them here is how a packfile loses its head.
let (p, n) = one(b"0006hi0000PACK");
assert_eq!(p, Packet::Data(b"hi".to_vec()));
assert_eq!(n, 6);
}
#[test]
fn every_proper_prefix_of_every_packet_kind_is_incomplete() {
let mut big = b"fff0".to_vec();
big.extend_from_slice(&vec![b'x'; MAX_PAYLOAD]);
let cases: Vec<Vec<u8>> = vec![
b"0000".to_vec(),
b"0001".to_vec(),
b"0002".to_vec(),
b"0004".to_vec(),
b"0006hi".to_vec(),
b"002fsome rather longer payload than four bytes\n".to_vec(),
big,
];
for full in cases {
for cut in 0..full.len() {
let got = decode(&full[..cut]);
match got {
Ok(Decoded::Incomplete) => {}
other => panic!(
"prefix of {} bytes (of {}) must be Incomplete, got {:?}",
cut,
full.len(),
other.map_err(|e| e.to_string())
),
}
}
// ...and the whole thing is not incomplete.
assert!(matches!(decode(&full), Ok(Decoded::Packet(..))));
}
}
#[test]
fn a_header_split_mid_way_is_incomplete_not_bad_length() {
// The classic: a read boundary lands inside the 4 hex digits. Reporting
// BadLength here drops a healthy connection, and only sometimes.
for cut in 0..4 {
assert_eq!(decode(&b"0006hi"[..cut]).unwrap(), Decoded::Incomplete);
}
}
#[test]
fn non_hex_length_is_rejected_with_the_bytes_it_saw() {
for bad in [&b"zzzz"[..], &b"SSH-"[..], &b"00 6"[..], &b"fatal"[..4]] {
match decode(bad) {
Err(Error::BadLength(got)) => assert_eq!(&got[..], &bad[..4]),
other => panic!("expected BadLength for {bad:?}, got {other:?}"),
}
}
}
#[test]
fn length_0003_is_reserved_and_never_a_packet() {
assert!(matches!(decode(b"0003"), Err(Error::ReservedLength)));
assert!(matches!(decode(b"0003xxxx"), Err(Error::ReservedLength)));
}
#[test]
fn an_over_long_length_fails_on_the_header_without_waiting_for_bytes() {
// Only the 4 header bytes are supplied. If this returned Incomplete the
// connection would block until a read timeout on bytes that will never come,
// and the fault would present as a network problem.
for (head, payload) in [(&b"fff1"[..], MAX_PAYLOAD + 1), (&b"ffff"[..], 65531)] {
match decode(head) {
Err(Error::TooLong(n)) => assert_eq!(n, payload),
other => panic!("expected TooLong for {head:?}, got {other:?}"),
}
}
// ...and the largest legal header is accepted, so the guard is not simply
// rejecting everything large.
let mut ok = b"fff0".to_vec();
ok.extend_from_slice(&vec![b'x'; MAX_PAYLOAD]);
let (p, n) = one(&ok);
assert_eq!(n, MAX_LINE);
assert_eq!(p.payload().unwrap().len(), MAX_PAYLOAD);
}
#[test]
fn uppercase_hex_is_accepted() {
// gunnar writes lowercase, but the spec permits either and rejecting a
// conformant peer buys nothing.
assert_eq!(one(b"000Aabcdef"), (Packet::Data(b"abcdef".to_vec()), 10));
assert_eq!(one(b"000aabcdef"), (Packet::Data(b"abcdef".to_vec()), 10));
}
#[test]
fn text_strips_exactly_one_trailing_newline() {
assert_eq!(
Packet::Data(b"want abc\n".to_vec()).text(),
Some(&b"want abc"[..])
);
assert_eq!(
Packet::Data(b"want abc".to_vec()).text(),
Some(&b"want abc"[..])
);
// The second newline is content, not framing.
assert_eq!(
Packet::Data(b"abc\n\n".to_vec()).text(),
Some(&b"abc\n"[..])
);
assert_eq!(Packet::Data(b"\n".to_vec()).text(), Some(&b""[..]));
assert_eq!(Packet::Data(Vec::new()).text(), Some(&b""[..]));
}
#[test]
fn text_and_payload_are_none_for_control_packets() {
for p in [Packet::Flush, Packet::Delim, Packet::ResponseEnd] {
assert_eq!(p.payload(), None);
assert_eq!(p.text(), None);
assert_eq!(p.text_str(), None);
}
assert!(Packet::Flush.is_flush());
assert!(!Packet::Delim.is_flush());
}
#[test]
fn text_str_declines_non_utf8_without_calling_it_a_framing_error() {
let p = Packet::Data(vec![0xff, 0xfe, b'\n']);
assert_eq!(p.text(), Some(&[0xffu8, 0xfe][..]));
assert_eq!(p.text_str(), None);
assert_eq!(Packet::Data(b"ok\n".to_vec()).text_str(), Some("ok"));
}
#[test]
fn encode_then_decode_reproduces_the_packet_sequence() {
// A round-trip is worth having *in addition to* the literals above, never
// instead of them.
let packets = vec![
Packet::Data(b"command=fetch".to_vec()),
Packet::Delim,
Packet::Data(b"want 1234\n".to_vec()),
Packet::Data(Vec::new()),
Packet::Data(vec![0u8; 300]),
Packet::ResponseEnd,
Packet::Flush,
];
let mut wire = Vec::new();
for p in &packets {
write_packet(&mut wire, p).unwrap();
}
let mut rest = &wire[..];
let mut got = Vec::new();
while !rest.is_empty() {
match decode(rest).unwrap() {
Decoded::Packet(p, n) => {
got.push(p);
rest = &rest[n..];
}
Decoded::Incomplete => panic!("truncated"),
}
}
assert_eq!(got, packets);
}
#[test]
fn arbitrary_bytes_never_panic_and_never_over_consume() {
// Fuzz-shaped: a deterministic xorshift stream through the one-shot decoder.
// Whatever it decides, it may never claim more bytes than it was given.
let mut state: u64 = 0x9e37_79b9_7f4a_7c15;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..2000 {
let len = (next() % 64) as usize;
let buf: Vec<u8> = (0..len)
.map(|_| {
// Bias towards hex digits so headers are often plausible.
let r = next();
if r % 3 == 0 {
b"0123456789abcdefABCDEF"[(r >> 8) as usize % 22]
} else {
(r >> 8) as u8
}
})
.collect();
if let Ok(Decoded::Packet(_, n)) = decode(&buf) {
assert!(n <= buf.len(), "consumed {n} of {}", buf.len());
assert!(n >= 4);
}
}
}