diff --git a/crates/scion/Cargo.toml b/crates/scion/Cargo.toml index ca8bd64..4ce19e2 100644 --- a/crates/scion/Cargo.toml +++ b/crates/scion/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +serde = { version = "1.0.188", features = ["derive"] } +thiserror = "1.0.48" diff --git a/crates/scion/src/address.rs b/crates/scion/src/address.rs new file mode 100644 index 0000000..daff43c --- /dev/null +++ b/crates/scion/src/address.rs @@ -0,0 +1,24 @@ +use thiserror; + +mod asn; +pub use asn::Asn; + +mod isd; +pub use isd::Isd; + +mod ia; +pub use ia::IA; + +#[derive(Eq, PartialEq, Clone, Debug, thiserror::Error)] +pub enum AddressParseError { + #[error("AS number out of range, expected at most 2^48 - 1")] + AsnOutOfRange, + #[error("AS string contains a part that is not a 2-byte HEX")] + InvalidAsnPart(String), + #[error("invalid AS number string (expected format xxxx:xxxx:xxxx, found {0})")] + InvalidAsnString(String), + #[error("ISD number not parsable as u16")] + InvalidIsdString(String), + #[error("invalid string (expected format d-xxxx:xxxx:xxxx, found {0})")] + InvalidIaString(String), +} diff --git a/crates/scion/src/address/asn.rs b/crates/scion/src/address/asn.rs new file mode 100644 index 0000000..e49c934 --- /dev/null +++ b/crates/scion/src/address/asn.rs @@ -0,0 +1,193 @@ +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use super::AddressParseError; + +/// A SCION autonomous system (AS) number +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Asn(u64); + +impl Asn { + /// A SCION AS number representing the wildcard AS. + pub const WILDCARD: Self = Self(0); + /// The number of bits in a SCION AS number + pub const BITS: u32 = 48; + + const BITS_PER_PART: u32 = 16; + const MAX_VALUE: u64 = (1 << Self::BITS) - 1; + + /// Creates a new AS from a u64 value. + /// + /// # Panics + /// + /// This function panics if the provided id is greater than the maximum AS number, 2^48 - 1. + pub fn new(id: u64) -> Self { + Asn::try_from(id).expect("value within AS number range") + } + + /// Returns the AS number as a u64 integer. + pub fn as_u64(&self) -> u64 { + self.0 + } + + /// Return true for the special 'wildcard' AS number, 0. + pub fn is_wildcard(&self) -> bool { + self == &Self::WILDCARD + } +} + +impl Display for Asn { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.is_wildcard() { + return write!(f, "0"); + } + + for i in (0..(Asn::BITS / Asn::BITS_PER_PART)).rev() { + let asn_part = self.as_u64() >> (Asn::BITS_PER_PART * i) & u64::from(u16::MAX); + let separator = if i != 0 { ":" } else { "" }; + + write!(f, "{:x}{}", asn_part, separator)?; + } + + Ok(()) + } +} + +impl From for u64 { + fn from(value: Asn) -> Self { + value.as_u64() + } +} + +impl TryFrom for Asn { + type Error = AddressParseError; + + fn try_from(value: u64) -> Result { + if value > Asn::MAX_VALUE { + Err(AddressParseError::AsnOutOfRange) + } else { + Ok(Asn(value)) + } + } +} + +impl FromStr for Asn { + type Err = AddressParseError; + + fn from_str(asn_string: &str) -> Result { + if asn_string == "0" { + return Ok(Self::WILDCARD); + } + + let mut result = 0u64; + let mut n_parts = 0; + + for asn_part in asn_string.split(':') { + match u16::from_str_radix(asn_part, 16) { + Ok(value) => { + result <<= Asn::BITS_PER_PART; + result |= u64::from(value); + } + Err(_) => return Err(AddressParseError::InvalidAsnPart(asn_string.into())), + } + n_parts += 1; + } + + if n_parts != (Asn::BITS / Asn::BITS_PER_PART) { + return Err(AddressParseError::InvalidAsnString(asn_string.into())); + } + + // Should not panic as there are exactly 3 parts, each of 16 bits + Ok(Asn::new(result)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod parse { + use super::*; + + macro_rules! test_success { + ($name:ident, $input:expr, $expected:expr) => { + #[test] + fn $name() { + assert_eq!(Asn::from_str($input).unwrap(), $expected); + } + }; + } + + test_success!(zero, "0", Asn::WILDCARD); + test_success!(zero_with_colon, "0:0:0", Asn::WILDCARD); + test_success!(low_bit, "0:0:1", Asn(1)); + test_success!(high_bit, "1:0:0", Asn(0x000100000000)); + test_success!(max, "ffff:ffff:ffff", Asn(Asn::MAX_VALUE)); + + macro_rules! test_error { + ($name:ident, $input:expr, $expected:expr) => { + #[test] + fn $name() { + assert_eq!(Asn::from_str($input).unwrap_err(), $expected); + } + }; + } + + test_error!( + errs_on_only_colon, + ":", + AddressParseError::InvalidAsnPart(":".into()) + ); + test_error!( + errs_extra_colon, + "0:0:0:", + AddressParseError::InvalidAsnPart("0:0:0:".into()) + ); + test_error!( + errs_too_few, + "0:0", + AddressParseError::InvalidAsnString("0:0".into()) + ); + test_error!( + errs_invalid_part, + ":0:0", + AddressParseError::InvalidAsnPart(":0:0".into()) + ); + test_error!( + errs_out_of_range, + "10000:0:0", + AddressParseError::InvalidAsnPart("10000:0:0".into()) + ); + test_error!( + errs_out_of_range2, + "0:0:10000", + AddressParseError::InvalidAsnPart("0:0:10000".into()) + ); + test_error!( + errs_invalid_format, + "0:0x0:0", + AddressParseError::InvalidAsnPart("0:0x0:0".into()) + ); + } + + mod display { + use super::*; + + #[test] + fn large() { + assert_eq!(Asn(0xff00000000ab).to_string(), "ff00:0:ab"); + } + + #[test] + fn large_symmetric() { + assert_eq!(Asn(0x0001fcd10001).to_string(), "1:fcd1:1"); + } + + #[test] + fn max() { + assert_eq!(Asn(Asn::MAX_VALUE).to_string(), "ffff:ffff:ffff"); + } + } +} diff --git a/crates/scion/src/address/ia.rs b/crates/scion/src/address/ia.rs new file mode 100644 index 0000000..05ad1e2 --- /dev/null +++ b/crates/scion/src/address/ia.rs @@ -0,0 +1,207 @@ +use std::{ + fmt::{Debug, Display, Formatter}, + str::FromStr, +}; + +use serde::Deserialize; + +use super::{AddressParseError, Asn, Isd}; + +/// The combined ISD and AS identifier of a SCION AS. +#[derive(Copy, Clone, Eq, PartialEq, Deserialize, Hash)] +#[serde(try_from = "String")] +pub struct IA(u64); + +impl IA { + /// A SCION IA of the special wildcard IA, 0-0. + pub const WILDCARD: Self = Self(0); + + // /// Create a new identifier from a 64-bit value. + // /// + // /// The highest 16 bits should constitute the ISD number, and the lower + // /// 48 bits form the AS number. + // pub fn new(value: u64) -> Self { + // Self(value) + // } + + /// Construct a new identifier from ISD and AS identifiers. + pub fn new(isd: Isd, asn: Asn) -> Self { + Self(u64::from(isd.as_u16()) << Asn::BITS | asn.as_u64()) + } + + /// Return the ISD associated with this identifier. + pub fn isd(&self) -> Isd { + Isd::new(u16::try_from(self.0 >> Asn::BITS).expect("only the 16 high-order bits")) + } + + /// Return the AS number associated with this identifier. + pub fn asn(&self) -> Asn { + Asn::new(self.0 & 0xffff_ffff_ffff) + } + + /// Return true if either the ISD or AS numbers are wildcards + pub fn is_wildcard(&self) -> bool { + self.isd().is_wildcard() || self.asn().is_wildcard() + } + + /// Return the IA as a 64-bit integer. + /// + /// The highest 16 bits constitute the ISD number, and the lower 48 bits form the + /// AS number. + pub fn as_u64(&self) -> u64 { + self.0 + } +} + +impl Debug for IA { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("IA({:#018x})", self.0)) + } +} + +impl Display for IA { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}", self.isd(), self.asn()) + } +} + +impl FromStr for IA { + type Err = AddressParseError; + + fn from_str(string: &str) -> Result { + let n_separators = string.chars().filter(|c| *c == '-').take(2).count(); + if n_separators != 1 { + return Err(Self::Err::InvalidIaString(string.into())); + } + + if let Some((isd_str, asn_str)) = string.split_once('-') { + Ok(IA::new(Isd::from_str(isd_str)?, Asn::from_str(asn_str)?)) + } else { + Err(Self::Err::InvalidIaString(string.into())) + } + } +} + +impl TryFrom for IA { + type Error = AddressParseError; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + +impl From for u64 { + fn from(value: IA) -> Self { + value.as_u64() + } +} + +impl From for IA { + fn from(value: u64) -> Self { + IA(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::address::{Asn, Isd}; + + macro_rules! test_new_and_get { + ($name:ident, $ia:expr, $isd:expr, $asn:expr) => { + mod $name { + use super::*; + + #[test] + fn construct() { + assert_eq!($ia, IA::new($isd, $asn)); + } + + #[test] + fn get_isd() { + assert_eq!($isd, $ia.isd()); + } + + #[test] + fn get_asn() { + assert_eq!($asn, $ia.asn()); + } + } + }; + } + + test_new_and_get!(wildcard, IA(0), Isd::new(0), Asn::new(0)); + test_new_and_get!( + long, + IA(0x0001_ff00_0000_00ab), + Isd::new(1), + Asn::new(0xff00_0000_00ab) + ); + test_new_and_get!( + max_and_min, + IA(0xffff_0000_0000_0000), + Isd::new(0xffff), + Asn::new(0) + ); + test_new_and_get!( + min_and_max, + IA(0x0000_ffff_ffff_ffff), + Isd::new(0), + Asn::new(0xffff_ffff_ffff) + ); + + mod display { + use super::*; + + #[test] + fn simple() { + assert_eq!(IA(0x0001_ff00_0000_00ab).to_string(), "1-ff00:0:ab"); + } + + #[test] + fn wildcard() { + assert_eq!(IA(0).to_string(), "0-0"); + } + + #[test] + fn max_ia() { + assert_eq!( + IA(0xffff_ffff_ffff_ffff).to_string(), + "65535-ffff:ffff:ffff" + ); + } + } + + mod parse { + use super::*; + + macro_rules! test_success { + ($name:ident, $input:expr, $expected:expr) => { + #[test] + fn $name() { + assert_eq!(IA::from_str($input).unwrap(), $expected); + } + }; + } + + test_success!(max, "65535-ffff:ffff:ffff", IA(0xffff_ffff_ffff_ffff)); + test_success!(wildcard, "0-0", IA::WILDCARD); + test_success!(min_non_wildcard, "1-0:0:1", IA(0x0001_0000_0000_0001)); + + #[test] + fn invalid() { + assert_eq!( + IA::from_str("a-0:0:1").unwrap_err(), + AddressParseError::InvalidIsdString("a".into()) + ); + } + + #[test] + fn invalid_parts() { + assert_eq!( + IA::from_str("1-1-0:0:1").unwrap_err(), + AddressParseError::InvalidIaString("1-1-0:0:1".into()) + ); + } + } +} diff --git a/crates/scion/src/address/isd.rs b/crates/scion/src/address/isd.rs new file mode 100644 index 0000000..c217cd3 --- /dev/null +++ b/crates/scion/src/address/isd.rs @@ -0,0 +1,56 @@ +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use super::AddressParseError; + +/// Identifier of a SCION Isolation Domain +/// +/// See formatting and allocations here: +/// https://github.com/scionproto/scion/wiki/ISD-and-AS-numbering#isd-numbers +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Isd(u16); + +impl Isd { + /// The SCION ISD number representing the wildcard ISD. + pub const WILDCARD: Self = Self(0); + /// The number of bits in a SCION ISD number + pub const BITS: u32 = u16::BITS; + + /// Create a new ISD from a 16-bit value. + pub fn new(id: u16) -> Self { + Self(id) + } + + /// Return the identifier as a 16-bit value. + pub fn as_u16(&self) -> u16 { + self.0 + } + + // Return true for the special 'wildcard' AS number + pub fn is_wildcard(&self) -> bool { + self == &Self::WILDCARD + } +} + +impl Display for Isd { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for Isd { + type Err = AddressParseError; + + /// Parses an ISD from a decimal string. + /// + /// ISD 0 is parsed without any errors. + fn from_str(string: &str) -> Result { + if let Ok(value) = u16::from_str(string) { + Ok(Isd::new(value)) + } else { + Err(Self::Err::InvalidIsdString(string.into())) + } + } +} diff --git a/crates/scion/src/lib.rs b/crates/scion/src/lib.rs index 8b13789..f74634c 100644 --- a/crates/scion/src/lib.rs +++ b/crates/scion/src/lib.rs @@ -1 +1 @@ - +pub mod address;