sciparse 0.6.1

Zero-copy SCION packet parsing, serialization and control plane components
Documentation
// Copyright 2026 Anapaya Systems
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Model for one-hop paths between neighboring border routers in SCION.

use tinyvec::{array_vec, tiny_vec};

use crate::{
    core::{
        convert::{FromView, TryFromModel},
        encode::WireEncode,
        model::Model,
    },
    dataplane_path::{
        onehop::{layout::OneHopPathLayout, view::OneHopPathView},
        standard::{
            mac::{ForwardingKey, algo::mac_beta_step},
            model::{HopField, InfoField, Segment, StandardPath},
            types::{HopFieldFlags, HopFieldMac, InfoFieldFlags},
        },
        types::PathReverseError,
    },
};

/// Represents a one-hop path between neighboring border routers in SCION.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OneHopPath {
    /// Info field
    pub info: InfoField,
    /// Hop fields
    pub hops: [HopField; 2],
}
impl OneHopPath {
    /// Creates a new OneHopPath from the given info field and hop fields.
    ///
    /// No validation is performed on the provided data.
    #[inline]
    pub const fn new_from_parts(info: InfoField, hops: [HopField; 2]) -> Self {
        Self { info, hops }
    }

    /// Creates a new OneHopPath with the given parameters, calculating the MACs for the hop fields.
    #[inline]
    pub fn new(
        egress_interface: u16,
        segment_id: u16,
        timestamp: u32,
        forwarding_key: ForwardingKey,
        expiration_units: u8,
    ) -> Self {
        let info = InfoField {
            flags: InfoFieldFlags::CONS_DIR,
            segment_id,
            timestamp,
        };

        let hop1 = HopField {
            flags: HopFieldFlags::empty(),
            cons_ingress: 0,
            cons_egress: egress_interface,
            expiration_units,
            mac: HopFieldMac([0u8; 6]),
        }
        .with_calculated_mac(info.segment_id, info.timestamp, &forwarding_key);

        // When creating a one-hop path, the second hop is typically empty and serves as a
        // placeholder.
        let hop2 = HopField::empty();

        Self {
            info,
            hops: [hop1, hop2],
        }
    }

    /// Sets the second hop field with the given ingress interface and recalculates the MAC.
    ///
    /// # Parameters
    /// * `ingress_interface` is the interface on which the packet is expected to arrive at the
    ///   second hop.
    /// * `forwarding_key` is the key used for MAC calculation
    /// * `segment_id_was_advanced` indicates whether the segment ID was advanced to the next hop
    ///   before calling this method, or if it is still at the first hop
    #[inline]
    pub fn set_second_hop(
        &mut self,
        ingress_interface: u16,
        forwarding_key: ForwardingKey,
        segment_id_was_advanced: bool,
    ) {
        let beta = if segment_id_was_advanced {
            self.info.segment_id
        } else {
            mac_beta_step(self.info.segment_id, *self.hops[0].mac)
        };

        self.hops[1] = HopField {
            flags: HopFieldFlags::empty(),
            cons_ingress: ingress_interface,
            cons_egress: 0,
            expiration_units: 0,
            mac: HopFieldMac([0u8; 6]),
        }
        .with_calculated_mac(beta, self.info.timestamp, &forwarding_key);
    }

    /// Reverses the one-hop path to create a standard path with two hops in the opposite direction.
    ///
    /// If the second hop field is not set, this will return an error containing the original
    /// OneHopPath.
    ///
    /// ## Note
    /// This assumes that the Segment ID was advanced to the last hop of the path.
    ///
    /// Since the One Hop does not track which hop it is currently on, the caller must ensure that
    /// the Segment ID is correctly set to reflect the final hop of the path before calling this
    /// method.
    pub fn try_into_reversed_standard_path(self) -> Result<StandardPath, (PathReverseError, Self)> {
        if self.hops[1].cons_ingress == 0 {
            // The second hop is not set, we cannot reverse the path
            return Err((
                PathReverseError::new(
                    "Cannot reverse a one-hop path whose second hop has not been set yet",
                ),
                self,
            ));
        }

        let OneHopPath {
            mut info,
            hops: [hop1, hop2],
        } = self;

        // Segment ID is final segment ID, so we can use it directly, we just need to reverse the
        // CONS_DIR flag to indicate the reversed direction.
        info.flags ^= InfoFieldFlags::CONS_DIR;

        let standard_path = StandardPath {
            current_info_field: 0,
            current_hop_field: 0,
            segments: array_vec!(Segment {
                info_field: info,
                hop_fields: tiny_vec!(hop2, hop1),
            }),
        };

        Ok(standard_path)
    }

    /// Reverses the one-hop path in place by swapping the two hop fields.
    ///
    /// Returns an error if the second hop field has not been set yet (i.e., its ingress
    /// interface is still 0, meaning the path is incomplete).
    #[inline]
    pub fn try_reverse(&mut self) -> Result<(), PathReverseError> {
        if self.hops[1].cons_ingress == 0 {
            return Err(PathReverseError::new(
                "Cannot reverse a one-hop path whose second hop has not been set yet",
            ));
        }

        self.hops.swap(0, 1);
        self.info.flags ^= InfoFieldFlags::CONS_DIR;

        Ok(())
    }
}
impl WireEncode for OneHopPath {
    #[inline]
    fn required_size(&self) -> usize {
        OneHopPathLayout::SIZE_BYTES
    }

    #[inline]
    fn wire_valid(&self) -> Result<(), crate::core::encode::InvalidStructureError> {
        // No specific validation rules for OneHopPath, so we consider it always valid.
        Ok(())
    }

    #[inline]
    unsafe fn encode_unchecked(&self, buf: &mut [u8]) -> usize {
        unsafe {
            use OneHopPathLayout as OHPL;
            // Encode the info field
            self.info
                .encode_unchecked(&mut buf[OHPL::INFO_FIELD.aligned_byte_range()]);

            // Encode the hop fields
            self.hops[0].encode_unchecked(&mut buf[OHPL::HOP_FIELD_1.aligned_byte_range()]);
            self.hops[1].encode_unchecked(&mut buf[OHPL::HOP_FIELD_2.aligned_byte_range()]);
        }

        OneHopPathLayout::SIZE_BYTES
    }
}
impl Model for OneHopPath {
    type ViewType = OneHopPathView;
}
impl TryFromModel for OneHopPathView {
    type ModelType = OneHopPath;
}
impl FromView for OneHopPath {
    type ViewType = OneHopPathView;

    #[inline]
    fn from_view(view: &Self::ViewType) -> Self {
        let info: InfoField = InfoField::from_view(view.info_field());
        let [hop1, hop2] = view.hop_fields();
        let hops = [HopField::from_view(hop1), HopField::from_view(hop2)];

        Self { info, hops }
    }
}

/// Support for [`proptest::arbitrary`].
#[cfg(feature = "proptest")]
pub mod ptest {
    use ::proptest::prelude::*;

    use super::*;

    /// Configuration for generating arbitrary [`OneHopPath`] values.
    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
    pub struct ArbitraryOneHopPathContext {
        // Not implemented yet, but would allow providing ForwardingKeys for generating valid MACs,
        // or even generating paths valid on a topology
    }

    impl Arbitrary for OneHopPath {
        type Parameters = ArbitraryOneHopPathContext;
        type Strategy = BoxedStrategy<Self>;

        fn arbitrary_with(_ctx: Self::Parameters) -> Self::Strategy {
            // For simplicity, we generate random values for the fields without ensuring valid MACs.
            // In a more advanced implementation, we could use the context to generate valid paths.
            (
                any::<InfoField>(),
                any::<HopField>(),
                any::<Option<HopField>>(),
            )
                .prop_map(|(info, hop1, hop2)| {
                    Self {
                        info,
                        hops: [hop1, hop2.unwrap_or_else(HopField::empty)],
                    }
                })
                .boxed()
        }
    }
}