ubl_types/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3#![cfg_attr(not(feature = "std"), no_std)]
4//! Shared atomic identifiers, DIM helpers, and parsers reused across the stack.
5
6extern crate alloc;
7use alloc::string::{String, ToString};
8use serde::{Deserialize, Serialize};
9
10#[cfg(feature = "strict")]
11use regex::Regex;
12
13/// Macro para newtypes de IDs simples (String-based).
14#[macro_export]
15macro_rules! newtype_id {
16    ($name:ident) => {
17        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18        #[serde(transparent)]
19        #[doc = concat!(stringify!($name), " identifier newtype (string, trimmed; strict regex when feature=\"strict\").")]
20        pub struct $name(pub String);
21        impl core::fmt::Display for $name {
22            fn fmt(&self, f:&mut core::fmt::Formatter<'_>)->core::fmt::Result{ f.write_str(&self.0) }
23        }
24        impl core::str::FromStr for $name {
25            type Err = &'static str;
26            fn from_str(s:&str)->Result<Self,Self::Err>{
27                let s=s.trim();
28                if s.is_empty(){return Err("empty");}
29                #[cfg(feature="strict")]
30                {
31                    // ids alfanuméricos, -, _, :, . (simples e estável)
32                    static PAT: &str = r"^[A-Za-z0-9._:-]{1,128}$";
33                    let re = Regex::new(PAT).unwrap();
34                    if !re.is_match(s){ return Err("invalid chars"); }
35                }
36                Ok(Self(s.to_string()))
37            }
38        }
39    };
40}
41
42newtype_id!(AppId);
43newtype_id!(TenantId);
44newtype_id!(NodeId);
45newtype_id!(ActorId);
46newtype_id!(TraceId);
47
48/// Dimensão (DIM) do protocolo (u16, big-endian no fio).
49#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub struct Dim(pub u16);
51impl Dim {
52    /// Converte "0x00A1" ou "161" para `Dim`.
53    ///
54    /// # Errors
55    ///
56    /// Retorna erro se o valor não for hexadecimal ou decimal válido.
57    pub fn parse(s: &str) -> Result<Self, &'static str> {
58        let s = s.trim();
59        s.strip_prefix("0x").map_or_else(
60            || s.parse::<u16>().map(Dim).map_err(|_| "bad dec"),
61            |h| u16::from_str_radix(h, 16).map(Dim).map_err(|_| "bad hex"),
62        )
63    }
64    /// Cria a partir de string hexa (sem "0x").
65    ///
66    /// # Errors
67    ///
68    /// Retorna erro se o valor não for hexadecimal válido.
69    pub fn from_hex(h: &str) -> Result<Self, &'static str> {
70        u16::from_str_radix(h, 16).map(Dim).map_err(|_| "bad hex")
71    }
72    /// Representação "0xHHHH".
73    #[must_use]
74    pub fn to_hex(self) -> alloc::string::String {
75        alloc::format!("0x{:04X}", self.0)
76    }
77    /// Valor bruto.
78    #[must_use]
79    pub const fn as_u16(self) -> u16 {
80        self.0
81    }
82}
83
84#[cfg(feature = "ulid")]
85/// Geradores ULID para IDs comuns.
86pub mod gen {
87    use super::{ActorId, TraceId};
88    /// Gera `TraceId` com ULID.
89    #[must_use]
90    pub fn new_ulid_trace() -> TraceId {
91        TraceId(ulid::Ulid::new().to_string())
92    }
93    /// Gera `ActorId` com ULID.
94    #[must_use]
95    pub fn new_ulid_actor() -> ActorId {
96        ActorId(ulid::Ulid::new().to_string())
97    }
98}
99
100// ══════════════════════════════════════════════════════════════════════════════
101// Cryptographic primitive wrappers (hex-serialized)
102// ══════════════════════════════════════════════════════════════════════════════
103
104use core::fmt;
105use serde::{Deserializer, Serializer};
106use thiserror::Error;
107
108/// 32-byte Content ID (BLAKE3 hash). Serializes as lowercase hex (no `0x` prefix).
109#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
110pub struct Cid32(pub [u8; 32]);
111
112impl fmt::Debug for Cid32 {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "Cid32({})", hex::encode(self.0))
115    }
116}
117impl fmt::Display for Cid32 {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.write_str(&hex::encode(self.0))
120    }
121}
122impl Serialize for Cid32 {
123    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
124        s.serialize_str(&hex::encode(self.0))
125    }
126}
127impl<'de> Deserialize<'de> for Cid32 {
128    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
129        let s = <&str>::deserialize(d)?;
130        let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
131        if bytes.len() != 32 {
132            return Err(serde::de::Error::custom("Cid32 must be 32 bytes"));
133        }
134        let mut out = [0u8; 32];
135        out.copy_from_slice(&bytes);
136        Ok(Cid32(out))
137    }
138}
139impl Cid32 {
140    /// Creates a `Cid32` from a hex string.
141    ///
142    /// # Errors
143    ///
144    /// Returns `AtomError::Hex` if decoding fails or `AtomError::Length` if not 32 bytes.
145    pub fn from_hex(s: &str) -> Result<Self, AtomError> {
146        let bytes = hex::decode(s).map_err(|_| AtomError::Hex)?;
147        if bytes.len() != 32 {
148            return Err(AtomError::Length { expected: 32, actual: bytes.len() });
149        }
150        let mut out = [0u8; 32];
151        out.copy_from_slice(&bytes);
152        Ok(Cid32(out))
153    }
154    /// Returns the hex representation.
155    #[must_use]
156    pub fn to_hex(&self) -> String {
157        hex::encode(self.0)
158    }
159}
160
161/// 32-byte Ed25519 public key. Serializes as lowercase hex.
162#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
163pub struct PublicKeyBytes(pub [u8; 32]);
164
165impl fmt::Debug for PublicKeyBytes {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        write!(f, "PublicKeyBytes({})", hex::encode(self.0))
168    }
169}
170impl fmt::Display for PublicKeyBytes {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        f.write_str(&hex::encode(self.0))
173    }
174}
175impl Serialize for PublicKeyBytes {
176    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
177        s.serialize_str(&hex::encode(self.0))
178    }
179}
180impl<'de> Deserialize<'de> for PublicKeyBytes {
181    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
182        let s = <&str>::deserialize(d)?;
183        let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
184        if bytes.len() != 32 {
185            return Err(serde::de::Error::custom("PublicKeyBytes must be 32 bytes"));
186        }
187        let mut out = [0u8; 32];
188        out.copy_from_slice(&bytes);
189        Ok(PublicKeyBytes(out))
190    }
191}
192
193/// 64-byte Ed25519 signature. Serializes as lowercase hex.
194#[derive(Clone, Copy, PartialEq, Eq, Hash)]
195pub struct SignatureBytes(pub [u8; 64]);
196
197impl Default for SignatureBytes {
198    fn default() -> Self {
199        Self([0u8; 64])
200    }
201}
202
203impl fmt::Debug for SignatureBytes {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "SignatureBytes({}..)", &hex::encode(self.0)[..16])
206    }
207}
208impl fmt::Display for SignatureBytes {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        f.write_str(&hex::encode(self.0))
211    }
212}
213impl Serialize for SignatureBytes {
214    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
215        s.serialize_str(&hex::encode(self.0))
216    }
217}
218impl<'de> Deserialize<'de> for SignatureBytes {
219    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
220        let s = <&str>::deserialize(d)?;
221        let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
222        if bytes.len() != 64 {
223            return Err(serde::de::Error::custom("SignatureBytes must be 64 bytes"));
224        }
225        let mut out = [0u8; 64];
226        out.copy_from_slice(&bytes);
227        Ok(SignatureBytes(out))
228    }
229}
230
231/// Textual intent with canonical bytes (whitespace-insensitive).
232#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
233pub struct Intent {
234    /// Original text received (for audit trail).
235    pub raw: String,
236    /// Canonical bytes (e.g., NFC + collapsed whitespace).
237    pub canon: alloc::vec::Vec<u8>,
238}
239
240impl Intent {
241    /// Creates an intent, collapsing whitespace deterministically.
242    #[must_use]
243    pub fn from_raw(raw: impl Into<String>) -> Self {
244        let raw = raw.into();
245        let canon = normalize_ws(&raw).into_bytes();
246        Self { raw, canon }
247    }
248    /// Returns canonical bytes.
249    #[must_use]
250    pub fn as_bytes(&self) -> &[u8] {
251        &self.canon
252    }
253}
254
255/// Deterministic whitespace normalization (ASCII-first, collapse runs).
256fn normalize_ws(s: &str) -> String {
257    let mut out = String::with_capacity(s.len());
258    let mut prev_space = false;
259    for ch in s.chars() {
260        let is_space = ch.is_whitespace();
261        if is_space {
262            if !prev_space {
263                out.push(' ');
264            }
265        } else {
266            out.push(ch);
267        }
268        prev_space = is_space;
269    }
270    out.trim().to_string()
271}
272
273/// Shared basic errors for atomic types.
274#[derive(Debug, Error)]
275pub enum AtomError {
276    /// Hex decoding failed.
277    #[error("hex decode error")]
278    Hex,
279    /// Length mismatch.
280    #[error("length mismatch: expected {expected}, got {actual}")]
281    Length {
282        /// Expected length.
283        expected: usize,
284        /// Actual length.
285        actual: usize,
286    },
287    /// Invalid intent format.
288    #[error("invalid intent")]
289    InvalidIntent,
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn cid32_roundtrip() {
298        let c = Cid32([0xAB; 32]);
299        let j = serde_json::to_string(&c).unwrap();
300        assert_eq!(j.len(), 66); // " + 64 hex + "
301        let de: Cid32 = serde_json::from_str(&j).unwrap();
302        assert_eq!(c.0, de.0);
303    }
304
305    #[test]
306    fn intent_ws_canonical() {
307        let i1 = Intent::from_raw("  hello   world ");
308        let i2 = Intent::from_raw("hello world");
309        assert_eq!(i1.canon, i2.canon);
310    }
311
312    #[test]
313    fn pk_sig_roundtrip() {
314        let pk = PublicKeyBytes([0x22; 32]);
315        let sig = SignatureBytes([0x33; 64]);
316        let jp = serde_json::to_string(&pk).unwrap();
317        let js = serde_json::to_string(&sig).unwrap();
318        let _dpk: PublicKeyBytes = serde_json::from_str(&jp).unwrap();
319        let _ds: SignatureBytes = serde_json::from_str(&js).unwrap();
320    }
321}