Skip to main content

active_infinity/
decode.rs

1//! Decoder: `AINF` bytes → canonical [`InfinityModule`].
2//!
3//! Decoding never runs on unvalidated bytes: [`decode`] validates first and
4//! returns a [`crate::validate::Validated`] internally, so a decoded module is
5//! guaranteed to satisfy every structural invariant (bounds, sorted tables,
6//! acyclicity, reachability, hash). Materialisation is a straightforward
7//! translation of validated records into the owned IR; the zero-copy path
8//! lives in [`crate::view`] for callers that do not need ownership.
9
10// Module-level: item docs for this layer live in docs/spec/layout.md and
11// docs/spec/streaming.md; the crate root keeps deny(missing_docs) for the
12// hand-written public surface.
13#![allow(missing_docs)]
14
15use crate::error::{Error, Result};
16use crate::ir::{
17    Annotation, AnnotationKind, Constraint, InfinityModule, Inode, InterpPart, NodeId, PatchOp,
18    ProvEntry, ProvValue, Provenance, SchemaFieldSpec, TypeSpec,
19};
20use crate::layout::{DecodeOpts, ANN_KIND_AI_IMMUTABLE, ANN_KIND_CONSTRAINT, ANN_KIND_INTENT};
21use crate::validate::{validate, Validated};
22
23/// Decode with default options (hash verified, canonical form not re-proved).
24pub fn decode(bytes: &[u8]) -> Result<InfinityModule> {
25    decode_with(bytes, DecodeOpts::default())
26}
27
28/// Decode with explicit options.
29pub fn decode_with(bytes: &[u8], opts: DecodeOpts) -> Result<InfinityModule> {
30    let validated = Validated::new(bytes, opts)?;
31    decode_validated(&validated)
32}
33
34/// Materialise an already validated image.
35pub fn decode_validated(validated: &Validated<'_>) -> Result<InfinityModule> {
36    let count = validated.node_count();
37    let mut nodes = Vec::with_capacity(count as usize);
38    for index in 0..count {
39        nodes.push(decode_node(validated, NodeId(index))?);
40    }
41    let root = validated.root()?;
42    let bindings: Vec<(String, NodeId)> = validated
43        .bindings()?
44        .into_iter()
45        .map(|(name, id)| (name.to_string(), id))
46        .collect();
47
48    let mut annotations =
49        Vec::with_capacity(validated.span(crate::layout::Section::Annotations).count as usize);
50    for index in 0..validated.span(crate::layout::Section::Annotations).count {
51        let (node, kind, a, b) = validated.annotation(index)?;
52        let kind = match kind {
53            ANN_KIND_INTENT => AnnotationKind::Intent(validated.string(a)?.to_string()),
54            ANN_KIND_CONSTRAINT => AnnotationKind::Constraint {
55                source: validated.string(a)?.to_string(),
56                expr: NodeId(b),
57            },
58            ANN_KIND_AI_IMMUTABLE => AnnotationKind::AiImmutable(a == 1),
59            _ => {
60                return Err(Error::Annotation {
61                    id: index,
62                    reason: "unknown annotation kind",
63                })
64            }
65        };
66        annotations.push(Annotation {
67            node: NodeId(node),
68            kind,
69        });
70    }
71
72    let provenance = validated.provenance()?;
73    let mut module = InfinityModule::from_parts(nodes, root, bindings, annotations, provenance);
74    module.set_extensions(decode_extensions(validated)?);
75    module.validate_structure(u32::MAX as u64, 0)?;
76    Ok(module)
77}
78
79/// Read the extension directory and its payloads back into the module.
80fn decode_extensions(validated: &Validated<'_>) -> Result<Vec<(u64, Vec<u8>)>> {
81    let count = validated.span(crate::layout::Section::Extensions).count;
82    let mut out = Vec::with_capacity(count as usize);
83    for index in 0..count {
84        let (tag, offset, len) = validated.extension_entry(index)?;
85        out.push((tag, validated.extension_payload(offset, len)?.to_vec()));
86    }
87    Ok(out)
88}
89
90fn decode_node(validated: &Validated<'_>, id: NodeId) -> Result<Inode> {
91    let rec = validated.node(id)?;
92    let node = match rec.kind {
93        0 => Inode::Null,
94        1 => Inode::Bool(rec.a == 1),
95        2 => Inode::Int(rec.a as i64),
96        3 => Inode::Float(f64::from_bits(rec.a)),
97        4 => Inode::Str(validated.string(rec.aux)?.to_string()),
98        5 => Inode::DurationMs(rec.a),
99        6 => Inode::Quantity {
100            value: NodeId(rec.aux),
101            unit: validated.unit(rec.a as u32)?,
102        },
103        7 => {
104            let mut elements = Vec::with_capacity(rec.b as usize);
105            for offset in rec.a..rec.a + rec.b {
106                elements.push(NodeId(validated.child(offset as u32)?));
107            }
108            Inode::Array { elements }
109        }
110        8 => {
111            let mut fields = Vec::with_capacity(rec.b as usize);
112            for offset in rec.a..rec.a + rec.b {
113                let (name, node) = validated.field(offset as u32)?;
114                fields.push((validated.string(name)?.to_string(), node));
115            }
116            Inode::Table { fields }
117        }
118        9 => Inode::Secret(crate::ir::SecretBytes::new(
119            validated.secret(rec.a as u32)?.to_vec(),
120        )),
121        10 => Inode::SecretRef {
122            locator: NodeId(rec.a as u32),
123        },
124        11 => Inode::Ref(NodeId(rec.a as u32)),
125        12 => Inode::Env {
126            name: validated.string(rec.aux)?.to_string(),
127            default: if rec.a == u32::MAX as u64 {
128                None
129            } else {
130                Some(NodeId(rec.a as u32))
131            },
132        },
133        13 => Inode::Ident(validated.string(rec.aux)?.to_string()),
134        14 => {
135            let mut parts = Vec::with_capacity(rec.b as usize);
136            for offset in rec.a..rec.a + rec.b {
137                let (tag, value) = validated.part(offset as u32)?;
138                parts.push(match tag {
139                    crate::layout::PART_LITERAL => {
140                        InterpPart::Literal(validated.string(value)?.to_string())
141                    }
142                    _ => InterpPart::Expr(NodeId(value)),
143                });
144            }
145            Inode::Interpolate { parts }
146        }
147        15 => Inode::Binary {
148            op: crate::ir::BinOp::from_code(rec.aux).ok_or(Error::Node {
149                id: id.0,
150                reason: "unknown binary operator",
151            })?,
152            lhs: NodeId(rec.a as u32),
153            rhs: NodeId(rec.b as u32),
154        },
155        16 => Inode::Unary {
156            op: crate::ir::UnOp::from_code(rec.aux).ok_or(Error::Node {
157                id: id.0,
158                reason: "unknown unary operator",
159            })?,
160            expr: NodeId(rec.a as u32),
161        },
162        17 => Inode::Constrained {
163            target: NodeId(rec.a as u32),
164            constraint: decode_constraint(validated, id, rec.aux, rec.b as u32, rec.c as u32)?,
165        },
166        18 => Inode::Patch(decode_patch(
167            validated,
168            id,
169            rec.aux,
170            rec.b as u32,
171            rec.c as u32,
172        )?),
173        19 => Inode::SchemaField(decode_schema_field(
174            validated,
175            id,
176            rec.aux,
177            rec.b as u32,
178            rec.c as u32,
179        )?),
180        20 => {
181            let mut fields = Vec::with_capacity(rec.b as usize);
182            for offset in rec.a..rec.a + rec.b {
183                let (name, node) = validated.field(offset as u32)?;
184                fields.push((validated.string(name)?.to_string(), node));
185            }
186            Inode::SchemaTable {
187                fields,
188                closed: rec.flags & crate::layout::NODE_FLAG_0 != 0,
189            }
190        }
191        21 => Inode::SchemaArray {
192            element: NodeId(rec.a as u32),
193        },
194        22 => Inode::Semantic {
195            tag: crate::ir::SemanticTag::from_code(rec.aux).ok_or(Error::Node {
196                id: id.0,
197                reason: "unknown semantic tag",
198            })?,
199            inner: NodeId(rec.a as u32),
200        },
201        23 => {
202            let (hash, _, _) = validated.blob_entry(rec.a as u32)?;
203            let payload = if rec.flags & crate::layout::NODE_FLAG_1 != 0 {
204                Some(validated.blob(rec.a as u32)?.to_vec())
205            } else {
206                None
207            };
208            Inode::Blob {
209                hash,
210                size: rec.b,
211                tainted: rec.flags & crate::layout::NODE_FLAG_0 != 0,
212                media_type: validated.string(rec.aux)?.to_string(),
213                payload,
214            }
215        }
216        24 => Inode::ContentAddressed {
217            media_type: NodeId(rec.a as u32),
218            data: NodeId(rec.b as u32),
219        },
220        _ => {
221            return Err(Error::Node {
222                id: id.0,
223                reason: "unknown node kind",
224            })
225        }
226    };
227    Ok(node)
228}
229
230fn decode_constraint(
231    validated: &Validated<'_>,
232    id: NodeId,
233    kind: u32,
234    start: u32,
235    len: u32,
236) -> Result<Constraint> {
237    let mut min = None;
238    let mut max = None;
239    let mut items = Vec::new();
240    let mut pattern = None;
241    let mut custom = None;
242    for offset in start..start + len {
243        let (tag, value) = validated.aux(offset)?;
244        match tag {
245            crate::layout::AUX_CONSTRAINT_MIN => min = Some(validated.number(value)?),
246            crate::layout::AUX_CONSTRAINT_MAX => max = Some(validated.number(value)?),
247            crate::layout::AUX_CONSTRAINT_ENUM_ITEM => {
248                items.push(validated.string(value)?.to_string())
249            }
250            crate::layout::AUX_CONSTRAINT_PATTERN => {
251                pattern = Some(validated.string(value)?.to_string())
252            }
253            crate::layout::AUX_CONSTRAINT_CUSTOM => {
254                custom = Some(validated.string(value)?.to_string())
255            }
256            _ => {
257                return Err(Error::Node {
258                    id: id.0,
259                    reason: "unknown constraint aux tag",
260                })
261            }
262        }
263    }
264    let constraint = match kind {
265        crate::layout::CONSTRAINT_KIND_MIN_MAX => Constraint::MinMax { min, max },
266        crate::layout::CONSTRAINT_KIND_ENUM => Constraint::Enum(items),
267        crate::layout::CONSTRAINT_KIND_PATTERN => {
268            Constraint::Pattern(pattern.ok_or(Error::Node {
269                id: id.0,
270                reason: "pattern constraint without a pattern",
271            })?)
272        }
273        crate::layout::CONSTRAINT_KIND_ODD => Constraint::Odd,
274        crate::layout::CONSTRAINT_KIND_CUSTOM => Constraint::Custom(custom.ok_or(Error::Node {
275            id: id.0,
276            reason: "custom constraint without a payload",
277        })?),
278        _ => {
279            return Err(Error::Node {
280                id: id.0,
281                reason: "unknown constraint kind",
282            })
283        }
284    };
285    Ok(constraint)
286}
287
288fn decode_patch(
289    validated: &Validated<'_>,
290    id: NodeId,
291    kind: u32,
292    start: u32,
293    len: u32,
294) -> Result<PatchOp> {
295    let mut path = None;
296    let mut value = None;
297    let mut to = None;
298    for offset in start..start + len {
299        let (tag, item) = validated.aux(offset)?;
300        match tag {
301            crate::layout::AUX_PATCH_PATH => path = Some(validated.string(item)?.to_string()),
302            crate::layout::AUX_PATCH_VALUE => value = Some(NodeId(item)),
303            crate::layout::AUX_PATCH_RENAME_TO => to = Some(validated.string(item)?.to_string()),
304            _ => {
305                return Err(Error::Node {
306                    id: id.0,
307                    reason: "unknown patch aux tag",
308                })
309            }
310        }
311    }
312    let path = path.ok_or(Error::Node {
313        id: id.0,
314        reason: "patch without a path",
315    })?;
316    let op = match kind {
317        crate::layout::PATCH_KIND_SET => PatchOp::Set {
318            path,
319            value: value.ok_or(Error::Node {
320                id: id.0,
321                reason: "set patch without a value",
322            })?,
323        },
324        crate::layout::PATCH_KIND_REMOVE => PatchOp::Remove { path },
325        crate::layout::PATCH_KIND_RENAME => PatchOp::Rename {
326            from: path,
327            to: to.ok_or(Error::Node {
328                id: id.0,
329                reason: "rename patch without a destination",
330            })?,
331        },
332        _ => {
333            return Err(Error::Node {
334                id: id.0,
335                reason: "unknown patch kind",
336            })
337        }
338    };
339    Ok(op)
340}
341
342fn decode_schema_field(
343    validated: &Validated<'_>,
344    id: NodeId,
345    type_id: u32,
346    start: u32,
347    len: u32,
348) -> Result<SchemaFieldSpec> {
349    let typ = decode_type(validated, type_id)?;
350    let mut spec = SchemaFieldSpec::bare(typ);
351    for offset in start..start + len {
352        let (tag, value) = validated.aux(offset)?;
353        match tag {
354            crate::layout::AUX_SCHEMA_MIN => spec.min = Some(NodeId(value)),
355            crate::layout::AUX_SCHEMA_MAX => spec.max = Some(NodeId(value)),
356            crate::layout::AUX_SCHEMA_MIN_LEN => spec.min_len = Some(NodeId(value)),
357            crate::layout::AUX_SCHEMA_MAX_LEN => spec.max_len = Some(NodeId(value)),
358            crate::layout::AUX_SCHEMA_PATTERN => spec.pattern = Some(NodeId(value)),
359            crate::layout::AUX_SCHEMA_DEFAULT => spec.default = Some(NodeId(value)),
360            crate::layout::AUX_SCHEMA_DEPRECATED => {
361                spec.deprecated = Some(validated.string(value)?.to_string())
362            }
363            _ => {
364                return Err(Error::Node {
365                    id: id.0,
366                    reason: "unknown schema aux tag",
367                })
368            }
369        }
370    }
371    Ok(spec)
372}
373
374fn decode_type(validated: &Validated<'_>, index: u32) -> Result<TypeSpec> {
375    let (code, flags, start, len) = validated.type_entry(index)?;
376    let spec = match code {
377        0 => TypeSpec::Null,
378        1 => TypeSpec::Bool,
379        2 => TypeSpec::Int,
380        3 => TypeSpec::Float,
381        4 => TypeSpec::String,
382        5 => TypeSpec::Duration,
383        6 => TypeSpec::Bytes,
384        7 => TypeSpec::Table {
385            closed: flags & 1 != 0,
386        },
387        8 => {
388            let mut items = Vec::new();
389            for offset in start..start + len {
390                let (tag, value) = validated.aux(offset)?;
391                if tag != crate::layout::AUX_TYPE_ENUM_ITEM {
392                    return Err(Error::Type {
393                        id: index,
394                        reason: "unexpected aux record",
395                    });
396                }
397                items.push(validated.string(value)?.to_string());
398            }
399            TypeSpec::Enum(items)
400        }
401        9 => TypeSpec::Secret,
402        10 => TypeSpec::Any,
403        11 => TypeSpec::Blob,
404        _ => {
405            return Err(Error::Type {
406                id: index,
407                reason: "unknown type code",
408            })
409        }
410    };
411    Ok(spec)
412}
413
414/// Decode only the provenance block (cheap metadata read).
415pub fn decode_provenance(bytes: &[u8]) -> Result<Option<Provenance>> {
416    let meta = validate(bytes)?;
417    let _ = meta;
418    let validated = Validated::new(bytes, DecodeOpts::default())?;
419    validated.provenance()
420}
421
422/// Decode only the annotations (cheap metadata read).
423pub fn decode_annotations(bytes: &[u8]) -> Result<Vec<Annotation>> {
424    let validated = Validated::new(bytes, DecodeOpts::default())?;
425    let mut out = Vec::new();
426    for index in 0..validated.span(crate::layout::Section::Annotations).count {
427        let (node, kind, a, b) = validated.annotation(index)?;
428        let kind = match kind {
429            ANN_KIND_INTENT => AnnotationKind::Intent(validated.string(a)?.to_string()),
430            ANN_KIND_CONSTRAINT => AnnotationKind::Constraint {
431                source: validated.string(a)?.to_string(),
432                expr: NodeId(b),
433            },
434            _ => AnnotationKind::AiImmutable(a == 1),
435        };
436        out.push(Annotation {
437            node: NodeId(node),
438            kind,
439        });
440    }
441    Ok(out)
442}
443
444/// Decode provenance entries into a stable, sorted vector of owned pairs.
445pub fn provenance_pairs(prov: &Provenance) -> Vec<(String, ProvValue)> {
446    let mut pairs: Vec<(String, ProvValue)> = prov
447        .entries
448        .iter()
449        .map(|entry: &ProvEntry| (entry.key.clone(), entry.value.clone()))
450        .collect();
451    pairs.sort_by(|a, b| a.0.cmp(&b.0));
452    pairs
453}