1use crate::{CommonHeader, MessageType, NnrpError, CURRENT_VERSION_MAJOR, CURRENT_WIRE_FORMAT};
2
3pub const CLIENT_HELLO_METADATA_LEN: usize = 64;
4pub const SERVER_HELLO_ACK_METADATA_LEN: usize = 80;
5pub const SESSION_PATCH_METADATA_LEN: usize = 36;
6pub const SESSION_PATCH_ACK_METADATA_LEN: usize = 48;
7pub const RESULT_HINT_METADATA_LEN: usize = 16;
8pub const TRANSPORT_PROBE_METADATA_LEN: usize = 16;
9pub const TRANSPORT_PROBE_ACK_METADATA_LEN: usize = 16;
10pub const SESSION_MIGRATE_METADATA_LEN: usize = 24;
11pub const SESSION_MIGRATE_ACK_METADATA_LEN: usize = 24;
12pub const ERROR_METADATA_LEN: usize = 32;
13
14pub const SESSION_PATCH_FIELD_KNOWN_MASK: u32 = 0x0000_007f;
15pub const SERVER_HELLO_ACK_FLAGS_KNOWN_MASK: u32 = 0x0000_0001;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[repr(u32)]
19pub enum ResultHintBudgetPolicy {
20 None = 0,
21 Full = 1,
22 Partial = 2,
23 StaleReuse = 3,
24 Drop = 4,
25}
26
27impl ResultHintBudgetPolicy {
28 pub fn try_from_u32(value: u32) -> Result<Self, NnrpError> {
29 match value {
30 0 => Ok(Self::None),
31 1 => Ok(Self::Full),
32 2 => Ok(Self::Partial),
33 3 => Ok(Self::StaleReuse),
34 4 => Ok(Self::Drop),
35 _ => Err(NnrpError::UnknownEnumValue {
36 enum_name: "result_hint_budget_policy",
37 value: value as u64,
38 }),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[repr(u32)]
45pub enum ResultHintCongestionState {
46 None = 0,
47 Steady = 1,
48 Elevated = 2,
49 Saturated = 3,
50}
51
52impl ResultHintCongestionState {
53 pub fn try_from_u32(value: u32) -> Result<Self, NnrpError> {
54 match value {
55 0 => Ok(Self::None),
56 1 => Ok(Self::Steady),
57 2 => Ok(Self::Elevated),
58 3 => Ok(Self::Saturated),
59 _ => Err(NnrpError::UnknownEnumValue {
60 enum_name: "result_hint_congestion_state",
61 value: value as u64,
62 }),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68#[repr(u32)]
69pub enum ResultHintReason {
70 None = 0,
71 QueueFull = 1,
72 ServerBusy = 2,
73 BudgetExceeded = 3,
74 Superseded = 4,
75}
76
77impl ResultHintReason {
78 pub fn try_from_u32(value: u32) -> Result<Self, NnrpError> {
79 match value {
80 0 => Ok(Self::None),
81 1 => Ok(Self::QueueFull),
82 2 => Ok(Self::ServerBusy),
83 3 => Ok(Self::BudgetExceeded),
84 4 => Ok(Self::Superseded),
85 _ => Err(NnrpError::UnknownEnumValue {
86 enum_name: "result_hint_reason",
87 value: value as u64,
88 }),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94#[repr(u16)]
95pub enum SessionPatchAckStatus {
96 Accepted = 0,
97 PartiallyApplied = 1,
98 Rejected = 2,
99}
100
101impl SessionPatchAckStatus {
102 pub fn try_from_u16(value: u16) -> Result<Self, NnrpError> {
103 match value {
104 0 => Ok(Self::Accepted),
105 1 => Ok(Self::PartiallyApplied),
106 2 => Ok(Self::Rejected),
107 _ => Err(NnrpError::UnknownEnumValue {
108 enum_name: "session_patch_ack_status",
109 value: value as u64,
110 }),
111 }
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[repr(u16)]
117pub enum SessionPatchRejectReason {
118 None = 0,
119 UnsupportedField = 1,
120 InvalidRange = 2,
121 UnsupportedStrategy = 3,
122 InvalidLaneMask = 4,
123 RateLimited = 5,
124}
125
126impl SessionPatchRejectReason {
127 pub fn try_from_u16(value: u16) -> Result<Self, NnrpError> {
128 match value {
129 0 => Ok(Self::None),
130 1 => Ok(Self::UnsupportedField),
131 2 => Ok(Self::InvalidRange),
132 3 => Ok(Self::UnsupportedStrategy),
133 4 => Ok(Self::InvalidLaneMask),
134 5 => Ok(Self::RateLimited),
135 _ => Err(NnrpError::UnknownEnumValue {
136 enum_name: "session_patch_reject_reason",
137 value: value as u64,
138 }),
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144#[repr(u32)]
145pub enum TransportId {
146 Unspecified = 0,
147 Quic = 1,
148 Tcp = 2,
149}
150
151impl TransportId {
152 pub fn try_from_u32(value: u32) -> Result<Self, NnrpError> {
153 match value {
154 0 => Ok(Self::Unspecified),
155 1 => Ok(Self::Quic),
156 2 => Ok(Self::Tcp),
157 _ => Err(NnrpError::UnknownEnumValue {
158 enum_name: "transport_id",
159 value: value as u64,
160 }),
161 }
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166#[repr(u32)]
167pub enum ErrorScope {
168 Connection = 0,
169 Session = 1,
170 Frame = 2,
171}
172
173impl ErrorScope {
174 pub fn try_from_u32(value: u32) -> Result<Self, NnrpError> {
175 match value {
176 0 => Ok(Self::Connection),
177 1 => Ok(Self::Session),
178 2 => Ok(Self::Frame),
179 _ => Err(NnrpError::UnknownEnumValue {
180 enum_name: "error_scope",
181 value: value as u64,
182 }),
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub struct ClientHelloMetadata {
189 pub min_version_major: u8,
190 pub max_version_major: u8,
191 pub supported_wire_format_bitmap: u16,
192 pub supported_profile_bitmap: u32,
193 pub supported_payload_kind_bitmap: u32,
194 pub supported_codec_bitmap: u32,
195 pub supported_compression_bitmap: u32,
196 pub supported_dtype_bitmap: u32,
197 pub supported_layout_bitmap: u32,
198 pub cache_digest_bitmap: u16,
199 pub cache_object_bitmap: u16,
200 pub cache_namespace_count: u16,
201 pub max_lane_count: u16,
202 pub max_cache_entries: u32,
203 pub max_cache_bytes: u32,
204 pub target_cadence_x100: u16,
205 pub latency_budget_ms: u16,
206 pub quality_tier: u16,
207 pub degrade_policy: u16,
208 pub requested_session_id: u32,
209 pub auth_bytes: u32,
210 pub control_extension_bytes: u32,
211}
212
213impl ClientHelloMetadata {
214 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
215 require_len(source, CLIENT_HELLO_METADATA_LEN)?;
216 let metadata = Self {
217 min_version_major: source[0],
218 max_version_major: source[1],
219 supported_wire_format_bitmap: read_u16(source, 2),
220 supported_profile_bitmap: read_u32(source, 4),
221 supported_payload_kind_bitmap: read_u32(source, 8),
222 supported_codec_bitmap: read_u32(source, 12),
223 supported_compression_bitmap: read_u32(source, 16),
224 supported_dtype_bitmap: read_u32(source, 20),
225 supported_layout_bitmap: read_u32(source, 24),
226 cache_digest_bitmap: read_u16(source, 28),
227 cache_object_bitmap: read_u16(source, 30),
228 cache_namespace_count: read_u16(source, 32),
229 max_lane_count: read_u16(source, 34),
230 max_cache_entries: read_u32(source, 36),
231 max_cache_bytes: read_u32(source, 40),
232 target_cadence_x100: read_u16(source, 44),
233 latency_budget_ms: read_u16(source, 46),
234 quality_tier: read_u16(source, 48),
235 degrade_policy: read_u16(source, 50),
236 requested_session_id: read_u32(source, 52),
237 auth_bytes: read_u32(source, 56),
238 control_extension_bytes: read_u32(source, 60),
239 };
240 metadata.validate_capability_window()?;
241 Ok(metadata)
242 }
243
244 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
245 require_destination_len(destination, CLIENT_HELLO_METADATA_LEN)?;
246 self.validate_capability_window()?;
247
248 destination[..CLIENT_HELLO_METADATA_LEN].fill(0);
249 destination[0] = self.min_version_major;
250 destination[1] = self.max_version_major;
251 write_u16(destination, 2, self.supported_wire_format_bitmap);
252 write_u32(destination, 4, self.supported_profile_bitmap);
253 write_u32(destination, 8, self.supported_payload_kind_bitmap);
254 write_u32(destination, 12, self.supported_codec_bitmap);
255 write_u32(destination, 16, self.supported_compression_bitmap);
256 write_u32(destination, 20, self.supported_dtype_bitmap);
257 write_u32(destination, 24, self.supported_layout_bitmap);
258 write_u16(destination, 28, self.cache_digest_bitmap);
259 write_u16(destination, 30, self.cache_object_bitmap);
260 write_u16(destination, 32, self.cache_namespace_count);
261 write_u16(destination, 34, self.max_lane_count);
262 write_u32(destination, 36, self.max_cache_entries);
263 write_u32(destination, 40, self.max_cache_bytes);
264 write_u16(destination, 44, self.target_cadence_x100);
265 write_u16(destination, 46, self.latency_budget_ms);
266 write_u16(destination, 48, self.quality_tier);
267 write_u16(destination, 50, self.degrade_policy);
268 write_u32(destination, 52, self.requested_session_id);
269 write_u32(destination, 56, self.auth_bytes);
270 write_u32(destination, 60, self.control_extension_bytes);
271 Ok(())
272 }
273
274 pub fn to_bytes(&self) -> Result<[u8; CLIENT_HELLO_METADATA_LEN], NnrpError> {
275 let mut bytes = [0u8; CLIENT_HELLO_METADATA_LEN];
276 self.write(&mut bytes)?;
277 Ok(bytes)
278 }
279
280 pub fn validate_capability_window(&self) -> Result<(), NnrpError> {
281 if self.min_version_major > self.max_version_major {
282 return Err(NnrpError::InvalidProtocolCombination {
283 rule: "CLIENT_HELLO version window must be ordered",
284 });
285 }
286 if CURRENT_VERSION_MAJOR < self.min_version_major
287 || CURRENT_VERSION_MAJOR > self.max_version_major
288 {
289 return Err(NnrpError::InvalidProtocolCombination {
290 rule: "CLIENT_HELLO version window must include NNRP/1",
291 });
292 }
293 if !has_bitmap_bit(
294 self.supported_wire_format_bitmap as u64,
295 CURRENT_WIRE_FORMAT as u32,
296 ) {
297 return Err(NnrpError::InvalidProtocolCombination {
298 rule: "CLIENT_HELLO supported_wire_format_bitmap must include wire_format 0",
299 });
300 }
301 require_nonzero(
302 "CLIENT_HELLO supported_profile_bitmap",
303 self.supported_profile_bitmap,
304 )?;
305 require_nonzero(
306 "CLIENT_HELLO supported_payload_kind_bitmap",
307 self.supported_payload_kind_bitmap,
308 )?;
309 Ok(())
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub struct ServerHelloAckMetadata {
315 pub selected_version_major: u8,
316 pub selected_wire_format: u8,
317 pub auth_status: u8,
318 pub session_id: u32,
319 pub accepted_profile_bitmap: u32,
320 pub accepted_payload_kind_bitmap: u32,
321 pub accepted_codec_bitmap: u32,
322 pub accepted_compression_bitmap: u32,
323 pub accepted_dtype_bitmap: u32,
324 pub accepted_layout_bitmap: u32,
325 pub cache_digest_bitmap: u32,
326 pub cache_object_bitmap: u32,
327 pub max_cache_entries: u32,
328 pub max_cache_bytes: u32,
329 pub max_lane_count: u16,
330 pub max_concurrent_frames: u16,
331 pub target_cadence_x100: u16,
332 pub latency_budget_ms: u16,
333 pub quality_tier: u16,
334 pub degrade_policy: u16,
335 pub max_body_bytes: u32,
336 pub token_ttl_ms: u32,
337 pub retry_after_ms: u32,
338 pub control_extension_bytes: u32,
339 pub server_flags: u32,
340}
341
342impl ServerHelloAckMetadata {
343 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
344 require_len(source, SERVER_HELLO_ACK_METADATA_LEN)?;
345 validate_zero_u8("server_hello_ack.reserved0", source[3])?;
346 let server_flags = read_u32(source, 76);
347 validate_mask_u32(server_flags, SERVER_HELLO_ACK_FLAGS_KNOWN_MASK)?;
348
349 Ok(Self {
350 selected_version_major: source[0],
351 selected_wire_format: source[1],
352 auth_status: source[2],
353 session_id: read_u32(source, 4),
354 accepted_profile_bitmap: read_u32(source, 8),
355 accepted_payload_kind_bitmap: read_u32(source, 12),
356 accepted_codec_bitmap: read_u32(source, 16),
357 accepted_compression_bitmap: read_u32(source, 20),
358 accepted_dtype_bitmap: read_u32(source, 24),
359 accepted_layout_bitmap: read_u32(source, 28),
360 cache_digest_bitmap: read_u32(source, 32),
361 cache_object_bitmap: read_u32(source, 36),
362 max_cache_entries: read_u32(source, 40),
363 max_cache_bytes: read_u32(source, 44),
364 max_lane_count: read_u16(source, 48),
365 max_concurrent_frames: read_u16(source, 50),
366 target_cadence_x100: read_u16(source, 52),
367 latency_budget_ms: read_u16(source, 54),
368 quality_tier: read_u16(source, 56),
369 degrade_policy: read_u16(source, 58),
370 max_body_bytes: read_u32(source, 60),
371 token_ttl_ms: read_u32(source, 64),
372 retry_after_ms: read_u32(source, 68),
373 control_extension_bytes: read_u32(source, 72),
374 server_flags,
375 })
376 }
377
378 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
379 require_destination_len(destination, SERVER_HELLO_ACK_METADATA_LEN)?;
380 validate_mask_u32(self.server_flags, SERVER_HELLO_ACK_FLAGS_KNOWN_MASK)?;
381
382 destination[..SERVER_HELLO_ACK_METADATA_LEN].fill(0);
383 destination[0] = self.selected_version_major;
384 destination[1] = self.selected_wire_format;
385 destination[2] = self.auth_status;
386 write_u32(destination, 4, self.session_id);
387 write_u32(destination, 8, self.accepted_profile_bitmap);
388 write_u32(destination, 12, self.accepted_payload_kind_bitmap);
389 write_u32(destination, 16, self.accepted_codec_bitmap);
390 write_u32(destination, 20, self.accepted_compression_bitmap);
391 write_u32(destination, 24, self.accepted_dtype_bitmap);
392 write_u32(destination, 28, self.accepted_layout_bitmap);
393 write_u32(destination, 32, self.cache_digest_bitmap);
394 write_u32(destination, 36, self.cache_object_bitmap);
395 write_u32(destination, 40, self.max_cache_entries);
396 write_u32(destination, 44, self.max_cache_bytes);
397 write_u16(destination, 48, self.max_lane_count);
398 write_u16(destination, 50, self.max_concurrent_frames);
399 write_u16(destination, 52, self.target_cadence_x100);
400 write_u16(destination, 54, self.latency_budget_ms);
401 write_u16(destination, 56, self.quality_tier);
402 write_u16(destination, 58, self.degrade_policy);
403 write_u32(destination, 60, self.max_body_bytes);
404 write_u32(destination, 64, self.token_ttl_ms);
405 write_u32(destination, 68, self.retry_after_ms);
406 write_u32(destination, 72, self.control_extension_bytes);
407 write_u32(destination, 76, self.server_flags);
408 Ok(())
409 }
410
411 pub fn to_bytes(&self) -> Result<[u8; SERVER_HELLO_ACK_METADATA_LEN], NnrpError> {
412 let mut bytes = [0u8; SERVER_HELLO_ACK_METADATA_LEN];
413 self.write(&mut bytes)?;
414 Ok(bytes)
415 }
416
417 pub fn validate_against_client_hello(
418 &self,
419 client_hello: &ClientHelloMetadata,
420 ) -> Result<(), NnrpError> {
421 if self.selected_version_major < client_hello.min_version_major
422 || self.selected_version_major > client_hello.max_version_major
423 {
424 return Err(NnrpError::InvalidProtocolCombination {
425 rule: "SERVER_HELLO_ACK selected version must be inside client window",
426 });
427 }
428 if !has_bitmap_bit(
429 client_hello.supported_wire_format_bitmap as u64,
430 self.selected_wire_format as u32,
431 ) {
432 return Err(NnrpError::InvalidProtocolCombination {
433 rule: "SERVER_HELLO_ACK selected wire format must be client-supported",
434 });
435 }
436 require_subset(
437 "SERVER_HELLO_ACK accepted_profile_bitmap",
438 self.accepted_profile_bitmap,
439 client_hello.supported_profile_bitmap,
440 )?;
441 require_subset(
442 "SERVER_HELLO_ACK accepted_payload_kind_bitmap",
443 self.accepted_payload_kind_bitmap,
444 client_hello.supported_payload_kind_bitmap,
445 )?;
446 require_subset(
447 "SERVER_HELLO_ACK accepted_codec_bitmap",
448 self.accepted_codec_bitmap,
449 client_hello.supported_codec_bitmap,
450 )?;
451 require_subset(
452 "SERVER_HELLO_ACK accepted_compression_bitmap",
453 self.accepted_compression_bitmap,
454 client_hello.supported_compression_bitmap,
455 )?;
456 require_subset(
457 "SERVER_HELLO_ACK accepted_dtype_bitmap",
458 self.accepted_dtype_bitmap,
459 client_hello.supported_dtype_bitmap,
460 )?;
461 require_subset(
462 "SERVER_HELLO_ACK accepted_layout_bitmap",
463 self.accepted_layout_bitmap,
464 client_hello.supported_layout_bitmap,
465 )?;
466 require_subset(
467 "SERVER_HELLO_ACK cache_digest_bitmap",
468 self.cache_digest_bitmap,
469 client_hello.cache_digest_bitmap as u32,
470 )?;
471 require_subset(
472 "SERVER_HELLO_ACK cache_object_bitmap",
473 self.cache_object_bitmap,
474 client_hello.cache_object_bitmap as u32,
475 )?;
476 Ok(())
477 }
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub struct SessionPatchMetadata {
482 pub profile_id: u16,
483 pub patch_mask: u32,
484 pub target_cadence_x100: u32,
485 pub quality_tier: u16,
486 pub degrade_policy: u16,
487 pub active_lane_mask: u64,
488 pub preferred_codec_bitmap: u32,
489 pub preferred_compression_bitmap: u32,
490 pub profile_patch_bytes: u32,
491}
492
493impl SessionPatchMetadata {
494 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
495 require_len(source, SESSION_PATCH_METADATA_LEN)?;
496 validate_zero_u16("session_patch.reserved0", read_u16(source, 2))?;
497 let patch_mask = read_u32(source, 4);
498 validate_mask_u32(patch_mask, SESSION_PATCH_FIELD_KNOWN_MASK)?;
499
500 Ok(Self {
501 profile_id: read_u16(source, 0),
502 patch_mask,
503 target_cadence_x100: read_u32(source, 8),
504 quality_tier: read_u16(source, 12),
505 degrade_policy: read_u16(source, 14),
506 active_lane_mask: read_u64(source, 16),
507 preferred_codec_bitmap: read_u32(source, 24),
508 preferred_compression_bitmap: read_u32(source, 28),
509 profile_patch_bytes: read_u32(source, 32),
510 })
511 }
512
513 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
514 require_destination_len(destination, SESSION_PATCH_METADATA_LEN)?;
515 validate_mask_u32(self.patch_mask, SESSION_PATCH_FIELD_KNOWN_MASK)?;
516
517 destination[..SESSION_PATCH_METADATA_LEN].fill(0);
518 write_u16(destination, 0, self.profile_id);
519 write_u32(destination, 4, self.patch_mask);
520 write_u32(destination, 8, self.target_cadence_x100);
521 write_u16(destination, 12, self.quality_tier);
522 write_u16(destination, 14, self.degrade_policy);
523 write_u64(destination, 16, self.active_lane_mask);
524 write_u32(destination, 24, self.preferred_codec_bitmap);
525 write_u32(destination, 28, self.preferred_compression_bitmap);
526 write_u32(destination, 32, self.profile_patch_bytes);
527 Ok(())
528 }
529
530 pub fn to_bytes(&self) -> Result<[u8; SESSION_PATCH_METADATA_LEN], NnrpError> {
531 let mut bytes = [0u8; SESSION_PATCH_METADATA_LEN];
532 self.write(&mut bytes)?;
533 Ok(bytes)
534 }
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538pub struct SessionPatchAckMetadata {
539 pub ack_status: SessionPatchAckStatus,
540 pub reject_reason: SessionPatchRejectReason,
541 pub applied_patch_mask: u32,
542 pub rejected_patch_mask: u32,
543 pub retry_after_ms: u32,
544 pub effective_profile_id: u16,
545 pub effective_target_cadence_x100: u32,
546 pub effective_quality_tier: u16,
547 pub effective_degrade_policy: u16,
548 pub effective_lane_mask: u64,
549 pub effective_codec_bitmap: u32,
550 pub effective_compression_bitmap: u32,
551 pub profile_patch_ack_bytes: u32,
552}
553
554impl SessionPatchAckMetadata {
555 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
556 require_len(source, SESSION_PATCH_ACK_METADATA_LEN)?;
557 validate_zero_u16("session_patch_ack.reserved0", read_u16(source, 18))?;
558 let applied_patch_mask = read_u32(source, 4);
559 let rejected_patch_mask = read_u32(source, 8);
560 validate_mask_u32(applied_patch_mask, SESSION_PATCH_FIELD_KNOWN_MASK)?;
561 validate_mask_u32(rejected_patch_mask, SESSION_PATCH_FIELD_KNOWN_MASK)?;
562
563 Ok(Self {
564 ack_status: SessionPatchAckStatus::try_from_u16(read_u16(source, 0))?,
565 reject_reason: SessionPatchRejectReason::try_from_u16(read_u16(source, 2))?,
566 applied_patch_mask,
567 rejected_patch_mask,
568 retry_after_ms: read_u32(source, 12),
569 effective_profile_id: read_u16(source, 16),
570 effective_target_cadence_x100: read_u32(source, 20),
571 effective_quality_tier: read_u16(source, 24),
572 effective_degrade_policy: read_u16(source, 26),
573 effective_lane_mask: read_u64(source, 28),
574 effective_codec_bitmap: read_u32(source, 36),
575 effective_compression_bitmap: read_u32(source, 40),
576 profile_patch_ack_bytes: read_u32(source, 44),
577 })
578 }
579
580 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
581 require_destination_len(destination, SESSION_PATCH_ACK_METADATA_LEN)?;
582 validate_mask_u32(self.applied_patch_mask, SESSION_PATCH_FIELD_KNOWN_MASK)?;
583 validate_mask_u32(self.rejected_patch_mask, SESSION_PATCH_FIELD_KNOWN_MASK)?;
584
585 destination[..SESSION_PATCH_ACK_METADATA_LEN].fill(0);
586 write_u16(destination, 0, self.ack_status as u16);
587 write_u16(destination, 2, self.reject_reason as u16);
588 write_u32(destination, 4, self.applied_patch_mask);
589 write_u32(destination, 8, self.rejected_patch_mask);
590 write_u32(destination, 12, self.retry_after_ms);
591 write_u16(destination, 16, self.effective_profile_id);
592 write_u32(destination, 20, self.effective_target_cadence_x100);
593 write_u16(destination, 24, self.effective_quality_tier);
594 write_u16(destination, 26, self.effective_degrade_policy);
595 write_u64(destination, 28, self.effective_lane_mask);
596 write_u32(destination, 36, self.effective_codec_bitmap);
597 write_u32(destination, 40, self.effective_compression_bitmap);
598 write_u32(destination, 44, self.profile_patch_ack_bytes);
599 Ok(())
600 }
601
602 pub fn to_bytes(&self) -> Result<[u8; SESSION_PATCH_ACK_METADATA_LEN], NnrpError> {
603 let mut bytes = [0u8; SESSION_PATCH_ACK_METADATA_LEN];
604 self.write(&mut bytes)?;
605 Ok(bytes)
606 }
607}
608
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub struct ResultHintMetadata {
611 pub applied_budget_policy: ResultHintBudgetPolicy,
612 pub congestion_state: ResultHintCongestionState,
613 pub reason: ResultHintReason,
614 pub retry_after_ms: u32,
615}
616
617impl ResultHintMetadata {
618 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
619 require_len(source, RESULT_HINT_METADATA_LEN)?;
620 Ok(Self {
621 applied_budget_policy: ResultHintBudgetPolicy::try_from_u32(read_u32(source, 0))?,
622 congestion_state: ResultHintCongestionState::try_from_u32(read_u32(source, 4))?,
623 reason: ResultHintReason::try_from_u32(read_u32(source, 8))?,
624 retry_after_ms: read_u32(source, 12),
625 })
626 }
627
628 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
629 require_destination_len(destination, RESULT_HINT_METADATA_LEN)?;
630 write_u32(destination, 0, self.applied_budget_policy as u32);
631 write_u32(destination, 4, self.congestion_state as u32);
632 write_u32(destination, 8, self.reason as u32);
633 write_u32(destination, 12, self.retry_after_ms);
634 Ok(())
635 }
636
637 pub fn to_bytes(&self) -> Result<[u8; RESULT_HINT_METADATA_LEN], NnrpError> {
638 let mut bytes = [0u8; RESULT_HINT_METADATA_LEN];
639 self.write(&mut bytes)?;
640 Ok(bytes)
641 }
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq)]
645pub struct TransportProbeMetadata {
646 pub probe_id: u32,
647 pub probe_payload_bytes: u32,
648 pub client_send_ts_us: u64,
649}
650
651impl TransportProbeMetadata {
652 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
653 require_len(source, TRANSPORT_PROBE_METADATA_LEN)?;
654 Ok(Self {
655 probe_id: read_u32(source, 0),
656 probe_payload_bytes: read_u32(source, 4),
657 client_send_ts_us: read_u64(source, 8),
658 })
659 }
660
661 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
662 require_destination_len(destination, TRANSPORT_PROBE_METADATA_LEN)?;
663 write_u32(destination, 0, self.probe_id);
664 write_u32(destination, 4, self.probe_payload_bytes);
665 write_u64(destination, 8, self.client_send_ts_us);
666 Ok(())
667 }
668
669 pub fn to_bytes(&self) -> Result<[u8; TRANSPORT_PROBE_METADATA_LEN], NnrpError> {
670 let mut bytes = [0u8; TRANSPORT_PROBE_METADATA_LEN];
671 self.write(&mut bytes)?;
672 Ok(bytes)
673 }
674}
675
676#[derive(Debug, Clone, Copy, PartialEq, Eq)]
677pub struct TransportProbeAckMetadata {
678 pub probe_id: u32,
679 pub server_recv_ts_us: u64,
680}
681
682impl TransportProbeAckMetadata {
683 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
684 require_len(source, TRANSPORT_PROBE_ACK_METADATA_LEN)?;
685 validate_zero_u32("transport_probe_ack.reserved0", read_u32(source, 4))?;
686 Ok(Self {
687 probe_id: read_u32(source, 0),
688 server_recv_ts_us: read_u64(source, 8),
689 })
690 }
691
692 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
693 require_destination_len(destination, TRANSPORT_PROBE_ACK_METADATA_LEN)?;
694 destination[..TRANSPORT_PROBE_ACK_METADATA_LEN].fill(0);
695 write_u32(destination, 0, self.probe_id);
696 write_u64(destination, 8, self.server_recv_ts_us);
697 Ok(())
698 }
699
700 pub fn to_bytes(&self) -> Result<[u8; TRANSPORT_PROBE_ACK_METADATA_LEN], NnrpError> {
701 let mut bytes = [0u8; TRANSPORT_PROBE_ACK_METADATA_LEN];
702 self.write(&mut bytes)?;
703 Ok(bytes)
704 }
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
708pub struct SessionMigrateMetadata {
709 pub old_transport_id: TransportId,
710 pub new_transport_id: TransportId,
711 pub last_result_frame_id: u64,
712 pub client_migrate_ts_us: u64,
713}
714
715impl SessionMigrateMetadata {
716 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
717 require_len(source, SESSION_MIGRATE_METADATA_LEN)?;
718 let old_transport_id = TransportId::try_from_u32(read_u32(source, 0))?;
719 let new_transport_id = TransportId::try_from_u32(read_u32(source, 4))?;
720 validate_specified_transport(old_transport_id, "session_migrate.old_transport_id")?;
721 validate_specified_transport(new_transport_id, "session_migrate.new_transport_id")?;
722
723 Ok(Self {
724 old_transport_id,
725 new_transport_id,
726 last_result_frame_id: read_u64(source, 8),
727 client_migrate_ts_us: read_u64(source, 16),
728 })
729 }
730
731 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
732 require_destination_len(destination, SESSION_MIGRATE_METADATA_LEN)?;
733 validate_specified_transport(self.old_transport_id, "session_migrate.old_transport_id")?;
734 validate_specified_transport(self.new_transport_id, "session_migrate.new_transport_id")?;
735
736 write_u32(destination, 0, self.old_transport_id as u32);
737 write_u32(destination, 4, self.new_transport_id as u32);
738 write_u64(destination, 8, self.last_result_frame_id);
739 write_u64(destination, 16, self.client_migrate_ts_us);
740 Ok(())
741 }
742
743 pub fn to_bytes(&self) -> Result<[u8; SESSION_MIGRATE_METADATA_LEN], NnrpError> {
744 let mut bytes = [0u8; SESSION_MIGRATE_METADATA_LEN];
745 self.write(&mut bytes)?;
746 Ok(bytes)
747 }
748}
749
750#[derive(Debug, Clone, Copy, PartialEq, Eq)]
751pub struct SessionMigrateAckMetadata {
752 pub accept_code: u32,
753 pub resume_from_frame_id: u64,
754 pub grace_window_ms: u32,
755 pub server_migrate_ts_us: u64,
756}
757
758impl SessionMigrateAckMetadata {
759 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
760 require_len(source, SESSION_MIGRATE_ACK_METADATA_LEN)?;
761 Ok(Self {
762 accept_code: read_u32(source, 0),
763 resume_from_frame_id: read_u64(source, 4),
764 grace_window_ms: read_u32(source, 12),
765 server_migrate_ts_us: read_u64(source, 16),
766 })
767 }
768
769 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
770 require_destination_len(destination, SESSION_MIGRATE_ACK_METADATA_LEN)?;
771 write_u32(destination, 0, self.accept_code);
772 write_u64(destination, 4, self.resume_from_frame_id);
773 write_u32(destination, 12, self.grace_window_ms);
774 write_u64(destination, 16, self.server_migrate_ts_us);
775 Ok(())
776 }
777
778 pub fn to_bytes(&self) -> Result<[u8; SESSION_MIGRATE_ACK_METADATA_LEN], NnrpError> {
779 let mut bytes = [0u8; SESSION_MIGRATE_ACK_METADATA_LEN];
780 self.write(&mut bytes)?;
781 Ok(bytes)
782 }
783}
784
785#[derive(Debug, Clone, Copy, PartialEq, Eq)]
786pub struct ErrorMetadata {
787 pub error_code: u32,
788 pub error_scope: ErrorScope,
789 pub is_fatal: bool,
790 pub retry_after_ms: u32,
791 pub related_session_id: u32,
792 pub related_frame_id: u32,
793 pub related_view_id: u32,
794 pub diagnostic_bytes: u32,
795}
796
797impl ErrorMetadata {
798 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
799 require_len(source, ERROR_METADATA_LEN)?;
800 let fatal_flag = read_u32(source, 8);
801 if fatal_flag > 1 {
802 return Err(NnrpError::InvalidProtocolCombination {
803 rule: "ERROR is_fatal must be 0 or 1",
804 });
805 }
806
807 Ok(Self {
808 error_code: read_u32(source, 0),
809 error_scope: ErrorScope::try_from_u32(read_u32(source, 4))?,
810 is_fatal: fatal_flag != 0,
811 retry_after_ms: read_u32(source, 12),
812 related_session_id: read_u32(source, 16),
813 related_frame_id: read_u32(source, 20),
814 related_view_id: read_u32(source, 24),
815 diagnostic_bytes: read_u32(source, 28),
816 })
817 }
818
819 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
820 require_destination_len(destination, ERROR_METADATA_LEN)?;
821 write_u32(destination, 0, self.error_code);
822 write_u32(destination, 4, self.error_scope as u32);
823 write_u32(destination, 8, u32::from(self.is_fatal));
824 write_u32(destination, 12, self.retry_after_ms);
825 write_u32(destination, 16, self.related_session_id);
826 write_u32(destination, 20, self.related_frame_id);
827 write_u32(destination, 24, self.related_view_id);
828 write_u32(destination, 28, self.diagnostic_bytes);
829 Ok(())
830 }
831
832 pub fn to_bytes(&self) -> Result<[u8; ERROR_METADATA_LEN], NnrpError> {
833 let mut bytes = [0u8; ERROR_METADATA_LEN];
834 self.write(&mut bytes)?;
835 Ok(bytes)
836 }
837}
838
839pub fn validate_empty_control_header(
840 header: &CommonHeader,
841 expected_message_type: MessageType,
842) -> Result<(), NnrpError> {
843 if header.message_type != expected_message_type || header.meta_len != 0 || header.body_len != 0
844 {
845 return Err(NnrpError::InvalidProtocolCombination {
846 rule: "empty control message requires expected type, meta_len=0, and body_len=0",
847 });
848 }
849 Ok(())
850}
851
852pub fn validate_close_header(header: &CommonHeader) -> Result<(), NnrpError> {
853 if header.message_type != MessageType::Close || header.meta_len != 0 {
854 return Err(NnrpError::InvalidProtocolCombination {
855 rule: "CLOSE requires message_type=CLOSE and meta_len=0",
856 });
857 }
858 Ok(())
859}
860
861fn require_len(source: &[u8], expected: usize) -> Result<(), NnrpError> {
862 if source.len() < expected {
863 return Err(NnrpError::SourceTooShort {
864 expected,
865 actual: source.len(),
866 });
867 }
868 Ok(())
869}
870
871fn require_destination_len(destination: &[u8], expected: usize) -> Result<(), NnrpError> {
872 if destination.len() < expected {
873 return Err(NnrpError::DestinationTooShort {
874 expected,
875 actual: destination.len(),
876 });
877 }
878 Ok(())
879}
880
881fn require_nonzero(rule: &'static str, value: u32) -> Result<(), NnrpError> {
882 if value == 0 {
883 return Err(NnrpError::InvalidProtocolCombination { rule });
884 }
885 Ok(())
886}
887
888fn require_subset(rule: &'static str, accepted: u32, supported: u32) -> Result<(), NnrpError> {
889 if accepted & !supported != 0 {
890 return Err(NnrpError::InvalidProtocolCombination { rule });
891 }
892 Ok(())
893}
894
895fn has_bitmap_bit(bitmap: u64, bit: u32) -> bool {
896 bit < 64 && bitmap & (1u64 << bit) != 0
897}
898
899fn validate_zero_u8(field: &'static str, value: u8) -> Result<(), NnrpError> {
900 if value != 0 {
901 return Err(NnrpError::NonZeroReservedField { field });
902 }
903 Ok(())
904}
905
906fn validate_zero_u16(field: &'static str, value: u16) -> Result<(), NnrpError> {
907 if value != 0 {
908 return Err(NnrpError::NonZeroReservedField { field });
909 }
910 Ok(())
911}
912
913fn validate_zero_u32(field: &'static str, value: u32) -> Result<(), NnrpError> {
914 if value != 0 {
915 return Err(NnrpError::NonZeroReservedField { field });
916 }
917 Ok(())
918}
919
920fn validate_mask_u32(value: u32, allowed: u32) -> Result<(), NnrpError> {
921 if value & !allowed != 0 {
922 return Err(NnrpError::ReservedBitsSet {
923 value: value as u64,
924 allowed: allowed as u64,
925 });
926 }
927 Ok(())
928}
929
930fn validate_specified_transport(
931 transport_id: TransportId,
932 rule: &'static str,
933) -> Result<(), NnrpError> {
934 if transport_id == TransportId::Unspecified {
935 return Err(NnrpError::InvalidProtocolCombination { rule });
936 }
937 Ok(())
938}
939
940fn read_u16(source: &[u8], offset: usize) -> u16 {
941 u16::from_le_bytes(source[offset..offset + 2].try_into().expect("slice length"))
942}
943
944fn read_u32(source: &[u8], offset: usize) -> u32 {
945 u32::from_le_bytes(source[offset..offset + 4].try_into().expect("slice length"))
946}
947
948fn read_u64(source: &[u8], offset: usize) -> u64 {
949 u64::from_le_bytes(source[offset..offset + 8].try_into().expect("slice length"))
950}
951
952fn write_u16(destination: &mut [u8], offset: usize, value: u16) {
953 destination[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
954}
955
956fn write_u32(destination: &mut [u8], offset: usize, value: u32) {
957 destination[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
958}
959
960fn write_u64(destination: &mut [u8], offset: usize, value: u64) {
961 destination[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
962}
963
964#[cfg(test)]
965mod tests {
966 use super::*;
967 use crate::{HeaderFlags, MessageType};
968
969 #[test]
970 fn client_hello_metadata_round_trips_python_golden_vector() {
971 let bytes = hex_to_bytes("01010100010000000100000003000000030000002100000003000000010007000100020040000000000001007017640002000000000000006000000000000000");
972
973 let metadata = ClientHelloMetadata::parse(&bytes).unwrap();
974
975 assert_eq!(metadata.min_version_major, 1);
976 assert_eq!(metadata.max_version_major, 1);
977 assert_eq!(metadata.supported_wire_format_bitmap, 1);
978 assert_eq!(metadata.supported_profile_bitmap, 1);
979 assert_eq!(metadata.supported_payload_kind_bitmap, 1);
980 assert_eq!(metadata.max_lane_count, 2);
981 assert_eq!(metadata.auth_bytes, 96);
982 assert_eq!(metadata.target_cadence_x100, 6000);
983 assert_eq!(metadata.to_bytes().unwrap().as_slice(), bytes.as_slice());
984 }
985
986 #[test]
987 fn hello_ack_rejects_capability_denial_mismatch() {
988 let hello = ClientHelloMetadata::parse(&hex_to_bytes("01010100010000000100000003000000030000002100000003000000010007000100020040000000000001007017640002000000000000006000000000000000")).unwrap();
989 let ack = ServerHelloAckMetadata {
990 selected_version_major: 1,
991 selected_wire_format: 0,
992 auth_status: 0,
993 session_id: 42,
994 accepted_profile_bitmap: 0x2,
995 accepted_payload_kind_bitmap: 0x1,
996 accepted_codec_bitmap: 0x1,
997 accepted_compression_bitmap: 0x1,
998 accepted_dtype_bitmap: 0x1,
999 accepted_layout_bitmap: 0x1,
1000 cache_digest_bitmap: 0,
1001 cache_object_bitmap: 0,
1002 max_cache_entries: 0,
1003 max_cache_bytes: 0,
1004 max_lane_count: 1,
1005 max_concurrent_frames: 1,
1006 target_cadence_x100: 0,
1007 latency_budget_ms: 0,
1008 quality_tier: 0,
1009 degrade_policy: 0,
1010 max_body_bytes: 0,
1011 token_ttl_ms: 0,
1012 retry_after_ms: 0,
1013 control_extension_bytes: 0,
1014 server_flags: 0,
1015 };
1016
1017 assert_eq!(
1018 ack.validate_against_client_hello(&hello),
1019 Err(NnrpError::InvalidProtocolCombination {
1020 rule: "SERVER_HELLO_ACK accepted_profile_bitmap"
1021 })
1022 );
1023 }
1024
1025 #[test]
1026 fn server_hello_ack_round_trips_and_validates_against_client_window() {
1027 let hello = ClientHelloMetadata::parse(&hex_to_bytes("01010100010000000100000003000000030000002100000003000000010007000100020040000000000001007017640002000000000000006000000000000000")).unwrap();
1028 let ack = ServerHelloAckMetadata {
1029 selected_version_major: 1,
1030 selected_wire_format: 0,
1031 auth_status: 0,
1032 session_id: 42,
1033 accepted_profile_bitmap: 0x0001,
1034 accepted_payload_kind_bitmap: 0x0001,
1035 accepted_codec_bitmap: 0x0003,
1036 accepted_compression_bitmap: 0x0003,
1037 accepted_dtype_bitmap: 0x0001,
1038 accepted_layout_bitmap: 0x0001,
1039 cache_digest_bitmap: 0x0001,
1040 cache_object_bitmap: 0x0007,
1041 max_cache_entries: 512,
1042 max_cache_bytes: 16 * 1024 * 1024,
1043 max_lane_count: 2,
1044 max_concurrent_frames: 2,
1045 target_cadence_x100: 6000,
1046 latency_budget_ms: 100,
1047 quality_tier: 2,
1048 degrade_policy: 2,
1049 max_body_bytes: 32 * 1024 * 1024,
1050 token_ttl_ms: 300_000,
1051 retry_after_ms: 0,
1052 control_extension_bytes: 0,
1053 server_flags: 1,
1054 };
1055 let bytes = ack.to_bytes().unwrap();
1056
1057 assert_eq!(ServerHelloAckMetadata::parse(&bytes).unwrap(), ack);
1058 ack.validate_against_client_hello(&hello).unwrap();
1059 }
1060
1061 #[test]
1062 fn hello_and_ack_reject_invalid_windows_reserved_fields_and_flags() {
1063 let mut hello = ClientHelloMetadata::parse(&hex_to_bytes("01010100010000000100000003000000030000002100000003000000010007000100020040000000000001007017640002000000000000006000000000000000")).unwrap();
1064 hello.min_version_major = 2;
1065 hello.max_version_major = 1;
1066 assert_eq!(
1067 hello.validate_capability_window(),
1068 Err(NnrpError::InvalidProtocolCombination {
1069 rule: "CLIENT_HELLO version window must be ordered"
1070 })
1071 );
1072 hello.min_version_major = 2;
1073 hello.max_version_major = 2;
1074 assert_eq!(
1075 hello.validate_capability_window(),
1076 Err(NnrpError::InvalidProtocolCombination {
1077 rule: "CLIENT_HELLO version window must include NNRP/1"
1078 })
1079 );
1080 hello.min_version_major = 1;
1081 hello.max_version_major = 1;
1082 hello.supported_wire_format_bitmap = 0;
1083 assert_eq!(
1084 hello.validate_capability_window(),
1085 Err(NnrpError::InvalidProtocolCombination {
1086 rule: "CLIENT_HELLO supported_wire_format_bitmap must include wire_format 0"
1087 })
1088 );
1089 hello.supported_wire_format_bitmap = 1;
1090 hello.supported_profile_bitmap = 0;
1091 assert_eq!(
1092 hello.validate_capability_window(),
1093 Err(NnrpError::InvalidProtocolCombination {
1094 rule: "CLIENT_HELLO supported_profile_bitmap"
1095 })
1096 );
1097 hello.supported_profile_bitmap = 1;
1098 hello.supported_payload_kind_bitmap = 0;
1099 assert_eq!(
1100 hello.validate_capability_window(),
1101 Err(NnrpError::InvalidProtocolCombination {
1102 rule: "CLIENT_HELLO supported_payload_kind_bitmap"
1103 })
1104 );
1105
1106 let mut ack_bytes = [0u8; SERVER_HELLO_ACK_METADATA_LEN];
1107 ack_bytes[3] = 1;
1108 assert_eq!(
1109 ServerHelloAckMetadata::parse(&ack_bytes),
1110 Err(NnrpError::NonZeroReservedField {
1111 field: "server_hello_ack.reserved0"
1112 })
1113 );
1114 ack_bytes[3] = 0;
1115 write_u32(&mut ack_bytes, 76, 2);
1116 assert_eq!(
1117 ServerHelloAckMetadata::parse(&ack_bytes),
1118 Err(NnrpError::ReservedBitsSet {
1119 value: 2,
1120 allowed: SERVER_HELLO_ACK_FLAGS_KNOWN_MASK as u64
1121 })
1122 );
1123 }
1124
1125 #[test]
1126 fn session_patch_metadata_round_trips_python_golden_vector() {
1127 let bytes = hex_to_bytes(
1128 "1d0000005d00000028230000680105000300000000000000050000000000000010000000",
1129 );
1130
1131 let metadata = SessionPatchMetadata::parse(&bytes).unwrap();
1132
1133 assert_eq!(metadata.profile_id, 29);
1134 assert_eq!(metadata.patch_mask, 0x5d);
1135 assert_eq!(metadata.target_cadence_x100, 9000);
1136 assert_eq!(metadata.active_lane_mask, 3);
1137 assert_eq!(metadata.profile_patch_bytes, 16);
1138 assert_eq!(metadata.to_bytes().unwrap().as_slice(), bytes.as_slice());
1139 }
1140
1141 #[test]
1142 fn session_patch_ack_metadata_round_trips_python_golden_vector() {
1143 let bytes = hex_to_bytes("010003001100000044000000000000000200000028230000680105000300000000000000010000000300000010000000");
1144
1145 let metadata = SessionPatchAckMetadata::parse(&bytes).unwrap();
1146
1147 assert_eq!(metadata.ack_status, SessionPatchAckStatus::PartiallyApplied);
1148 assert_eq!(
1149 metadata.reject_reason,
1150 SessionPatchRejectReason::UnsupportedStrategy
1151 );
1152 assert_eq!(metadata.effective_profile_id, 2);
1153 assert_eq!(metadata.effective_target_cadence_x100, 9000);
1154 assert_eq!(metadata.profile_patch_ack_bytes, 16);
1155 assert_eq!(metadata.to_bytes().unwrap().as_slice(), bytes.as_slice());
1156 }
1157
1158 #[test]
1159 fn result_hint_probe_and_migrate_metadata_round_trip() {
1160 let hint = ResultHintMetadata {
1161 applied_budget_policy: ResultHintBudgetPolicy::Partial,
1162 congestion_state: ResultHintCongestionState::Elevated,
1163 reason: ResultHintReason::ServerBusy,
1164 retry_after_ms: 20,
1165 };
1166 assert_eq!(
1167 ResultHintMetadata::parse(&hint.to_bytes().unwrap()).unwrap(),
1168 hint
1169 );
1170
1171 let probe = TransportProbeMetadata {
1172 probe_id: 17,
1173 probe_payload_bytes: 32768,
1174 client_send_ts_us: 123456789,
1175 };
1176 assert_eq!(
1177 TransportProbeMetadata::parse(&probe.to_bytes().unwrap()).unwrap(),
1178 probe
1179 );
1180
1181 let ack = TransportProbeAckMetadata {
1182 probe_id: 17,
1183 server_recv_ts_us: 223456789,
1184 };
1185 assert_eq!(
1186 TransportProbeAckMetadata::parse(&ack.to_bytes().unwrap()).unwrap(),
1187 ack
1188 );
1189
1190 let migrate = SessionMigrateMetadata {
1191 old_transport_id: TransportId::Quic,
1192 new_transport_id: TransportId::Tcp,
1193 last_result_frame_id: 44,
1194 client_migrate_ts_us: 3000,
1195 };
1196 assert_eq!(
1197 SessionMigrateMetadata::parse(&migrate.to_bytes().unwrap()).unwrap(),
1198 migrate
1199 );
1200
1201 let migrate_ack = SessionMigrateAckMetadata {
1202 accept_code: 0,
1203 resume_from_frame_id: 45,
1204 grace_window_ms: 250,
1205 server_migrate_ts_us: 4000,
1206 };
1207 assert_eq!(
1208 SessionMigrateAckMetadata::parse(&migrate_ack.to_bytes().unwrap()).unwrap(),
1209 migrate_ack
1210 );
1211 }
1212
1213 #[test]
1214 fn result_hint_rejects_unknown_enum_values() {
1215 let bytes = [1u8, 0, 0, 0, 1, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0];
1216
1217 assert_eq!(
1218 ResultHintMetadata::parse(&bytes),
1219 Err(NnrpError::UnknownEnumValue {
1220 enum_name: "result_hint_reason",
1221 value: 99
1222 })
1223 );
1224 }
1225
1226 #[test]
1227 fn inherited_control_enums_accept_all_stable_assignments() {
1228 for value in 0..=4 {
1229 assert!(ResultHintBudgetPolicy::try_from_u32(value).is_ok());
1230 assert!(ResultHintReason::try_from_u32(value).is_ok());
1231 }
1232 for value in 0..=3 {
1233 assert!(ResultHintCongestionState::try_from_u32(value).is_ok());
1234 }
1235 for value in 0..=2 {
1236 assert!(SessionPatchAckStatus::try_from_u16(value).is_ok());
1237 assert!(ErrorScope::try_from_u32(value as u32).is_ok());
1238 }
1239 for value in 0..=5 {
1240 assert!(SessionPatchRejectReason::try_from_u16(value).is_ok());
1241 }
1242 for value in 0..=2 {
1243 assert!(TransportId::try_from_u32(value).is_ok());
1244 }
1245
1246 assert!(ResultHintBudgetPolicy::try_from_u32(99).is_err());
1247 assert!(ResultHintCongestionState::try_from_u32(99).is_err());
1248 assert!(SessionPatchAckStatus::try_from_u16(99).is_err());
1249 assert!(SessionPatchRejectReason::try_from_u16(99).is_err());
1250 assert!(TransportId::try_from_u32(99).is_err());
1251 assert!(ErrorScope::try_from_u32(99).is_err());
1252 }
1253
1254 #[test]
1255 fn migrate_rejects_unspecified_transport() {
1256 let mut bytes = [0u8; SESSION_MIGRATE_METADATA_LEN];
1257 write_u32(&mut bytes, 4, TransportId::Tcp as u32);
1258
1259 assert_eq!(
1260 SessionMigrateMetadata::parse(&bytes),
1261 Err(NnrpError::InvalidProtocolCombination {
1262 rule: "session_migrate.old_transport_id"
1263 })
1264 );
1265 }
1266
1267 #[test]
1268 fn error_metadata_and_empty_control_headers_validate() {
1269 let metadata = ErrorMetadata {
1270 error_code: 0x000b,
1271 error_scope: ErrorScope::Session,
1272 is_fatal: false,
1273 retry_after_ms: 500,
1274 related_session_id: 42,
1275 related_frame_id: 0,
1276 related_view_id: 0,
1277 diagnostic_bytes: 24,
1278 };
1279 assert_eq!(
1280 ErrorMetadata::parse(&metadata.to_bytes().unwrap()).unwrap(),
1281 metadata
1282 );
1283
1284 let mut ping = CommonHeader::new(MessageType::Ping, 0, 0);
1285 ping.flags = HeaderFlags::CAN_DROP;
1286 validate_empty_control_header(&ping, MessageType::Ping).unwrap();
1287 assert_eq!(
1288 validate_empty_control_header(&ping, MessageType::Pong),
1289 Err(NnrpError::InvalidProtocolCombination {
1290 rule: "empty control message requires expected type, meta_len=0, and body_len=0"
1291 })
1292 );
1293
1294 let mut close = CommonHeader::new(MessageType::Close, 0, 5);
1295 close.body_len = 5;
1296 validate_close_header(&close).unwrap();
1297 }
1298
1299 #[test]
1300 fn control_metadata_rejects_short_buffers_and_bad_error_fatal_flag() {
1301 assert_eq!(
1302 ClientHelloMetadata::parse(&[0u8; CLIENT_HELLO_METADATA_LEN - 1]),
1303 Err(NnrpError::SourceTooShort {
1304 expected: CLIENT_HELLO_METADATA_LEN,
1305 actual: CLIENT_HELLO_METADATA_LEN - 1
1306 })
1307 );
1308 let hello = ClientHelloMetadata::parse(&hex_to_bytes("01010100010000000100000003000000030000002100000003000000010007000100020040000000000001007017640002000000000000006000000000000000")).unwrap();
1309 assert_eq!(
1310 hello.write(&mut [0u8; CLIENT_HELLO_METADATA_LEN - 1]),
1311 Err(NnrpError::DestinationTooShort {
1312 expected: CLIENT_HELLO_METADATA_LEN,
1313 actual: CLIENT_HELLO_METADATA_LEN - 1
1314 })
1315 );
1316
1317 assert_eq!(
1318 ServerHelloAckMetadata::parse(&[0u8; SERVER_HELLO_ACK_METADATA_LEN - 1]),
1319 Err(NnrpError::SourceTooShort {
1320 expected: SERVER_HELLO_ACK_METADATA_LEN,
1321 actual: SERVER_HELLO_ACK_METADATA_LEN - 1
1322 })
1323 );
1324
1325 let mut error = [0u8; ERROR_METADATA_LEN];
1326 write_u32(&mut error, 8, 2);
1327 assert_eq!(
1328 ErrorMetadata::parse(&error),
1329 Err(NnrpError::InvalidProtocolCombination {
1330 rule: "ERROR is_fatal must be 0 or 1"
1331 })
1332 );
1333
1334 assert_eq!(
1335 ResultHintMetadata::parse(&[0u8; RESULT_HINT_METADATA_LEN - 1]),
1336 Err(NnrpError::SourceTooShort {
1337 expected: RESULT_HINT_METADATA_LEN,
1338 actual: RESULT_HINT_METADATA_LEN - 1
1339 })
1340 );
1341 let hint = ResultHintMetadata {
1342 applied_budget_policy: ResultHintBudgetPolicy::None,
1343 congestion_state: ResultHintCongestionState::None,
1344 reason: ResultHintReason::None,
1345 retry_after_ms: 0,
1346 };
1347 assert_eq!(
1348 hint.write(&mut [0u8; RESULT_HINT_METADATA_LEN - 1]),
1349 Err(NnrpError::DestinationTooShort {
1350 expected: RESULT_HINT_METADATA_LEN,
1351 actual: RESULT_HINT_METADATA_LEN - 1
1352 })
1353 );
1354 }
1355
1356 fn hex_to_bytes(hex: &str) -> Vec<u8> {
1357 assert_eq!(hex.len() % 2, 0);
1358 (0..hex.len())
1359 .step_by(2)
1360 .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).unwrap())
1361 .collect()
1362 }
1363}