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