orderable-bytes 0.1.0

Canonical, order-preserving fixed-length byte encodings for plaintext types — feed into ORE or OPE schemes that compare encrypted values lexicographically
Documentation
//! Canonical, order-preserving fixed-length byte encodings for the
//! `chrono` types `NaiveDate` and `DateTime<Utc>`.
//!
//! Each submodule exposes a `to_orderable_bytes` function and an
//! `ENCODED_LEN` constant. The bytes returned have the property that
//! byte-wise lex comparison agrees with chronological ordering (and byte
//! equality with value equality), so any comparison-as-bytes scheme
//! (`ore-rs` BlockORE, OPE, an ordered hash) inherits those properties on
//! the resulting digest.

/// Order-preserving byte encoding for [`::chrono::NaiveDate`].
pub mod naive_date {
    use ::chrono::{Datelike, NaiveDate};

    /// Number of bytes in the canonical orderable-bytes form.
    pub const ENCODED_LEN: usize = 4;

    /// Build the canonical, order-preserving byte encoding of a `NaiveDate`.
    ///
    /// `NaiveDate::num_days_from_ce()` returns an `i32` whose ordering
    /// matches chronological order. Sign-flipping `i32 → u32` (XOR with
    /// `1u32 << 31`) preserves order while making the value unsigned, then
    /// big-endian byte serialisation gives a 4-byte sequence whose lex
    /// order matches the natural date order.
    pub fn to_orderable_bytes(d: &NaiveDate) -> [u8; ENCODED_LEN] {
        let biased = (d.num_days_from_ce() as u32) ^ (1u32 << 31);
        biased.to_be_bytes()
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        fn ymd(year: i32, month: u32, day: u32) -> NaiveDate {
            NaiveDate::from_ymd_opt(year, month, day).unwrap()
        }

        #[test]
        fn year_one_biases_to_known_u32() {
            // Year 1 day 1 has num_days_from_ce = 1 ⇒ sign-flipped u32 = 0x8000_0001.
            assert_eq!(to_orderable_bytes(&ymd(1, 1, 1)), [0x80, 0x00, 0x00, 0x01]);
        }

        #[test]
        fn byte_order_matches_chronological_order() {
            let ascending = [
                NaiveDate::MIN,
                ymd(-10000, 1, 1),
                ymd(-1, 12, 31),
                ymd(1, 1, 1),
                ymd(1970, 1, 1),
                ymd(2000, 1, 1),
                ymd(2026, 4, 29),
                ymd(10000, 1, 1),
                NaiveDate::MAX,
            ];
            for window in ascending.windows(2) {
                let a = to_orderable_bytes(&window[0]);
                let b = to_orderable_bytes(&window[1]);
                assert!(
                    a < b,
                    "to_orderable_bytes({}) < to_orderable_bytes({}) failed",
                    window[0],
                    window[1]
                );
            }
        }
    }
}

/// Order-preserving byte encoding for [`::chrono::DateTime<::chrono::Utc>`].
pub mod datetime_utc {
    use ::chrono::{DateTime, Utc};

    /// Number of bytes in the canonical orderable-bytes form.
    pub const ENCODED_LEN: usize = 12;

    /// Build the canonical, order-preserving byte encoding of a
    /// `DateTime<Utc>`.
    ///
    /// Layout: a sign-flipped `i64` Unix timestamp (8 bytes BE) followed by
    /// the `u32` subsecond nanosecond count (4 bytes BE). Sign-flipping the
    /// timestamp via `XOR (1u64 << 63)` makes the bias-encoded value sort
    /// chronologically as an unsigned big-endian integer; appending the
    /// subsecond field gives a strict tiebreaker on nanos within the same
    /// whole second. `timestamp_subsec_nanos` returns values in
    /// `0..2_000_000_000` (the upper half encodes leap-second moments),
    /// which fits in `u32` and preserves chronological order.
    pub fn to_orderable_bytes(dt: &DateTime<Utc>) -> [u8; ENCODED_LEN] {
        let secs = dt.timestamp();
        let nanos = dt.timestamp_subsec_nanos();
        let secs_biased = (secs as u64) ^ (1u64 << 63);
        let mut out = [0u8; ENCODED_LEN];
        out[..8].copy_from_slice(&secs_biased.to_be_bytes());
        out[8..].copy_from_slice(&nanos.to_be_bytes());
        out
    }

    #[cfg(test)]
    mod tests {
        use super::*;
        use ::chrono::TimeZone;

        fn dt(secs: i64, nanos: u32) -> DateTime<Utc> {
            Utc.timestamp_opt(secs, nanos).single().unwrap()
        }

        #[test]
        fn unix_epoch_canonical_bytes() {
            // 1970-01-01T00:00:00Z: timestamp = 0, subsec = 0. Sign-flip on
            // `0_i64` gives `0x8000_0000_0000_0000`.
            assert_eq!(
                to_orderable_bytes(&dt(0, 0)),
                [0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
            );
        }

        #[test]
        fn byte_order_matches_chronological_order() {
            let ascending = [
                DateTime::<Utc>::MIN_UTC,
                dt(-1_000_000_000_000, 0),
                dt(-1_000_000_000, 0),
                dt(-1, 999_999_999),
                dt(0, 0),
                dt(0, 1),
                dt(100, 100),
                dt(100, 200),
                dt(101, 0),
                dt(1_000_000_000, 0),
                dt(1_000_000_000_000, 0),
                DateTime::<Utc>::MAX_UTC,
            ];
            for window in ascending.windows(2) {
                let a = to_orderable_bytes(&window[0]);
                let b = to_orderable_bytes(&window[1]);
                assert!(
                    a < b,
                    "to_orderable_bytes({}) < to_orderable_bytes({}) failed",
                    window[0],
                    window[1]
                );
            }
        }
    }
}