Skip to main content

sciparse/proto/header/
model.rs

1// Copyright 2026 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! SCION header models
16
17use crate::{
18    address::addr::ScionAddr,
19    core::{
20        convert::{FromView, TryFromView},
21        encode::{EncodeError, InvalidStructureError, WireEncode},
22        layout::Layout,
23        view::ViewConversionError,
24        write::unchecked_bit_range_be_write,
25    },
26    dataplane_path::{model::DpPath, types::PathType},
27    header::{
28        layout::{AddressHeaderLayout, CommonHeaderLayout, ScionHeaderLayout},
29        view::ScionHeaderView,
30    },
31    payload::ProtocolNumber,
32    scion::{
33        address::host_addr::{WireHostAddr, WireHostAddrType},
34        identifier::isd_asn::IsdAsn,
35    },
36};
37
38/// Represents a SCION packet header
39///
40/// This structure contains all the fields of a SCION packet header,
41/// including the common header, address header, and path information.
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct ScionPacketHeader {
44    /// The common header of the SCION packet
45    pub common: CommonHeader,
46    /// The address header of the SCION packet
47    pub address: AddressHeader,
48    /// The path information of the SCION packet
49    pub path: DpPath,
50}
51impl ScionPacketHeader {
52    /// Returns the size of the SCION packet header in 4-byte units used in the header length field.
53    #[inline]
54    fn size_units(&self) -> u8 {
55        (self.required_size() / 4) as u8
56    }
57}
58// Wire Encode (needs size, so can't use trait)
59impl ScionPacketHeader {
60    /// Returns the size required for the wire encoding.
61    ///
62    /// ## Safety
63    /// This size must be correct, it is used to validate buffer sizes in `encode`.
64    /// If this size is smaller than the actual encoded size, undefined behavior will occur.
65    #[inline]
66    pub fn required_size(&self) -> usize {
67        CommonHeaderLayout::SIZE_BYTES + self.address.required_size() + self.path.required_size()
68    }
69
70    /// Validates that all fields in the structure are valid for encoding.
71    ///
72    /// Note: This only checks the minimal set of fields required for encoding, do not expect
73    /// comprehensive validation.
74    ///
75    /// Returns Ok(()) if valid, otherwise a static error reference.
76    #[inline]
77    pub fn wire_valid(&self) -> Result<(), InvalidStructureError> {
78        let required_size = self.required_size();
79        if !required_size.is_multiple_of(4) {
80            return Err(InvalidStructureError::from(
81                "header size must be a multiple of 4 bytes",
82            ));
83        }
84
85        if required_size > ScionHeaderLayout::MAX_SIZE_BYTES {
86            return Err(InvalidStructureError::from(
87                "header size exceeds maximum encodeable value of 1020 bytes",
88            ));
89        }
90
91        self.common.valid()?;
92        self.address.wire_valid()?;
93        self.path.wire_valid()?;
94        Ok(())
95    }
96
97    /// Writes the wire encoding into the provided buffer.
98    ///
99    /// Returns the number of bytes written.
100    ///
101    /// ## SAFETY
102    /// 1. The buffer must be at least `self.required_size()` bytes long
103    /// 2. The structure must be valid for encoding, i.e., `self.valid()` must return `Ok(())`
104    #[inline]
105    pub unsafe fn encode_unchecked(&self, buf: &mut [u8], payload_size: u16) -> usize {
106        unsafe {
107            use CommonHeaderLayout as CHL;
108            // Encode common header
109            self.common.encode_unchecked(
110                buf,
111                self.size_units(),
112                self.path.path_type(),
113                self.address.dst_addr_type(),
114                self.address.src_addr_type(),
115                payload_size,
116            );
117
118            // Encode address header
119            let offset = CHL::SIZE_BYTES;
120            let address_buf = buf.split_at_mut_unchecked(offset).1;
121            self.address.encode_unchecked(address_buf);
122
123            // Encode path
124            let offset = offset + self.address.required_size();
125            let path_buf = buf.split_at_mut_unchecked(offset).1;
126            self.path.encode_unchecked(path_buf);
127        }
128
129        self.required_size()
130    }
131
132    /// Writes the wire encoding into the provided buffer.
133    ///
134    /// Returns the number of bytes written on success, or `Err(usize)` of the required size if the
135    /// buffer is too small or the packet.
136    ///
137    /// The buffer must be at least `self.required_size()` bytes long.
138    #[inline]
139    pub fn try_encode(&self, buf: &mut [u8], payload_size: u16) -> Result<usize, EncodeError> {
140        self.wire_valid()?;
141
142        let required_size = self.required_size();
143        if buf.len() < required_size {
144            return Err(EncodeError::BufferTooSmall(required_size));
145        }
146
147        // SAFETY: buffer length is checked above
148        Ok(unsafe { self.encode_unchecked(buf, payload_size) })
149    }
150}
151impl TryFromView for ScionPacketHeader {
152    type ViewType = ScionHeaderView;
153    #[inline]
154    fn try_from_view(view: &Self::ViewType) -> Result<Self, ViewConversionError> {
155        Ok(ScionPacketHeader {
156            common: CommonHeader::from_view(view),
157            address: AddressHeader::try_from_view(view)?,
158            path: DpPath::from_view(&view.path()),
159        })
160    }
161}
162
163/// Represents the common header of a SCION packet
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165pub struct CommonHeader {
166    /// Traffic class of the SCION packet
167    pub traffic_class: u8,
168    /// Flow ID of the SCION packet
169    pub flow_id: u32,
170    /// Next header type
171    ///
172    /// Indicates the type of header that follows the SCION header. E.g., UDP, TCP, etc.
173    pub next_header: ProtocolNumber,
174}
175// Wire Encode (needs size, so can't use trait)
176impl CommonHeader {
177    /// Validates that all fields in the `CommonHeader` are valid for encoding
178    #[inline]
179    pub fn valid(&self) -> Result<(), InvalidStructureError> {
180        use CommonHeaderLayout as CHL;
181
182        if self.flow_id > CHL::FLOW_ID_RNG.max_uint() as u32 {
183            return Err("flow_id exceeds maximum encodeable value".into());
184        }
185
186        // Payload size is u16, so all values are valid
187        // Next header is a u8, so all values are valid
188
189        Ok(())
190    }
191
192    const VERSION: u8 = 0;
193    /// Encodes the `CommonHeader` into the provided buffer
194    /// # Safety
195    /// - The implementation may use unchecked indexing operations and relies on the caller to
196    ///   provide a sufficiently large buffer (at least `CommonHeaderLayout::SIZE_BYTES` bytes).
197    #[inline]
198    pub unsafe fn encode_unchecked(
199        &self,
200        buf: &mut [u8],
201        header_len_units: u8,
202        path_type: PathType,
203        dst_addr_type: WireHostAddrType,
204        src_addr_type: WireHostAddrType,
205        payload_size: u16,
206    ) -> usize {
207        unsafe {
208            use CommonHeaderLayout as CHL;
209            // Encode common header
210            unchecked_bit_range_be_write(buf, CHL::VERSION_RNG, Self::VERSION);
211            unchecked_bit_range_be_write(buf, CHL::TRAFFIC_CLASS_RNG, self.traffic_class);
212            unchecked_bit_range_be_write(buf, CHL::FLOW_ID_RNG, self.flow_id);
213            unchecked_bit_range_be_write::<u8>(buf, CHL::NEXT_HEADER_RNG, self.next_header.into());
214            unchecked_bit_range_be_write(buf, CHL::HEADER_LEN_RNG, header_len_units);
215            unchecked_bit_range_be_write(buf, CHL::PAYLOAD_LEN_RNG, payload_size);
216            unchecked_bit_range_be_write::<u8>(buf, CHL::PATH_TYPE_RNG, path_type.into());
217            let dst_addr_info: u8 = dst_addr_type.into();
218            unchecked_bit_range_be_write::<u8>(buf, CHL::DST_ADDR_INFO_RNG, dst_addr_info);
219            let src_addr_info: u8 = src_addr_type.into();
220            unchecked_bit_range_be_write::<u8>(buf, CHL::SRC_ADDR_INFO_RNG, src_addr_info);
221            unchecked_bit_range_be_write(buf, CHL::RSV_RNG, 0u16); // Reserved
222        }
223
224        CommonHeaderLayout::SIZE_BYTES
225    }
226}
227impl FromView for CommonHeader {
228    type ViewType = ScionHeaderView;
229    #[inline]
230    fn from_view(view: &Self::ViewType) -> Self {
231        debug_assert!(view.version() == Self::VERSION, "Unsupported SCION version");
232        CommonHeader {
233            traffic_class: view.traffic_class(),
234            flow_id: view.flow_id(),
235            next_header: view.next_header(),
236        }
237    }
238}
239
240/// Represents the address header of a SCION packet
241#[derive(Debug, Clone, PartialEq, Eq, Hash)]
242pub struct AddressHeader {
243    /// Destination ISD
244    pub dst_ia: IsdAsn,
245    /// Source ISD
246    pub src_ia: IsdAsn,
247    /// Destination host address
248    pub dst_host_addr: WireHostAddr,
249    /// Source host address
250    pub src_host_addr: WireHostAddr,
251}
252impl AddressHeader {
253    /// Constructs an `AddressHeader` from the given source and destination `ScionAddr`
254    #[inline]
255    pub fn new(src: ScionAddr, dst: ScionAddr) -> Self {
256        AddressHeader {
257            dst_ia: dst.isd_asn(),
258            src_ia: src.isd_asn(),
259            dst_host_addr: dst.host().into(),
260            src_host_addr: src.host().into(),
261        }
262    }
263
264    /// Returns the destination address type
265    #[inline]
266    pub fn dst_addr_type(&self) -> WireHostAddrType {
267        self.dst_host_addr.addr_type()
268    }
269
270    /// Returns the source address type
271    #[inline]
272    pub fn src_addr_type(&self) -> WireHostAddrType {
273        self.src_host_addr.addr_type()
274    }
275}
276impl WireEncode for AddressHeader {
277    #[inline]
278    fn required_size(&self) -> usize {
279        AddressHeaderLayout::new(
280            self.dst_host_addr.required_size() as u8,
281            self.src_host_addr.required_size() as u8,
282        )
283        .size_bytes()
284    }
285
286    #[inline]
287    fn wire_valid(&self) -> Result<(), InvalidStructureError> {
288        // ISD and ASN are newtypes, so assumed valid
289
290        self.dst_host_addr.wire_valid()?;
291        self.src_host_addr.wire_valid()?;
292
293        Ok(())
294    }
295
296    #[inline]
297    unsafe fn encode_unchecked(&self, buf: &mut [u8]) -> usize {
298        unsafe {
299            use AddressHeaderLayout as AHL;
300            unchecked_bit_range_be_write(buf, AHL::DST_ISD_RNG, self.dst_ia.isd().0);
301            unchecked_bit_range_be_write(buf, AHL::DST_AS_RNG, self.dst_ia.asn().0);
302            unchecked_bit_range_be_write(buf, AHL::SRC_ISD_RNG, self.src_ia.isd().0);
303            unchecked_bit_range_be_write(buf, AHL::SRC_AS_RNG, self.src_ia.asn().0);
304
305            let layout = AddressHeaderLayout::new(
306                self.src_host_addr.required_size() as u8,
307                self.dst_host_addr.required_size() as u8,
308            );
309
310            {
311                let dst_host_buf =
312                    buf.get_unchecked_mut(layout.dst_host_addr_range().aligned_byte_range());
313                self.dst_host_addr.encode_unchecked(dst_host_buf);
314            }
315
316            {
317                let src_host_buf =
318                    buf.get_unchecked_mut(layout.src_host_addr_range().aligned_byte_range());
319                self.src_host_addr.encode_unchecked(src_host_buf);
320            }
321        }
322
323        self.required_size()
324    }
325}
326impl TryFromView for AddressHeader {
327    type ViewType = ScionHeaderView;
328    #[inline]
329    fn try_from_view(view: &Self::ViewType) -> Result<Self, ViewConversionError> {
330        Ok(AddressHeader {
331            dst_ia: view.dst_ia(),
332            src_ia: view.src_ia(),
333            dst_host_addr: view
334                .dst_host_addr()
335                .map_err(|_| ViewConversionError::Other("invalid dst_host_addr"))?,
336            src_host_addr: view
337                .src_host_addr()
338                .map_err(|_| ViewConversionError::Other("invalid src_host_addr"))?,
339        })
340    }
341}
342
343/// Support for [`proptest::arbitrary`].
344#[cfg(feature = "proptest")]
345pub mod ptest {
346    use ::proptest::prelude::*;
347
348    use super::*;
349    use crate::dataplane_path::model::DpPath;
350
351    /// Configuration for generating arbitrary [`ScionPacketHeader`] values.
352    ///
353    /// Composes sub-parameters for the address header (host address variant weights)
354    /// and the path.
355    #[derive(Debug, Clone, Default)]
356    pub struct ArbitraryScionPacketHeaderParams {
357        /// Parameters for generating destination host addresses.
358        pub dst_host_addr: <WireHostAddr as Arbitrary>::Parameters,
359        /// Parameters for generating source host addresses.
360        pub src_host_addr: <WireHostAddr as Arbitrary>::Parameters,
361        /// Parameters for generating paths.
362        pub path: <DpPath as Arbitrary>::Parameters,
363    }
364
365    impl Arbitrary for ScionPacketHeader {
366        type Parameters = ArbitraryScionPacketHeaderParams;
367        type Strategy = BoxedStrategy<Self>;
368
369        fn arbitrary_with(params: Self::Parameters) -> Self::Strategy {
370            let traffic_class = any::<u8>();
371            let flow_id = 0u32..=0xF_FFFFu32;
372            let next_header = any::<u8>();
373
374            let dst_ia = any::<IsdAsn>();
375            let src_ia = any::<IsdAsn>();
376
377            let dst_host_addr = WireHostAddr::arbitrary_with(params.dst_host_addr);
378            let src_host_addr = WireHostAddr::arbitrary_with(params.src_host_addr);
379
380            let path = DpPath::arbitrary_with(params.path);
381
382            (
383                traffic_class,
384                flow_id,
385                next_header,
386                dst_ia,
387                src_ia,
388                dst_host_addr,
389                src_host_addr,
390                path,
391            )
392                .prop_map(
393                    |(
394                        traffic_class,
395                        flow_id,
396                        next_header,
397                        dst_ia,
398                        src_ia,
399                        dst_host_addr,
400                        src_host_addr,
401                        path,
402                    )| {
403                        Self {
404                            common: CommonHeader {
405                                traffic_class,
406                                flow_id,
407                                next_header: next_header.into(),
408                            },
409                            address: AddressHeader {
410                                dst_ia,
411                                src_ia,
412                                dst_host_addr,
413                                src_host_addr,
414                            },
415                            path,
416                        }
417                    },
418                )
419                .boxed()
420        }
421    }
422}