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

nordisk/holger

plugin/abi/tests/codec_survives_what_json_did_not.rs

//! The codec guard, written against the two concrete failures of the format it
//! replaces.
//!
//! znippy's `wasm_loader::parse_json_to_row` splits on `,` and `:` before
//! stripping quotes, and writes no null — so a value containing a separator
//! silently corrupts the row, and an absent field is indistinguishable from an
//! empty one. Both are properties of that wire format, so both are asserted
//! here as *values*, not as "it round-tripped".
//!
//! Each test names the field that differs when it fails; `assert!(decoded.is_ok())`
//! would pass just as happily on a codec that dropped every payload.

use holger_plugin_abi::codec::{DecodeError, Reader, Writer};
use holger_plugin_abi::wire::*;

/// Every byte sequence that broke the JSON row, in one value.
const NASTY: &str = r#"a,b:c"d{}[]  ,: e"#;

#[test]
fn a_value_full_of_separators_survives_verbatim() {
    let id = WireArtifactId {
        namespace: Some(NASTY.to_string()),
        name: format!("name{NASTY}"),
        version: "1.0:2,3".to_string(),
    };

    let back = WireArtifactId::from_bytes(&id.to_bytes()).expect("decode");

    // Assert the CONTENT of each field. A codec that returned an empty id would
    // satisfy a bare `assert_eq!(back, back)`.
    assert_eq!(
        back.namespace.as_deref(),
        Some(NASTY),
        "namespace corrupted by separators"
    );
    assert_eq!(
        back.name,
        format!("name{NASTY}"),
        "name corrupted by separators"
    );
    assert_eq!(back.version, "1.0:2,3", "version corrupted by separators");
    assert_eq!(back, id);
}

#[test]
fn none_and_empty_string_are_different_on_the_wire() {
    let absent = WireArtifactId {
        namespace: None,
        name: "x".into(),
        version: "1".into(),
    };
    let empty = WireArtifactId {
        namespace: Some(String::new()),
        name: "x".into(),
        version: "1".into(),
    };

    assert_ne!(
        absent.to_bytes(),
        empty.to_bytes(),
        "None and Some(\"\") encode identically"
    );

    assert_eq!(
        WireArtifactId::from_bytes(&absent.to_bytes())
            .unwrap()
            .namespace,
        None
    );
    assert_eq!(
        WireArtifactId::from_bytes(&empty.to_bytes())
            .unwrap()
            .namespace,
        Some(String::new()),
        "an empty namespace decoded as absent"
    );
}

#[test]
fn a_nul_byte_and_non_ascii_survive_a_body() {
    let body = vec![0u8, 255, 10, 13, b'"', b',', b':', 0xc3, 0xa5];
    let resp: Result<Option<Vec<u8>>, String> = Ok(Some(body.clone()));
    let back = decode_fetch_response(&encode_fetch_response(&resp)).expect("decode");
    assert_eq!(
        back,
        Ok(Some(body)),
        "binary body did not survive the codec"
    );
}

#[test]
fn an_empty_body_is_not_an_absent_body() {
    let empty: Result<Option<Vec<u8>>, String> = Ok(Some(Vec::new()));
    let absent: Result<Option<Vec<u8>>, String> = Ok(None);
    assert_ne!(
        encode_fetch_response(&empty),
        encode_fetch_response(&absent)
    );
    assert_eq!(
        decode_fetch_response(&encode_fetch_response(&empty)).unwrap(),
        Ok(Some(vec![]))
    );
    assert_eq!(
        decode_fetch_response(&encode_fetch_response(&absent)).unwrap(),
        Ok(None)
    );
}

#[test]
fn an_error_message_containing_separators_arrives_whole() {
    let msg = format!("store failed: {NASTY}");
    let resp: Result<(), String> = Err(msg.clone());
    assert_eq!(
        decode_unit_response(&encode_unit_response(&resp)).unwrap(),
        Err(msg)
    );
}

#[test]
fn http_headers_with_commas_do_not_merge() {
    let resp = Ok(WireHttpResponse {
        status: 206,
        headers: vec![
            (
                "content-type".into(),
                "text/plain; charset=utf-8, x=1".into(),
            ),
            ("x-empty".into(), String::new()),
        ],
        body: b"hi".to_vec(),
    });
    let back = decode_http_response(&encode_http_response(&resp))
        .unwrap()
        .unwrap();
    assert_eq!(
        back.headers.len(),
        2,
        "a comma inside a header value split it into two"
    );
    assert_eq!(back.headers[0].1, "text/plain; charset=utf-8, x=1");
    assert_eq!(back.headers[1].1, "", "an empty header value vanished");
    assert_eq!(back.status, 206);
}

// ─── The decoder must be loud, not lenient ───────────────────────────
//
// Every one of these is a shape a *hostile or skewed* module can put on the
// wire. A codec that answered `Ok` to any of them would let garbage reach a
// real field, which is the failure this whole format exists to prevent.

#[test]
fn a_truncated_buffer_is_an_error_not_a_default() {
    let full = WireArtifactId {
        namespace: None,
        name: "abc".into(),
        version: "1".into(),
    }
    .to_bytes();
    for cut in 0..full.len() {
        let err = WireArtifactId::from_bytes(&full[..cut]);
        assert!(
            err.is_err(),
            "a {cut}-byte prefix decoded as a valid ArtifactId: {err:?}"
        );
    }
}

#[test]
fn a_length_prefix_larger_than_the_buffer_is_refused() {
    // "namespace = None", then a name whose length claims 4 GiB.
    let mut w = Writer::new();
    w.u8(0).u32(u32::MAX);
    let err = WireArtifactId::from_bytes(&w.finish()).unwrap_err();
    assert!(
        matches!(
            err,
            DecodeError::LengthOverflow {
                field: "ArtifactId.name",
                ..
            }
        ),
        "a 4 GiB length prefix was not refused: {err:?}"
    );
}

#[test]
fn an_unknown_option_tag_is_refused() {
    let mut w = Writer::new();
    w.u8(7); // neither 0 nor 1
    let err = WireArtifactId::from_bytes(&w.finish()).unwrap_err();
    assert!(
        matches!(
            err,
            DecodeError::BadTag {
                field: "ArtifactId.namespace",
                tag: 7
            }
        ),
        "tag 7 was accepted as an Option discriminant: {err:?}"
    );
}

#[test]
fn trailing_bytes_are_refused_rather_than_ignored() {
    let mut bytes = WireArtifactId {
        namespace: None,
        name: "a".into(),
        version: "1".into(),
    }
    .to_bytes();
    bytes.push(0xff);
    let err = WireArtifactId::from_bytes(&bytes).unwrap_err();
    assert!(
        matches!(err, DecodeError::TrailingBytes { left: 1, .. }),
        "a trailing byte was silently ignored: {err:?}"
    );
}

#[test]
fn invalid_utf8_in_a_string_is_named() {
    let mut w = Writer::new();
    w.u8(0); // namespace: None
    w.bytes(&[0xff, 0xfe]); // name: not UTF-8
    let err = WireArtifactId::from_bytes(&w.finish()).unwrap_err();
    assert!(
        matches!(
            err,
            DecodeError::NotUtf8 {
                field: "ArtifactId.name"
            }
        ),
        "invalid UTF-8 was accepted: {err:?}"
    );
}

#[test]
fn a_huge_declared_count_cannot_preallocate() {
    // A list response claiming u32::MAX entries with no bytes behind them.
    let mut w = Writer::new();
    w.u8(0).u32(u32::MAX);
    let err = decode_list_response(&w.finish()).unwrap_err();
    assert!(
        matches!(
            err,
            DecodeError::LengthOverflow {
                field: "list.count",
                ..
            }
        ),
        "an absurd entry count was not bounded by the buffer: {err:?}"
    );
}

#[test]
fn the_reader_reports_what_is_left() {
    let mut r = Reader::new(&[1, 2, 3, 4]);
    assert_eq!(r.remaining(), 4);
    assert_eq!(r.u16("t").unwrap(), 0x0201);
    assert_eq!(r.remaining(), 2);
    assert!(r.expect_end("t").is_err());
}

#[test]
fn the_manifest_round_trips_every_field() {
    let m = WireManifest {
        abi_version: ABI_VERSION,
        handler: "skid,bladnir:x".into(),
        format: "znippy".into(),
        writable: true,
        identity: Some(ModuleIdentity::new("nordisk", "skidbladnir", "1.2.3")),
    };
    let back = WireManifest::from_bytes(&m.to_bytes()).unwrap();
    assert_eq!(back.handler, "skid,bladnir:x");
    assert_eq!(back.abi_version, ABI_VERSION);
    assert!(back.writable, "the writable flag inverted across the codec");
    assert_eq!(back, m);
}

#[test]
fn a_list_entry_carries_its_size_and_content_type() {
    let entries = vec![WireArtifactEntry {
        id: WireArtifactId {
            namespace: Some("ns".into()),
            name: "n".into(),
            version: "v".into(),
        },
        size_bytes: -1,
        content_type: "application/x-znippy".into(),
    }];
    let back = decode_list_response(&encode_list_response(&Ok(entries.clone())))
        .unwrap()
        .unwrap();
    assert_eq!(
        back[0].size_bytes, -1,
        "a negative size did not survive as i64"
    );
    assert_eq!(back[0].content_type, "application/x-znippy");
    assert_eq!(back, entries);
}

#[test]
fn store_helpers_round_trip() {
    let rows = vec![("a,b".to_string(), 0u64), ("c:d".to_string(), u64::MAX)];
    assert_eq!(
        decode_store_listing(&encode_store_listing(&rows)).unwrap(),
        rows
    );
    assert_eq!(decode_store_get(&encode_store_get(&None)).unwrap(), None);
    assert_eq!(
        decode_store_get(&encode_store_get(&Some(vec![]))).unwrap(),
        Some(vec![]),
        "an empty stored blob decoded as absent"
    );
}