feat(v1): harden typed runtime boundaries
Elect one canonical EffectBatch codec, remove the parallel postcard API, and strengthen fail-closed host, form, sync, WASM, macro, generated-contract, and test-harness proofs with mutation-driven coverage. req: wire/008 req: wire/009 req: wire/010 req: push/008 req: client_local/015 req: client_local/016 req: client_local/017 req: client_local/018 req: client_local/019 req: sync/024 req: sync/025 req: sync/026 req: sync/027 req: sync/028 req: sync/029 req: test/020 req: test/021
This commit is contained in:
+86
-30
@@ -1485,23 +1485,20 @@ fn base64_url_no_pad(input: &[u8]) -> String {
|
||||
let mut out = String::with_capacity((input.len() * 4).div_ceil(3));
|
||||
let mut chunks = input.chunks_exact(3);
|
||||
for chunk in &mut chunks {
|
||||
let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
|
||||
out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[(n & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[(chunk[0] >> 2) as usize] as char);
|
||||
out.push(ALPHABET[(((chunk[0] & 0x03) << 4) + (chunk[1] >> 4)) as usize] as char);
|
||||
out.push(ALPHABET[(((chunk[1] & 0x0f) << 2) + (chunk[2] >> 6)) as usize] as char);
|
||||
out.push(ALPHABET[(chunk[2] & 0x3f) as usize] as char);
|
||||
}
|
||||
match chunks.remainder() {
|
||||
[a] => {
|
||||
let n = (*a as u32) << 16;
|
||||
out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[(a >> 2) as usize] as char);
|
||||
out.push(ALPHABET[((a & 0x03) << 4) as usize] as char);
|
||||
}
|
||||
[a, b] => {
|
||||
let n = ((*a as u32) << 16) | ((*b as u32) << 8);
|
||||
out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[(a >> 2) as usize] as char);
|
||||
out.push(ALPHABET[(((a & 0x03) << 4) + (b >> 4)) as usize] as char);
|
||||
out.push(ALPHABET[((b & 0x0f) << 2) as usize] as char);
|
||||
}
|
||||
[] => {}
|
||||
_ => unreachable!(),
|
||||
@@ -1528,24 +1525,22 @@ fn parse_urlencoded_pairs(body: &[u8]) -> Result<Vec<(String, String)>, Interact
|
||||
|
||||
fn percent_decode(input: &[u8]) -> Result<String, InteractionFormRejection> {
|
||||
let mut out = Vec::with_capacity(input.len());
|
||||
let mut i = 0;
|
||||
while i < input.len() {
|
||||
match input[i] {
|
||||
b'+' => {
|
||||
out.push(b' ');
|
||||
i += 1;
|
||||
}
|
||||
b'%' if i + 2 < input.len() => {
|
||||
let high = hex(input[i + 1]).ok_or(InteractionFormRejection::InvalidBody)?;
|
||||
let low = hex(input[i + 2]).ok_or(InteractionFormRejection::InvalidBody)?;
|
||||
out.push((high << 4) | low);
|
||||
i += 3;
|
||||
}
|
||||
b'%' => return Err(InteractionFormRejection::InvalidBody),
|
||||
byte => {
|
||||
out.push(byte);
|
||||
i += 1;
|
||||
let mut bytes = input.iter().copied();
|
||||
while let Some(byte) = bytes.next() {
|
||||
match byte {
|
||||
b'+' => out.push(b' '),
|
||||
b'%' => {
|
||||
let high = bytes
|
||||
.next()
|
||||
.and_then(hex)
|
||||
.ok_or(InteractionFormRejection::InvalidBody)?;
|
||||
let low = bytes
|
||||
.next()
|
||||
.and_then(hex)
|
||||
.ok_or(InteractionFormRejection::InvalidBody)?;
|
||||
out.push(high * 16 + low);
|
||||
}
|
||||
byte => out.push(byte),
|
||||
}
|
||||
}
|
||||
String::from_utf8(out).map_err(|_| InteractionFormRejection::InvalidBody)
|
||||
@@ -1563,7 +1558,8 @@ fn hex(byte: u8) -> Option<u8> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
encode_sse_batch, html_with_root_fingerprint, sse, BuildFingerprint, InteractionForm,
|
||||
base64_url_no_pad, encode_sse_batch, html_with_root_fingerprint, parse_urlencoded_pairs,
|
||||
percent_decode, sse, BuildFingerprint, InteractionForm, InteractionFormRejection,
|
||||
HEMX_SSE_EVENT,
|
||||
};
|
||||
use axum::{
|
||||
@@ -1615,6 +1611,65 @@ mod tests {
|
||||
assert_eq!(root.text().collect::<String>(), "Docs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64url_transport_matches_rfc_4648_vectors_without_padding() {
|
||||
for (input, expected) in [
|
||||
(b"".as_slice(), ""),
|
||||
(b"f".as_slice(), "Zg"),
|
||||
(b"fo".as_slice(), "Zm8"),
|
||||
(b"foo".as_slice(), "Zm9v"),
|
||||
(b"foob".as_slice(), "Zm9vYg"),
|
||||
(b"fooba".as_slice(), "Zm9vYmE"),
|
||||
(b"foobar".as_slice(), "Zm9vYmFy"),
|
||||
(&[0xfb, 0xff, 0xff], "-___"),
|
||||
(&[0x00, 0x0f, 0x00], "AA8A"),
|
||||
(&[0x00, 0xcf, 0x00], "AM8A"),
|
||||
(&[0xff], "_w"),
|
||||
(&[0xff, 0xff], "__8"),
|
||||
] {
|
||||
assert_eq!(base64_url_no_pad(input), expected);
|
||||
}
|
||||
// req: push/008 test
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn urlencoded_decoder_handles_standard_escapes_and_rejects_malformed_input() {
|
||||
assert_eq!(
|
||||
parse_urlencoded_pairs(
|
||||
b"empty=&space=+&slash=%2f&digit=%39&upper=%4A&lower=%4a&repeat=1&repeat=2"
|
||||
),
|
||||
Ok(vec![
|
||||
("empty".into(), String::new()),
|
||||
("space".into(), " ".into()),
|
||||
("slash".into(), "/".into()),
|
||||
("digit".into(), "9".into()),
|
||||
("upper".into(), "J".into()),
|
||||
("lower".into(), "J".into()),
|
||||
("repeat".into(), "1".into()),
|
||||
("repeat".into(), "2".into()),
|
||||
])
|
||||
);
|
||||
assert_eq!(parse_urlencoded_pairs(b""), Ok(Vec::new()));
|
||||
for malformed in [
|
||||
b"bad=%".as_slice(),
|
||||
b"bad=%0".as_slice(),
|
||||
b"bad=%gg".as_slice(),
|
||||
b"bad=%0g".as_slice(),
|
||||
b"%gg=value".as_slice(),
|
||||
] {
|
||||
assert_eq!(
|
||||
parse_urlencoded_pairs(malformed),
|
||||
Err(InteractionFormRejection::InvalidBody)
|
||||
);
|
||||
}
|
||||
assert_eq!(percent_decode(b"a+b%2Fc"), Ok("a b/c".into()));
|
||||
assert_eq!(
|
||||
InteractionForm::parse_urlencoded(b"__h=1&bad=%"),
|
||||
Err(InteractionFormRejection::InvalidBody)
|
||||
);
|
||||
// test req: form/002 req: failure/003
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sse_response_streams_base64_url_effect_batches() {
|
||||
let batch = EffectBatch {
|
||||
@@ -1623,6 +1678,7 @@ mod tests {
|
||||
ops: Vec::new(),
|
||||
};
|
||||
let encoded = encode_sse_batch(&batch);
|
||||
assert_eq!(encoded, "SEVNWAEAAAALAAAAAAAAAAAAAAA");
|
||||
|
||||
let response = sse(stream::iter([Ok::<_, Infallible>(batch)])).into_response();
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user