codetics-ocpp-core 0.1.0

Codetics OCPP: version-independent OCPP-J building blocks (RPC framing, error codes, RFC 3339 date-time, validation, subprotocol negotiation, message ids)
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)

//! Message id generation.
//!
//! A CALL message id MUST differ from every id previously used by the same
//! sender on any connection with the same station identity (1.6-J errata
//! §3.1/§3.7, 2.0.1 Part 4 §4.1.4). Random 122-bit UUIDv4-formatted ids
//! satisfy this across reconnects and service restarts without any shared
//! state, and fit the 36-character limit exactly.

use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

/// Lock-free generator; one instance can be shared by all connections.
pub struct MessageIdGenerator {
    state: AtomicU64,
    key: u64,
}

impl Default for MessageIdGenerator {
    fn default() -> Self {
        Self::new()
    }
}

impl MessageIdGenerator {
    pub fn new() -> Self {
        // `RandomState` is seeded from the OS random source once per process.
        let mut h = RandomState::new().build_hasher();
        h.write_u128(
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos(),
        );
        h.write_u32(std::process::id());
        let seed = h.finish();
        let mut h2 = RandomState::new().build_hasher();
        h2.write_u64(seed);
        MessageIdGenerator {
            state: AtomicU64::new(seed),
            key: h2.finish() | 1,
        }
    }

    fn next_u64(&self) -> u64 {
        // SplitMix64 over an atomic counter: unique sequence, well mixed output.
        let z = self
            .state
            .fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed);
        let mut z = z ^ self.key;
        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
        z ^ (z >> 31)
    }

    /// Returns a new id formatted as a UUID version 4 (36 characters).
    pub fn next_id(&self) -> String {
        let hi = (self.next_u64() & 0xFFFF_FFFF_FFFF_0FFF) | 0x0000_0000_0000_4000;
        let lo = (self.next_u64() & 0x3FFF_FFFF_FFFF_FFFF) | 0x8000_0000_0000_0000;
        format!(
            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
            hi >> 32,
            (hi >> 16) & 0xFFFF,
            hi & 0xFFFF,
            lo >> 48,
            lo & 0xFFFF_FFFF_FFFF
        )
    }
}

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

    #[test]
    fn unique_and_well_formed() {
        let g = MessageIdGenerator::new();
        let mut seen = HashSet::new();
        for _ in 0..100_000 {
            let id = g.next_id();
            assert_eq!(id.len(), 36);
            assert_eq!(&id[14..15], "4");
            assert!(matches!(&id[19..20], "8" | "9" | "a" | "b"));
            assert!(seen.insert(id));
        }
    }
}