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