Skip to main content

nnrp_core/
control.rs

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}