use crate::control::{
ChallengeResponse, MAX_REFUSAL_REASON, RefusalKind, RequestId, TransportResponse,
};
use crate::error::ProtocolError;
use crate::framing::PREFIX_LEN;
pub const TAG_LEN: usize = 1;
pub const REQUEST_ID_LEN: usize = 8;
pub const CLIENT_PROTOCOL_VERSION: u16 = 1;
pub const TAG_APP: u8 = 0x09;
pub const TAG_RESPONSE_HEARTBEAT: u8 = 0x01;
pub const TAG_BATCH_END: u8 = 0x02;
pub const TAG_SERVER_BUSY: u8 = 0x04;
pub const TAG_CHALLENGE: u8 = 0x05;
pub const TAG_CHALLENGE_RESPONSE: u8 = 0x06;
pub const TAG_AUTH_FAILED: u8 = 0x07;
pub const TAG_SERVER_READY: u8 = 0x08;
pub const TAG_BATCH_END_DEGRADED: u8 = 0x0A;
pub const TAG_REQUEST: u8 = 0x0B;
pub const TAG_REFUSED: u8 = 0x0C;
const CONTROL_TAGS: [u8; 10] = [
TAG_RESPONSE_HEARTBEAT,
TAG_BATCH_END,
TAG_SERVER_BUSY,
TAG_CHALLENGE,
TAG_CHALLENGE_RESPONSE,
TAG_AUTH_FAILED,
TAG_SERVER_READY,
TAG_BATCH_END_DEGRADED,
TAG_REQUEST,
TAG_REFUSED,
];
pub const RETIRED_TAGS: [u8; 1] = [0x03];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TagRule {
AppTagOutOfRange,
OutOfRange(u8),
Duplicate(u8),
IsAppTag(u8),
}
pub(crate) const fn tag_rule_violation(control: &[u8], retired: &[u8], app: u8) -> Option<TagRule> {
if app == 0x00 || app >= 0x10 {
return Some(TagRule::AppTagOutOfRange);
}
let total = control.len() + retired.len();
let mut i = 0;
while i < total {
let tag = tag_at(control, retired, i);
if tag == 0x00 || tag >= 0x10 {
return Some(TagRule::OutOfRange(tag));
}
if tag == app {
return Some(TagRule::IsAppTag(tag));
}
let mut j = i + 1;
while j < total {
if tag_at(control, retired, j) == tag {
return Some(TagRule::Duplicate(tag));
}
j += 1;
}
i += 1;
}
None
}
const fn tag_at(control: &[u8], retired: &[u8], i: usize) -> u8 {
if i < control.len() {
control[i]
} else {
retired[i - control.len()]
}
}
const _: () = assert!(
tag_rule_violation(&CONTROL_TAGS, &RETIRED_TAGS, TAG_APP).is_none(),
"the protocol's tags break a rule of the tag space: see `tag_rule_violation`"
);
pub const BATCH_END_FRAME_LEN: usize = PREFIX_LEN + TAG_LEN + REQUEST_ID_LEN;
pub const MAX_CONTROL_FRAME_LEN: usize =
PREFIX_LEN + TAG_LEN + REQUEST_ID_LEN + 2 + MAX_REFUSAL_REASON;
const ID_OFFSET: usize = TAG_LEN;
#[inline]
pub fn write_terminal_id(frame: &mut [u8; BATCH_END_FRAME_LEN], id: RequestId) {
frame[PREFIX_LEN + ID_OFFSET..].copy_from_slice(&id.0.to_le_bytes());
}
#[inline]
pub fn terminal_request_id(payload: &[u8]) -> Option<RequestId> {
match payload.first()? {
&TAG_BATCH_END | &TAG_BATCH_END_DEGRADED | &TAG_SERVER_BUSY | &TAG_REFUSED => {
read_id(payload)
}
_ => None,
}
}
#[inline]
pub(crate) fn read_id(payload: &[u8]) -> Option<RequestId> {
let id = payload.get(ID_OFFSET..ID_OFFSET + REQUEST_ID_LEN)?;
Some(RequestId(u64::from_le_bytes(
id.try_into().expect("a slice of the ID's length"),
)))
}
fn truncate_reason(reason: &str) -> &str {
if reason.len() <= MAX_REFUSAL_REASON {
return reason;
}
let mut end = MAX_REFUSAL_REASON;
while !reason.is_char_boundary(end) {
end -= 1;
}
&reason[..end]
}
pub fn encode_transport_response(
response: &TransportResponse,
buf: &mut [u8],
) -> Result<usize, ProtocolError> {
let reason = match response {
TransportResponse::Refused { reason, .. } => truncate_reason(reason),
_ => "",
};
let body_len = match response {
TransportResponse::Heartbeat
| TransportResponse::AuthFailed
| TransportResponse::ServerReady => 0,
TransportResponse::BatchEnd { .. }
| TransportResponse::BatchEndDegraded { .. }
| TransportResponse::ServerBusy { .. } => REQUEST_ID_LEN,
TransportResponse::Refused { .. } => REQUEST_ID_LEN + 2 + reason.len(),
TransportResponse::Challenge { .. } => CHALLENGE_VERSION_LEN + CHALLENGE_NONCE_LEN,
};
let needed = PREFIX_LEN + TAG_LEN + body_len;
let Some(frame) = buf.get_mut(..needed) else {
return Err(ProtocolError::Truncated);
};
let (tag, id) = match *response {
TransportResponse::Heartbeat => (TAG_RESPONSE_HEARTBEAT, None),
TransportResponse::BatchEnd { id } => (TAG_BATCH_END, Some(id)),
TransportResponse::BatchEndDegraded { id } => (TAG_BATCH_END_DEGRADED, Some(id)),
TransportResponse::ServerBusy { id } => (TAG_SERVER_BUSY, Some(id)),
TransportResponse::Refused { id, .. } => (TAG_REFUSED, Some(id)),
TransportResponse::Challenge { .. } => (TAG_CHALLENGE, None),
TransportResponse::AuthFailed => (TAG_AUTH_FAILED, None),
TransportResponse::ServerReady => (TAG_SERVER_READY, None),
};
let payload_len = (TAG_LEN + body_len) as u32;
frame[..PREFIX_LEN].copy_from_slice(&payload_len.to_le_bytes());
frame[PREFIX_LEN] = tag;
let mut pos = PREFIX_LEN + TAG_LEN;
if let Some(id) = id {
frame[pos..pos + REQUEST_ID_LEN].copy_from_slice(&id.0.to_le_bytes());
pos += REQUEST_ID_LEN;
}
match *response {
TransportResponse::Refused { kind, .. } => {
frame[pos] = kind.to_byte();
frame[pos + 1] = reason.len() as u8;
frame[pos + 2..].copy_from_slice(reason.as_bytes());
}
TransportResponse::Challenge { nonce } => {
frame[pos..pos + CHALLENGE_VERSION_LEN]
.copy_from_slice(&CLIENT_PROTOCOL_VERSION.to_le_bytes());
frame[pos + CHALLENGE_VERSION_LEN..].copy_from_slice(&nonce);
}
TransportResponse::Heartbeat
| TransportResponse::BatchEnd { .. }
| TransportResponse::BatchEndDegraded { .. }
| TransportResponse::ServerBusy { .. }
| TransportResponse::AuthFailed
| TransportResponse::ServerReady => {}
}
Ok(needed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Refused<'a> {
pub id: RequestId,
pub kind: RefusalKind,
pub reason: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BadRefusal {
Truncated,
UnknownKind(u8),
ReasonLength { declared: usize, available: usize },
ReasonNotUtf8,
}
impl std::fmt::Display for BadRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated => f.write_str("a refusal too short for its fields"),
Self::UnknownKind(kind) => write!(f, "a refusal of unknown kind {kind:#04x}"),
Self::ReasonLength {
declared,
available,
} => write!(
f,
"a refusal declaring a {declared}-byte reason with {available} bytes after it"
),
Self::ReasonNotUtf8 => f.write_str("a refusal whose reason is not UTF-8"),
}
}
}
pub fn decode_refused(payload: &[u8]) -> Result<Refused<'_>, BadRefusal> {
let id = read_id(payload).ok_or(BadRefusal::Truncated)?;
let fields = ID_OFFSET + REQUEST_ID_LEN;
let [kind, len, reason @ ..] = payload.get(fields..).ok_or(BadRefusal::Truncated)? else {
return Err(BadRefusal::Truncated);
};
let kind = RefusalKind::from_byte(*kind).ok_or(BadRefusal::UnknownKind(*kind))?;
let declared = usize::from(*len);
if reason.len() != declared {
return Err(BadRefusal::ReasonLength {
declared,
available: reason.len(),
});
}
let reason = std::str::from_utf8(reason).map_err(|_| BadRefusal::ReasonNotUtf8)?;
Ok(Refused { id, kind, reason })
}
const CHALLENGE_VERSION_LEN: usize = 2;
pub const CHALLENGE_NONCE_LEN: usize = 32;
pub const CHALLENGE_LEN: usize = TAG_LEN + CHALLENGE_VERSION_LEN + CHALLENGE_NONCE_LEN;
pub const CHALLENGE_V0_LEN: usize = TAG_LEN + CHALLENGE_NONCE_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BadChallenge {
NotAChallenge { len: usize, tag: Option<u8> },
Version { node: u16 },
}
pub fn decode_challenge(payload: &[u8]) -> Result<[u8; CHALLENGE_NONCE_LEN], BadChallenge> {
let not_a_challenge = BadChallenge::NotAChallenge {
len: payload.len(),
tag: payload.first().copied(),
};
let Some((&TAG_CHALLENGE, rest)) = payload.split_first() else {
return Err(not_a_challenge);
};
match payload.len() {
CHALLENGE_V0_LEN => Err(BadChallenge::Version { node: 0 }),
CHALLENGE_LEN => {
let (version, nonce) = rest.split_at(CHALLENGE_VERSION_LEN);
let version = u16::from_le_bytes(version.try_into().expect("two bytes"));
if version != CLIENT_PROTOCOL_VERSION {
return Err(BadChallenge::Version { node: version });
}
Ok(nonce.try_into().expect("the nonce's length"))
}
_ => Err(not_a_challenge),
}
}
pub const CHALLENGE_RESPONSE_LEN: usize = TAG_LEN + 64 + 32;
pub fn encode_challenge_response(
response: &ChallengeResponse,
buf: &mut [u8],
) -> Result<usize, ProtocolError> {
if buf.len() < CHALLENGE_RESPONSE_LEN {
return Err(ProtocolError::Truncated);
}
buf[0] = TAG_CHALLENGE_RESPONSE;
buf[1..65].copy_from_slice(&response.signature);
buf[65..97].copy_from_slice(&response.public_key);
Ok(CHALLENGE_RESPONSE_LEN)
}
pub fn decode_challenge_response(buf: &[u8]) -> Result<ChallengeResponse, ProtocolError> {
if buf.len() < CHALLENGE_RESPONSE_LEN {
return Err(ProtocolError::Truncated);
}
let tag = buf[0];
if tag != TAG_CHALLENGE_RESPONSE {
return Err(ProtocolError::UnknownTag(tag));
}
let mut signature = [0u8; 64];
signature.copy_from_slice(&buf[1..65]);
let mut public_key = [0u8; 32];
public_key.copy_from_slice(&buf[65..97]);
Ok(ChallengeResponse {
signature,
public_key,
})
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn every_response(id: RequestId) -> [TransportResponse; 8] {
[
TransportResponse::Heartbeat,
TransportResponse::BatchEnd { id },
TransportResponse::BatchEndDegraded { id },
TransportResponse::ServerBusy { id },
TransportResponse::Refused {
id,
kind: RefusalKind::Malformed,
reason: "short",
},
TransportResponse::Challenge { nonce: [0x5A; 32] },
TransportResponse::AuthFailed,
TransportResponse::ServerReady,
]
}
fn encoded(response: &TransportResponse) -> Vec<u8> {
let mut buf = [0u8; MAX_CONTROL_FRAME_LEN];
let n = encode_transport_response(response, &mut buf).unwrap();
buf[..n].to_vec()
}
#[test]
fn tag_only_variants_are_five_bytes() {
for variant in [
TransportResponse::Heartbeat,
TransportResponse::AuthFailed,
TransportResponse::ServerReady,
] {
let frame = encoded(&variant);
assert_eq!(frame.len(), PREFIX_LEN + TAG_LEN, "{variant:?}");
assert_eq!(u32::from_le_bytes(frame[..4].try_into().unwrap()), 1);
assert_eq!(terminal_request_id(&frame[PREFIX_LEN..]), None);
}
}
#[test]
fn each_terminal_encodes_with_its_id_and_decodes_back() {
let id = RequestId(0x0102_0304_0506_0708);
for (response, tag) in [
(TransportResponse::BatchEnd { id }, TAG_BATCH_END),
(
TransportResponse::BatchEndDegraded { id },
TAG_BATCH_END_DEGRADED,
),
(TransportResponse::ServerBusy { id }, TAG_SERVER_BUSY),
] {
let frame = encoded(&response);
assert_eq!(frame.len(), BATCH_END_FRAME_LEN, "{response:?}");
assert_eq!(
u32::from_le_bytes(frame[..4].try_into().unwrap()) as usize,
TAG_LEN + REQUEST_ID_LEN
);
assert_eq!(frame[PREFIX_LEN], tag);
assert_eq!(&frame[PREFIX_LEN + 1..], &id.0.to_le_bytes());
assert_eq!(terminal_request_id(&frame[PREFIX_LEN..]), Some(id));
}
let refused = encoded(&TransportResponse::Refused {
id,
kind: RefusalKind::PermissionDenied,
reason: "no",
});
assert_eq!(terminal_request_id(&refused[PREFIX_LEN..]), Some(id));
let zero = encoded(&TransportResponse::BatchEnd { id: RequestId(0) });
assert_eq!(terminal_request_id(&zero[PREFIX_LEN..]), Some(RequestId(0)));
}
#[test]
fn terminal_request_id_is_none_for_other_tags_and_short_heads() {
assert_eq!(terminal_request_id(&[]), None);
for tag in 0..=u8::MAX {
let head = [&[tag][..], &[7; REQUEST_ID_LEN]].concat();
let terminal = matches!(
tag,
TAG_BATCH_END | TAG_BATCH_END_DEGRADED | TAG_SERVER_BUSY | TAG_REFUSED
);
assert_eq!(
terminal_request_id(&head).is_some(),
terminal,
"tag {tag:#04x}"
);
for cut in 0..head.len() {
assert_eq!(terminal_request_id(&head[..cut]), None, "tag {tag:#04x}");
}
}
}
#[test]
fn every_encoded_tag_is_a_live_control_tag() {
for variant in &every_response(RequestId(1)) {
let frame = encoded(variant);
assert_ne!(frame[4], TAG_APP, "{variant:?}");
assert!(CONTROL_TAGS.contains(&frame[4]), "{variant:?}");
assert!(!RETIRED_TAGS.contains(&frame[4]), "{variant:?}");
}
}
#[test]
fn the_tag_rules_hold_and_are_checked() {
assert_eq!(
tag_rule_violation(&CONTROL_TAGS, &RETIRED_TAGS, TAG_APP),
None
);
assert_eq!(tag_rule_violation(&[1, 2, 3], &[4], 9), None);
assert_eq!(
tag_rule_violation(&[1, 2, 1], &[], 9),
Some(TagRule::Duplicate(1))
);
assert_eq!(
tag_rule_violation(&[1, 3], &[3], 9),
Some(TagRule::Duplicate(3))
);
assert_eq!(
tag_rule_violation(&[1, 0x00], &[], 9),
Some(TagRule::OutOfRange(0x00))
);
assert_eq!(
tag_rule_violation(&[1], &[0x10], 9),
Some(TagRule::OutOfRange(0x10))
);
assert_eq!(
tag_rule_violation(&[1, 9], &[], 9),
Some(TagRule::IsAppTag(9))
);
assert_eq!(
tag_rule_violation(&[1], &[], 0x00),
Some(TagRule::AppTagOutOfRange)
);
assert_eq!(
tag_rule_violation(&[1], &[], 0x10),
Some(TagRule::AppTagOutOfRange)
);
}
#[test]
fn every_refusal_kind_round_trips_and_no_other_byte_is_one() {
for kind in [RefusalKind::PermissionDenied, RefusalKind::Malformed] {
assert_eq!(RefusalKind::from_byte(kind.to_byte()), Some(kind));
}
for byte in 0..=u8::MAX {
match RefusalKind::from_byte(byte) {
Some(kind) => assert_eq!(kind.to_byte(), byte),
None => assert!(!(1..=2).contains(&byte), "{byte}"),
}
}
assert_eq!(RefusalKind::from_byte(0), None, "a zeroed byte is no kind");
}
fn refused_round_trip(reason: &'static str) -> (Vec<u8>, String) {
let id = RequestId(42);
let frame = encoded(&TransportResponse::Refused {
id,
kind: RefusalKind::Malformed,
reason,
});
let refused = decode_refused(&frame[PREFIX_LEN..]).unwrap();
assert_eq!(refused.id, id);
assert_eq!(refused.kind, RefusalKind::Malformed);
let reason = refused.reason.to_string();
(frame, reason)
}
#[test]
fn a_refusal_carries_its_kind_and_reason() {
let (frame, reason) = refused_round_trip("auditor role may not notarize");
assert_eq!(reason, "auditor role may not notarize");
assert_eq!(frame[PREFIX_LEN], TAG_REFUSED);
assert_eq!(
frame.len(),
PREFIX_LEN + TAG_LEN + REQUEST_ID_LEN + 2 + reason.len()
);
let (_, empty) = refused_round_trip("");
assert_eq!(empty, "");
}
#[test]
fn a_refusal_reason_is_truncated_on_a_character_boundary() {
let at: &'static str = "a".repeat(MAX_REFUSAL_REASON).leak();
let (frame, reason) = refused_round_trip(at);
assert_eq!(reason, at);
assert_eq!(frame.len(), MAX_CONTROL_FRAME_LEN);
assert_eq!(MAX_CONTROL_FRAME_LEN, 270);
assert_eq!(frame.len() - PREFIX_LEN, 266, "with the tag");
let past: &'static str = "b".repeat(MAX_REFUSAL_REASON + 1).leak();
assert_eq!(refused_round_trip(past).1, &past[..MAX_REFUSAL_REASON]);
let far: &'static str = "c".repeat(4 * MAX_REFUSAL_REASON).leak();
assert_eq!(refused_round_trip(far).1.len(), MAX_REFUSAL_REASON);
for lead in MAX_REFUSAL_REASON - 3..=MAX_REFUSAL_REASON {
let s: &'static str = format!("{}€tail", "d".repeat(lead)).leak();
let reason = refused_round_trip(s).1;
assert!(reason.len() <= MAX_REFUSAL_REASON, "lead {lead}");
assert!(s.starts_with(&reason), "lead {lead}");
let euro_fits = lead + '€'.len_utf8() <= MAX_REFUSAL_REASON;
assert_eq!(reason.ends_with('€'), euro_fits, "lead {lead}");
if !euro_fits {
assert_eq!(reason.len(), lead, "lead {lead}");
}
}
}
#[test]
fn a_malformed_refusal_is_refused() {
let good = encoded(&TransportResponse::Refused {
id: RequestId(1),
kind: RefusalKind::PermissionDenied,
reason: "abc",
});
let payload = &good[PREFIX_LEN..];
assert!(decode_refused(payload).is_ok());
for cut in 0..TAG_LEN + REQUEST_ID_LEN + 2 {
assert_eq!(decode_refused(&payload[..cut]), Err(BadRefusal::Truncated));
}
assert_eq!(
decode_refused(&payload[..payload.len() - 1]),
Err(BadRefusal::ReasonLength {
declared: 3,
available: 2
})
);
let long = [payload, b"x"].concat();
assert!(matches!(
decode_refused(&long),
Err(BadRefusal::ReasonLength {
declared: 3,
available: 4
})
));
let kind_at = TAG_LEN + REQUEST_ID_LEN;
for kind in [0x00, 0x03, 0xFF] {
let mut bad = payload.to_vec();
bad[kind_at] = kind;
assert_eq!(decode_refused(&bad), Err(BadRefusal::UnknownKind(kind)));
}
let mut bad = payload.to_vec();
bad[kind_at + 2] = 0xFF;
assert_eq!(decode_refused(&bad), Err(BadRefusal::ReasonNotUtf8));
for err in [
BadRefusal::Truncated,
BadRefusal::UnknownKind(3),
BadRefusal::ReasonLength {
declared: 3,
available: 2,
},
BadRefusal::ReasonNotUtf8,
] {
assert!(err.to_string().contains("refusal"), "{err}");
}
}
#[test]
fn encode_refuses_a_buffer_too_small_for_the_variant() {
for variant in every_response(RequestId(9)) {
let len = encoded(&variant).len();
let mut buf = vec![0u8; len - 1];
assert!(
matches!(
encode_transport_response(&variant, &mut buf),
Err(ProtocolError::Truncated)
),
"{variant:?}"
);
let mut buf = vec![0u8; len];
assert_eq!(encode_transport_response(&variant, &mut buf).unwrap(), len);
}
}
#[test]
fn the_challenge_carries_the_version_ahead_of_the_nonce() {
let nonce = [0xAB; 32];
let frame = encoded(&TransportResponse::Challenge { nonce });
assert_eq!(frame.len(), PREFIX_LEN + CHALLENGE_LEN);
assert_eq!(CHALLENGE_LEN, 35);
assert_eq!(
u32::from_le_bytes(frame[..4].try_into().unwrap()) as usize,
CHALLENGE_LEN
);
assert_eq!(frame[4], TAG_CHALLENGE);
assert_eq!(&frame[5..7], &CLIENT_PROTOCOL_VERSION.to_le_bytes());
assert_eq!(&frame[7..], &nonce);
assert_eq!(decode_challenge(&frame[PREFIX_LEN..]), Ok(nonce));
}
#[test]
fn a_challenge_from_another_protocol_names_its_version() {
let v0 = [&[TAG_CHALLENGE][..], &[0x11; 32]].concat();
assert_eq!(v0.len(), CHALLENGE_V0_LEN);
assert_eq!(
decode_challenge(&v0),
Err(BadChallenge::Version { node: 0 })
);
for version in [0, 2, u16::MAX] {
let other = [&[TAG_CHALLENGE][..], &version.to_le_bytes(), &[0x11; 32]].concat();
assert_eq!(
decode_challenge(&other),
Err(BadChallenge::Version { node: version })
);
}
for wrong in [
vec![TAG_SERVER_READY],
vec![TAG_CHALLENGE; 34],
vec![TAG_CHALLENGE; 36],
[&[TAG_BATCH_END][..], &[0; 34]].concat(),
] {
assert_eq!(
decode_challenge(&wrong),
Err(BadChallenge::NotAChallenge {
len: wrong.len(),
tag: wrong.first().copied()
})
);
}
assert_eq!(
decode_challenge(&[]),
Err(BadChallenge::NotAChallenge { len: 0, tag: None })
);
}
#[test]
fn decode_valid_challenge_response() {
let sig = [0x11; 64];
let pubkey = [0x22; 32];
let mut buf = [0u8; 97];
buf[0] = TAG_CHALLENGE_RESPONSE;
buf[1..65].copy_from_slice(&sig);
buf[65..97].copy_from_slice(&pubkey);
let cr = decode_challenge_response(&buf).unwrap();
assert_eq!(cr.signature, sig);
assert_eq!(cr.public_key, pubkey);
}
#[test]
fn challenge_response_round_trips() {
let response = ChallengeResponse {
signature: [0x33; 64],
public_key: [0x44; 32],
};
let mut buf = [0u8; CHALLENGE_RESPONSE_LEN];
let written = encode_challenge_response(&response, &mut buf).unwrap();
assert_eq!(written, CHALLENGE_RESPONSE_LEN);
assert_eq!(decode_challenge_response(&buf).unwrap(), response);
let mut short = [0u8; CHALLENGE_RESPONSE_LEN - 1];
assert!(matches!(
encode_challenge_response(&response, &mut short),
Err(ProtocolError::Truncated)
));
}
#[test]
fn decode_truncated() {
let buf = [0u8; 50]; assert!(matches!(
decode_challenge_response(&buf),
Err(ProtocolError::Truncated)
));
}
#[test]
fn decode_wrong_tag() {
let mut buf = [0u8; CHALLENGE_RESPONSE_LEN];
buf[0] = 99; assert!(matches!(
decode_challenge_response(&buf),
Err(ProtocolError::UnknownTag(99))
));
}
proptest! {
#[test]
fn the_template_writer_matches_the_encoder(id in any::<u64>(), stale in any::<u64>()) {
let id = RequestId(id);
for (make, name) in [
(
(|id| TransportResponse::BatchEnd { id }) as fn(RequestId) -> TransportResponse,
"BatchEnd",
),
(|id| TransportResponse::BatchEndDegraded { id }, "BatchEndDegraded"),
] {
let mut template = [0u8; BATCH_END_FRAME_LEN];
let n = encode_transport_response(&make(RequestId(stale)), &mut template).unwrap();
prop_assert_eq!(n, BATCH_END_FRAME_LEN, "{}", name);
write_terminal_id(&mut template, id);
prop_assert_eq!(template.to_vec(), encoded(&make(id)), "{}", name);
}
}
}
}