1use std::collections::HashMap;
33use std::sync::RwLock;
34
35pub const UNSET_LABEL_ID: LabelId = LabelId(0);
37
38pub const FIRST_USER_LABEL_ID: u32 = 64;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub struct LabelId(pub u32);
49
50impl LabelId {
51 #[inline]
53 pub const fn new(id: u32) -> Self {
54 Self(id)
55 }
56
57 #[inline]
59 pub const fn as_u32(self) -> u32 {
60 self.0
61 }
62
63 #[inline]
65 pub const fn is_unset(self) -> bool {
66 self.0 == 0
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75#[repr(u8)]
76pub enum Namespace {
77 Node = 0,
78 Edge = 1,
79}
80
81impl Namespace {
82 fn from_u8(v: u8) -> Option<Self> {
83 match v {
84 0 => Some(Self::Node),
85 1 => Some(Self::Edge),
86 _ => None,
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum LabelRegistryError {
94 LabelTooLong { len: usize, max: usize },
96 Malformed { offset: usize, reason: &'static str },
98 LockPoisoned,
100}
101
102impl std::fmt::Display for LabelRegistryError {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 match self {
105 Self::LabelTooLong { len, max } => {
106 write!(f, "label too long: {} bytes (max {})", len, max)
107 }
108 Self::Malformed { offset, reason } => {
109 write!(f, "malformed registry at offset {}: {}", offset, reason)
110 }
111 Self::LockPoisoned => write!(f, "label registry lock poisoned"),
112 }
113 }
114}
115
116impl std::error::Error for LabelRegistryError {}
117
118pub const MAX_LABEL_LEN: usize = 512;
120
121#[derive(Debug)]
127pub struct LabelRegistry {
128 inner: RwLock<RegistryInner>,
129}
130
131#[derive(Debug)]
132struct RegistryInner {
133 by_label: HashMap<(Namespace, String), LabelId>,
135 by_id: Vec<Option<(Namespace, String)>>,
138 next_id: u32,
140}
141
142impl LabelRegistry {
143 pub fn with_legacy_seed() -> Self {
147 let reg = Self::empty();
148 for (raw, name) in LEGACY_NODE_LABELS {
151 reg.intern_with_id(Namespace::Node, name, LabelId(legacy_node_id(*raw)))
152 .expect("legacy node label seed");
153 }
154 for (raw, name) in LEGACY_EDGE_LABELS {
155 reg.intern_with_id(Namespace::Edge, name, LabelId(legacy_edge_id(*raw)))
156 .expect("legacy edge label seed");
157 }
158 if let Ok(mut g) = reg.inner.write() {
160 g.next_id = FIRST_USER_LABEL_ID;
161 }
162 reg
163 }
164
165 pub fn empty() -> Self {
168 Self {
169 inner: RwLock::new(RegistryInner {
170 by_label: HashMap::new(),
171 by_id: vec![None],
173 next_id: 1,
174 }),
175 }
176 }
177
178 pub fn intern(&self, ns: Namespace, label: &str) -> Result<LabelId, LabelRegistryError> {
180 if label.len() > MAX_LABEL_LEN {
181 return Err(LabelRegistryError::LabelTooLong {
182 len: label.len(),
183 max: MAX_LABEL_LEN,
184 });
185 }
186 let mut g = self
187 .inner
188 .write()
189 .map_err(|_| LabelRegistryError::LockPoisoned)?;
190 if let Some(&id) = g.by_label.get(&(ns, label.to_string())) {
191 return Ok(id);
192 }
193 let id = LabelId(g.next_id);
194 g.next_id = g
195 .next_id
196 .checked_add(1)
197 .expect("LabelId u32 space exhausted (>4B labels)");
198 let key = (ns, label.to_string());
199 g.by_label.insert(key.clone(), id);
200 let idx = id.0 as usize;
201 if g.by_id.len() <= idx {
202 g.by_id.resize(idx + 1, None);
203 }
204 g.by_id[idx] = Some(key);
205 Ok(id)
206 }
207
208 pub fn lookup(&self, ns: Namespace, label: &str) -> Option<LabelId> {
210 let g = self.inner.read().ok()?;
211 g.by_label.get(&(ns, label.to_string())).copied()
212 }
213
214 pub fn resolve(&self, id: LabelId) -> Option<(Namespace, String)> {
217 if id.is_unset() {
218 return None;
219 }
220 let g = self.inner.read().ok()?;
221 g.by_id.get(id.0 as usize).cloned().flatten()
222 }
223
224 pub fn label_of(&self, ns: Namespace, id: LabelId) -> Option<String> {
227 self.resolve(id)
228 .filter(|(found_ns, _)| *found_ns == ns)
229 .map(|(_, l)| l)
230 }
231
232 pub fn len(&self) -> usize {
234 self.inner.read().map(|g| g.by_label.len()).unwrap_or(0)
235 }
236
237 pub fn is_empty(&self) -> bool {
239 self.len() == 0
240 }
241
242 pub fn legacy_node_label_id(disc: u8) -> LabelId {
246 if (disc as usize) < LEGACY_NODE_LABELS.len() {
247 LabelId(legacy_node_id(disc))
248 } else {
249 UNSET_LABEL_ID
250 }
251 }
252
253 pub fn legacy_edge_label_id(disc: u8) -> LabelId {
256 if (disc as usize) < LEGACY_EDGE_LABELS.len() {
257 LabelId(legacy_edge_id(disc))
258 } else {
259 UNSET_LABEL_ID
260 }
261 }
262
263 pub fn encode(&self) -> Result<Vec<u8>, LabelRegistryError> {
266 let g = self
267 .inner
268 .read()
269 .map_err(|_| LabelRegistryError::LockPoisoned)?;
270 let entries: Vec<(LabelId, Namespace, &str)> = g
272 .by_id
273 .iter()
274 .enumerate()
275 .filter_map(|(i, slot)| {
276 slot.as_ref()
277 .map(|(ns, label)| (LabelId(i as u32), *ns, label.as_str()))
278 })
279 .collect();
280 let mut buf = Vec::with_capacity(4 + entries.len() * 16);
281 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
282 for (id, ns, label) in entries {
283 buf.extend_from_slice(&id.0.to_le_bytes());
284 buf.push(ns as u8);
285 let bytes = label.as_bytes();
286 buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
287 buf.extend_from_slice(bytes);
288 }
289 Ok(buf)
290 }
291
292 pub fn decode(data: &[u8]) -> Result<Self, LabelRegistryError> {
296 let reg = Self::empty();
297 if data.len() < 4 {
298 return Err(LabelRegistryError::Malformed {
299 offset: 0,
300 reason: "header truncated",
301 });
302 }
303 let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
304 let mut off = 4;
305 for _ in 0..count {
306 if data.len() < off + 7 {
307 return Err(LabelRegistryError::Malformed {
308 offset: off,
309 reason: "entry header truncated",
310 });
311 }
312 let id = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
313 let ns = Namespace::from_u8(data[off + 4]).ok_or(LabelRegistryError::Malformed {
314 offset: off + 4,
315 reason: "unknown namespace",
316 })?;
317 let len = u16::from_le_bytes([data[off + 5], data[off + 6]]) as usize;
318 off += 7;
319 if data.len() < off + len {
320 return Err(LabelRegistryError::Malformed {
321 offset: off,
322 reason: "label bytes truncated",
323 });
324 }
325 let label = std::str::from_utf8(&data[off..off + len]).map_err(|_| {
326 LabelRegistryError::Malformed {
327 offset: off,
328 reason: "label not utf8",
329 }
330 })?;
331 reg.intern_with_id(ns, label, LabelId(id))?;
332 off += len;
333 }
334 if let Ok(mut g) = reg.inner.write() {
337 let max_id = g
338 .by_id
339 .iter()
340 .enumerate()
341 .filter_map(|(i, slot)| slot.as_ref().map(|_| i as u32))
342 .max()
343 .unwrap_or(0);
344 g.next_id = max_id.saturating_add(1).max(FIRST_USER_LABEL_ID);
345 }
346 Ok(reg)
347 }
348
349 fn intern_with_id(
352 &self,
353 ns: Namespace,
354 label: &str,
355 id: LabelId,
356 ) -> Result<(), LabelRegistryError> {
357 if label.len() > MAX_LABEL_LEN {
358 return Err(LabelRegistryError::LabelTooLong {
359 len: label.len(),
360 max: MAX_LABEL_LEN,
361 });
362 }
363 let mut g = self
364 .inner
365 .write()
366 .map_err(|_| LabelRegistryError::LockPoisoned)?;
367 let idx = id.0 as usize;
368 if g.by_id.len() <= idx {
369 g.by_id.resize(idx + 1, None);
370 }
371 let key = (ns, label.to_string());
372 if let Some(existing) = &g.by_id[idx] {
373 if existing != &key {
374 return Err(LabelRegistryError::Malformed {
375 offset: 0,
376 reason: "id collision with different label",
377 });
378 }
379 return Ok(());
380 }
381 g.by_id[idx] = Some(key.clone());
382 g.by_label.insert(key, id);
383 Ok(())
384 }
385}
386
387impl Default for LabelRegistry {
388 fn default() -> Self {
389 Self::with_legacy_seed()
390 }
391}
392
393const LEGACY_NODE_LABELS: &[(u8, &str)] = &[
402 (0, "host"),
403 (1, "service"),
404 (2, "credential"),
405 (3, "vulnerability"),
406 (4, "endpoint"),
407 (5, "technology"),
408 (6, "user"),
409 (7, "domain"),
410 (8, "certificate"),
411];
412
413const LEGACY_EDGE_LABELS: &[(u8, &str)] = &[
414 (0, "has_service"),
415 (1, "has_endpoint"),
416 (2, "uses_tech"),
417 (3, "auth_access"),
418 (4, "affected_by"),
419 (5, "contains"),
420 (6, "connects_to"),
421 (7, "related_to"),
422 (8, "has_user"),
423 (9, "has_cert"),
424];
425
426fn legacy_node_id(disc: u8) -> u32 {
430 1 + disc as u32
431}
432
433fn legacy_edge_id(disc: u8) -> u32 {
434 10 + disc as u32
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn empty_registry_has_no_entries_but_sentinel_resolves_to_none() {
443 let r = LabelRegistry::empty();
444 assert!(r.is_empty());
445 assert_eq!(r.resolve(UNSET_LABEL_ID), None);
446 assert_eq!(r.lookup(Namespace::Node, "anything"), None);
447 }
448
449 #[test]
450 fn intern_is_idempotent() {
451 let r = LabelRegistry::empty();
452 let a = r.intern(Namespace::Node, "order").unwrap();
453 let b = r.intern(Namespace::Node, "order").unwrap();
454 assert_eq!(a, b);
455 assert_eq!(r.len(), 1);
456 }
457
458 #[test]
459 fn namespaces_are_independent() {
460 let r = LabelRegistry::empty();
461 let n = r.intern(Namespace::Node, "host").unwrap();
462 let e = r.intern(Namespace::Edge, "host").unwrap();
463 assert_ne!(
464 n, e,
465 "same label in different namespaces must get distinct ids"
466 );
467 assert_eq!(r.label_of(Namespace::Node, n).as_deref(), Some("host"));
468 assert_eq!(r.label_of(Namespace::Edge, e).as_deref(), Some("host"));
469 assert_eq!(r.label_of(Namespace::Node, e), None);
470 }
471
472 #[test]
473 fn legacy_seed_populates_reserved_range() {
474 let r = LabelRegistry::with_legacy_seed();
475 let host_id = r.lookup(Namespace::Node, "host").unwrap();
477 assert_eq!(host_id, LabelId(1));
478 assert_eq!(LabelRegistry::legacy_node_label_id(0), host_id);
479 let edge_id = r.lookup(Namespace::Edge, "has_service").unwrap();
481 assert_eq!(edge_id, LabelId(10));
482 assert_eq!(LabelRegistry::legacy_edge_label_id(0), edge_id);
483 }
484
485 #[test]
486 fn user_labels_start_at_first_user_id() {
487 let r = LabelRegistry::with_legacy_seed();
488 let id = r.intern(Namespace::Node, "order").unwrap();
489 assert_eq!(id, LabelId(FIRST_USER_LABEL_ID));
490 let id2 = r.intern(Namespace::Node, "product").unwrap();
491 assert_eq!(id2, LabelId(FIRST_USER_LABEL_ID + 1));
492 }
493
494 #[test]
495 fn round_trip_encode_decode() {
496 let r = LabelRegistry::with_legacy_seed();
497 r.intern(Namespace::Node, "order").unwrap();
498 r.intern(Namespace::Node, "product").unwrap();
499 r.intern(Namespace::Edge, "purchased").unwrap();
500 let bytes = r.encode().unwrap();
501 let restored = LabelRegistry::decode(&bytes).unwrap();
502 assert_eq!(restored.len(), r.len());
503 assert_eq!(
504 restored.lookup(Namespace::Node, "order"),
505 r.lookup(Namespace::Node, "order")
506 );
507 assert_eq!(
508 restored.lookup(Namespace::Edge, "purchased"),
509 r.lookup(Namespace::Edge, "purchased")
510 );
511 let new_id = restored.intern(Namespace::Node, "shipment").unwrap();
513 let prior_max = r.lookup(Namespace::Node, "product").unwrap();
514 assert!(new_id.0 > prior_max.0);
515 }
516
517 #[test]
518 fn decode_rejects_truncated_input() {
519 let bad = vec![0xff, 0xff, 0xff];
520 assert!(matches!(
521 LabelRegistry::decode(&bad),
522 Err(LabelRegistryError::Malformed { .. })
523 ));
524 }
525
526 #[test]
527 fn decode_rejects_invalid_namespace() {
528 let mut bad = Vec::new();
530 bad.extend_from_slice(&1u32.to_le_bytes());
531 bad.extend_from_slice(&64u32.to_le_bytes());
532 bad.push(99);
533 bad.extend_from_slice(&4u16.to_le_bytes());
534 bad.extend_from_slice(b"test");
535 let err = LabelRegistry::decode(&bad).unwrap_err();
536 assert!(matches!(err, LabelRegistryError::Malformed { .. }));
537 }
538
539 #[test]
540 fn label_too_long_is_rejected() {
541 let r = LabelRegistry::empty();
542 let big = "x".repeat(MAX_LABEL_LEN + 1);
543 assert!(matches!(
544 r.intern(Namespace::Node, &big),
545 Err(LabelRegistryError::LabelTooLong { .. })
546 ));
547 }
548
549 #[test]
550 fn concurrent_intern_yields_consistent_ids() {
551 use std::sync::Arc;
552 use std::thread;
553
554 let r = Arc::new(LabelRegistry::empty());
555 let handles: Vec<_> = (0..16)
556 .map(|i| {
557 let r = Arc::clone(&r);
558 thread::spawn(move || {
559 let mut ids = Vec::new();
560 for j in 0..50 {
561 let label = format!("label_{}_{}", i % 4, j);
562 ids.push(r.intern(Namespace::Node, &label).unwrap());
563 }
564 ids
565 })
566 })
567 .collect();
568 for h in handles {
569 h.join().unwrap();
570 }
571 let mut seen_ids = std::collections::HashSet::new();
574 for i in 0..4 {
575 for j in 0..50 {
576 let label = format!("label_{}_{}", i, j);
577 let id = r.lookup(Namespace::Node, &label).unwrap();
578 assert!(seen_ids.insert(id), "duplicate id {:?} for {}", id, label);
579 }
580 }
581 assert_eq!(seen_ids.len(), 200);
582 }
583
584 #[test]
585 fn unset_id_never_resolves() {
586 let r = LabelRegistry::with_legacy_seed();
587 assert!(UNSET_LABEL_ID.is_unset());
588 assert_eq!(r.resolve(UNSET_LABEL_ID), None);
589 assert_eq!(r.label_of(Namespace::Node, UNSET_LABEL_ID), None);
590 }
591}