Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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);
        }
    }
}