Skip to main content

codetics_ocpp_core/
msgid.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! Message id generation.
5//!
6//! A CALL message id MUST differ from every id previously used by the same
7//! sender on any connection with the same station identity (1.6-J errata
8//! §3.1/§3.7, 2.0.1 Part 4 §4.1.4). Random 122-bit UUIDv4-formatted ids
9//! satisfy this across reconnects and service restarts without any shared
10//! state, and fit the 36-character limit exactly.
11
12use std::collections::hash_map::RandomState;
13use std::hash::{BuildHasher, Hasher};
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::time::{SystemTime, UNIX_EPOCH};
16
17/// Lock-free generator; one instance can be shared by all connections.
18pub struct MessageIdGenerator {
19    state: AtomicU64,
20    key: u64,
21}
22
23impl Default for MessageIdGenerator {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl MessageIdGenerator {
30    pub fn new() -> Self {
31        // `RandomState` is seeded from the OS random source once per process.
32        let mut h = RandomState::new().build_hasher();
33        h.write_u128(
34            SystemTime::now()
35                .duration_since(UNIX_EPOCH)
36                .unwrap_or_default()
37                .as_nanos(),
38        );
39        h.write_u32(std::process::id());
40        let seed = h.finish();
41        let mut h2 = RandomState::new().build_hasher();
42        h2.write_u64(seed);
43        MessageIdGenerator {
44            state: AtomicU64::new(seed),
45            key: h2.finish() | 1,
46        }
47    }
48
49    fn next_u64(&self) -> u64 {
50        // SplitMix64 over an atomic counter: unique sequence, well mixed output.
51        let z = self
52            .state
53            .fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed);
54        let mut z = z ^ self.key;
55        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
56        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
57        z ^ (z >> 31)
58    }
59
60    /// Returns a new id formatted as a UUID version 4 (36 characters).
61    pub fn next_id(&self) -> String {
62        let hi = (self.next_u64() & 0xFFFF_FFFF_FFFF_0FFF) | 0x0000_0000_0000_4000;
63        let lo = (self.next_u64() & 0x3FFF_FFFF_FFFF_FFFF) | 0x8000_0000_0000_0000;
64        format!(
65            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
66            hi >> 32,
67            (hi >> 16) & 0xFFFF,
68            hi & 0xFFFF,
69            lo >> 48,
70            lo & 0xFFFF_FFFF_FFFF
71        )
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use std::collections::HashSet;
79
80    #[test]
81    fn unique_and_well_formed() {
82        let g = MessageIdGenerator::new();
83        let mut seen = HashSet::new();
84        for _ in 0..100_000 {
85            let id = g.next_id();
86            assert_eq!(id.len(), 36);
87            assert_eq!(&id[14..15], "4");
88            assert!(matches!(&id[19..20], "8" | "9" | "a" | "b"));
89            assert!(seen.insert(id));
90        }
91    }
92}