Skip to main content

armour_typ/
typ.rs

1use serde::{Serialize, Serializer, ser::SerializeMap};
2use xxhash_rust::xxh3::Xxh3;
3
4use crate::scalar::ScalarTyp;
5
6pub type NamedField = (&'static str, Typ);
7pub type Arr<T> = &'static [T];
8pub type Str = &'static str;
9pub type Map<T> = Arr<(u8, T)>;
10
11#[derive(PartialEq, Clone, Serialize, Debug, Copy, Hash)]
12#[serde(tag = "type", content = "data")]
13pub enum Fields {
14    Named(Arr<NamedField>),
15    Unnamed(Arr<Typ>),
16}
17
18/**
19named structs, unnamed structs and tuples
20если нет названий полей в структуре, то это неименованная структура или кортеж.
21*/
22#[derive(PartialEq, Clone, Serialize, Debug, Copy, Hash)]
23pub struct StructType {
24    pub name: &'static str,
25    pub fields: Fields,
26}
27
28fn variants<S, T>(variants: Map<T>, serializer: S) -> Result<S::Ok, S::Error>
29where
30    S: Serializer,
31    T: Serialize,
32{
33    let mut map = serializer.serialize_map(Some(variants.len()))?;
34    for (key, value) in variants {
35        map.serialize_entry(key, value)?;
36    }
37    map.end()
38}
39
40#[derive(PartialEq, Clone, Serialize, Debug, Copy, Hash)]
41pub struct EnumType {
42    pub name: Str,
43    #[serde(serialize_with = "variants")]
44    pub variants: Map<NamedField>,
45}
46
47#[derive(PartialEq, Eq, Clone, Serialize, Debug, Copy, Hash)]
48pub struct SimpleEnumType {
49    pub name: Str,
50    #[serde(serialize_with = "variants")]
51    pub variants: Map<Str>,
52}
53
54#[derive(PartialEq, Clone, Debug, Copy, Hash)]
55pub enum Typ {
56    Scalar(ScalarTyp),
57    Array(u32, &'static Typ),
58    Vec(&'static Typ),
59    Optional(&'static Typ),
60    SimpleEnum(SimpleEnumType),
61    Struct(StructType),
62    Enum(EnumType),
63    Custom(&'static str, &'static [Typ]),
64}
65
66/// Composite-only mirror for serde; leaves serialize via [`ScalarTyp`] (flat wire).
67#[derive(Serialize)]
68#[serde(tag = "type", content = "data")]
69enum TypComposite {
70    Array(u32, &'static Typ),
71    Vec(&'static Typ),
72    Optional(&'static Typ),
73    SimpleEnum(SimpleEnumType),
74    Struct(StructType),
75    Enum(EnumType),
76    Custom(&'static str, &'static [Typ]),
77}
78
79impl Serialize for Typ {
80    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
81        match self {
82            Typ::Scalar(s) => s.serialize(serializer),
83            Typ::Array(n, inner) => TypComposite::Array(*n, inner).serialize(serializer),
84            Typ::Vec(inner) => TypComposite::Vec(inner).serialize(serializer),
85            Typ::Optional(inner) => TypComposite::Optional(inner).serialize(serializer),
86            Typ::SimpleEnum(e) => TypComposite::SimpleEnum(*e).serialize(serializer),
87            Typ::Struct(s) => TypComposite::Struct(*s).serialize(serializer),
88            Typ::Enum(e) => TypComposite::Enum(*e).serialize(serializer),
89            Typ::Custom(name, args) => TypComposite::Custom(name, args).serialize(serializer),
90        }
91    }
92}
93
94/// Format tag prefixing every `Typ::h` digest. Bump the trailing version when the
95/// canonical encoding below changes (it invalidates all persisted/protocol hashes).
96const HASH_FORMAT_TAG: &[u8] = b"armour-typ/typ/v1";
97
98/// Write a length as a fixed-width `u64` LE — NEVER `usize`, so the digest is
99/// independent of the target's pointer width (`wasm32` vs 64-bit). This is the
100/// whole point of the manual traversal; see `width_independent` test below.
101#[inline]
102fn hash_len(hasher: &mut Xxh3, len: usize) {
103    hasher.update(&(len as u64).to_le_bytes());
104}
105
106#[inline]
107fn hash_str(hasher: &mut Xxh3, value: &str) {
108    hash_len(hasher, value.len());
109    hasher.update(value.as_bytes());
110}
111
112fn hash_scalar(hasher: &mut Xxh3, scalar: &ScalarTyp) {
113    use ScalarTyp::*;
114    // One explicit u8 tag per variant — do NOT delegate to derive `Hash`, whose
115    // enum discriminant is an `isize` (width-dependent via `write_isize`).
116    let tag: u8 = match scalar {
117        Bool => 0,
118        U8 => 1,
119        U16 => 2,
120        U32 => 3,
121        U64 => 4,
122        I32 => 5,
123        I64 => 6,
124        F32 => 7,
125        F64 => 8,
126        Str => 9,
127        Datetime => 10,
128        Timestamp => 11,
129        Decimal => 12,
130        Id32 => 13,
131        Id64 => 14,
132        Fuid => 15,
133        LowId => 16,
134        Bytes => 17,
135        Void => 18,
136        RustJson => 19,
137        JsonBytes => 20,
138        ArrayBytes(_) => 21,
139    };
140    hasher.update(&[tag]);
141    if let ArrayBytes(n) = scalar {
142        hasher.update(&n.to_le_bytes());
143    }
144}
145
146fn hash_fields(hasher: &mut Xxh3, fields: &Fields) {
147    match fields {
148        Fields::Named(items) => {
149            hasher.update(&[0u8]);
150            hash_len(hasher, items.len());
151            for (name, typ) in *items {
152                hash_str(hasher, name);
153                hash_typ(hasher, typ);
154            }
155        }
156        Fields::Unnamed(items) => {
157            hasher.update(&[1u8]);
158            hash_len(hasher, items.len());
159            for typ in *items {
160                hash_typ(hasher, typ);
161            }
162        }
163    }
164}
165
166fn hash_typ(hasher: &mut Xxh3, typ: &Typ) {
167    match typ {
168        Typ::Scalar(s) => {
169            hasher.update(&[0u8]);
170            hash_scalar(hasher, s);
171        }
172        Typ::Array(n, inner) => {
173            hasher.update(&[1u8]);
174            hasher.update(&n.to_le_bytes());
175            hash_typ(hasher, inner);
176        }
177        Typ::Vec(inner) => {
178            hasher.update(&[2u8]);
179            hash_typ(hasher, inner);
180        }
181        Typ::Optional(inner) => {
182            hasher.update(&[3u8]);
183            hash_typ(hasher, inner);
184        }
185        Typ::SimpleEnum(e) => {
186            hasher.update(&[4u8]);
187            hash_str(hasher, e.name);
188            hash_len(hasher, e.variants.len());
189            for (discr, name) in e.variants {
190                hasher.update(&[*discr]);
191                hash_str(hasher, name);
192            }
193        }
194        Typ::Struct(s) => {
195            hasher.update(&[5u8]);
196            hash_str(hasher, s.name);
197            hash_fields(hasher, &s.fields);
198        }
199        Typ::Enum(e) => {
200            hasher.update(&[6u8]);
201            hash_str(hasher, e.name);
202            hash_len(hasher, e.variants.len());
203            for (discr, (name, payload)) in e.variants {
204                hasher.update(&[*discr]);
205                hash_str(hasher, name);
206                hash_typ(hasher, payload);
207            }
208        }
209        Typ::Custom(name, args) => {
210            hasher.update(&[7u8]);
211            hash_str(hasher, name);
212            hash_len(hasher, args.len());
213            for arg in *args {
214                hash_typ(hasher, arg);
215            }
216        }
217    }
218}
219
220impl Typ {
221    /// Schema hash persisted in armdb (`db.info`/`CollectionInfo`) and checked on
222    /// open, and used as the schema-negotiation fast-path hint.
223    ///
224    /// The traversal is **pointer-width-independent by construction**: it only ever
225    /// feeds fixed-width bytes into the hasher (u8 tags, `u64` LE lengths, `u32` LE
226    /// scalar counts, raw string bytes). It deliberately does NOT use derive
227    /// `std::hash::Hash`, whose `[T]` length prefix (`write_usize`) and enum
228    /// discriminant (`write_isize`) differ between `wasm32` and 64-bit targets.
229    ///
230    /// Byte output MUST stay stable; drift is caught by the golden test below.
231    pub fn h(&self) -> u64 {
232        let mut hasher = Xxh3::new();
233        hasher.update(HASH_FORMAT_TAG);
234        hash_typ(&mut hasher, self);
235        hasher.digest()
236    }
237}
238
239/// Public schema-version hash used by hash-first schema negotiation.
240///
241/// This is the same deterministic xxh3-based hash as [`Typ::h`]. It is not a
242/// cryptographic hash; callers must treat equal hashes as a fast-path hint and
243/// retain schema-based fallback behavior for peer mismatches.
244#[inline]
245pub fn schema_hash(t: &Typ) -> u64 {
246    t.h()
247}
248
249#[cfg(test)]
250mod golden {
251    use super::*;
252    use crate::scalar::ScalarTyp;
253
254    /// One entry per `Typ` variant family. The third field is the expected
255    /// `typ_hash`, captured once from `print_golden` (Step 5). NEVER hand-edit
256    /// these values to silence a failure — a mismatch means the persisted
257    /// schema-hash format drifted and must be investigated.
258    fn golden_cases() -> Vec<(&'static str, Typ, u64)> {
259        use ScalarTyp::*;
260        vec![
261            ("Bool", Typ::Scalar(Bool), 0x844969503cd52142),
262            ("U8", Typ::Scalar(U8), 0xc8cadc679de83264),
263            ("U16", Typ::Scalar(U16), 0x31f140c9e9d24a6a),
264            ("U32", Typ::Scalar(U32), 0xb6e382b85cc8ad16),
265            ("U64", Typ::Scalar(U64), 0x3e2513ed9422dbf3),
266            ("I32", Typ::Scalar(I32), 0x3eed686985c17e30),
267            ("I64", Typ::Scalar(I64), 0x6dcdfb5efac1eff5),
268            ("F32", Typ::Scalar(F32), 0xd4256e6355df3aa6),
269            ("F64", Typ::Scalar(F64), 0x25b32f6d7a5db6d7),
270            ("Str", Typ::Scalar(Str), 0x2aa6e56e9ce67c6d),
271            ("Datetime", Typ::Scalar(Datetime), 0xf789bd4fa70b626b),
272            ("Timestamp", Typ::Scalar(Timestamp), 0x31205013816d027c),
273            ("Decimal", Typ::Scalar(Decimal), 0x8bda406d6c8e133b),
274            ("Id32", Typ::Scalar(Id32), 0x30e0670bf6dea824),
275            ("Id64", Typ::Scalar(Id64), 0xc1c4537aede104ae),
276            ("Fuid", Typ::Scalar(Fuid), 0x313781a3f9e8ffbb),
277            ("LowId", Typ::Scalar(LowId), 0x09b9aaafe34aeb45),
278            ("Bytes", Typ::Scalar(Bytes), 0x08276244715f6ff1),
279            ("Void", Typ::Scalar(Void), 0xed774f18fb9a6e91),
280            ("RustJson", Typ::Scalar(RustJson), 0x3874a1693f60d5d7),
281            ("JsonBytes", Typ::Scalar(JsonBytes), 0xb4ccb5d58b3e668b),
282            ("ArrayBytes", Typ::Scalar(ArrayBytes(4)), 0x58f9ebfb19da8930),
283            (
284                "Array",
285                Typ::Array(3, &Typ::Scalar(U64)),
286                0xe8fb2cd993687efe,
287            ),
288            ("Vec", Typ::Vec(&Typ::Scalar(U64)), 0x0def01338fe2656c),
289            (
290                "Optional",
291                Typ::Optional(&Typ::Scalar(U64)),
292                0xa03ca0cb992f08da,
293            ),
294            (
295                "SimpleEnum",
296                Typ::SimpleEnum(SimpleEnumType {
297                    name: "SE",
298                    variants: &[(0, "A"), (1, "B")],
299                }),
300                0x52315acdab3124db,
301            ),
302            (
303                "StructNamed",
304                Typ::Struct(StructType {
305                    name: "S",
306                    fields: Fields::Named(&[("a", Typ::Scalar(U64)), ("b", Typ::Scalar(Str))]),
307                }),
308                0xa453f5fe90303c85,
309            ),
310            (
311                "StructUnnamed",
312                Typ::Struct(StructType {
313                    name: "",
314                    fields: Fields::Unnamed(&[Typ::Scalar(U64), Typ::Scalar(Bool)]),
315                }),
316                0x067f1d66238d5e94,
317            ),
318            (
319                "Enum",
320                Typ::Enum(EnumType {
321                    name: "E",
322                    variants: &[(0, ("A", Typ::Scalar(U64))), (1, ("B", Typ::Scalar(Bool)))],
323                }),
324                0x9d717d3549fb9528,
325            ),
326            (
327                "Custom",
328                Typ::Custom("C", &[Typ::Scalar(U64), Typ::Scalar(Str)]),
329                0x04ff19c55360b278,
330            ),
331        ]
332    }
333
334    /// Run explicitly to (re)generate golden values:
335    /// `cargo test -p armour-typ golden::print_golden -- --ignored --nocapture`
336    #[test]
337    #[ignore]
338    fn print_golden() {
339        for (name, t, _) in golden_cases() {
340            println!("(\"{name}\", …, 0x{:016x}),", t.h());
341        }
342    }
343
344    #[test]
345    fn golden_typ_hash() {
346        for (name, t, expected) in golden_cases() {
347            assert_eq!(t.h(), expected, "typ_hash drift for variant `{name}`");
348        }
349    }
350
351    /// Exhaustiveness: new `ScalarTyp` variant fails to compile without a golden case.
352    #[allow(dead_code)]
353    fn _scalar_guard(s: &ScalarTyp) {
354        use ScalarTyp::*;
355        match s {
356            Bool | U8 | U16 | U32 | U64 | I32 | I64 | F32 | F64 | Str | Datetime | Timestamp
357            | Decimal | Id32 | Id64 | Fuid | LowId | Bytes | Void | RustJson | JsonBytes => {}
358            ArrayBytes(_) => {}
359        }
360    }
361
362    /// Exhaustiveness: new composite `Typ` variant fails to compile without a golden case.
363    #[allow(dead_code)]
364    fn _typ_guard(t: &Typ) {
365        match t {
366            Typ::Scalar(_) => {}
367            Typ::Array(_, _) => {}
368            Typ::Vec(_) => {}
369            Typ::Optional(_) => {}
370            Typ::SimpleEnum(_) => {}
371            Typ::Struct(_) => {}
372            Typ::Enum(_) => {}
373            Typ::Custom(_, _) => {}
374        }
375    }
376}
377
378#[cfg(test)]
379#[allow(dead_code)]
380mod schema_hash_tests {
381    use super::*;
382    use crate::get_type::GetType;
383    use crate::scalar::ScalarTyp;
384
385    #[derive(crate::GetType)]
386    struct NamedOrderA {
387        first: u32,
388        second: u64,
389    }
390
391    #[derive(crate::GetType)]
392    struct NamedOrderB {
393        second: u64,
394        first: u32,
395    }
396
397    #[test]
398    fn free_schema_hash_matches_typ_h() {
399        let typ = Typ::Vec(&Typ::Scalar(ScalarTyp::U64));
400        assert_eq!(schema_hash(&typ), typ.h());
401    }
402
403    #[test]
404    fn get_type_schema_hash_matches_type_constant_hash() {
405        assert_eq!(<u32 as GetType>::schema_hash(), <u32 as GetType>::TYPE.h());
406        assert_eq!(
407            <NamedOrderA as GetType>::schema_hash(),
408            <NamedOrderA as GetType>::TYPE.h()
409        );
410    }
411
412    #[test]
413    fn reordered_named_fields_have_distinct_hashes() {
414        assert_ne!(
415            <NamedOrderA as GetType>::schema_hash(),
416            <NamedOrderB as GetType>::schema_hash()
417        );
418    }
419
420    // --- distinctness invariants the manual traversal must preserve ---------
421
422    #[test]
423    fn named_and_unnamed_fields_differ() {
424        use ScalarTyp::*;
425        let named = Typ::Struct(StructType {
426            name: "S",
427            fields: Fields::Named(&[("a", Typ::Scalar(U64)), ("b", Typ::Scalar(Bool))]),
428        });
429        let unnamed = Typ::Struct(StructType {
430            name: "S",
431            fields: Fields::Unnamed(&[Typ::Scalar(U64), Typ::Scalar(Bool)]),
432        });
433        assert_ne!(named.h(), unnamed.h());
434    }
435
436    #[test]
437    fn scalar_variants_are_distinct() {
438        use ScalarTyp::*;
439        // A length-prefixed-only fix would still collide same-shape scalars; the
440        // per-variant u8 tag keeps them apart.
441        assert_ne!(Typ::Scalar(U32).h(), Typ::Scalar(I32).h());
442        assert_ne!(Typ::Scalar(U64).h(), Typ::Scalar(Timestamp).h());
443    }
444
445    #[test]
446    fn enum_variant_discriminant_value_matters() {
447        use ScalarTyp::*;
448        let a = Typ::Enum(EnumType {
449            name: "E",
450            variants: &[(0, ("A", Typ::Scalar(U64)))],
451        });
452        let b = Typ::Enum(EnumType {
453            name: "E",
454            variants: &[(1, ("A", Typ::Scalar(U64)))],
455        });
456        assert_ne!(a.h(), b.h());
457    }
458
459    #[test]
460    fn scalar_and_array_counts_matter() {
461        use ScalarTyp::*;
462        assert_ne!(
463            Typ::Scalar(ArrayBytes(4)).h(),
464            Typ::Scalar(ArrayBytes(8)).h()
465        );
466        assert_ne!(
467            Typ::Array(3, &Typ::Scalar(U64)).h(),
468            Typ::Array(4, &Typ::Scalar(U64)).h()
469        );
470    }
471}