bbx_net/
address.rs

1//! Block identification and address parsing for network messages.
2//!
3//! Provides `NodeId` for unique identification and `AddressPath` for
4//! parsing OSC-style address patterns that target DSP blocks.
5
6use std::sync::atomic::{AtomicU64, Ordering};
7
8use bbx_core::random::XorShiftRng;
9
10use crate::error::{NetError, Result};
11
12static NODE_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
13
14/// A 128-bit node identifier generated from XorShiftRng.
15///
16/// NodeIds uniquely identify clients and nodes in the network. They are
17/// formatted as UUID-style strings for human readability and protocol
18/// compatibility.
19#[repr(C)]
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
21pub struct NodeId {
22    pub high: u64,
23    pub low: u64,
24}
25
26impl NodeId {
27    /// Generate a new random NodeId using XorShiftRng.
28    ///
29    /// # Example
30    ///
31    /// ```
32    /// use bbx_core::random::XorShiftRng;
33    /// use bbx_net::address::NodeId;
34    ///
35    /// let mut rng = XorShiftRng::new(12345);
36    /// let node_id = NodeId::generate(&mut rng);
37    /// ```
38    pub fn generate(rng: &mut XorShiftRng) -> Self {
39        let sample1 = (rng.next_noise_sample() + 1.0) / 2.0;
40        let sample2 = (rng.next_noise_sample() + 1.0) / 2.0;
41        Self {
42            high: (sample1 * u64::MAX as f64) as u64,
43            low: (sample2 * u64::MAX as f64) as u64,
44        }
45    }
46
47    /// Generate a NodeId with mixed entropy sources for improved unpredictability.
48    ///
49    /// Combines multiple entropy sources: time, process ID, memory address (ASLR),
50    /// and a monotonic counter to create a more unpredictable seed.
51    pub fn generate_with_entropy(clock_micros: u64) -> Self {
52        let counter = NODE_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
53        let pid = std::process::id() as u64;
54        let addr_entropy = &counter as *const _ as u64;
55
56        let seed = clock_micros.wrapping_mul(0x517cc1b727220a95)
57            ^ pid.rotate_left(17)
58            ^ addr_entropy.rotate_left(31)
59            ^ counter.wrapping_mul(0x2545f4914f6cdd1d);
60
61        let mut rng = XorShiftRng::new(seed.max(1));
62        Self::generate(&mut rng)
63    }
64
65    /// Generate a unique NodeId that doesn't collide with existing IDs.
66    ///
67    /// Uses `generate_with_entropy` internally and loops until finding an ID
68    /// that the `exists` predicate returns false for.
69    pub fn generate_unique<F>(clock_micros: u64, mut exists: F) -> Self
70    where
71        F: FnMut(&NodeId) -> bool,
72    {
73        loop {
74            let candidate = Self::generate_with_entropy(clock_micros);
75            if !exists(&candidate) {
76                return candidate;
77            }
78        }
79    }
80
81    /// Create a NodeId from explicit high and low parts.
82    pub const fn from_parts(high: u64, low: u64) -> Self {
83        Self { high, low }
84    }
85
86    /// Format as hyphenated UUID string.
87    ///
88    /// Returns a string in the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
89    pub fn to_uuid_string(&self) -> String {
90        format!(
91            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
92            (self.high >> 32) as u32,
93            ((self.high >> 16) & 0xFFFF) as u16,
94            (self.high & 0xFFFF) as u16,
95            ((self.low >> 48) & 0xFFFF) as u16,
96            (self.low & 0xFFFF_FFFF_FFFF)
97        )
98    }
99
100    /// Parse from UUID string.
101    ///
102    /// Accepts the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
103    pub fn from_uuid_string(s: &str) -> Result<Self> {
104        let s = s.trim();
105        let parts: Vec<&str> = s.split('-').collect();
106
107        if parts.len() != 5 {
108            return Err(NetError::InvalidNodeId);
109        }
110
111        let p0 = u32::from_str_radix(parts[0], 16).map_err(|_| NetError::InvalidNodeId)?;
112        let p1 = u16::from_str_radix(parts[1], 16).map_err(|_| NetError::InvalidNodeId)?;
113        let p2 = u16::from_str_radix(parts[2], 16).map_err(|_| NetError::InvalidNodeId)?;
114        let p3 = u16::from_str_radix(parts[3], 16).map_err(|_| NetError::InvalidNodeId)?;
115        let p4 = u64::from_str_radix(parts[4], 16).map_err(|_| NetError::InvalidNodeId)?;
116
117        if p4 > 0xFFFF_FFFF_FFFF {
118            return Err(NetError::InvalidNodeId);
119        }
120
121        let high = ((p0 as u64) << 32) | ((p1 as u64) << 16) | (p2 as u64);
122        let low = ((p3 as u64) << 48) | p4;
123
124        Ok(Self { high, low })
125    }
126}
127
128/// Parsed OSC-style address path.
129///
130/// Supports formats:
131/// - `/block/<uuid>/param/<name>` - Specific block parameter
132/// - `/blocks/param/<name>` - Broadcast to all blocks
133#[derive(Debug, Clone)]
134pub struct AddressPath {
135    /// Target block (None for broadcast).
136    pub block_id: Option<NodeId>,
137    /// Parameter name.
138    pub param_name: String,
139}
140
141impl AddressPath {
142    /// Parse an OSC address string.
143    ///
144    /// # Supported Formats
145    ///
146    /// - `/block/<uuid>/param/<name>` - Specific block parameter
147    /// - `/blocks/param/<name>` - Broadcast to all blocks
148    ///
149    /// # Example
150    ///
151    /// ```
152    /// use bbx_net::address::AddressPath;
153    ///
154    /// let path = AddressPath::parse("/blocks/param/gain").unwrap();
155    /// assert!(path.block_id.is_none());
156    /// assert_eq!(path.param_name, "gain");
157    /// ```
158    pub fn parse(address: &str) -> Result<Self> {
159        let parts: Vec<&str> = address.split('/').filter(|s| !s.is_empty()).collect();
160
161        match parts.as_slice() {
162            ["block", uuid, "param", name] => {
163                let block_id = NodeId::from_uuid_string(uuid)?;
164                Ok(Self {
165                    block_id: Some(block_id),
166                    param_name: (*name).to_string(),
167                })
168            }
169            ["blocks", "param", name] => Ok(Self {
170                block_id: None,
171                param_name: (*name).to_string(),
172            }),
173            _ => Err(NetError::InvalidAddress),
174        }
175    }
176
177    /// Format as an OSC address string.
178    pub fn to_address_string(&self) -> String {
179        match &self.block_id {
180            Some(id) => format!("/block/{}/param/{}", id.to_uuid_string(), self.param_name),
181            None => format!("/blocks/param/{}", self.param_name),
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_node_id_generate() {
192        let mut rng = XorShiftRng::new(12345);
193        let id1 = NodeId::generate(&mut rng);
194        let id2 = NodeId::generate(&mut rng);
195
196        assert_ne!(id1, id2);
197        assert_ne!(id1.high, 0);
198        assert_ne!(id1.low, 0);
199    }
200
201    #[test]
202    fn test_node_id_deterministic() {
203        let mut rng1 = XorShiftRng::new(42);
204        let mut rng2 = XorShiftRng::new(42);
205
206        let id1 = NodeId::generate(&mut rng1);
207        let id2 = NodeId::generate(&mut rng2);
208
209        assert_eq!(id1, id2);
210    }
211
212    #[test]
213    fn test_node_id_uuid_roundtrip() {
214        let mut rng = XorShiftRng::new(54321);
215        let original = NodeId::generate(&mut rng);
216
217        let uuid_str = original.to_uuid_string();
218        let parsed = NodeId::from_uuid_string(&uuid_str).unwrap();
219
220        assert_eq!(original, parsed);
221    }
222
223    #[test]
224    fn test_node_id_uuid_format() {
225        let id = NodeId::from_parts(0x12345678_abcd_ef01, 0x2345_6789abcdef01);
226        let uuid = id.to_uuid_string();
227
228        assert_eq!(uuid, "12345678-abcd-ef01-2345-6789abcdef01");
229    }
230
231    #[test]
232    fn test_node_id_invalid_uuid() {
233        assert!(NodeId::from_uuid_string("invalid").is_err());
234        assert!(NodeId::from_uuid_string("12345678-abcd").is_err());
235        assert!(NodeId::from_uuid_string("zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz").is_err());
236    }
237
238    #[test]
239    fn test_address_path_broadcast() {
240        let path = AddressPath::parse("/blocks/param/gain").unwrap();
241        assert!(path.block_id.is_none());
242        assert_eq!(path.param_name, "gain");
243    }
244
245    #[test]
246    fn test_address_path_targeted() {
247        let id = NodeId::from_parts(0x12345678_abcd_ef01, 0x2345_6789abcdef01);
248        let address = format!("/block/{}/param/volume", id.to_uuid_string());
249
250        let path = AddressPath::parse(&address).unwrap();
251        assert_eq!(path.block_id, Some(id));
252        assert_eq!(path.param_name, "volume");
253    }
254
255    #[test]
256    fn test_address_path_roundtrip() {
257        let original = AddressPath {
258            block_id: Some(NodeId::from_parts(0xaabb_ccdd_eeff_0011, 0x2233_445566778899)),
259            param_name: "frequency".to_string(),
260        };
261
262        let address_str = original.to_address_string();
263        let parsed = AddressPath::parse(&address_str).unwrap();
264
265        assert_eq!(original.block_id, parsed.block_id);
266        assert_eq!(original.param_name, parsed.param_name);
267    }
268
269    #[test]
270    fn test_address_path_invalid() {
271        assert!(AddressPath::parse("/invalid/path").is_err());
272        assert!(AddressPath::parse("/block/notauuid/param/test").is_err());
273        assert!(AddressPath::parse("").is_err());
274    }
275
276    #[test]
277    fn test_generate_with_entropy_uniqueness() {
278        let mut ids = std::collections::HashSet::new();
279        for _ in 0..10_000 {
280            let id = NodeId::generate_with_entropy(0);
281            assert!(ids.insert(id), "Collision detected");
282        }
283    }
284
285    #[test]
286    fn test_generate_unique_avoids_collision() {
287        let existing = NodeId::from_parts(123, 456);
288        let mut attempts = 0;
289        let new_id = NodeId::generate_unique(0, |id| {
290            attempts += 1;
291            *id == existing && attempts == 1
292        });
293        assert_ne!(new_id, existing);
294    }
295}