Skip to main content

sioul_core/
ids.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Ids given to what is made (docs/database.md, "The format of what
5//! travels"): a money line, a split, a preset, a time session. Your devices
6//! merge each by its id (`sioul_sync::share`, format 2), so that two the same
7//! stay two, and one changed on two devices stays one.
8//!
9//! 128 bits, random, as the sharing's own ids are, shaped as a UUID
10//! (version 4): two halves hashed by keys the system draws at random for
11//! this process (`RandomState`, as `everywhere::new_id` does), over the time,
12//! a counter and the process. Nothing of what the id names is in it.
13
14use std::hash::{BuildHasher, Hasher};
15use std::sync::atomic::{AtomicU64, Ordering};
16
17/// A new id: "1c0e5d2a-…-4…", never the same twice.
18pub fn new() -> String {
19    static COUNT: AtomicU64 = AtomicU64::new(0);
20    let count = COUNT.fetch_add(1, Ordering::Relaxed);
21    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos();
22    let half = |which: u8| {
23        let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
24        hasher.write_u128(now);
25        hasher.write_u64(count);
26        hasher.write_u32(std::process::id());
27        hasher.write_u8(which);
28        hasher.finish().to_le_bytes()
29    };
30    let mut bytes = [0u8; 16];
31    bytes[..8].copy_from_slice(&half(0));
32    bytes[8..].copy_from_slice(&half(1));
33    // Version 4 (random), variant 1, as a UUID says it.
34    bytes[6] = (bytes[6] & 0x0f) | 0x40;
35    bytes[8] = (bytes[8] & 0x3f) | 0x80;
36    let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
37    format!("{}-{}-{}-{}-{}", &hex[..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..])
38}
39
40#[cfg(test)]
41mod tests {
42    #[test]
43    fn ids_are_new_each_time_and_shaped_as_uuids() {
44        let ids: std::collections::BTreeSet<String> = (0..10_000).map(|_| super::new()).collect();
45        assert_eq!(ids.len(), 10_000);
46        for id in ids.iter().take(50) {
47            assert_eq!(id.len(), 36, "{id}");
48            assert_eq!(id.as_bytes()[14], b'4', "{id}");
49            assert!(matches!(id.as_bytes()[19], b'8' | b'9' | b'a' | b'b'), "{id}");
50            assert!(id.chars().all(|c| c == '-' || c.is_ascii_hexdigit()), "{id}");
51        }
52    }
53}