626 lines
24 KiB
Rust
626 lines
24 KiB
Rust
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
use versevdi_core::input::{
|
|
decode_feedback, decode_input, encode_feedback, encode_input, FecStatus, FeedbackEvent,
|
|
};
|
|
use versevdi_core::media::{MediaFragment, Reassembler};
|
|
use versevdi_core::wire::{
|
|
CapabilityProfile, ClientSessionAuthority, ConnectionManifest, NativeTunnelCredential,
|
|
TunnelAdmissionRequest,
|
|
};
|
|
|
|
fn fixture(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures")
|
|
.join(name)
|
|
}
|
|
|
|
#[test]
|
|
fn copied_protocol_rc5_fixtures_have_immutable_hashes() {
|
|
let cases = [
|
|
(
|
|
"tunnel-v1.tsv",
|
|
"31a884800031c17844b9a8789702cf0d3639838ccfeb819c2bc3f0a5462ac5df",
|
|
),
|
|
(
|
|
"datagram-v2.tsv",
|
|
"65b9f6f018af624033562a78aae0b685df331c5e857d5f3d5d415aefa9d5b97b",
|
|
),
|
|
(
|
|
"gateway-input-feedback-v1.tsv",
|
|
"91b4dc1eb756476637ea7deebcb297256cb56d8e9c3d635088511af6b8914bda",
|
|
),
|
|
(
|
|
"manifest.json",
|
|
"76c33b33864d85f7d4a3761798eab52ffadbbe5fc313cf4e6e03369cfdc8df9a",
|
|
),
|
|
];
|
|
|
|
for (name, expected) in cases {
|
|
let output = Command::new("shasum")
|
|
.args(["-a", "256"])
|
|
.arg(fixture(name))
|
|
.output()
|
|
.expect("shasum must be installed for fixture verification");
|
|
assert!(output.status.success(), "shasum failed for {name}");
|
|
let actual = String::from_utf8(output.stdout).expect("shasum output is UTF-8");
|
|
assert_eq!(&actual[..64], expected, "fixture drifted: {name}");
|
|
}
|
|
}
|
|
|
|
fn valid_manifest() -> &'static [u8] {
|
|
br#"{
|
|
"version":"1","purpose":"launch","session_id":"session","reconnect_sequence":0,
|
|
"gateway":{"id":"gateway","addresses":["gateway.test:443"],"public_identity":"gateway.test"},
|
|
"tunnel":{"versions":["verse-gateway-v1/1"],"features":["control.v1","input.absolute.v1","input.scroll.v1"]},
|
|
"profile":{"id":"standard","bounds":{"minimum_kbps":1000,"target_kbps":5000,"maximum_kbps":10000},"display_mode":{"resolution_width":1920,"resolution_height":1080,"fps":60}},
|
|
"grant":{"opaque_value":"ggggggggggggggggggggggggggggggggggggggggggg","expires_at":"2099-01-01T00:00:00Z","audience":"audience"},
|
|
"correlation_id":"correlation"
|
|
}"#
|
|
}
|
|
|
|
fn capabilities() -> CapabilityProfile {
|
|
CapabilityProfile::new(
|
|
"quic-tls13",
|
|
"datagram-v2",
|
|
"encoded",
|
|
"encoded",
|
|
"server",
|
|
vec!["h264-opus".to_owned(), "hevc-opus".to_owned()],
|
|
)
|
|
.expect("literal capability profile is valid")
|
|
}
|
|
|
|
const VALID_CERTIFICATE_PEM: &str = "-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----";
|
|
|
|
fn credential_json(certificate_chain_pem: &str, trust_bundle_pem: &str) -> Vec<u8> {
|
|
serde_json::to_vec(&serde_json::json!({
|
|
"client_device_id": "device",
|
|
"device_key_id": "key",
|
|
"certificate_chain_pem": certificate_chain_pem,
|
|
"trust_bundle_pem": trust_bundle_pem,
|
|
"expires_at": "2099-01-01T00:00:00Z",
|
|
}))
|
|
.expect("literal credential is JSON-encodable")
|
|
}
|
|
|
|
#[test]
|
|
fn strict_rc5_dtos_reject_duplicate_trailing_unknown_and_provider_fields() {
|
|
assert!(ConnectionManifest::decode(valid_manifest()).is_ok());
|
|
assert!(
|
|
ConnectionManifest::decode(br#"{"version":"1","version":"1","purpose":"launch"}"#).is_err()
|
|
);
|
|
|
|
let trailing = [valid_manifest(), b" {}"].concat();
|
|
assert!(ConnectionManifest::decode(&trailing).is_err());
|
|
|
|
let unknown = String::from_utf8(valid_manifest().to_vec())
|
|
.expect("fixture is UTF-8")
|
|
.replacen(
|
|
"\"correlation_id\"",
|
|
"\"unknown\":true,\"correlation_id\"",
|
|
1,
|
|
);
|
|
assert!(ConnectionManifest::decode(unknown.as_bytes()).is_err());
|
|
|
|
let provider = String::from_utf8(valid_manifest().to_vec())
|
|
.expect("fixture is UTF-8")
|
|
.replacen(
|
|
"\"addresses\"",
|
|
"\"providerIdentity\":\"hidden\",\"addresses\"",
|
|
1,
|
|
);
|
|
assert!(ConnectionManifest::decode(provider.as_bytes()).is_err());
|
|
|
|
let authority = br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}"#;
|
|
let authority_trailing = [authority.as_slice(), b" {}"].concat();
|
|
assert!(ClientSessionAuthority::decode(&authority_trailing).is_err());
|
|
let authority_duplicate = String::from_utf8(authority.to_vec())
|
|
.expect("fixture is UTF-8")
|
|
.replacen(
|
|
"\"session_id\":",
|
|
"\"session_id\":\"duplicate\",\"session_id\":",
|
|
1,
|
|
);
|
|
assert!(ClientSessionAuthority::decode(authority_duplicate.as_bytes()).is_err());
|
|
|
|
let admission_provider = format!(
|
|
r#"{{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","grant":"{}","reconnect_sequence":0,"client_nonce":"{}","device_signature":"{}","provider_identity":"forbidden","capabilities":{{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}}}"#,
|
|
"g".repeat(43),
|
|
"n".repeat(16),
|
|
"A".repeat(86),
|
|
);
|
|
assert!(TunnelAdmissionRequest::decode(admission_provider.as_bytes()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn manifest_public_identity_requires_dns_sni_not_ip_or_uuid() {
|
|
for invalid_identity in ["127.0.0.1", "::1", "550e8400-e29b-41d4-a716-446655440000"] {
|
|
let manifest = String::from_utf8(valid_manifest().to_vec())
|
|
.expect("fixture is UTF-8")
|
|
.replace("gateway.test\"}", &format!("{invalid_identity}\"}}"));
|
|
assert!(
|
|
ConnectionManifest::decode(manifest.as_bytes()).is_err(),
|
|
"non-DNS SNI accepted: {invalid_identity}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rc5_manifest_credential_and_authority_enforce_bounds_and_bindings() {
|
|
let manifest = ConnectionManifest::decode(valid_manifest()).expect("valid manifest");
|
|
manifest
|
|
.validate_at("2026-08-12T00:00:00Z")
|
|
.expect("unexpired manifest");
|
|
|
|
let credential = NativeTunnelCredential::decode(&credential_json(
|
|
VALID_CERTIFICATE_PEM,
|
|
VALID_CERTIFICATE_PEM,
|
|
))
|
|
.expect("valid credential");
|
|
credential
|
|
.validate_at("2026-08-12T00:00:00Z")
|
|
.expect("unexpired credential");
|
|
assert!(NativeTunnelCredential::decode(
|
|
br#"{"client_device_id":"device","device_key_id":"key","certificate_chain_pem":"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----","trust_bundle_pem":"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----","client_private_key_pem":"forbidden","expires_at":"2099-01-01T00:00:00Z"}"#,
|
|
)
|
|
.is_err());
|
|
|
|
let authority = ClientSessionAuthority::decode(
|
|
br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}"#,
|
|
)
|
|
.expect("valid client-safe authority");
|
|
authority
|
|
.validate_binding(&manifest, &capabilities(), "2026-08-12T00:00:00Z")
|
|
.expect("authority is bound and is a capability subset");
|
|
|
|
let provider_authority = br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"provider_profile":"apollo"}"#;
|
|
assert!(ClientSessionAuthority::decode(provider_authority).is_err());
|
|
let provider_route = br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"stream_host":"provider.invalid"}"#;
|
|
assert!(ClientSessionAuthority::decode(provider_route).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn client_authority_directly_rejects_every_manifest_binding_mismatch() {
|
|
let manifest = ConnectionManifest::decode(valid_manifest()).expect("valid manifest");
|
|
let offered = CapabilityProfile::new(
|
|
"quic-tls13",
|
|
"datagram-v2",
|
|
"encoded",
|
|
"encoded",
|
|
"server",
|
|
vec!["h264-opus".to_owned()],
|
|
)
|
|
.expect("offered capabilities");
|
|
let valid = serde_json::json!({
|
|
"version": "1",
|
|
"session_id": "session",
|
|
"gateway_id": "gateway",
|
|
"audience": "audience",
|
|
"reconnect_sequence": 0,
|
|
"expires_at": "2098-01-01T00:00:00Z",
|
|
"capabilities": {
|
|
"transport": "quic-tls13",
|
|
"framing": "datagram-v2",
|
|
"media": "encoded",
|
|
"audio": "encoded",
|
|
"source_rate_control": "server",
|
|
"client_decode": ["h264-opus"],
|
|
},
|
|
});
|
|
for case in [
|
|
"session",
|
|
"gateway",
|
|
"audience",
|
|
"reconnect",
|
|
"expired",
|
|
"beyond-grant",
|
|
"capability",
|
|
] {
|
|
let mut value = valid.clone();
|
|
match case {
|
|
"session" => value["session_id"] = "other".into(),
|
|
"gateway" => value["gateway_id"] = "other".into(),
|
|
"audience" => value["audience"] = "other".into(),
|
|
"reconnect" => value["reconnect_sequence"] = 1.into(),
|
|
"expired" => value["expires_at"] = "2026-08-12T00:00:00Z".into(),
|
|
"beyond-grant" => value["expires_at"] = "2100-01-01T00:00:00Z".into(),
|
|
"capability" => {
|
|
value["capabilities"]["client_decode"] =
|
|
serde_json::json!(["h264-opus", "hevc-opus"]);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
let authority =
|
|
ClientSessionAuthority::decode(&serde_json::to_vec(&value).expect("encode authority"))
|
|
.expect("structurally valid authority");
|
|
assert_eq!(
|
|
authority.validate_binding(&manifest, &offered, "2026-08-12T00:00:00Z"),
|
|
Err(versevdi_core::error::CoreError::AuthorityRejected),
|
|
"binding mismatch accepted: {case}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_tunnel_credential_rejects_private_key_pem_in_certificate_fields() {
|
|
for field in ["certificate_chain_pem", "trust_bundle_pem"] {
|
|
let private_key = "-----BEGIN PRIVATE KEY-----\nAQID\n-----END PRIVATE KEY-----";
|
|
let credential = if field == "certificate_chain_pem" {
|
|
credential_json(private_key, VALID_CERTIFICATE_PEM)
|
|
} else {
|
|
credential_json(VALID_CERTIFICATE_PEM, private_key)
|
|
};
|
|
assert!(
|
|
NativeTunnelCredential::decode(&credential).is_err(),
|
|
"private key armor accepted in {field}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_tunnel_credential_accepts_one_or_more_certificate_blocks() {
|
|
let two_certificates = format!("{VALID_CERTIFICATE_PEM}\n\n{VALID_CERTIFICATE_PEM}\n");
|
|
assert!(NativeTunnelCredential::decode(&credential_json(
|
|
&two_certificates,
|
|
VALID_CERTIFICATE_PEM,
|
|
))
|
|
.is_ok());
|
|
}
|
|
|
|
fn assert_credential_pem_rejected(invalid_values: &[&str]) {
|
|
for invalid in invalid_values {
|
|
assert!(
|
|
NativeTunnelCredential::decode(&credential_json(invalid, VALID_CERTIFICATE_PEM))
|
|
.is_err(),
|
|
"invalid certificate chain accepted"
|
|
);
|
|
assert!(
|
|
NativeTunnelCredential::decode(&credential_json(VALID_CERTIFICATE_PEM, invalid))
|
|
.is_err(),
|
|
"invalid trust bundle accepted"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_tunnel_credential_rejects_bare_certificate_text() {
|
|
assert_credential_pem_rejected(&["certificate"]);
|
|
}
|
|
|
|
#[test]
|
|
fn native_tunnel_credential_rejects_non_certificate_pem_labels() {
|
|
assert_credential_pem_rejected(&["-----BEGIN PUBLIC KEY-----\nAQID\n-----END PUBLIC KEY-----"]);
|
|
}
|
|
|
|
#[test]
|
|
fn native_tunnel_credential_rejects_malformed_or_incomplete_certificate_armor() {
|
|
assert_credential_pem_rejected(&[
|
|
"-----BEGIN CERTIFICATE-----\nAQID",
|
|
"-----BEGIN CERTIFICATE-----\n!!!!\n-----END CERTIFICATE-----",
|
|
"-----BEGIN CERTIFICATE-----\nAQI\n-----END CERTIFICATE-----",
|
|
"-----BEGIN CERTIFICATE-----\nAQJ=\n-----END CERTIFICATE-----",
|
|
]);
|
|
}
|
|
|
|
#[test]
|
|
fn native_tunnel_credential_rejects_junk_between_or_after_certificate_blocks() {
|
|
let between = format!("{VALID_CERTIFICATE_PEM}\njunk\n{VALID_CERTIFICATE_PEM}");
|
|
let after = format!("{VALID_CERTIFICATE_PEM}\njunk");
|
|
assert_credential_pem_rejected(&[&between, &after]);
|
|
}
|
|
|
|
#[test]
|
|
fn native_tunnel_credential_rejects_empty_certificate_blocks() {
|
|
assert_credential_pem_rejected(&["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"]);
|
|
}
|
|
|
|
#[test]
|
|
fn expiry_mismatch_and_capability_escalation_are_rejected() {
|
|
let manifest = ConnectionManifest::decode(valid_manifest()).expect("valid manifest");
|
|
assert!(manifest.validate_at("2100-01-01T00:00:00Z").is_err());
|
|
let credential = NativeTunnelCredential::decode(&credential_json(
|
|
VALID_CERTIFICATE_PEM,
|
|
VALID_CERTIFICATE_PEM,
|
|
))
|
|
.expect("valid credential");
|
|
assert!(credential.validate_at("2099-01-01T00:00:00Z").is_err());
|
|
|
|
let mismatched = ClientSessionAuthority::decode(
|
|
br#"{"version":"1","session_id":"other","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}"#,
|
|
)
|
|
.expect("shape is valid");
|
|
assert!(mismatched
|
|
.validate_binding(&manifest, &capabilities(), "2026-08-12T00:00:00Z")
|
|
.is_err());
|
|
|
|
let escalated = ClientSessionAuthority::decode(
|
|
br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus","hevc-opus"]}}"#,
|
|
)
|
|
.expect("shape is valid");
|
|
let h264_only = CapabilityProfile::new(
|
|
"quic-tls13",
|
|
"datagram-v2",
|
|
"encoded",
|
|
"encoded",
|
|
"server",
|
|
vec!["h264-opus".to_owned()],
|
|
)
|
|
.expect("literal capability profile");
|
|
assert!(escalated
|
|
.validate_binding(&manifest, &h264_only, "2026-08-12T00:00:00Z")
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn admission_transcript_matches_rc5_literal() {
|
|
let request = TunnelAdmissionRequest::decode(
|
|
format!(
|
|
r#"{{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","grant":"{}","reconnect_sequence":0,"client_nonce":"{}","device_signature":"{}","capabilities":{{"transport":"quic-tls13","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}}}"#,
|
|
"g".repeat(43),
|
|
"n".repeat(16),
|
|
"A".repeat(86),
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("valid admission request");
|
|
|
|
assert_eq!(
|
|
request.admission_transcript(),
|
|
format!(
|
|
"versevdi/tunnel-admission/v17:session7:gateway8:audience43:{}1:016:{}10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus",
|
|
"g".repeat(43),
|
|
"n".repeat(16),
|
|
)
|
|
.into_bytes()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn admission_rejects_non_raw_base64url_nonce_and_signature() {
|
|
for (nonce, signature) in [
|
|
("!".repeat(16), "A".repeat(86)),
|
|
("A".repeat(17), "A".repeat(86)),
|
|
("A".repeat(16), "!".repeat(86)),
|
|
("A".repeat(16), format!("{}B", "A".repeat(85))),
|
|
] {
|
|
let request = format!(
|
|
r#"{{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","grant":"{}","reconnect_sequence":0,"client_nonce":"{}","device_signature":"{}","capabilities":{{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}}}"#,
|
|
"g".repeat(43),
|
|
nonce,
|
|
signature,
|
|
);
|
|
assert!(TunnelAdmissionRequest::decode(request.as_bytes()).is_err());
|
|
}
|
|
}
|
|
|
|
fn decode_hex(value: &str) -> Vec<u8> {
|
|
value
|
|
.as_bytes()
|
|
.chunks_exact(2)
|
|
.map(|pair| {
|
|
let text = std::str::from_utf8(pair).expect("fixture hex is ASCII");
|
|
u8::from_str_radix(text, 16).expect("fixture hex is valid")
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn fixture_rows(name: &str) -> impl Iterator<Item = Vec<&'static str>> {
|
|
let data = match name {
|
|
"datagram-v2.tsv" => include_str!("fixtures/datagram-v2.tsv"),
|
|
"gateway-input-feedback-v1.tsv" => {
|
|
include_str!("fixtures/gateway-input-feedback-v1.tsv")
|
|
}
|
|
_ => unreachable!("known fixture"),
|
|
};
|
|
data.lines()
|
|
.skip(1)
|
|
.map(|line| line.split('\t').collect::<Vec<_>>())
|
|
}
|
|
|
|
#[test]
|
|
fn datagram_v2_codec_matches_all_rc5_conformance_rows() {
|
|
for row in fixture_rows("datagram-v2.tsv") {
|
|
let bytes = decode_hex(row[3].strip_prefix("hex=").expect("hex fixture"));
|
|
match row[4] {
|
|
"valid" => {
|
|
let fragment = MediaFragment::decode(&bytes).expect(row[0]);
|
|
assert_eq!(fragment.encode().expect(row[0]), bytes, "{}", row[0]);
|
|
}
|
|
error => assert_eq!(
|
|
MediaFragment::decode(&bytes).expect_err(row[0]).code(),
|
|
error.strip_prefix("invalid:").expect("invalid fixture"),
|
|
"{}",
|
|
row[0]
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn vgi1_and_vgf1_codecs_match_all_rc5_conformance_rows() {
|
|
let features = ["input.absolute.v1", "input.scroll.v1"];
|
|
for row in fixture_rows("gateway-input-feedback-v1.tsv") {
|
|
let bytes = decode_hex(row[3].strip_prefix("hex=").expect("hex fixture"));
|
|
let result = match row[2] {
|
|
"gateway_input" => {
|
|
decode_input(&bytes, &features).and_then(|event| encode_input(&event, &features))
|
|
}
|
|
"gateway_feedback" => decode_feedback(&bytes).and_then(|event| encode_feedback(&event)),
|
|
_ => unreachable!("known fixture kind"),
|
|
};
|
|
match row[4] {
|
|
"valid" => assert_eq!(result.expect(row[0]), bytes, "{}", row[0]),
|
|
error => assert_eq!(
|
|
result.expect_err(row[0]).code(),
|
|
error.strip_prefix("invalid:").expect("invalid fixture"),
|
|
"{}",
|
|
row[0]
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn datagram_v2_reassembly_is_bounded_reordered_and_duplicate_safe() {
|
|
let first = MediaFragment::new_video(7, 42, 0, 2, b"hello".to_vec()).expect("fragment");
|
|
let second = MediaFragment::new_video(7, 42, 1, 2, b" world".to_vec()).expect("fragment");
|
|
let mut reassembler = Reassembler::new();
|
|
|
|
assert!(reassembler
|
|
.push(second.clone(), 10)
|
|
.expect("second")
|
|
.is_none());
|
|
assert!(reassembler.push(second, 11).expect("duplicate").is_none());
|
|
let unit = reassembler
|
|
.push(first, 12)
|
|
.expect("first")
|
|
.expect("complete unit");
|
|
assert_eq!(unit.payload, b"hello world");
|
|
assert_eq!(reassembler.incomplete_units(), 0);
|
|
|
|
for sequence in 0..5 {
|
|
let fragment = MediaFragment::new_video(sequence, 1, 0, 2, vec![0]).expect("fragment");
|
|
assert!(reassembler.push(fragment, 20 + u64::from(sequence)).is_ok());
|
|
}
|
|
assert_eq!(reassembler.incomplete_units(), 4);
|
|
assert_eq!(reassembler.evicted_units(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn datagram_v2_boundary_loop_covers_payload_fragment_count_and_audio() {
|
|
for payload_length in [0, 1, 1_177] {
|
|
let fragment = MediaFragment::new_audio(
|
|
u32::try_from(payload_length).expect("small"),
|
|
1,
|
|
0,
|
|
1,
|
|
vec![0; payload_length],
|
|
)
|
|
.expect("boundary is valid");
|
|
assert_eq!(
|
|
MediaFragment::decode(&fragment.encode().expect("encode")).expect("decode"),
|
|
fragment
|
|
);
|
|
}
|
|
assert!(MediaFragment::new_audio(1, 1, 0, 1, vec![0; 1_178]).is_err());
|
|
assert!(MediaFragment::new_audio(1, 1, 0, 0, Vec::new()).is_err());
|
|
assert!(MediaFragment::new_audio(1, 1, 0, 892, Vec::new()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn reassembly_discards_conflicts_and_expires_after_250_ms() {
|
|
let mut reassembler = Reassembler::new();
|
|
let first = MediaFragment::new_video(1, 1, 0, 2, vec![1]).expect("fragment");
|
|
reassembler.push(first, 0).expect("first");
|
|
let conflict = MediaFragment::new_video(1, 1, 0, 2, vec![2]).expect("fragment");
|
|
assert!(reassembler.push(conflict, 1).is_err());
|
|
assert_eq!(reassembler.incomplete_units(), 0);
|
|
|
|
let expiring = MediaFragment::new_video(2, 1, 0, 2, vec![1]).expect("fragment");
|
|
reassembler.push(expiring, 10).expect("first");
|
|
let next = MediaFragment::new_video(3, 1, 0, 2, vec![1]).expect("fragment");
|
|
reassembler.push(next, 261).expect("expiry sweep");
|
|
assert_eq!(reassembler.expired_units(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn reassembly_rejects_a_complete_unit_above_one_mebibyte() {
|
|
let mut reassembler = Reassembler::new();
|
|
for index in 0..891_u16 {
|
|
let fragment = MediaFragment::new_video(9, 1, index, 891, vec![0; 1_177])
|
|
.expect("individual fragment is bounded");
|
|
let result = reassembler.push(fragment, 0);
|
|
if index < 890 {
|
|
assert!(result.expect("within aggregate bound").is_none());
|
|
} else {
|
|
assert!(result.is_err());
|
|
}
|
|
}
|
|
assert_eq!(reassembler.incomplete_units(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn input_features_and_feedback_booleans_fail_closed() {
|
|
assert_eq!(
|
|
decode_input(&decode_hex("56474931060804d202370a0005a0"), &[])
|
|
.expect_err("absolute feature is required")
|
|
.code(),
|
|
"unsupported_feature"
|
|
);
|
|
assert_eq!(
|
|
decode_feedback(&decode_hex("564746310112000102"))
|
|
.expect_err("HDR is boolean")
|
|
.code(),
|
|
"length"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fec_feedback_enforces_rc5_go_field_invariants_on_decode_and_encode() {
|
|
for invalid in [
|
|
"56474631000200150000002a0005000300020000000200080002140001",
|
|
"56474631000200150000002a000500030002000a0002000b0002140001",
|
|
"56474631000200150000002a000500030002000a000200080003140001",
|
|
"56474631000200150000002a000500030002000a000200080002650001",
|
|
"56474631000200150000002a000500030002000a000200080002140000",
|
|
"56474631000200150000002a000500030002000a000200080002140101",
|
|
] {
|
|
assert_eq!(
|
|
decode_feedback(&decode_hex(invalid))
|
|
.expect_err("invalid FEC status")
|
|
.code(),
|
|
"field"
|
|
);
|
|
}
|
|
|
|
let valid = FecStatus {
|
|
frame_index: 42,
|
|
highest_received_sequence: 5,
|
|
next_contiguous_sequence: 3,
|
|
missing_before_highest: 2,
|
|
total_data_packets: 10,
|
|
total_parity_packets: 2,
|
|
received_data_packets: 8,
|
|
received_parity_packets: 2,
|
|
fec_percentage: 20,
|
|
multi_fec_block_index: 0,
|
|
multi_fec_block_count: 1,
|
|
};
|
|
let invalid = [
|
|
FecStatus {
|
|
total_data_packets: 0,
|
|
..valid.clone()
|
|
},
|
|
FecStatus {
|
|
received_data_packets: 11,
|
|
..valid.clone()
|
|
},
|
|
FecStatus {
|
|
received_parity_packets: 3,
|
|
..valid.clone()
|
|
},
|
|
FecStatus {
|
|
fec_percentage: 101,
|
|
..valid.clone()
|
|
},
|
|
FecStatus {
|
|
multi_fec_block_count: 0,
|
|
..valid.clone()
|
|
},
|
|
FecStatus {
|
|
multi_fec_block_index: 1,
|
|
..valid
|
|
},
|
|
];
|
|
for status in invalid {
|
|
assert_eq!(
|
|
encode_feedback(&FeedbackEvent::Fec(status))
|
|
.expect_err("invalid FEC status")
|
|
.code(),
|
|
"field"
|
|
);
|
|
}
|
|
}
|