diff --git a/src/new/base/mod.rs b/src/new/base/mod.rs index 417c4a7e4..a9bc2f648 100644 --- a/src/new/base/mod.rs +++ b/src/new/base/mod.rs @@ -202,7 +202,7 @@ mod charstr; pub use charstr::{CharStr, CharStrBuf, CharStrParseError}; mod serial; -pub use serial::Serial; +pub use serial::{SeqNumberU32, SoaSerial, Timestamp}; //--- Wire format @@ -255,9 +255,6 @@ pub mod compat { #[deprecated = "use 'crate::new::base::TTL' instead."] pub use record::Ttl; - #[deprecated = "use 'crate::new::base::Serial' instead."] - pub use serial::Serial; - pub mod header { #[deprecated = "use 'crate::new::base::HeaderFlags' instead."] pub use crate::new::base::HeaderFlags as Flags; @@ -355,9 +352,4 @@ pub mod compat { #[deprecated = "use 'crate::new::base::TTL' instead."] pub use crate::new::base::TTL as Ttl; } - - pub mod serial { - #[deprecated = "use 'crate::new::base::Serial' instead."] - pub use crate::new::base::Serial; - } } diff --git a/src/new/base/serial.rs b/src/new/base/serial.rs index f7fe98787..ea699d6a1 100644 --- a/src/new/base/serial.rs +++ b/src/new/base/serial.rs @@ -1,23 +1,83 @@ -//! Serial number arithmetic. +//! Numeric types using RFC 1982 Sequence Space Arithmetic. //! -//! See [RFC 1982](https://datatracker.ietf.org/doc/html/rfc1982). +//! This module includes types which work in the numberspace of 2^32. The +//! implementation reference is [RFC1982] which defines the "sequence space +//! arithmetic". +//! +//! The module contains three types: +//! +//! - [`SeqNumberU32`] +//! - [`SoaSerial`] +//! - [`Timestamp`] +//! +//! [`SeqNumberU32`] implements the arithmetics defined in [RFC1982] and +//! functions as the underlying data structure for the other types. +//! +//! [`SoaSerial`], as the name suggests, is used as the version number in the +//! context of the DNS zone. [`SoaSerial`] is primarily used in the serial +//! field of the [`Soa`] record type. Additionally [`SoaSerial`] is used in +//! [`ZoneMD`]. +//! +//! [`Timestamp`] is used to represent time since Unix Epoch modulo 2^32. +//! Therefore the type is not an accurate time representation but is rather +//! used to show relative differences in time. Apart from the usage in the +//! [`Rrsig`] record, the type is also used in the EDNS [`Cookie`]. +//! +//! [`Cookie`]: crate::new::edns::Cookie +//! [`Rrsig`]: crate::new::rdata::Rrsig +//! [`Soa`]: crate::new::rdata::Soa +//! [`ZoneMD`]: crate::new::rdata::ZoneMD +//! +//! [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982 -use core::{cmp::Ordering, fmt}; +// This module includes all serial types because they have a logical +// connection and they are used throughout the crate. Therefore it is +// difficult to put them into one specific location together or each one +// separate. + +use core::cmp::Ordering; +use core::fmt; +#[cfg(feature = "std")] +// Duration is only used in `std` functions. +use core::time::Duration; +#[cfg(feature = "std")] +use std::time::{SystemTime, UNIX_EPOCH}; use domain_macros::*; use super::wire::U32; -//----------- Serial --------------------------------------------------------- +/// 2^32 is used to simplify expressions. +const POW_2_32: u64 = 1 << 32; + +//----------- SoaSerial ------------------------------------------------------ -/// Serial number arithmetic. +/// Version number of a DNS zone. +/// +/// The SOA serial number declares the version number of the associated zone. +/// It is used to determine the most recent revision of a zone. /// -/// [`Serial`] implements the "Serial number arithmetic" defined in [RFC1982] -/// with a `SERIAL_BITS` value of 32. +/// [`SoaSerial`] commonly follows one of the following strategies: /// -/// [`Serial`] should not be used interchangably or be confused with the SOA -/// Serial Number, which is a [`Serial`] but not the sole user of "Serial -/// number arithmetic". +/// - Counter, it starts at 1 and on each change the number gets increased by +/// one. +/// - Seconds since Unix Epoch, when the zone is changed the number gets set +/// to the current number of seconds since Unix Epoch. +/// - Date including counter, on change the number gets set to the current +/// date in the format (`YYYYMMDDXX`). +/// +/// The mathematical operations are done using [`SeqNumberU32`]. The number +/// adheres to the mathematical properties of the "Serial Number Arithmetics" +/// see [RFC1982] with an unsigned 32 bit integer. +/// +/// Basic operations performed with a [`SoaSerial`]. +/// ``` +/// # use domain::new::base::SoaSerial; +/// let soa_serial = SoaSerial::new(u32::MAX); +/// let soa_serial_incremented = soa_serial.increment(); +/// let value: u32 = soa_serial_incremented.get(); +/// assert_eq!(value, 0); +/// ``` /// /// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982 #[derive( @@ -36,74 +96,526 @@ use super::wire::U32; UnsizedCopy, )] #[repr(transparent)] -pub struct Serial(pub U32); +pub struct SoaSerial(SeqNumberU32); + +impl SoaSerial { + /// Construct a new [`SoaSerial`]. + #[must_use] + pub const fn new(value: u32) -> Self { + SoaSerial(SeqNumberU32::new(value)) + } -//--- Construction + /// The raw [`u32`] underlying this [`SoaSerial`]. + #[must_use] + pub const fn get(self) -> u32 { + self.0.get() + } -impl Serial { - /// Construct a new [`Serial`] + /// Measure system time since Unix Epoch modulo 2^32. + /// + /// To avoid the possibility of a panic, the function handles the + /// possibility of a [`SystemTime::now()`] before [`UNIX_EPOCH`]. If the + /// current time is before the [`UNIX_EPOCH`] modulo operations are + /// applied to bring the value into the positive space. + /// + /// ``` + /// # use domain::new::base::SoaSerial; + /// # use std::time::{SystemTime, UNIX_EPOCH}; + /// let now = SoaSerial::now(); + /// let system_now = (UNIX_EPOCH.elapsed().unwrap().as_secs()) as u32; + /// assert!(now.get().wrapping_sub(system_now) <= 1); + /// ``` + #[cfg(feature = "std")] + #[must_use] + pub fn now() -> SoaSerial { + SoaSerial(SeqNumberU32::now()) + } + + /// Increase [`SoaSerial`] by 1. + /// + /// Further details in [`SeqNumberU32::increment()`]. + #[must_use] + pub const fn increment(self) -> Self { + SoaSerial(self.0.increment()) + } +} + +/// Comparison is forwarded to the underlying [`SeqNumberU32`]. +/// +/// Further details in the `PartialOrd` implementation of [`SeqNumberU32`]. +impl PartialOrd for SoaSerial { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} + +/// Format [`SoaSerial`] in a human readable way. +/// +/// All mentioned versioning strategies do not need special formatting or +/// computation. The string representation is therefore equivalent to the +/// decimal number representation. +/// +/// ``` +/// # use domain::new::base::SoaSerial; +/// assert_eq!(format!("{}", SoaSerial::new(42)), "42"); +/// ``` +impl fmt::Display for SoaSerial { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.get()) + } +} + +// --- From implementations + +/// The raw [`u32`] underlying this [`SoaSerial`]. +/// +/// Equivalent to [`SoaSerial::get()`]. +/// +/// ``` +/// # use domain::new::base::SoaSerial; +/// assert_eq!(42u32, SoaSerial::new(42).into()); +/// ``` +impl From for u32 { + fn from(value: SoaSerial) -> u32 { + value.get() + } +} + +/// Construct [`SoaSerial`] from [`u32`]. +/// +/// Equivalent to [`SoaSerial::new()`]. +/// +/// ``` +/// # use domain::new::base::SoaSerial; +/// assert_eq!(SoaSerial::new(42), 42.into()); +/// ``` +impl From for SoaSerial { + fn from(value: u32) -> Self { + SoaSerial::new(value) + } +} + +//----------- Timestamp --------------------------------------------------- + +/// Seconds since Unix Epoch modulo 2^32. +/// +/// [`Timestamp`] stores the seconds since Unix Epoch modulo 2^32. It is used +/// in [`Rrsig`] to keep track of `inception` and `expiration` time and in the +/// EDNS [`Cookie::timestamp()`]. +/// +/// The imperfect timekeeping due to the limited number space does not matter +/// too much in those cases. But the limitations have to be kept in mind. +/// More details about that can be found in [Section 3.1.5 of RFC4034] +/// "Signature Expiration and Inception Fields" and [Section 4.3 of RFC9018]. +/// +/// [`Timestamp`] can be constructed using a [`jiff::Timestamp`] using a +/// [`Timestamp::from()`]. +/// +/// The mathematical operations are done using [`SeqNumberU32`]. The number +/// adheres to the mathematical properties of the "Serial Number Arithmetics" +/// see [RFC1982] with an unsigned 32 bit integer. +/// +/// Basic operations performed with a [`Timestamp`]. +/// ``` +/// # use domain::new::base::Timestamp; +/// let timestamp: Timestamp = Timestamp::new(42); +/// let value: u32 = timestamp.as_seconds(); +/// assert_eq!(value, 42); +/// ``` +/// +/// [`Cookie::timestamp()`]: crate::new::edns::Cookie::timestamp() +/// [`Rrsig`]: crate::new::rdata::Rrsig +/// +/// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982 +/// [Section 3.1.5 of RFC4034]: https://datatracker.ietf.org/doc/html/rfc4034#section-3.1.5 +/// [Section 4.3 of RFC9018]: https://datatracker.ietf.org/doc/html/rfc9018#name-the-timestamp-sub-field +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct Timestamp(SeqNumberU32); + +impl Timestamp { + /// Construct a new [`Timestamp`]. #[must_use] pub const fn new(value: u32) -> Self { - Serial(U32::new(value)) + Timestamp(SeqNumberU32::new(value)) } - /// Get [`u32`] value of [`Serial`] + // This type has a specific unit, therefore the `get()` function was + // renamed in favor of `as_seconds`. + + /// Underlying seconds since Unix Epoch modulo 2^32. #[must_use] - pub const fn get(&self) -> u32 { + pub const fn as_seconds(self) -> u32 { self.0.get() } - /// Measure the current time (in seconds) in serial number space. + /// Measure system time since Unix Epoch modulo 2^32. + /// + /// To avoid the possibility of a panic, the function handles the + /// possibility of a [`SystemTime::now()`] before [`UNIX_EPOCH`]. If the + /// current time is before the [`UNIX_EPOCH`] modulo operations are applied + /// to bring the value into the positive space. + /// + /// ``` + /// # use domain::new::base::Timestamp; + /// # use std::time::{SystemTime, UNIX_EPOCH}; + /// let now = Timestamp::now(); + /// let system_now = (UNIX_EPOCH.elapsed().unwrap().as_secs()) as u32; + /// assert!(now.as_seconds().wrapping_sub(system_now) <= 1); + /// ``` + #[cfg(feature = "std")] + #[must_use] + pub fn now() -> Self { + Timestamp(SeqNumberU32::now()) + } + + /// Convert this [`Timestamp`] into [`SystemTime`] close to a reference + /// time. + /// + /// This method may be used to sort [`Timestamp`] values or to display a + /// [`Timestamp`] in a date and time format. + /// + /// The returned [`SystemTime`] meets the following requirements: + /// + /// 1) The returned [`SystemTime`] value has a duration since + /// [`UNIX_EPOCH`] that modulo `2**32` is equal to this + /// [`Timestamp`] value. + /// 2) The time difference between the [`SystemTime`] value and the + /// reference time fits in an [`i32`]. + /// + /// Because this [`Timestamp`] might not be representable by + /// [`SystemTime`] the function returns an [`Option`] over [`SystemTime`]. + /// `None` is returned if the Timestamp is smaller or bigger than + /// [`SystemTime::MIN`] or [`SystemTime::MAX`] respectively. + #[must_use] #[cfg(feature = "std")] - pub fn unix_time() -> Self { - use std::time::SystemTime; + pub fn to_system_time(self, reference: SystemTime) -> Option { + // Timestamp is a 32-bit value. We cannot just add UNIX_EPOCH because + // the timestamp may be too far in the future. We may have to add n * + // 2**32 for some unknown value of n. + // + // Epoch Reference Future + // |--------------------------------------- | -----------------------| + // [ i32 range ] + // + // [ POW_2_32 ][ POW_2_32 ][ timestamp ] + // + // The goal is to find a [`SystemTime`] which is inside the i32 range. - let time = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("The current time is after the Unix Epoch"); - Self::from(time.as_secs() as u32) + let mut timestamp_secs: i128 = self.as_seconds().into(); + let reference_secs: i128 = match reference.duration_since(UNIX_EPOCH) + { + Ok(secs) => secs.as_secs().into(), + Err(e) => -(e.duration().as_secs() as i128), + }; + + // Apply the offset that the `reference_secs` has to the UNIX_EPOCH, + // but without the lower 32-bit details. After that the value could + // still to far away but it is around in the right region. + timestamp_secs += + (reference_secs / POW_2_32 as i128) * POW_2_32 as i128; + + // The values could still be to far apart to fit into an i32 range. + // Therefore an addition or subtraction might be necessary. + if timestamp_secs - reference_secs < i32::MIN as i128 { + timestamp_secs += POW_2_32 as i128; + } else if timestamp_secs - reference_secs > i32::MAX as i128 { + timestamp_secs -= POW_2_32 as i128; + } + + // Now that the timestamp has been calculated we have to check if it + // is a negative or positive number and apply the correct function to + // the UNIX_EPOCH. + if timestamp_secs < 0 { + UNIX_EPOCH.checked_sub(Duration::from_secs( + timestamp_secs.unsigned_abs() as u64, + )) + } else { + UNIX_EPOCH.checked_add(Duration::from_secs(timestamp_secs as u64)) + } } } -//--- Interaction +/// Comparison is forwarded to the underlying [`SeqNumberU32`]. +/// +/// Further details in the `PartialOrd` implementation of [`SeqNumberU32`]. +impl PartialOrd for Timestamp { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} -impl Serial { - /// Increment this by a non-negative number. +/// Format [`Timestamp`] in a human readable way. +/// +/// This implementation displays [`Timestamp`] as the elapsed second stored. +/// +/// [`Timestamp`] is commonly displayed in one of two ways. The first, simple +/// way is to present it as the elapsed seconds since Unix Epoch modulo +/// 2^32 it stores. The more complex way to display [`Timestamp`] would be to +/// compute the precise date `self` refers to and display this date in the +/// form `YYYYMMDDHHmmSS`. +/// +/// The first option is implemented because it requires less computation and +/// is more generally usable. +/// +/// More details about the display variations can be found in [Section 3.2 of +/// RFC4034]. +/// +/// ``` +/// # use domain::new::base::Timestamp; +/// assert_eq!(format!("{}", Timestamp::new(42)), "42"); +/// ``` +/// +/// [Section 3.2 of RFC4034]: https://datatracker.ietf.org/doc/html/rfc4034#section-3.2 +impl fmt::Display for Timestamp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_seconds()) + } +} + +// --- From Implementations + +/// Construct [`Timestamp`] from [`u32`]. +/// +/// Equivalent to [`Timestamp::as_seconds()`]. +/// +/// ``` +/// # use domain::new::base::Timestamp; +/// assert_eq!(42u32, Timestamp::new(42).into()); +/// ``` +impl From for u32 { + fn from(value: Timestamp) -> u32 { + value.as_seconds() + } +} + +/// The raw [`u32`] underlying this [`Timestamp`]. +/// +/// Equivalent to [`Timestamp::new()`]. +/// +/// ``` +/// # use domain::new::base::Timestamp; +/// assert_eq!(Timestamp::new(42), 42.into()); +/// ``` +impl From for Timestamp { + fn from(value: u32) -> Self { + Timestamp::new(value) + } +} + +/// Construct [`Timestamp`] from a [`jiff::Timestamp`]. +/// +/// Precision is lost during the conversion because of the smaller storage +/// primitive. +/// ``` +/// # use domain::new::base::Timestamp; +/// let ts = Timestamp::from( +/// jiff::Timestamp::new(u32::MAX as i64 + 10, 0).unwrap(), +/// ); +/// assert_eq!(ts.as_seconds(), 9); +/// ``` +impl From for Timestamp { + fn from(value: jiff::Timestamp) -> Self { + Timestamp::new(value.as_second().rem_euclid(POW_2_32 as i64) as u32) + } +} + +// --- Compatibility for old base +// The following implementations are implemented to make the transition easier +// to new base but will be deprecated in the near future. + +impl Timestamp { + /// Returns the [`Timestamp`] as a raw integer. + #[must_use] + pub fn into_int(self) -> u32 { + self.as_seconds() + } +} + +impl From for crate::rdata::dnssec::Timestamp { + fn from(ts: Timestamp) -> Self { + crate::rdata::dnssec::Timestamp::from(ts.as_seconds()) + } +} + +//----------- SeqNumberU32 --------------------------------------------------- + +/// 32-bit unsigned integer using [RFC1982] sequence number space arithmetic. +/// +/// This type implements the mathematical properties defined in [RFC1982]. +/// Section 3 in [RFC1982] defines that there are only two possible operations +/// on a Serial; addition and comparison. +/// +/// The type is used as a backend for [`SoaSerial`] and [`Timestamp`]. +/// +/// ``` +/// # use domain::new::base::SeqNumberU32; +/// let seq_number: SeqNumberU32 = SeqNumberU32::new(u32::MAX).increment(); +/// let value: u32 = seq_number.get(); +/// assert_eq!(value, 0); +/// ``` +/// +/// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982 +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct SeqNumberU32(U32); + +impl SeqNumberU32 { + /// Construct a new [`SeqNumberU32`]. + #[must_use] + pub const fn new(value: u32) -> Self { + SeqNumberU32(U32::new(value)) + } + + /// The raw [`u32`] underlying this [`SeqNumberU32`]. + #[must_use] + pub const fn get(self) -> u32 { + self.0.get() + } +} + +impl SeqNumberU32 { + /// Increment [`SeqNumberU32`] by 1. /// - /// The number must be in the range `[0, 2^31 - 1]`. An [`i32`] is used - /// instead of a [`u32`] because it is easier to understand and implement - /// a non-negative check versus the upper range check. + /// Incrementing [`SeqNumberU32`] by more than one serves little purpose + /// and might be achieved easier by constructing it from a primitive + /// number. /// - /// Section 7 in [RFC1982] states, in particular for the SOA Serial - /// Number, but for any Serial number using a "SERIAL_BITS" value of 32: - /// "The maximum defined increment is 2147483647 (2^31 - 1)." + /// [Section 3.1 of RFC1982] states that the maximum allowed increment is + /// `(2^31)-1`. A function would therefore need to verify that the + /// increment is smaller than that and panic/fail if this is not + /// satisfied. To avoid the potential risk of failure, the type only + /// offers incrementation by 1. /// - /// # Panics + /// [Section 3.1 of RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982#section-3.1 + #[must_use] + pub const fn increment(self) -> Self { + SeqNumberU32::new(self.0.get().wrapping_add(1)) + } + + /// Measure system time since Unix Epoch modulo 2^32. + /// + /// This function is private because the functionality is not suitable for + /// the type because [`SeqNumberU32`] has no associated time semantics, + /// only its descendants [`SoaSerial`] and [`Timestamp`] do. Therefore the + /// function logic is in [`SeqNumberU32`] to avoid code duplication. /// - /// Panics if the number is negative. + /// This function is passively tested by the doc tests in + /// [`SoaSerial::now()`] and [`Timestamp::now()`]. /// - /// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982#section-7 - pub fn inc(self, num: i32) -> Self { - assert!(num >= 0, "Cannot subtract from a `Serial`"); - self.0.get().wrapping_add_signed(num).into() + /// To prevent the function from panicking, it handles the possibility of + /// a [`SystemTime::now()`] before [`UNIX_EPOCH`]. In that case the value + /// is brought into the positive space through a modulo operation. Similar + /// to the example below, where `62` is the amount of seconds before + /// [`UNIX_EPOCH`] and `32` is representing the 32-Bit number space. + /// + /// ``` + /// debug_assert_eq!(32 - (62 % 32), 2); + /// ``` + #[cfg(feature = "std")] + #[must_use] + fn now() -> Self { + let now = SystemTime::now(); + let diff = match now.duration_since(UNIX_EPOCH) { + // Technically the `% POW_2_32` does the same as `diff as u32` + // (see return expression) but it is done here explicitly. + Ok(secs_after_epoch) => secs_after_epoch.as_secs() % POW_2_32, + Err(secs_before_epoch) => { + POW_2_32 - (secs_before_epoch.duration().as_secs() % POW_2_32) + } + }; + SeqNumberU32::new(diff as u32) } } //--- Ordering -impl PartialOrd for Serial { - /// The comparison of Serial Number values is defined in Section 3.2 - /// [RFC1982]. +impl PartialOrd for SeqNumberU32 { + /// The comparison of serial number values is defined in [Section 3.2 of + /// RFC1982]. /// - /// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982#section-3.2 + /// The comparison is special because the sequence number might wrap + /// around after reaching the maximum. The maximum difference between two + /// numbers is limited to less than 2^31. If two numbers are exactly 2^31 + /// apart the order is undefined. + /// + /// [Section 3.2 of RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982#section-3.2 + /// ``` + /// # use domain::new::base::SeqNumberU32; + /// # use std::cmp::Ordering; + /// // The numbers are exactly 2^31 apart, this is not defined. + /// assert_eq!( + /// SeqNumberU32::partial_cmp(&0.into(), &(1<<31).into()), + /// None + /// ); + /// // Simple, both numbers are equal. + /// assert_eq!( + /// SeqNumberU32::partial_cmp(&42.into(), &42.into()), + /// Some(Ordering::Equal) + /// ); + /// + /// // The left number is smaller and they are less than 2^31 apart. + /// assert_eq!( + /// SeqNumberU32::partial_cmp(&42.into(), &43.into()), + /// Some(Ordering::Less) + /// ); + /// + /// // This is special; the left number is numerically speaking smaller, + /// // but because the numbers are further apart than 2^31 it is assumed + /// // that the order is swapped and the left number is actually bigger + /// // because it has wrapped around. + /// assert_eq!( + /// SeqNumberU32::partial_cmp(&42.into(), &u32::MAX.into()), + /// Some(Ordering::Greater) + /// ); + /// ``` + // None -> this.abs_diff(other) == 2^31 + // Some(Ordering::Equal) -> this == other + // Some(Ordering::Less) -> this < other and (other - this) < 2^31 + // // 1 < 10 and 10 - 1 < 2^31 + // or + // this > other and (this - other) > 2^31 + // // u32::MAX > 1 and u32::MAX - 1 > 2^31 + // Some(Ordering::Greater) -> this > other and (this - other) < 2^31 + // // 10 > 1 and 10 - 1 < 2^31 + // or + // this < other and (this - other) > 2^31 + // // 10 < u32::MAX and u32::MAX - 1 > 2^31 fn partial_cmp(&self, other: &Self) -> Option { let (lhs, rhs) = (self.0.get(), other.0.get()); - if lhs == rhs { Some(Ordering::Equal) - } else if lhs.abs_diff(rhs) == 1 << 31 { + } else if lhs.abs_diff(rhs) == (1 << 31) { None - } else if (lhs < rhs) ^ (lhs.abs_diff(rhs) > (1 << 31)) { + } else if (lhs < rhs) == (lhs.abs_diff(rhs) < (1 << 31)) { Some(Ordering::Less) } else { Some(Ordering::Greater) @@ -111,46 +623,235 @@ impl PartialOrd for Serial { } } -//--- Conversion to and from native integer types +// --- From implementations -impl From for Serial { - fn from(value: u32) -> Self { - Self(U32::new(value)) - } -} - -impl From for u32 { - fn from(value: Serial) -> Self { - value.0.get() +/// Construct [`SeqNumberU32`] from [`u32`]. +/// +/// Equivalent to [`SeqNumberU32::get()`]. +/// +/// ``` +/// # use domain::new::base::SeqNumberU32; +/// assert_eq!(42u32, SeqNumberU32::new(42).into()); +/// ``` +impl From for u32 { + fn from(value: SeqNumberU32) -> u32 { + value.get() } } -//--- Formatting - -impl fmt::Display for Serial { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.get().fmt(f) +/// The raw [`u32`] underlying this [`SeqNumberU32`]. +/// +/// Equivalent to [`SeqNumberU32::new()`]. +/// +/// ``` +/// # use domain::new::base::SeqNumberU32; +/// assert_eq!(SeqNumberU32::new(42), 42.into()); +/// ``` +impl From for SeqNumberU32 { + fn from(value: u32) -> Self { + SeqNumberU32::new(value) } } -//============ Tests ========================================================= - #[cfg(test)] -mod test { - use super::Serial; +mod serial_test { + use core::{cmp::Ordering, time::Duration}; + use std::{println, time::UNIX_EPOCH}; + + use super::{SeqNumberU32, SoaSerial, Timestamp}; #[test] fn comparisons() { // TODO: Use property-based testing. - assert!(Serial::from(u32::MAX) > Serial::from(u32::MAX / 2 + 1)); - assert!(Serial::from(0) > Serial::from(u32::MAX)); - assert!(Serial::from(1) > Serial::from(0)); + assert!( + SeqNumberU32::from(u32::MAX) + > SeqNumberU32::from(u32::MAX / 2 + 1) + ); + assert!(SeqNumberU32::from(0) > SeqNumberU32::from(u32::MAX)); + assert!(SeqNumberU32::from(1) > SeqNumberU32::from(0)); + + assert!( + SoaSerial::from(u32::MAX) > SoaSerial::from(u32::MAX / 2 + 1) + ); + assert!(SoaSerial::from(0) > SoaSerial::from(u32::MAX)); + assert!(SoaSerial::from(1) > SoaSerial::from(0)); + + assert!( + Timestamp::from(u32::MAX) > Timestamp::from(u32::MAX / 2 + 1) + ); + assert!(Timestamp::from(0) > Timestamp::from(u32::MAX)); + assert!(Timestamp::from(1) > Timestamp::from(0)); + + assert_eq!( + SeqNumberU32::new(0) + .partial_cmp(&SeqNumberU32::new((1 << 31) - 1)), + Some(Ordering::Less) + ); + assert_eq!( + SeqNumberU32::new(0) + .partial_cmp(&SeqNumberU32::new((1 << 31) + 1)), + Some(Ordering::Greater) + ); + + assert_eq!( + SeqNumberU32::new(0).partial_cmp(&SeqNumberU32::new(1 << 31)), + None + ); } #[test] fn operations() { // TODO: Use property-based testing. - assert_eq!(u32::from(Serial::from(1).inc(1)), 2); - assert_eq!(u32::from(Serial::from(u32::MAX).inc(1)), 0); + assert_eq!(u32::from(SeqNumberU32::from(1).increment()), 2); + assert_eq!(u32::from(SeqNumberU32::from(u32::MAX).increment()), 0); + + assert_eq!(u32::from(SoaSerial::from(1).increment()), 2); + assert_eq!(u32::from(SoaSerial::from(u32::MAX).increment()), 0); + } + + #[test] + fn test_to_system_time() { + #[derive(Debug)] + struct Params { + ts: u32, + ref_ts: i128, + res: i128, + } + let tests = alloc::vec![ + // Simple cases, ts and ref_ts mod 2**32 are within 2*31-1. + // First ts less than ref_ts mod 2**32. + Params { + ts: 0x0000_0000, + ref_ts: 0x1_7fff_ffff, + res: 0x1_0000_0000, + }, + Params { + ts: 0x7fff_ffff, + ref_ts: 0x1_8000_0000, + res: 0x1_7fff_ffff, + }, + Params { + ts: 0x8000_0000, + ref_ts: 0x1_ffff_ffff, + res: 0x1_8000_0000, + }, + // Then ts larger than ref_ts mod 2**32. + Params { + ts: 0x7fff_ffff, + ref_ts: 0x1_0000_0000, + res: 0x1_7fff_ffff, + }, + Params { + ts: 0x8000_0000, + ref_ts: 0x1_7fff_ffff, + res: 0x1_8000_0000, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x1_8000_0000, + res: 0x1_ffff_ffff, + }, + // Next, cases where the difference between ts and ref_ts mod 2**32 + // are at least 2**31+1. + Params { + ts: 0x0000_0000, + ref_ts: 0x1_8000_0001, + res: 0x2_0000_0000, + }, + Params { + ts: 0x7fff_fffe, + ref_ts: 0x1_ffff_ffff, + res: 0x2_7fff_fffe, + }, + Params { + ts: 0x8000_0001, + ref_ts: 0x1_0000_0000, + res: 0x0_8000_0001, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x1_7fff_fffe, + res: 0x0_ffff_ffff, + }, + // Test cases where the difference is exactly 2**31. + Params { + ts: 0x0000_0000, + ref_ts: 0x1_8000_0000, + res: 0x1_0000_0000, + }, + Params { + ts: 0x7fff_ffff, + ref_ts: 0x1_ffff_ffff, + res: 0x1_7fff_ffff, + }, + Params { + ts: 0x8000_0000, + ref_ts: 0x1_0000_0000, + res: 0x0_8000_0000, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x1_7fff_ffff, + res: 0x0_ffff_ffff, + }, + // Special case: ERA 0. We *want* numbers before UNIX_EPOCH + Params { + ts: 0x8000_0001, + ref_ts: 0x0_0000_0000, + res: -0x7FFF_FFFF // 0x0_8000_0001, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x0_7fff_fffe, + res: -1 // 0x0_ffff_ffff, + }, + Params { + ts: 0x8000_0000, + ref_ts: 0x0_0000_0000, + res: -0x0_8000_0000 // 0x0_8000_0000, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x0_7fff_ffff, + res: -1 // 0x0_ffff_ffff, + }, + Params { + ts: 1, + ref_ts: -1, + res: 1 + }, + Params { + ts: 1, + ref_ts: -0x1_0000_0000, // 2^32 -1, + res: -0x1_0000_0000 + 1 + }, + Params { + ts: 1 + 0x8000_0000, + ref_ts: -0x1_0000_0000, // 2^32 -1, + res: -0x2_0000_0000 + (1 + 0x8000_0000) + }, + ]; + + for t in tests { + println!("Test {:?}", t); + let ts = Timestamp::new(t.ts); + let ref_ts = if t.ref_ts < 0 { + UNIX_EPOCH + .checked_sub(Duration::from_secs( + t.ref_ts.unsigned_abs() as u64 + )) + .unwrap() + } else { + UNIX_EPOCH + .checked_add(Duration::from_secs(t.ref_ts as u64)) + .unwrap() + }; + let res = ts.to_system_time(ref_ts).unwrap(); + let res = match res.duration_since(UNIX_EPOCH) { + Ok(o) => o.as_secs() as i128, + Err(e) => -(e.duration().as_secs() as i128), + }; + assert_eq!(res, t.res); + } } } diff --git a/src/new/edns/cookie.rs b/src/new/edns/cookie.rs index b86e584a0..8eff6d529 100644 --- a/src/new/edns/cookie.rs +++ b/src/new/edns/cookie.rs @@ -14,7 +14,7 @@ use core::ops::Range; use crate::{ new::base::{ - Serial, + Timestamp, wire::{ AsBytes, BuildBytes, ParseBytes, ParseBytesZC, SplitBytes, SplitBytesZC, @@ -90,7 +90,7 @@ impl ClientCookie { bytes = [1, 0, 0, 0].build_bytes(bytes)?; hasher.write(&[1, 0, 0, 0]); - let timestamp = Serial::unix_time(); + let timestamp = Timestamp::now(); bytes = timestamp.build_bytes(bytes)?; hasher.write(timestamp.as_bytes()); @@ -152,7 +152,7 @@ pub struct Cookie { reserved: [u8; 3], /// When this cookie was made. - timestamp: Serial, + timestamp: Timestamp, /// The hash of this cookie. hash: [u8], @@ -183,7 +183,7 @@ impl Cookie { /// the 4-byte timestamp of the cookie is returned. /// /// [RFC 9018]: https://datatracker.ietf.org/doc/html/rfc9018 - pub fn timestamp(&self) -> Serial { + pub fn timestamp(&self) -> Timestamp { self.timestamp } } @@ -206,7 +206,7 @@ impl Cookie { &self, addr: IpAddr, secret: &[u8; 16], - validity: Range, + validity: Range, ) -> Result<(), CookieError> { use core::hash::Hasher; diff --git a/src/new/rdata/basic/soa.rs b/src/new/rdata/basic/soa.rs index 86eb2b3b2..28a10278c 100644 --- a/src/new/rdata/basic/soa.rs +++ b/src/new/rdata/basic/soa.rs @@ -10,7 +10,7 @@ use crate::new::base::parse::{ }; use crate::new::base::{ CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, - Serial, wire::*, + SoaSerial, wire::*, }; //----------- Soa ------------------------------------------------------------ @@ -163,11 +163,11 @@ pub struct Soa { /// number increases (by a relatively small value) upon every change, any /// strategy is valid. /// - /// This field is represented using [`Serial`], which provides special + /// This field is represented using [`SoaSerial`], which provides special /// "sequence space arithmetic". This ensures that ordering comparisons /// are well-defined even if the number overflows modulo `2^32`. See its /// documentation for more information. - pub serial: Serial, + pub serial: SoaSerial, /// The number of seconds to wait until refreshing the zone. /// diff --git a/src/new/rdata/dnssec/rrsig.rs b/src/new/rdata/dnssec/rrsig.rs index c2115d5f5..6d717489d 100644 --- a/src/new/rdata/dnssec/rrsig.rs +++ b/src/new/rdata/dnssec/rrsig.rs @@ -1,20 +1,13 @@ //! The RRSIG record data type. #[cfg(feature = "std")] -use core::cmp; use core::cmp::Ordering; -use core::fmt; -#[cfg(feature = "std")] -use core::time::Duration; -#[cfg(feature = "std")] -use std::time::{SystemTime, UNIX_EPOCH}; - use domain_macros::*; use crate::new::base::build::BuildInMessage; use crate::new::base::name::{CanonicalName, Name, NameCompressor}; use crate::new::base::wire::{AsBytes, BuildBytes, TruncationError, U16}; -use crate::new::base::{CanonicalRecordData, RType, Serial, TTL}; +use crate::new::base::{CanonicalRecordData, RType, TTL, Timestamp}; use super::SecAlg; @@ -36,10 +29,10 @@ pub struct Rrsig<'a> { pub ttl: TTL, /// The point in time when the signature expires. - pub expiration: Serial, + pub expiration: Timestamp, /// The point in time when the signature was created. - pub inception: Serial, + pub inception: Timestamp, /// The key tag of the key used to make the signature. pub keytag: U16, @@ -133,243 +126,6 @@ impl Rrsig<'_> { /// Return the expiration time of the signature. pub fn expiration(&self) -> Timestamp { - Timestamp(self.expiration) - } -} - -//------------ Timestamp ------------------------------------------------------ - -/// A Timestamp for RRSIG Records. -/// -/// DNS uses 32 bit timestamps that are conceptionally -/// viewed as the 32 bit modulus of a larger number space. Because of that, -/// special rules apply when processing these values. -/// -/// [RFC 4034] defines Timestamps as the number of seconds elepased since -/// since 1 January 1970 00:00:00 UTC, ignoring leap seconds. Timestamps -/// are compared using so-called "Serial number arithmetic", as defined in -/// [RFC 1982]. -/// -/// The RFC defines the semantics for doing arithmetics in the -/// face of these wrap-arounds. This type implements these semantics atop a -/// native `u32`. The RFC defines two operations: addition and comparison. -/// -/// For addition, the amount added can only be a positive number of up to -/// `2^31 - 1`. Because of this, we decided to not implement the -/// `Add` trait but rather have a dedicated method `add` so as to not cause -/// surprise panics. -/// -/// Timestamps only implement a partial ordering. That is, there are -/// pairs of values that are not equal but there still isn’t one value larger -/// than the other. Since this is neatly implemented by the `PartialOrd` -/// trait, the type implements that. -/// -/// [RFC 1982]: https://tools.ietf.org/html/rfc1982 -/// [RFC 4034]: https://tools.ietf.org/html/rfc4034 - -#[derive(Clone, Copy, Debug, PartialEq)] -/* -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -*/ -pub struct Timestamp(Serial); - -impl Timestamp { - /* - /// Returns a serial number for the current Unix time. - #[cfg(feature = "std")] - #[must_use] - pub fn now() -> Self { - Self(Serial::now()) - } - */ - - /* - /// Scan a serial represention signature time value. - /// - /// In [RRSIG] records, the expiration and inception times are given as - /// serial values. Their representation format can either be the - /// value or a specific date in `YYYYMMDDHHmmSS` format. - /// - /// [RRSIG]: Rrsig - pub fn scan(scanner: &mut S) -> Result { - scanner.scan_ascii_str(|token| { - if token.len() <= 10 { - let time = token.parse::().map_err(|_| { - S::Error::custom("illegal signature time") - })?; - - Ok(Self(Serial(time))) - } else if token.len() == 14 { - let time = jiff::fmt::strtime::parse("%Y%m%d%H%M%S", token) - .and_then(|mut time| { - // The timestamp did not explicitly state a time zone, so - // we have to manually mark it as UTC. - time.set_offset(Some(jiff::tz::Offset::UTC)); - time.to_timestamp() - }) - .map_err(|_| { - S::Error::custom("illegal signature time") - })?; - - Ok(Self(Serial(time.as_second() as u32))) - } else { - Err(S::Error::custom("illegal signature time")) - } - }) - } - */ - - /// Returns the timestamp as a raw integer. - #[must_use] - pub fn into_int(self) -> u32 { - self.0.into() - } - - /// Returns a [`SystemTime`] close to a reference time. - /// - /// The returned [`SystemTime`] meets the following requirements: - /// - /// 1) The [`SystemTime`] value has a duration since `UNIX_EPOCH` that - /// modulo `2**32` is equal to our [`Timestamp`] value. - /// 2) The time difference between the [`SystemTime`] value and the - /// reference time fits in an [`i32`]. - /// - /// This can be used to sort [`Timestamp`] values. - #[must_use] - #[cfg(feature = "std")] - pub fn to_system_time(self, reference: SystemTime) -> SystemTime { - // Timestamp is a 32-bit value. We cannot just add UNIX_EPOCH because - // the timestamp may be too far in the future. We may have to add - // n * 2**32 for some unknown value of n. - const POW_2_32: u64 = 0x1_0000_0000; - let ref_secs = - reference.duration_since(UNIX_EPOCH).unwrap().as_secs(); - let k = ref_secs / POW_2_32; - let ref_secs_mod = ref_secs % POW_2_32; - let ts_secs = self.into_int() as u64; - let ts_secs = if ts_secs < ref_secs_mod { - if ref_secs_mod - ts_secs <= POW_2_32 / 2 { - // Close enough, use k. - ts_secs + k * POW_2_32 - } else { - // ts_secs is really beyond ref_secs, use k+1. - ts_secs + (k + 1) * POW_2_32 - } - } else { - // ts_secs >= ref_secs_mod - if ts_secs - ref_secs_mod < POW_2_32 / 2 { - // Close enough, use k. - ts_secs + k * POW_2_32 - } else { - // ts_secs is really old than ref_secs. Try to use k-1 - // but only if k is not zero. - let k = if k > 0 { k - 1 } else { k }; - ts_secs + k * POW_2_32 - } - }; - UNIX_EPOCH + Duration::from_secs(ts_secs) - } -} - -/* -/// # Parsing and Composing -/// -impl Timestamp { - pub const COMPOSE_LEN: u16 = Serial::COMPOSE_LEN; - - pub fn parse + ?Sized>( - parser: &mut Parser<'_, Octs>, - ) -> Result { - Serial::parse(parser).map(Self) - } - - pub fn compose( - &self, - target: &mut Target, - ) -> Result<(), Target::AppendError> { - self.0.compose(target) - } -} -*/ - -//--- From and FromStr - -impl From for Timestamp { - fn from(item: u32) -> Self { - Self(Serial::from(item)) - } -} - -/* -impl str::FromStr for Timestamp { - type Err = IllegalSignatureTime; - - /// Parses a timestamp value from a string. - /// - /// The presentation format can either be their integer value or a - /// specific date in `YYYYMMDDHHmmSS` format. - fn from_str(src: &str) -> Result { - if !src.is_ascii() { - return Err(IllegalSignatureTime(())); - } - if src.len() == 14 { - let time = jiff::fmt::strtime::parse("%Y%m%d%H%M%S", src) - .and_then(|mut time| { - // The timestamp did not explicitly state a time zone, so - // we have to manually mark it as UTC. - time.set_offset(Some(jiff::tz::Offset::UTC)); - time.to_timestamp() - }) - .map_err(|_| IllegalSignatureTime(()))?; - - Ok(Self(Serial(time.as_second() as u32))) - } else { - Serial::from_str(src) - .map(Timestamp) - .map_err(|_| IllegalSignatureTime(())) - } - } -} -*/ - -//--- Display - -impl fmt::Display for Timestamp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -//--- ZonefileFmt - -/* -impl ZonefileFmt for Timestamp { - fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { - p.write_token(self.0) - } -} -*/ - -//--- PartialOrd and CanonicalOrd - -#[cfg(feature = "std")] -impl cmp::PartialOrd for Timestamp { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } -} - -/* -impl CanonicalOrd for Timestamp { - fn canonical_cmp(&self, other: &Self) -> cmp::Ordering { - self.0.canonical_cmp(&other.0) - } -} -*/ - -impl From for crate::rdata::dnssec::Timestamp { - fn from(ts: Timestamp) -> Self { - let v: u32 = ts.0.into(); - v.into() + self.expiration } } diff --git a/src/new/rdata/zonemd.rs b/src/new/rdata/zonemd.rs index cf12f583e..ad0feba97 100644 --- a/src/new/rdata/zonemd.rs +++ b/src/new/rdata/zonemd.rs @@ -7,7 +7,7 @@ use core::{cmp::Ordering, fmt}; use crate::{ new::base::{ CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, - Serial, + SoaSerial, build::BuildInMessage, name::NameCompressor, wire::{ @@ -172,7 +172,7 @@ pub struct ZoneMD { /// field indicates that the ZONEMD record might simply be out-of-date. /// This can be used for diagnostics, but must not be used to infer /// anything about the integrity or authenticity of the zone. - pub serial: Serial, + pub serial: SoaSerial, /// The scheme used to compute the digest. ///