Skip to main content

vsh/
artifact.rs

1use std::collections::BTreeMap;
2use std::error::Error;
3use std::fmt;
4
5use vsh_monty::ExecutionStats;
6use vsh_policy::{RiskFlag, RiskManifest, RiskMetrics, read_set_digest, write_set_digest};
7use vsh_types::{
8    ContentVersion, DiffDigest, DiffEntry, DiffKind, DirectoryDigest, FileStamp, IntentDigest,
9    NodeKind, NodeState, PlatformFileId, PolicyDigest, ProgramDigest, ReadSetDigest,
10    RuntimeConfigDigest, SnapshotId, TransactionBinding, VPath, WriteSetDigest,
11};
12use vsh_vfs::{CanonicalDiff, ReadObservation, WritePrecondition};
13
14use crate::runtime::{ArtifactLimits, Receipt, RuntimeDecision, StageTimings};
15
16const ARTIFACT_MAGIC: &[u8; 8] = b"VSHPND01";
17
18#[derive(Clone)]
19pub(crate) struct PendingTransaction {
20    pub(crate) binding: TransactionBinding,
21    pub(crate) diff: CanonicalDiff,
22    pub(crate) read_set: BTreeMap<VPath, ReadObservation>,
23    pub(crate) write_set: BTreeMap<VPath, WritePrecondition>,
24    pub(crate) receipt: Receipt,
25}
26
27pub(crate) fn encode_pending(
28    artifact: &PendingTransaction,
29    limits: ArtifactLimits,
30) -> Result<Vec<u8>, ArtifactError> {
31    let mut output = Encoder::new(limits.max_bytes);
32    output.extend_from_slice(ARTIFACT_MAGIC)?;
33    encode_binding(&artifact.binding, &mut output)?;
34    output.push(u8::from(!artifact.receipt.changes.is_empty()))?;
35    encode_decision(&artifact.receipt.decision, &mut output)?;
36
37    let value_len =
38        postcard::experimental::serialized_size(&artifact.receipt.value).map_err(|source| {
39            ArtifactError::ValueCodec {
40                operation: "size",
41                detail: source.to_string(),
42            }
43        })?;
44    if value_len > limits.max_value_bytes {
45        return Err(ArtifactError::Limit {
46            field: "result value",
47            observed: value_len,
48            maximum: limits.max_value_bytes,
49        });
50    }
51    let mut value = Vec::new();
52    value
53        .try_reserve_exact(value_len)
54        .map_err(|source| ArtifactError::Allocation {
55            field: "result value",
56            requested: value_len,
57            detail: source.to_string(),
58        })?;
59    value.resize(value_len, 0);
60    postcard::to_slice(&artifact.receipt.value, value.as_mut_slice()).map_err(|source| {
61        ArtifactError::ValueCodec {
62            operation: "encode",
63            detail: source.to_string(),
64        }
65    })?;
66    encode_bytes(&value, &mut output)?;
67    if artifact.receipt.stdout.len() > limits.max_stdout_bytes {
68        return Err(ArtifactError::Limit {
69            field: "stdout",
70            observed: artifact.receipt.stdout.len(),
71            maximum: limits.max_stdout_bytes,
72        });
73    }
74    encode_bytes(artifact.receipt.stdout.as_bytes(), &mut output)?;
75    encode_execution_stats(artifact.receipt.execution, &mut output)?;
76    encode_timings(artifact.receipt.timings, &mut output)?;
77
78    if artifact.diff.entries().len() > limits.max_entries {
79        return Err(ArtifactError::Limit {
80            field: "diff entries",
81            observed: artifact.diff.entries().len(),
82            maximum: limits.max_entries,
83        });
84    }
85    encode_len(artifact.diff.entries().len(), &mut output)?;
86    for entry in artifact.diff.entries() {
87        encode_path(&entry.path, limits, &mut output)?;
88        encode_optional_state(entry.before, &mut output)?;
89        encode_optional_state(entry.after, &mut output)?;
90        output.push(diff_kind_tag(entry.kind))?;
91    }
92
93    if artifact.read_set.len() > limits.max_dependencies {
94        return Err(ArtifactError::Limit {
95            field: "read dependencies",
96            observed: artifact.read_set.len(),
97            maximum: limits.max_dependencies,
98        });
99    }
100    encode_len(artifact.read_set.len(), &mut output)?;
101    for (path, observation) in &artifact.read_set {
102        encode_path(path, limits, &mut output)?;
103        match observation.metadata {
104            None => output.push(0)?,
105            Some(None) => output.push(1)?,
106            Some(Some(state)) => {
107                output.push(2)?;
108                encode_state(state, &mut output)?;
109            }
110        }
111        encode_optional_digest(
112            observation.content.map(|value| *value.as_bytes()),
113            &mut output,
114        )?;
115        encode_optional_digest(
116            observation.directory.map(|value| *value.as_bytes()),
117            &mut output,
118        )?;
119    }
120
121    if artifact.write_set.len() > limits.max_dependencies {
122        return Err(ArtifactError::Limit {
123            field: "write dependencies",
124            observed: artifact.write_set.len(),
125            maximum: limits.max_dependencies,
126        });
127    }
128    encode_len(artifact.write_set.len(), &mut output)?;
129    for (path, precondition) in &artifact.write_set {
130        encode_path(path, limits, &mut output)?;
131        encode_optional_state(precondition.expected, &mut output)?;
132    }
133
134    Ok(output.finish())
135}
136
137pub(crate) fn decode_pending(
138    bytes: &[u8],
139    limits: ArtifactLimits,
140) -> Result<PendingTransaction, ArtifactError> {
141    if bytes.len() > limits.max_bytes {
142        return Err(ArtifactError::Limit {
143            field: "pending artifact",
144            observed: bytes.len(),
145            maximum: limits.max_bytes,
146        });
147    }
148    let mut decoder = Decoder::new(bytes);
149    if decoder.take(ARTIFACT_MAGIC.len())? != ARTIFACT_MAGIC {
150        return Err(decoder.corrupt("invalid pending-artifact header"));
151    }
152    let binding = decode_binding(&mut decoder)?;
153    let full_detail = match decoder.byte()? {
154        0 => false,
155        1 => true,
156        _ => return Err(decoder.corrupt("invalid receipt-detail tag")),
157    };
158    let decision = decode_decision(&mut decoder)?;
159    let value_bytes = decoder.length_prefixed(limits.max_value_bytes, "result value")?;
160    let value = postcard::from_bytes(value_bytes).map_err(|source| ArtifactError::ValueCodec {
161        operation: "decode",
162        detail: source.to_string(),
163    })?;
164    let stdout = decoder.string(limits.max_stdout_bytes, "stdout")?;
165    let execution = decode_execution_stats(&mut decoder)?;
166    let timings = decode_timings(&mut decoder)?;
167
168    let diff = decode_diff(&mut decoder, limits)?;
169    let read_set = decode_read_set(&mut decoder, limits)?;
170    let write_set = decode_write_set(&mut decoder, limits)?;
171    decoder.finish()?;
172
173    if binding.diff != diff.digest()
174        || binding.read_set != read_set_digest(&read_set)
175        || binding.write_set != write_set_digest(&write_set)
176    {
177        return Err(ArtifactError::BindingMismatch);
178    }
179    let state = match &decision {
180        RuntimeDecision::AutoApproved => vsh_types::TransactionState::AutoApproved,
181        RuntimeDecision::PendingApproval(_) => vsh_types::TransactionState::PendingApproval,
182        RuntimeDecision::Denied(_) => return Err(decoder.corrupt("denied artifact is pending")),
183    };
184    let changes = if full_detail {
185        diff.entries().to_vec()
186    } else {
187        Vec::new()
188    };
189    let receipt = Receipt {
190        transaction: binding.transaction_id(),
191        base_snapshot: binding.base_snapshot,
192        state,
193        decision,
194        diff: diff.digest(),
195        changed_paths: diff.entries().len(),
196        changes,
197        value,
198        stdout,
199        execution,
200        timings,
201        commit: None,
202    };
203    Ok(PendingTransaction {
204        binding,
205        diff,
206        read_set,
207        write_set,
208        receipt,
209    })
210}
211
212fn encode_binding(binding: &TransactionBinding, output: &mut Encoder) -> Result<(), ArtifactError> {
213    output.extend_from_slice(binding.base_snapshot.as_bytes())?;
214    output.extend_from_slice(binding.diff.as_bytes())?;
215    output.extend_from_slice(binding.read_set.as_bytes())?;
216    output.extend_from_slice(binding.write_set.as_bytes())?;
217    output.extend_from_slice(binding.program.as_bytes())?;
218    output.extend_from_slice(binding.policy.as_bytes())?;
219    output.extend_from_slice(binding.runtime_config.as_bytes())?;
220    encode_optional_digest(binding.intent.map(|value| *value.as_bytes()), output)
221}
222
223fn decode_diff(
224    decoder: &mut Decoder<'_>,
225    limits: ArtifactLimits,
226) -> Result<CanonicalDiff, ArtifactError> {
227    let entry_count = decoder.length(limits.max_entries, "diff entries")?;
228    let mut entries = Vec::with_capacity(entry_count);
229    for _ in 0..entry_count {
230        let path = decoder.path(limits)?;
231        let before = decode_optional_state(decoder)?;
232        let after = decode_optional_state(decoder)?;
233        let kind = decode_diff_kind(decoder.byte()?)
234            .ok_or_else(|| decoder.corrupt("unknown diff-kind tag"))?;
235        entries.push(DiffEntry {
236            path,
237            before,
238            after,
239            kind,
240        });
241    }
242    CanonicalDiff::from_entries(entries)
243        .map_err(|_| decoder.corrupt("decoded diff is not canonical"))
244}
245
246fn decode_read_set(
247    decoder: &mut Decoder<'_>,
248    limits: ArtifactLimits,
249) -> Result<BTreeMap<VPath, ReadObservation>, ArtifactError> {
250    let count = decoder.length(limits.max_dependencies, "read dependencies")?;
251    let mut read_set = BTreeMap::new();
252    for _ in 0..count {
253        let path = decoder.path(limits)?;
254        let metadata = match decoder.byte()? {
255            0 => None,
256            1 => Some(None),
257            2 => Some(Some(decode_state(decoder)?)),
258            _ => return Err(decoder.corrupt("unknown metadata-observation tag")),
259        };
260        let content = decode_optional_digest(decoder)?.map(vsh_types::BlobId::from_bytes);
261        let directory = decode_optional_digest(decoder)?.map(DirectoryDigest::from_bytes);
262        if read_set
263            .insert(
264                path,
265                ReadObservation {
266                    metadata,
267                    content,
268                    directory,
269                },
270            )
271            .is_some()
272        {
273            return Err(decoder.corrupt("duplicate read dependency"));
274        }
275    }
276    Ok(read_set)
277}
278
279fn decode_write_set(
280    decoder: &mut Decoder<'_>,
281    limits: ArtifactLimits,
282) -> Result<BTreeMap<VPath, WritePrecondition>, ArtifactError> {
283    let count = decoder.length(limits.max_dependencies, "write dependencies")?;
284    let mut write_set = BTreeMap::new();
285    for _ in 0..count {
286        let path = decoder.path(limits)?;
287        let expected = decode_optional_state(decoder)?;
288        if write_set
289            .insert(path, WritePrecondition { expected })
290            .is_some()
291        {
292            return Err(decoder.corrupt("duplicate write dependency"));
293        }
294    }
295    Ok(write_set)
296}
297
298fn decode_binding(decoder: &mut Decoder<'_>) -> Result<TransactionBinding, ArtifactError> {
299    Ok(TransactionBinding {
300        base_snapshot: SnapshotId::from_bytes(decoder.digest()?),
301        diff: DiffDigest::from_bytes(decoder.digest()?),
302        read_set: ReadSetDigest::from_bytes(decoder.digest()?),
303        write_set: WriteSetDigest::from_bytes(decoder.digest()?),
304        program: ProgramDigest::from_bytes(decoder.digest()?),
305        policy: PolicyDigest::from_bytes(decoder.digest()?),
306        runtime_config: RuntimeConfigDigest::from_bytes(decoder.digest()?),
307        intent: decode_optional_digest(decoder)?.map(IntentDigest::from_bytes),
308    })
309}
310
311fn encode_decision(decision: &RuntimeDecision, output: &mut Encoder) -> Result<(), ArtifactError> {
312    match decision {
313        RuntimeDecision::AutoApproved => output.push(1)?,
314        RuntimeDecision::PendingApproval(manifest) => {
315            output.push(2)?;
316            encode_risk_manifest(manifest, output)?;
317        }
318        RuntimeDecision::Denied(_) => {
319            return Err(ArtifactError::Unsupported {
320                reason: "denied transactions cannot be pending",
321            });
322        }
323    }
324    Ok(())
325}
326
327fn decode_decision(decoder: &mut Decoder<'_>) -> Result<RuntimeDecision, ArtifactError> {
328    match decoder.byte()? {
329        1 => Ok(RuntimeDecision::AutoApproved),
330        2 => decode_risk_manifest(decoder).map(RuntimeDecision::PendingApproval),
331        _ => Err(decoder.corrupt("unknown runtime-decision tag")),
332    }
333}
334
335fn encode_risk_manifest(
336    manifest: &RiskManifest,
337    output: &mut Encoder,
338) -> Result<(), ArtifactError> {
339    encode_risk_metrics(manifest.metrics, output)?;
340    encode_len(manifest.flags.len(), output)?;
341    for flag in &manifest.flags {
342        output.push(risk_flag_tag(*flag))?;
343    }
344    output.extend_from_slice(manifest.policy.as_bytes())?;
345    Ok(())
346}
347
348fn decode_risk_manifest(decoder: &mut Decoder<'_>) -> Result<RiskManifest, ArtifactError> {
349    let metrics = decode_risk_metrics(decoder)?;
350    let count = decoder.length(32, "risk flags")?;
351    let mut flags = Vec::with_capacity(count);
352    for _ in 0..count {
353        flags.push(
354            decode_risk_flag(decoder.byte()?)
355                .ok_or_else(|| decoder.corrupt("unknown risk-flag tag"))?,
356        );
357    }
358    if !flags.windows(2).all(|pair| pair[0] < pair[1]) {
359        return Err(decoder.corrupt("risk flags are not strictly ordered"));
360    }
361    Ok(RiskManifest {
362        metrics,
363        flags,
364        policy: PolicyDigest::from_bytes(decoder.digest()?),
365    })
366}
367
368fn encode_risk_metrics(metrics: RiskMetrics, output: &mut Encoder) -> Result<(), ArtifactError> {
369    encode_len(metrics.touched_paths, output)?;
370    encode_len(metrics.created_paths, output)?;
371    encode_len(metrics.modified_paths, output)?;
372    encode_len(metrics.deleted_paths, output)?;
373    encode_len(metrics.renamed_paths, output)?;
374    output.extend_from_slice(&metrics.changed_bytes.to_le_bytes())?;
375    output.extend_from_slice(&metrics.delete_ratio_bps.to_le_bytes())?;
376    encode_len(metrics.executable_changes, output)?;
377    encode_len(metrics.symlink_changes, output)
378}
379
380fn decode_risk_metrics(decoder: &mut Decoder<'_>) -> Result<RiskMetrics, ArtifactError> {
381    Ok(RiskMetrics {
382        touched_paths: decoder.usize()?,
383        created_paths: decoder.usize()?,
384        modified_paths: decoder.usize()?,
385        deleted_paths: decoder.usize()?,
386        renamed_paths: decoder.usize()?,
387        changed_bytes: decoder.u64()?,
388        delete_ratio_bps: decoder.u16()?,
389        executable_changes: decoder.usize()?,
390        symlink_changes: decoder.usize()?,
391    })
392}
393
394fn encode_execution_stats(
395    stats: ExecutionStats,
396    output: &mut Encoder,
397) -> Result<(), ArtifactError> {
398    output.extend_from_slice(&stats.os_calls.to_le_bytes())?;
399    output.extend_from_slice(&stats.read_bytes.to_le_bytes())?;
400    output.extend_from_slice(&stats.write_bytes.to_le_bytes())?;
401    output.extend_from_slice(&stats.directory_entries.to_le_bytes())?;
402    encode_len(stats.output_bytes, output)?;
403    output.extend_from_slice(&stats.denied_accesses.to_le_bytes())?;
404    output.extend_from_slice(&stats.result_bytes.to_le_bytes())?;
405    Ok(())
406}
407
408fn decode_execution_stats(decoder: &mut Decoder<'_>) -> Result<ExecutionStats, ArtifactError> {
409    Ok(ExecutionStats {
410        os_calls: decoder.u64()?,
411        read_bytes: decoder.u64()?,
412        write_bytes: decoder.u64()?,
413        directory_entries: decoder.u64()?,
414        output_bytes: decoder.usize()?,
415        denied_accesses: decoder.u64()?,
416        result_bytes: decoder.u64()?,
417    })
418}
419
420fn encode_timings(timings: StageTimings, output: &mut Encoder) -> Result<(), ArtifactError> {
421    output.extend_from_slice(&timings.snapshot_ns.to_le_bytes())?;
422    output.extend_from_slice(&timings.execute_ns.to_le_bytes())?;
423    output.extend_from_slice(&timings.diff_ns.to_le_bytes())?;
424    output.extend_from_slice(&timings.policy_ns.to_le_bytes())?;
425    output.extend_from_slice(&timings.bind_and_store_ns.to_le_bytes())?;
426    output.extend_from_slice(&timings.commit_ns.to_le_bytes())?;
427    output.extend_from_slice(&timings.total_ns.to_le_bytes())
428}
429
430fn decode_timings(decoder: &mut Decoder<'_>) -> Result<StageTimings, ArtifactError> {
431    Ok(StageTimings {
432        snapshot_ns: decoder.u64()?,
433        execute_ns: decoder.u64()?,
434        diff_ns: decoder.u64()?,
435        policy_ns: decoder.u64()?,
436        bind_and_store_ns: decoder.u64()?,
437        commit_ns: decoder.u64()?,
438        total_ns: decoder.u64()?,
439    })
440}
441
442fn encode_path(
443    path: &VPath,
444    limits: ArtifactLimits,
445    output: &mut Encoder,
446) -> Result<(), ArtifactError> {
447    if path.as_str().len() > limits.max_path_bytes {
448        return Err(ArtifactError::Limit {
449            field: "path",
450            observed: path.as_str().len(),
451            maximum: limits.max_path_bytes,
452        });
453    }
454    encode_bytes(path.as_str().as_bytes(), output)
455}
456
457fn encode_optional_state(
458    state: Option<NodeState>,
459    output: &mut Encoder,
460) -> Result<(), ArtifactError> {
461    match state {
462        None => output.push(0)?,
463        Some(state) => {
464            output.push(1)?;
465            encode_state(state, output)?;
466        }
467    }
468    Ok(())
469}
470
471fn decode_optional_state(decoder: &mut Decoder<'_>) -> Result<Option<NodeState>, ArtifactError> {
472    match decoder.byte()? {
473        0 => Ok(None),
474        1 => decode_state(decoder).map(Some),
475        _ => Err(decoder.corrupt("unknown optional-state tag")),
476    }
477}
478
479fn encode_state(state: NodeState, output: &mut Encoder) -> Result<(), ArtifactError> {
480    output.push(node_kind_tag(state.kind()))?;
481    output.extend_from_slice(&state.size().to_le_bytes())?;
482    output.extend_from_slice(&state.mode().to_le_bytes())?;
483    match state.content() {
484        None => output.push(0)?,
485        Some(ContentVersion::Blob(blob)) => {
486            output.push(1)?;
487            output.extend_from_slice(blob.as_bytes())?;
488        }
489        Some(ContentVersion::Stamp(stamp)) => {
490            output.push(2)?;
491            encode_stamp(stamp, output)?;
492        }
493        Some(_) => {
494            return Err(ArtifactError::Unsupported {
495                reason: "unknown node content version",
496            });
497        }
498    }
499    Ok(())
500}
501
502fn decode_state(decoder: &mut Decoder<'_>) -> Result<NodeState, ArtifactError> {
503    let kind = decode_node_kind(decoder.byte()?)
504        .ok_or_else(|| decoder.corrupt("unknown node-kind tag"))?;
505    let size = decoder.u64()?;
506    let mode = decoder.u32()?;
507    let state = match decoder.byte()? {
508        0 if kind == NodeKind::Directory && size == 0 => NodeState::directory(mode),
509        1 if kind == NodeKind::File => {
510            NodeState::file(vsh_types::BlobId::from_bytes(decoder.digest()?), size, mode)
511        }
512        1 if kind == NodeKind::Symlink => {
513            NodeState::symlink(vsh_types::BlobId::from_bytes(decoder.digest()?), size, mode)
514        }
515        2 => {
516            let stamp = decode_stamp(decoder)?;
517            if stamp.kind != kind || stamp.size != size || stamp.mode != mode {
518                return Err(decoder.corrupt("node state and metadata stamp disagree"));
519            }
520            NodeState::from_stamp(stamp)
521        }
522        _ => return Err(decoder.corrupt("invalid node content encoding")),
523    };
524    Ok(state)
525}
526
527fn encode_stamp(stamp: FileStamp, output: &mut Encoder) -> Result<(), ArtifactError> {
528    output.push(node_kind_tag(stamp.kind))?;
529    output.extend_from_slice(&stamp.size.to_le_bytes())?;
530    output.extend_from_slice(&stamp.mode.to_le_bytes())?;
531    output.extend_from_slice(&stamp.mtime_ns.to_le_bytes())?;
532    match stamp.ctime_ns {
533        None => output.push(0)?,
534        Some(value) => {
535            output.push(1)?;
536            output.extend_from_slice(&value.to_le_bytes())?;
537        }
538    }
539    output.extend_from_slice(&stamp.file_id.high.to_le_bytes())?;
540    output.extend_from_slice(&stamp.file_id.low.to_le_bytes())
541}
542
543fn decode_stamp(decoder: &mut Decoder<'_>) -> Result<FileStamp, ArtifactError> {
544    let kind = decode_node_kind(decoder.byte()?)
545        .ok_or_else(|| decoder.corrupt("unknown stamp node-kind tag"))?;
546    let size = decoder.u64()?;
547    let mode = decoder.u32()?;
548    let mtime_ns = decoder.i128()?;
549    let ctime_ns = match decoder.byte()? {
550        0 => None,
551        1 => Some(decoder.i128()?),
552        _ => return Err(decoder.corrupt("unknown optional ctime tag")),
553    };
554    Ok(FileStamp {
555        kind,
556        size,
557        mode,
558        mtime_ns,
559        ctime_ns,
560        file_id: PlatformFileId {
561            high: decoder.u64()?,
562            low: decoder.u64()?,
563        },
564    })
565}
566
567fn encode_optional_digest(
568    value: Option<[u8; 32]>,
569    output: &mut Encoder,
570) -> Result<(), ArtifactError> {
571    match value {
572        None => output.push(0),
573        Some(bytes) => {
574            output.push(1)?;
575            output.extend_from_slice(&bytes)
576        }
577    }
578}
579
580fn decode_optional_digest(decoder: &mut Decoder<'_>) -> Result<Option<[u8; 32]>, ArtifactError> {
581    match decoder.byte()? {
582        0 => Ok(None),
583        1 => decoder.digest().map(Some),
584        _ => Err(decoder.corrupt("unknown optional-digest tag")),
585    }
586}
587
588fn encode_bytes(bytes: &[u8], output: &mut Encoder) -> Result<(), ArtifactError> {
589    encode_len(bytes.len(), output)?;
590    output.extend_from_slice(bytes)
591}
592
593fn encode_len(value: usize, output: &mut Encoder) -> Result<(), ArtifactError> {
594    let value = u64::try_from(value).map_err(|_| ArtifactError::Unsupported {
595        reason: "host length cannot be encoded",
596    })?;
597    output.extend_from_slice(&value.to_le_bytes())
598}
599
600const fn node_kind_tag(kind: NodeKind) -> u8 {
601    match kind {
602        NodeKind::File => 1,
603        NodeKind::Directory => 2,
604        NodeKind::Symlink => 3,
605    }
606}
607
608const fn decode_node_kind(tag: u8) -> Option<NodeKind> {
609    match tag {
610        1 => Some(NodeKind::File),
611        2 => Some(NodeKind::Directory),
612        3 => Some(NodeKind::Symlink),
613        _ => None,
614    }
615}
616
617const fn diff_kind_tag(kind: DiffKind) -> u8 {
618    match kind {
619        DiffKind::Create => 1,
620        DiffKind::Delete => 2,
621        DiffKind::Modify => 3,
622        DiffKind::MetadataChange => 4,
623    }
624}
625
626const fn decode_diff_kind(tag: u8) -> Option<DiffKind> {
627    match tag {
628        1 => Some(DiffKind::Create),
629        2 => Some(DiffKind::Delete),
630        3 => Some(DiffKind::Modify),
631        4 => Some(DiffKind::MetadataChange),
632        _ => None,
633    }
634}
635
636const fn risk_flag_tag(flag: RiskFlag) -> u8 {
637    match flag {
638        RiskFlag::Mutation => 1,
639        RiskFlag::Deletion => 2,
640        RiskFlag::Rename => 3,
641        RiskFlag::ExecutableChange => 4,
642        RiskFlag::SymlinkChange => 5,
643        RiskFlag::LargeTouchedSet => 6,
644        RiskFlag::LargeByteChange => 7,
645    }
646}
647
648const fn decode_risk_flag(tag: u8) -> Option<RiskFlag> {
649    match tag {
650        1 => Some(RiskFlag::Mutation),
651        2 => Some(RiskFlag::Deletion),
652        3 => Some(RiskFlag::Rename),
653        4 => Some(RiskFlag::ExecutableChange),
654        5 => Some(RiskFlag::SymlinkChange),
655        6 => Some(RiskFlag::LargeTouchedSet),
656        7 => Some(RiskFlag::LargeByteChange),
657        _ => None,
658    }
659}
660
661struct Encoder {
662    bytes: Vec<u8>,
663    maximum: usize,
664}
665
666impl Encoder {
667    const fn new(maximum: usize) -> Self {
668        Self {
669            bytes: Vec::new(),
670            maximum,
671        }
672    }
673
674    fn push(&mut self, byte: u8) -> Result<(), ArtifactError> {
675        self.extend_from_slice(&[byte])
676    }
677
678    fn extend_from_slice(&mut self, bytes: &[u8]) -> Result<(), ArtifactError> {
679        let required = self
680            .bytes
681            .len()
682            .checked_add(bytes.len())
683            .ok_or(ArtifactError::Limit {
684                field: "pending artifact",
685                observed: usize::MAX,
686                maximum: self.maximum,
687            })?;
688        if required > self.maximum {
689            return Err(ArtifactError::Limit {
690                field: "pending artifact",
691                observed: required,
692                maximum: self.maximum,
693            });
694        }
695        if required > self.bytes.capacity() {
696            let doubled = self.bytes.capacity().max(2_048).saturating_mul(2);
697            let target = doubled.max(required).min(self.maximum);
698            self.bytes
699                .try_reserve_exact(target.saturating_sub(self.bytes.len()))
700                .map_err(|source| ArtifactError::Allocation {
701                    field: "pending artifact",
702                    requested: target,
703                    detail: source.to_string(),
704                })?;
705        }
706        self.bytes.extend_from_slice(bytes);
707        Ok(())
708    }
709
710    fn finish(self) -> Vec<u8> {
711        self.bytes
712    }
713}
714
715struct Decoder<'a> {
716    bytes: &'a [u8],
717    offset: usize,
718}
719
720impl<'a> Decoder<'a> {
721    const fn new(bytes: &'a [u8]) -> Self {
722        Self { bytes, offset: 0 }
723    }
724
725    fn take(&mut self, count: usize) -> Result<&'a [u8], ArtifactError> {
726        let end = self
727            .offset
728            .checked_add(count)
729            .ok_or_else(|| self.corrupt("artifact offset overflow"))?;
730        let value = self
731            .bytes
732            .get(self.offset..end)
733            .ok_or_else(|| self.corrupt("truncated pending artifact"))?;
734        self.offset = end;
735        Ok(value)
736    }
737
738    fn byte(&mut self) -> Result<u8, ArtifactError> {
739        Ok(self.take(1)?[0])
740    }
741
742    fn u16(&mut self) -> Result<u16, ArtifactError> {
743        Ok(u16::from_le_bytes(self.array()?))
744    }
745
746    fn u32(&mut self) -> Result<u32, ArtifactError> {
747        Ok(u32::from_le_bytes(self.array()?))
748    }
749
750    fn u64(&mut self) -> Result<u64, ArtifactError> {
751        Ok(u64::from_le_bytes(self.array()?))
752    }
753
754    fn i128(&mut self) -> Result<i128, ArtifactError> {
755        Ok(i128::from_le_bytes(self.array()?))
756    }
757
758    fn usize(&mut self) -> Result<usize, ArtifactError> {
759        usize::try_from(self.u64()?).map_err(|_| self.corrupt("length does not fit this host"))
760    }
761
762    fn length(&mut self, maximum: usize, field: &'static str) -> Result<usize, ArtifactError> {
763        let value = self.usize()?;
764        if value > maximum {
765            return Err(ArtifactError::Limit {
766                field,
767                observed: value,
768                maximum,
769            });
770        }
771        Ok(value)
772    }
773
774    fn length_prefixed(
775        &mut self,
776        maximum: usize,
777        field: &'static str,
778    ) -> Result<&'a [u8], ArtifactError> {
779        let length = self.length(maximum, field)?;
780        self.take(length)
781    }
782
783    fn string(&mut self, maximum: usize, field: &'static str) -> Result<String, ArtifactError> {
784        let bytes = self.length_prefixed(maximum, field)?;
785        std::str::from_utf8(bytes)
786            .map(str::to_owned)
787            .map_err(|_| self.corrupt("artifact string is not UTF-8"))
788    }
789
790    fn path(&mut self, limits: ArtifactLimits) -> Result<VPath, ArtifactError> {
791        let value = self.string(limits.max_path_bytes, "path")?;
792        VPath::parse(&value).map_err(|_| self.corrupt("artifact path is invalid"))
793    }
794
795    fn digest(&mut self) -> Result<[u8; 32], ArtifactError> {
796        self.array()
797    }
798
799    fn array<const N: usize>(&mut self) -> Result<[u8; N], ArtifactError> {
800        let mut output = [0_u8; N];
801        output.copy_from_slice(self.take(N)?);
802        Ok(output)
803    }
804
805    fn finish(&self) -> Result<(), ArtifactError> {
806        if self.offset == self.bytes.len() {
807            Ok(())
808        } else {
809            Err(self.corrupt("trailing pending-artifact bytes"))
810        }
811    }
812
813    const fn corrupt(&self, reason: &'static str) -> ArtifactError {
814        ArtifactError::Corrupt {
815            offset: self.offset,
816            reason,
817        }
818    }
819}
820
821/// Durable pending-artifact encoding or validation failure.
822#[derive(Debug)]
823pub enum ArtifactError {
824    /// One bounded field or complete artifact exceeded configured limits.
825    Limit {
826        /// Bounded field.
827        field: &'static str,
828        /// Observed units.
829        observed: usize,
830        /// Maximum accepted units.
831        maximum: usize,
832    },
833    /// A bounded allocation failed before any unbounded growth was attempted.
834    Allocation {
835        /// Buffer being allocated.
836        field: &'static str,
837        /// Requested byte capacity.
838        requested: usize,
839        /// Allocator failure detail.
840        detail: String,
841    },
842    /// The content-addressed artifact is malformed.
843    Corrupt {
844        /// Byte offset nearest the violation.
845        offset: usize,
846        /// Stable validation reason.
847        reason: &'static str,
848    },
849    /// The serialized Monty result could not be encoded or decoded.
850    ValueCodec {
851        /// Codec direction.
852        operation: &'static str,
853        /// Postcard error detail.
854        detail: String,
855    },
856    /// A future non-exhaustive value cannot be represented safely by this codec.
857    Unsupported {
858        /// Stable rejection reason.
859        reason: &'static str,
860    },
861    /// Recomputed diff/dependency identities do not match the transaction binding.
862    BindingMismatch,
863}
864
865impl fmt::Display for ArtifactError {
866    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
867        match self {
868            Self::Limit {
869                field,
870                observed,
871                maximum,
872            } => write!(
873                formatter,
874                "pending artifact {field} is {observed}; maximum is {maximum}"
875            ),
876            Self::Allocation {
877                field,
878                requested,
879                detail,
880            } => write!(
881                formatter,
882                "cannot allocate {requested} bytes for pending artifact {field}: {detail}"
883            ),
884            Self::Corrupt { offset, reason } => {
885                write!(
886                    formatter,
887                    "pending artifact is corrupt at byte {offset}: {reason}"
888                )
889            }
890            Self::ValueCodec { operation, detail } => {
891                write!(
892                    formatter,
893                    "cannot {operation} pending result value: {detail}"
894                )
895            }
896            Self::Unsupported { reason } => {
897                write!(
898                    formatter,
899                    "pending artifact contains an unsupported value: {reason}"
900                )
901            }
902            Self::BindingMismatch => formatter.write_str(
903                "pending artifact diff or dependencies do not match its transaction binding",
904            ),
905        }
906    }
907}
908
909impl Error for ArtifactError {}
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914    use vsh_monty::MontyObject;
915    use vsh_types::{BlobId, TransactionState};
916
917    fn fixture() -> PendingTransaction {
918        let path = VPath::parse("result.txt").unwrap();
919        let state = NodeState::file(BlobId::digest(b"result"), 6, 0o644);
920        let diff = CanonicalDiff::from_entries(vec![DiffEntry {
921            path: path.clone(),
922            before: None,
923            after: Some(state),
924            kind: DiffKind::Create,
925        }])
926        .unwrap();
927        let read_set = BTreeMap::new();
928        let write_set = BTreeMap::from([(path, WritePrecondition { expected: None })]);
929        let binding = TransactionBinding {
930            base_snapshot: SnapshotId::from_bytes([1; 32]),
931            diff: diff.digest(),
932            read_set: read_set_digest(&read_set),
933            write_set: write_set_digest(&write_set),
934            program: ProgramDigest::digest_source("artifact-test"),
935            policy: PolicyDigest::digest_canonical(b"artifact-policy"),
936            runtime_config: RuntimeConfigDigest::digest_canonical(b"artifact-runtime"),
937            intent: Some(IntentDigest::digest_text("create result")),
938        };
939        let receipt = Receipt {
940            transaction: binding.transaction_id(),
941            base_snapshot: binding.base_snapshot,
942            state: TransactionState::AutoApproved,
943            decision: RuntimeDecision::AutoApproved,
944            diff: diff.digest(),
945            changed_paths: 1,
946            changes: diff.entries().to_vec(),
947            value: MontyObject::Int(42),
948            stdout: "ok\n".to_owned(),
949            execution: ExecutionStats {
950                os_calls: 1,
951                write_bytes: 6,
952                output_bytes: 3,
953                result_bytes: 8,
954                ..ExecutionStats::default()
955            },
956            timings: StageTimings {
957                total_ns: 123,
958                ..StageTimings::default()
959            },
960            commit: None,
961        };
962        PendingTransaction {
963            binding,
964            diff,
965            read_set,
966            write_set,
967            receipt,
968        }
969    }
970
971    #[test]
972    fn pending_artifact_round_trip_preserves_exact_binding_and_receipt() {
973        let artifact = fixture();
974        let bytes = encode_pending(&artifact, ArtifactLimits::default()).unwrap();
975        let decoded = decode_pending(&bytes, ArtifactLimits::default()).unwrap();
976
977        assert_eq!(decoded.binding, artifact.binding);
978        assert_eq!(decoded.diff, artifact.diff);
979        assert_eq!(decoded.read_set, artifact.read_set);
980        assert_eq!(decoded.write_set, artifact.write_set);
981        assert_eq!(decoded.receipt.transaction, artifact.receipt.transaction);
982        assert_eq!(decoded.receipt.changes, artifact.receipt.changes);
983        assert_eq!(decoded.receipt.value, MontyObject::Int(42));
984        assert_eq!(decoded.receipt.stdout, "ok\n");
985        assert_eq!(decoded.receipt.execution, artifact.receipt.execution);
986        assert_eq!(decoded.receipt.timings, artifact.receipt.timings);
987    }
988
989    #[test]
990    fn pending_artifact_rejects_tampered_binding_and_trailing_bytes() {
991        let mut bytes = encode_pending(&fixture(), ArtifactLimits::default()).unwrap();
992        bytes[ARTIFACT_MAGIC.len() + 32] ^= 0x80;
993        assert!(matches!(
994            decode_pending(&bytes, ArtifactLimits::default()),
995            Err(ArtifactError::BindingMismatch)
996        ));
997
998        let mut bytes = encode_pending(&fixture(), ArtifactLimits::default()).unwrap();
999        bytes.push(0);
1000        assert!(matches!(
1001            decode_pending(&bytes, ArtifactLimits::default()),
1002            Err(ArtifactError::Corrupt {
1003                reason: "trailing pending-artifact bytes",
1004                ..
1005            })
1006        ));
1007    }
1008
1009    #[test]
1010    fn pending_artifact_limits_apply_before_unbounded_materialization() {
1011        let artifact = fixture();
1012        let limits = ArtifactLimits {
1013            max_value_bytes: 0,
1014            ..ArtifactLimits::default()
1015        };
1016        assert!(matches!(
1017            encode_pending(&artifact, limits),
1018            Err(ArtifactError::Limit {
1019                field: "result value",
1020                maximum: 0,
1021                ..
1022            })
1023        ));
1024
1025        let bytes = encode_pending(&artifact, ArtifactLimits::default()).unwrap();
1026        let limits = ArtifactLimits {
1027            max_bytes: bytes.len() - 1,
1028            ..ArtifactLimits::default()
1029        };
1030        assert!(matches!(
1031            encode_pending(&artifact, limits),
1032            Err(ArtifactError::Limit {
1033                field: "pending artifact",
1034                ..
1035            })
1036        ));
1037        assert!(matches!(
1038            decode_pending(&bytes, limits),
1039            Err(ArtifactError::Limit {
1040                field: "pending artifact",
1041                ..
1042            })
1043        ));
1044    }
1045
1046    #[test]
1047    fn artifact_error_messages_identify_each_failure_class() {
1048        let errors = [
1049            ArtifactError::Limit {
1050                field: "value",
1051                observed: 2,
1052                maximum: 1,
1053            },
1054            ArtifactError::Allocation {
1055                field: "value",
1056                requested: 2,
1057                detail: "test".to_owned(),
1058            },
1059            ArtifactError::Corrupt {
1060                offset: 1,
1061                reason: "test",
1062            },
1063            ArtifactError::ValueCodec {
1064                operation: "decode",
1065                detail: "test".to_owned(),
1066            },
1067            ArtifactError::Unsupported { reason: "test" },
1068            ArtifactError::BindingMismatch,
1069        ];
1070        let messages = errors.map(|error| error.to_string());
1071
1072        assert!(messages.iter().all(|message| !message.is_empty()));
1073        assert_eq!(
1074            messages
1075                .iter()
1076                .collect::<std::collections::BTreeSet<_>>()
1077                .len(),
1078            messages.len()
1079        );
1080    }
1081}