Skip to main content

uni_common/core/
id.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4use anyhow::{Result, anyhow};
5use multibase::Base;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::str::FromStr;
9
10/// Internal Vertex ID (64 bits) - pure auto-increment
11///
12/// VIDs are dense, sequential identifiers assigned on vertex creation.
13/// Unlike the previous design, VIDs no longer embed label information.
14/// Label lookups are done via the VidLabelsIndex.
15///
16/// For O(1) array indexing during query execution, use DenseIdx via VidRemapper.
17#[derive(Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
18pub struct Vid(u64);
19
20impl Vid {
21    /// Creates a new vertex ID from a raw u64 value.
22    pub fn new(id: u64) -> Self {
23        Self(id)
24    }
25
26    /// Returns the raw u64 value of this VID.
27    pub fn as_u64(&self) -> u64 {
28        self.0
29    }
30
31    /// Sentinel value representing an invalid/null VID.
32    pub const INVALID: Vid = Vid(u64::MAX);
33
34    /// Check if this VID is the invalid sentinel.
35    pub fn is_invalid(&self) -> bool {
36        self.0 == u64::MAX
37    }
38}
39
40impl From<u64> for Vid {
41    fn from(val: u64) -> Self {
42        Self(val)
43    }
44}
45
46impl From<Vid> for u64 {
47    fn from(vid: Vid) -> Self {
48        vid.0
49    }
50}
51
52impl Default for Vid {
53    fn default() -> Self {
54        Self::INVALID
55    }
56}
57
58impl fmt::Debug for Vid {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        if self.is_invalid() {
61            write!(f, "Vid(INVALID)")
62        } else {
63            write!(f, "Vid({})", self.0)
64        }
65    }
66}
67
68impl fmt::Display for Vid {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        write!(f, "{}", self.0)
71    }
72}
73
74impl FromStr for Vid {
75    type Err = anyhow::Error;
76
77    /// Parses a Vid from a numeric string.
78    fn from_str(s: &str) -> Result<Self> {
79        let id: u64 = s
80            .parse()
81            .map_err(|e| anyhow!("Invalid Vid '{}': {}", s, e))?;
82        Ok(Self::new(id))
83    }
84}
85
86/// Internal Edge ID (64 bits) - pure auto-increment
87///
88/// EIDs are dense, sequential identifiers assigned on edge creation.
89/// Unlike the previous design, EIDs no longer embed type information.
90/// Edge type lookups are done via the edge tables.
91#[derive(Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
92pub struct Eid(u64);
93
94impl Eid {
95    /// Creates a new edge ID from a raw u64 value.
96    pub fn new(id: u64) -> Self {
97        Self(id)
98    }
99
100    /// Returns the raw u64 value of this EID.
101    pub fn as_u64(&self) -> u64 {
102        self.0
103    }
104
105    /// Sentinel value representing an invalid/null EID.
106    pub const INVALID: Eid = Eid(u64::MAX);
107
108    /// Check if this EID is the invalid sentinel.
109    pub fn is_invalid(&self) -> bool {
110        self.0 == u64::MAX
111    }
112}
113
114impl From<u64> for Eid {
115    fn from(val: u64) -> Self {
116        Self(val)
117    }
118}
119
120impl From<Eid> for u64 {
121    fn from(eid: Eid) -> Self {
122        eid.0
123    }
124}
125
126impl Default for Eid {
127    fn default() -> Self {
128        Self::INVALID
129    }
130}
131
132impl fmt::Debug for Eid {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        if self.is_invalid() {
135            write!(f, "Eid(INVALID)")
136        } else {
137            write!(f, "Eid({})", self.0)
138        }
139    }
140}
141
142impl fmt::Display for Eid {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "{}", self.0)
145    }
146}
147
148impl FromStr for Eid {
149    type Err = anyhow::Error;
150
151    /// Parses an Eid from a numeric string.
152    fn from_str(s: &str) -> Result<Self> {
153        let id: u64 = s
154            .parse()
155            .map_err(|e| anyhow!("Invalid Eid '{}': {}", s, e))?;
156        Ok(Self::new(id))
157    }
158}
159
160/// Dense index for O(1) array access during query execution.
161///
162/// During query execution, we load subgraphs into memory with dense arrays.
163/// DenseIdx provides efficient indexing into these arrays, while VidRemapper
164/// handles the bidirectional mapping between sparse VIDs and dense indices.
165#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
166pub struct DenseIdx(pub u32);
167
168impl DenseIdx {
169    /// Creates a new dense index.
170    pub fn new(idx: u32) -> Self {
171        Self(idx)
172    }
173
174    /// Returns the index as usize for array indexing.
175    pub fn as_usize(&self) -> usize {
176        self.0 as usize
177    }
178
179    /// Returns the raw u32 value.
180    pub fn as_u32(&self) -> u32 {
181        self.0
182    }
183
184    /// Sentinel value for invalid index.
185    pub const INVALID: DenseIdx = DenseIdx(u32::MAX);
186
187    /// Check if this is the invalid sentinel.
188    pub fn is_invalid(&self) -> bool {
189        self.0 == u32::MAX
190    }
191}
192
193impl From<u32> for DenseIdx {
194    fn from(val: u32) -> Self {
195        Self(val)
196    }
197}
198
199impl From<usize> for DenseIdx {
200    fn from(val: usize) -> Self {
201        Self(val as u32)
202    }
203}
204
205impl fmt::Display for DenseIdx {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        write!(f, "{}", self.0)
208    }
209}
210
211/// UniId: 44-character base32 multibase string (SHA3-256)
212#[derive(Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
213pub struct UniId([u8; 32]);
214
215impl UniId {
216    pub fn from_bytes(bytes: [u8; 32]) -> Self {
217        Self(bytes)
218    }
219
220    /// Parses a UniId from a multibase-encoded string.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if:
225    /// - The string is not valid multibase
226    /// - The encoding is not Base32Lower (the canonical format for UniId)
227    /// - The decoded length is not exactly 32 bytes
228    ///
229    /// # Security
230    ///
231    /// **CWE-345 (Insufficient Verification)**: Validates that the input uses
232    /// the expected Base32Lower encoding, rejecting other multibase formats
233    /// that could cause interoperability issues or confusion.
234    pub fn from_multibase(s: &str) -> Result<Self> {
235        let (base, bytes) =
236            multibase::decode(s).map_err(|e| anyhow!("Multibase decode error: {}", e))?;
237
238        // Validate encoding matches our canonical format
239        if base != Base::Base32Lower {
240            return Err(anyhow!(
241                "UniId must use Base32Lower encoding, got {:?}",
242                base
243            ));
244        }
245
246        let inner: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
247            anyhow!("Invalid UniId length: expected 32 bytes, got {}", v.len())
248        })?;
249
250        Ok(Self(inner))
251    }
252
253    pub fn to_multibase(&self) -> String {
254        multibase::encode(Base::Base32Lower, self.0)
255    }
256
257    pub fn as_bytes(&self) -> &[u8; 32] {
258        &self.0
259    }
260}
261
262impl fmt::Debug for UniId {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(f, "UniId({})", self.to_multibase())
265    }
266}
267
268impl fmt::Display for UniId {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        write!(f, "{}", self.to_multibase())
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_vid_basic() {
280        let vid = Vid::new(12345);
281        assert_eq!(vid.as_u64(), 12345);
282        assert!(!vid.is_invalid());
283    }
284
285    #[test]
286    fn test_vid_invalid() {
287        let vid = Vid::INVALID;
288        assert!(vid.is_invalid());
289        assert_eq!(vid.as_u64(), u64::MAX);
290    }
291
292    #[test]
293    fn test_vid_from_str() {
294        let vid: Vid = "42".parse().unwrap();
295        assert_eq!(vid.as_u64(), 42);
296
297        // Round-trip through Display and FromStr
298        let original = Vid::new(12345678);
299        let s = original.to_string();
300        let parsed: Vid = s.parse().unwrap();
301        assert_eq!(original, parsed);
302
303        // Error cases
304        assert!("invalid".parse::<Vid>().is_err());
305        assert!("".parse::<Vid>().is_err());
306    }
307
308    #[test]
309    fn test_eid_basic() {
310        let eid = Eid::new(67890);
311        assert_eq!(eid.as_u64(), 67890);
312        assert!(!eid.is_invalid());
313    }
314
315    #[test]
316    fn test_eid_invalid() {
317        let eid = Eid::INVALID;
318        assert!(eid.is_invalid());
319        assert_eq!(eid.as_u64(), u64::MAX);
320    }
321
322    #[test]
323    fn test_eid_from_str() {
324        let eid: Eid = "100".parse().unwrap();
325        assert_eq!(eid.as_u64(), 100);
326
327        // Round-trip through Display and FromStr
328        let original = Eid::new(0xABCDEF);
329        let s = original.to_string();
330        let parsed: Eid = s.parse().unwrap();
331        assert_eq!(original, parsed);
332
333        // Error cases
334        assert!("invalid".parse::<Eid>().is_err());
335    }
336
337    #[test]
338    fn test_dense_idx() {
339        let idx = DenseIdx::new(100);
340        assert_eq!(idx.as_usize(), 100);
341        assert_eq!(idx.as_u32(), 100);
342        assert!(!idx.is_invalid());
343
344        let invalid = DenseIdx::INVALID;
345        assert!(invalid.is_invalid());
346    }
347
348    #[test]
349    fn test_uni_id_multibase() {
350        let bytes = [0u8; 32];
351        let uid = UniId(bytes);
352        let s = uid.to_multibase();
353        let decoded = UniId::from_multibase(&s).unwrap();
354        assert_eq!(uid, decoded);
355    }
356
357    /// Security tests for CWE-345 (Insufficient Verification).
358    mod security_tests {
359        use super::*;
360
361        /// CWE-345: UniId should reject non-Base32Lower encodings.
362        #[test]
363        fn test_uni_id_rejects_wrong_encoding() {
364            // Create a Base58Btc encoded string (different from our Base32Lower)
365            let bytes = [0u8; 32];
366            let base58_encoded = multibase::encode(multibase::Base::Base58Btc, bytes);
367
368            let result = UniId::from_multibase(&base58_encoded);
369            assert!(result.is_err());
370            assert!(
371                result
372                    .unwrap_err()
373                    .to_string()
374                    .contains("Base32Lower encoding")
375            );
376        }
377
378        /// CWE-345: UniId should reject wrong length.
379        #[test]
380        fn test_uni_id_rejects_wrong_length() {
381            // Encode only 16 bytes instead of 32
382            let short_bytes = [0u8; 16];
383            let encoded = multibase::encode(Base::Base32Lower, short_bytes);
384
385            let result = UniId::from_multibase(&encoded);
386            assert!(result.is_err());
387            assert!(
388                result
389                    .unwrap_err()
390                    .to_string()
391                    .contains("expected 32 bytes")
392            );
393        }
394    }
395}