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#[derive(Debug)]
823pub enum ArtifactError {
824 Limit {
826 field: &'static str,
828 observed: usize,
830 maximum: usize,
832 },
833 Allocation {
835 field: &'static str,
837 requested: usize,
839 detail: String,
841 },
842 Corrupt {
844 offset: usize,
846 reason: &'static str,
848 },
849 ValueCodec {
851 operation: &'static str,
853 detail: String,
855 },
856 Unsupported {
858 reason: &'static str,
860 },
861 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}