1use std::collections::BTreeMap;
2use std::error::Error;
3use std::fmt;
4
5use vsh_policy::{read_set_digest, write_set_digest};
6use vsh_types::{
7 BlobId, ContentVersion, FileStamp, NodeKind, NodeState, PlatformFileId, SnapshotId,
8 TransactionBinding, TransactionId, VPath,
9};
10use vsh_vfs::{CanonicalDiff, ReadObservation, WritePrecondition};
11
12const PLAN_MAGIC: &[u8; 8] = b"VSHCMT01";
13
14pub struct CommitPlan<'a> {
16 binding: TransactionBinding,
17 diff: &'a CanonicalDiff,
18 read_set: &'a BTreeMap<VPath, ReadObservation>,
19 write_set: &'a BTreeMap<VPath, WritePrecondition>,
20}
21
22impl<'a> CommitPlan<'a> {
23 pub fn new(
35 binding: &TransactionBinding,
36 diff: &'a CanonicalDiff,
37 read_set: &'a BTreeMap<VPath, ReadObservation>,
38 write_set: &'a BTreeMap<VPath, WritePrecondition>,
39 ) -> Result<Self, CommitPlanError> {
40 if binding.diff != diff.digest() {
41 return Err(CommitPlanError::DiffDigestMismatch);
42 }
43 if binding.read_set != read_set_digest(read_set) {
44 return Err(CommitPlanError::ReadSetDigestMismatch);
45 }
46 if binding.write_set != write_set_digest(write_set) {
47 return Err(CommitPlanError::WriteSetDigestMismatch);
48 }
49 let reserved = VPath::parse(crate::host::RUNTIME_DIRECTORY)
50 .expect("built-in runtime directory is a valid VPath");
51 for entry in diff.entries() {
52 if entry.path.is_root() {
53 return Err(CommitPlanError::RootMutation);
54 }
55 if entry.path.is_within(&reserved) {
56 return Err(CommitPlanError::ReservedPath {
57 path: entry.path.clone(),
58 });
59 }
60 let parent = entry
61 .path
62 .parent()
63 .expect("non-root diff paths have a parent");
64 if read_set
65 .get(&parent)
66 .is_none_or(|observation| observation.metadata.is_none())
67 {
68 return Err(CommitPlanError::MissingParentDependency {
69 path: entry.path.clone(),
70 parent,
71 });
72 }
73 let Some(precondition) = write_set.get(&entry.path) else {
74 return Err(CommitPlanError::MissingWritePrecondition {
75 path: entry.path.clone(),
76 });
77 };
78 if precondition.expected != entry.before {
79 let materialized_stamp = matches!(
83 (precondition.expected, entry.before),
84 (
85 Some(expected),
86 Some(before)
87 ) if matches!(expected.content(), Some(ContentVersion::Stamp(_)))
88 && matches!(before.content(), Some(ContentVersion::Blob(_)))
89 && expected.kind() == before.kind()
90 && expected.size() == before.size()
91 && expected.mode() == before.mode()
92 );
93 if !materialized_stamp {
94 return Err(CommitPlanError::BeforeStateMismatch {
95 path: entry.path.clone(),
96 });
97 }
98 }
99 if let Some(after) = entry.after
100 && after.kind() != NodeKind::Directory
101 && !matches!(after.content(), Some(ContentVersion::Blob(_)))
102 {
103 return Err(CommitPlanError::UnmaterializedAfterState {
104 path: entry.path.clone(),
105 });
106 }
107 }
108 Ok(Self {
109 binding: *binding,
110 diff,
111 read_set,
112 write_set,
113 })
114 }
115
116 #[must_use]
117 pub fn transaction(&self) -> TransactionId {
119 self.binding.transaction_id()
120 }
121
122 #[must_use]
123 pub const fn base_snapshot(&self) -> SnapshotId {
125 self.binding.base_snapshot
126 }
127
128 #[must_use]
129 pub const fn binding(&self) -> TransactionBinding {
131 self.binding
132 }
133
134 #[must_use]
135 pub const fn diff(&self) -> &CanonicalDiff {
137 self.diff
138 }
139
140 #[must_use]
141 pub const fn read_set(&self) -> &BTreeMap<VPath, ReadObservation> {
143 self.read_set
144 }
145
146 #[must_use]
147 pub const fn write_set(&self) -> &BTreeMap<VPath, WritePrecondition> {
149 self.write_set
150 }
151}
152
153#[derive(Clone, Debug, Eq, PartialEq)]
155pub enum CommitPlanError {
156 DiffDigestMismatch,
158 ReadSetDigestMismatch,
160 WriteSetDigestMismatch,
162 RootMutation,
164 ReservedPath {
166 path: VPath,
168 },
169 MissingWritePrecondition {
171 path: VPath,
173 },
174 MissingParentDependency {
176 path: VPath,
178 parent: VPath,
180 },
181 BeforeStateMismatch {
183 path: VPath,
185 },
186 UnmaterializedAfterState {
188 path: VPath,
190 },
191 TooManyOperations {
193 observed: usize,
195 maximum: usize,
197 },
198 PathTooLong {
200 path: VPath,
202 maximum: usize,
204 },
205 OperationCountOverflow,
207}
208
209impl fmt::Display for CommitPlanError {
210 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Self::DiffDigestMismatch => {
213 formatter.write_str("commit diff digest does not match its transaction binding")
214 }
215 Self::ReadSetDigestMismatch => {
216 formatter.write_str("commit read-set digest does not match its transaction binding")
217 }
218 Self::WriteSetDigestMismatch => formatter
219 .write_str("commit write-set digest does not match its transaction binding"),
220 Self::RootMutation => formatter.write_str("the workspace root cannot be mutated"),
221 Self::ReservedPath { path } => {
222 write!(formatter, "commit targets reserved VSH path {path}")
223 }
224 Self::MissingWritePrecondition { path } => {
225 write!(formatter, "diff path {path} has no write precondition")
226 }
227 Self::MissingParentDependency { path, parent } => write!(
228 formatter,
229 "diff path {path} has no metadata dependency for parent {parent}"
230 ),
231 Self::BeforeStateMismatch { path } => write!(
232 formatter,
233 "diff before-state and write precondition disagree at {path}"
234 ),
235 Self::UnmaterializedAfterState { path } => {
236 write!(formatter, "commit after-state at {path} is not blob-backed")
237 }
238 Self::TooManyOperations { observed, maximum } => write!(
239 formatter,
240 "commit has {observed} operations; maximum is {maximum}"
241 ),
242 Self::PathTooLong { path, maximum } => write!(
243 formatter,
244 "commit path {path} exceeds the {maximum}-byte limit"
245 ),
246 Self::OperationCountOverflow => {
247 formatter.write_str("commit operation count cannot be encoded")
248 }
249 }
250 }
251}
252
253impl Error for CommitPlanError {}
254
255#[derive(Clone, Debug, Eq, PartialEq)]
256pub(crate) enum Operation {
257 Quarantine {
258 path: VPath,
259 expected: NodeState,
260 slot: u32,
261 },
262 CreateDirectory {
263 path: VPath,
264 after: NodeState,
265 },
266 InstallFile {
267 path: VPath,
268 after: NodeState,
269 slot: u32,
270 },
271 InstallSymlink {
272 path: VPath,
273 after: NodeState,
274 slot: u32,
275 },
276 SetDirectoryMode {
277 path: VPath,
278 expected: NodeState,
279 after_mode: u32,
280 },
281}
282
283impl Operation {
284 pub(crate) fn path(&self) -> &VPath {
285 match self {
286 Self::Quarantine { path, .. }
287 | Self::CreateDirectory { path, .. }
288 | Self::InstallFile { path, .. }
289 | Self::InstallSymlink { path, .. }
290 | Self::SetDirectoryMode { path, .. } => path,
291 }
292 }
293}
294
295#[derive(Clone, Debug, Eq, PartialEq)]
296pub(crate) struct PreparedPlan {
297 pub(crate) transaction: TransactionId,
298 pub(crate) base_snapshot: SnapshotId,
299 pub(crate) operations: Vec<Operation>,
300 pub(crate) final_states: Vec<(VPath, Option<NodeState>)>,
301}
302
303impl PreparedPlan {
304 #[allow(clippy::too_many_lines)]
305 pub(crate) fn prepare(
306 plan: &CommitPlan<'_>,
307 max_operations: usize,
308 max_path_bytes: usize,
309 ) -> Result<Self, CommitPlanError> {
310 let mut destructive = Vec::new();
311 for entry in plan.diff.entries() {
312 let Some(before) = entry.before else {
313 continue;
314 };
315 let destructive_change = match entry.after {
316 None => true,
317 Some(after) if after.kind() != before.kind() => true,
318 Some(after) => matches!(after.kind(), NodeKind::File | NodeKind::Symlink),
319 };
320 if destructive_change {
321 destructive.push(entry.path.clone());
322 }
323 }
324 destructive.sort_unstable_by(|left, right| {
325 component_depth(left)
326 .cmp(&component_depth(right))
327 .then_with(|| left.cmp(right))
328 });
329 let mut destructive_roots = Vec::new();
330 for path in destructive {
331 if destructive_roots
332 .iter()
333 .any(|ancestor: &VPath| path.is_within(ancestor))
334 {
335 continue;
336 }
337 destructive_roots.push(path);
338 }
339
340 let mut operations = Vec::new();
341 let mut next_slot = 0_u32;
342 for path in &destructive_roots {
343 let expected = plan.write_set[path]
344 .expected
345 .expect("destructive diff entries have an existing precondition");
346 operations.push(Operation::Quarantine {
347 path: path.clone(),
348 expected,
349 slot: next_slot,
350 });
351 next_slot = next_slot
352 .checked_add(1)
353 .ok_or(CommitPlanError::OperationCountOverflow)?;
354 }
355
356 let mut directories = plan
357 .diff
358 .entries()
359 .iter()
360 .filter_map(|entry| {
361 let after = entry.after?;
362 let needs_create = after.kind() == NodeKind::Directory
363 && entry
364 .before
365 .is_none_or(|before| before.kind() != NodeKind::Directory);
366 needs_create.then(|| (entry.path.clone(), NodeState::directory(after.mode())))
367 })
368 .collect::<Vec<_>>();
369 directories.sort_unstable_by(|left, right| {
370 component_depth(&left.0)
371 .cmp(&component_depth(&right.0))
372 .then_with(|| left.0.cmp(&right.0))
373 });
374 for (path, after) in directories {
375 operations.push(Operation::CreateDirectory { path, after });
376 }
377
378 for entry in plan.diff.entries() {
379 let Some(after) = entry.after else {
380 continue;
381 };
382 match after.kind() {
383 NodeKind::File => {
384 operations.push(Operation::InstallFile {
385 path: entry.path.clone(),
386 after,
387 slot: next_slot,
388 });
389 next_slot = next_slot
390 .checked_add(1)
391 .ok_or(CommitPlanError::OperationCountOverflow)?;
392 }
393 NodeKind::Symlink => {
394 operations.push(Operation::InstallSymlink {
395 path: entry.path.clone(),
396 after,
397 slot: next_slot,
398 });
399 next_slot = next_slot
400 .checked_add(1)
401 .ok_or(CommitPlanError::OperationCountOverflow)?;
402 }
403 NodeKind::Directory => {
404 if let Some(before) = entry.before
405 && before.kind() == NodeKind::Directory
406 && before.mode() != after.mode()
407 {
408 operations.push(Operation::SetDirectoryMode {
409 path: entry.path.clone(),
410 expected: plan.write_set[&entry.path]
411 .expected
412 .expect("directory metadata change has a precondition"),
413 after_mode: after.mode(),
414 });
415 }
416 }
417 }
418 }
419
420 if operations.len() > max_operations {
421 return Err(CommitPlanError::TooManyOperations {
422 observed: operations.len(),
423 maximum: max_operations,
424 });
425 }
426 for path in operations.iter().map(Operation::path) {
427 if path.as_str().len() > max_path_bytes {
428 return Err(CommitPlanError::PathTooLong {
429 path: path.clone(),
430 maximum: max_path_bytes,
431 });
432 }
433 }
434 let final_states = plan
435 .diff
436 .entries()
437 .iter()
438 .map(|entry| {
439 let after = entry.after.map(|state| {
440 if state.kind() == NodeKind::Directory {
441 NodeState::directory(state.mode())
442 } else {
443 state
444 }
445 });
446 (entry.path.clone(), after)
447 })
448 .collect();
449 Ok(Self {
450 transaction: plan.transaction(),
451 base_snapshot: plan.base_snapshot(),
452 operations,
453 final_states,
454 })
455 }
456
457 pub(crate) fn encode(&self) -> Result<Vec<u8>, CommitPlanError> {
458 let operation_count = u32::try_from(self.operations.len())
459 .map_err(|_| CommitPlanError::OperationCountOverflow)?;
460 let final_count = u32::try_from(self.final_states.len())
461 .map_err(|_| CommitPlanError::OperationCountOverflow)?;
462 let mut output = Vec::new();
463 output.extend_from_slice(PLAN_MAGIC);
464 output.extend_from_slice(self.transaction.as_bytes());
465 output.extend_from_slice(self.base_snapshot.as_bytes());
466 output.extend_from_slice(&operation_count.to_le_bytes());
467 for operation in &self.operations {
468 encode_operation(operation, &mut output)?;
469 }
470 output.extend_from_slice(&final_count.to_le_bytes());
471 for (path, state) in &self.final_states {
472 encode_path(path, &mut output)?;
473 encode_optional_state(*state, &mut output);
474 }
475 let digest = plan_digest(&output);
476 output.extend_from_slice(&digest);
477 Ok(output)
478 }
479
480 pub(crate) fn decode(
481 bytes: &[u8],
482 max_operations: usize,
483 max_path_bytes: usize,
484 ) -> Result<Self, PlanDecodeError> {
485 if bytes.len() < PLAN_MAGIC.len() + 32 {
486 return Err(PlanDecodeError::Truncated);
487 }
488 let (payload, checksum) = bytes.split_at(bytes.len() - 32);
489 if plan_digest(payload).as_slice() != checksum {
490 return Err(PlanDecodeError::Checksum);
491 }
492 let mut reader = Reader::new(payload);
493 if reader.take(8)? != PLAN_MAGIC {
494 return Err(PlanDecodeError::Magic);
495 }
496 let transaction = TransactionId::from_bytes(reader.array()?);
497 let base_snapshot = SnapshotId::from_bytes(reader.array()?);
498 let operation_count = reader.u32()? as usize;
499 if operation_count > max_operations {
500 return Err(PlanDecodeError::Limit);
501 }
502 let mut operations = Vec::with_capacity(operation_count);
503 for _ in 0..operation_count {
504 operations.push(decode_operation(&mut reader, max_path_bytes)?);
505 }
506 let final_count = reader.u32()? as usize;
507 if final_count > max_operations {
508 return Err(PlanDecodeError::Limit);
509 }
510 let mut final_states = Vec::with_capacity(final_count);
511 for _ in 0..final_count {
512 let path = decode_path(&mut reader, max_path_bytes)?;
513 let state = decode_optional_state(&mut reader)?;
514 final_states.push((path, state));
515 }
516 if !reader.is_empty() {
517 return Err(PlanDecodeError::TrailingBytes);
518 }
519 Ok(Self {
520 transaction,
521 base_snapshot,
522 operations,
523 final_states,
524 })
525 }
526}
527
528fn component_depth(path: &VPath) -> usize {
529 if path.is_root() {
530 0
531 } else {
532 path.as_str().split('/').count()
533 }
534}
535
536fn slot_name(slot: u32) -> String {
537 format!("{slot:08x}")
538}
539
540pub(crate) fn stage_name(slot: u32) -> String {
541 slot_name(slot)
542}
543
544pub(crate) fn stage_link_name(slot: u32) -> String {
545 format!("{}.link", slot_name(slot))
546}
547
548pub(crate) fn quarantine_name(slot: u32) -> String {
549 slot_name(slot)
550}
551
552fn encode_operation(operation: &Operation, output: &mut Vec<u8>) -> Result<(), CommitPlanError> {
553 match operation {
554 Operation::Quarantine {
555 path,
556 expected,
557 slot,
558 } => {
559 output.push(1);
560 encode_path(path, output)?;
561 encode_state(*expected, output);
562 output.extend_from_slice(&slot.to_le_bytes());
563 }
564 Operation::CreateDirectory { path, after } => {
565 output.push(2);
566 encode_path(path, output)?;
567 encode_state(*after, output);
568 }
569 Operation::InstallFile { path, after, slot } => {
570 output.push(3);
571 encode_path(path, output)?;
572 encode_state(*after, output);
573 output.extend_from_slice(&slot.to_le_bytes());
574 }
575 Operation::InstallSymlink { path, after, slot } => {
576 output.push(4);
577 encode_path(path, output)?;
578 encode_state(*after, output);
579 output.extend_from_slice(&slot.to_le_bytes());
580 }
581 Operation::SetDirectoryMode {
582 path,
583 expected,
584 after_mode,
585 } => {
586 output.push(5);
587 encode_path(path, output)?;
588 encode_state(*expected, output);
589 output.extend_from_slice(&after_mode.to_le_bytes());
590 }
591 }
592 Ok(())
593}
594
595fn decode_operation(
596 reader: &mut Reader<'_>,
597 max_path: usize,
598) -> Result<Operation, PlanDecodeError> {
599 let tag = reader.u8()?;
600 let path = decode_path(reader, max_path)?;
601 match tag {
602 1 => Ok(Operation::Quarantine {
603 path,
604 expected: decode_state(reader)?,
605 slot: reader.u32()?,
606 }),
607 2 => Ok(Operation::CreateDirectory {
608 path,
609 after: decode_state(reader)?,
610 }),
611 3 => Ok(Operation::InstallFile {
612 path,
613 after: decode_state(reader)?,
614 slot: reader.u32()?,
615 }),
616 4 => Ok(Operation::InstallSymlink {
617 path,
618 after: decode_state(reader)?,
619 slot: reader.u32()?,
620 }),
621 5 => Ok(Operation::SetDirectoryMode {
622 path,
623 expected: decode_state(reader)?,
624 after_mode: reader.u32()?,
625 }),
626 _ => Err(PlanDecodeError::Tag),
627 }
628}
629
630fn encode_path(path: &VPath, output: &mut Vec<u8>) -> Result<(), CommitPlanError> {
631 let bytes = path.as_str().as_bytes();
632 let len = u32::try_from(bytes.len()).map_err(|_| CommitPlanError::PathTooLong {
633 path: path.clone(),
634 maximum: u32::MAX as usize,
635 })?;
636 output.extend_from_slice(&len.to_le_bytes());
637 output.extend_from_slice(bytes);
638 Ok(())
639}
640
641fn decode_path(reader: &mut Reader<'_>, max_path: usize) -> Result<VPath, PlanDecodeError> {
642 let len = reader.u32()? as usize;
643 if len > max_path {
644 return Err(PlanDecodeError::Limit);
645 }
646 let source = std::str::from_utf8(reader.take(len)?).map_err(|_| PlanDecodeError::Utf8)?;
647 VPath::parse(source).map_err(|_| PlanDecodeError::Path)
648}
649
650fn encode_optional_state(state: Option<NodeState>, output: &mut Vec<u8>) {
651 match state {
652 None => output.push(0),
653 Some(state) => {
654 output.push(1);
655 encode_state(state, output);
656 }
657 }
658}
659
660fn decode_optional_state(reader: &mut Reader<'_>) -> Result<Option<NodeState>, PlanDecodeError> {
661 match reader.u8()? {
662 0 => Ok(None),
663 1 => decode_state(reader).map(Some),
664 _ => Err(PlanDecodeError::Tag),
665 }
666}
667
668fn encode_state(state: NodeState, output: &mut Vec<u8>) {
669 output.push(state.kind().canonical_tag());
670 output.extend_from_slice(&state.size().to_le_bytes());
671 output.extend_from_slice(&state.mode().to_le_bytes());
672 match state.content() {
673 None => output.push(0),
674 Some(ContentVersion::Blob(blob)) => {
675 output.push(1);
676 output.extend_from_slice(blob.as_bytes());
677 }
678 Some(ContentVersion::Stamp(stamp)) => {
679 output.push(2);
680 encode_stamp(stamp, output);
681 }
682 Some(_) => unreachable!("all vsh-types content versions are explicitly encoded"),
683 }
684}
685
686fn decode_state(reader: &mut Reader<'_>) -> Result<NodeState, PlanDecodeError> {
687 let kind = decode_kind(reader.u8()?)?;
688 let size = reader.u64()?;
689 let mode = reader.u32()?;
690 match reader.u8()? {
691 0 if kind == NodeKind::Directory && size == 0 => Ok(NodeState::directory(mode)),
692 1 => {
693 let blob = BlobId::from_bytes(reader.array()?);
694 match kind {
695 NodeKind::File => Ok(NodeState::file(blob, size, mode)),
696 NodeKind::Symlink => Ok(NodeState::symlink(blob, size, mode)),
697 NodeKind::Directory => Err(PlanDecodeError::State),
698 }
699 }
700 2 => {
701 let stamp = decode_stamp(reader)?;
702 if stamp.kind != kind || stamp.size != size || stamp.mode != mode {
703 return Err(PlanDecodeError::State);
704 }
705 Ok(NodeState::from_stamp(stamp))
706 }
707 _ => Err(PlanDecodeError::State),
708 }
709}
710
711fn encode_stamp(stamp: FileStamp, output: &mut Vec<u8>) {
712 output.push(stamp.kind.canonical_tag());
713 output.extend_from_slice(&stamp.size.to_le_bytes());
714 output.extend_from_slice(&stamp.mode.to_le_bytes());
715 output.extend_from_slice(&stamp.mtime_ns.to_le_bytes());
716 match stamp.ctime_ns {
717 None => output.push(0),
718 Some(ctime) => {
719 output.push(1);
720 output.extend_from_slice(&ctime.to_le_bytes());
721 }
722 }
723 output.extend_from_slice(&stamp.file_id.high.to_le_bytes());
724 output.extend_from_slice(&stamp.file_id.low.to_le_bytes());
725}
726
727fn decode_stamp(reader: &mut Reader<'_>) -> Result<FileStamp, PlanDecodeError> {
728 let kind = decode_kind(reader.u8()?)?;
729 let size = reader.u64()?;
730 let mode = reader.u32()?;
731 let mtime_ns = reader.i128()?;
732 let ctime_ns = match reader.u8()? {
733 0 => None,
734 1 => Some(reader.i128()?),
735 _ => return Err(PlanDecodeError::Tag),
736 };
737 let high = reader.u64()?;
738 let low = reader.u64()?;
739 Ok(FileStamp {
740 kind,
741 size,
742 mode,
743 mtime_ns,
744 ctime_ns,
745 file_id: PlatformFileId { high, low },
746 })
747}
748
749fn decode_kind(tag: u8) -> Result<NodeKind, PlanDecodeError> {
750 match tag {
751 1 => Ok(NodeKind::File),
752 2 => Ok(NodeKind::Directory),
753 3 => Ok(NodeKind::Symlink),
754 _ => Err(PlanDecodeError::Tag),
755 }
756}
757
758fn plan_digest(bytes: &[u8]) -> [u8; 32] {
759 let mut hasher = blake3::Hasher::new();
760 hasher.update(b"vsh\0commit-plan-v1\0");
761 hasher.update(&(bytes.len() as u64).to_le_bytes());
762 hasher.update(bytes);
763 *hasher.finalize().as_bytes()
764}
765
766struct Reader<'a> {
767 bytes: &'a [u8],
768 offset: usize,
769}
770
771impl<'a> Reader<'a> {
772 const fn new(bytes: &'a [u8]) -> Self {
773 Self { bytes, offset: 0 }
774 }
775
776 fn take(&mut self, len: usize) -> Result<&'a [u8], PlanDecodeError> {
777 let end = self
778 .offset
779 .checked_add(len)
780 .ok_or(PlanDecodeError::Truncated)?;
781 let value = self
782 .bytes
783 .get(self.offset..end)
784 .ok_or(PlanDecodeError::Truncated)?;
785 self.offset = end;
786 Ok(value)
787 }
788
789 fn array<const N: usize>(&mut self) -> Result<[u8; N], PlanDecodeError> {
790 self.take(N)?
791 .try_into()
792 .map_err(|_| PlanDecodeError::Truncated)
793 }
794
795 fn u8(&mut self) -> Result<u8, PlanDecodeError> {
796 Ok(self.take(1)?[0])
797 }
798
799 fn u32(&mut self) -> Result<u32, PlanDecodeError> {
800 Ok(u32::from_le_bytes(self.array()?))
801 }
802
803 fn u64(&mut self) -> Result<u64, PlanDecodeError> {
804 Ok(u64::from_le_bytes(self.array()?))
805 }
806
807 fn i128(&mut self) -> Result<i128, PlanDecodeError> {
808 Ok(i128::from_le_bytes(self.array()?))
809 }
810
811 fn is_empty(&self) -> bool {
812 self.offset == self.bytes.len()
813 }
814}
815
816#[derive(Clone, Copy, Debug, Eq, PartialEq)]
818pub enum PlanDecodeError {
819 Truncated,
821 Checksum,
823 Magic,
825 Tag,
827 Utf8,
829 Path,
831 State,
833 Limit,
835 TrailingBytes,
837}
838
839impl fmt::Display for PlanDecodeError {
840 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
841 formatter.write_str(match self {
842 Self::Truncated => "commit plan is truncated",
843 Self::Checksum => "commit plan checksum mismatch",
844 Self::Magic => "commit plan has an unknown format",
845 Self::Tag => "commit plan contains an unknown tag",
846 Self::Utf8 => "commit plan path is not UTF-8",
847 Self::Path => "commit plan path is invalid",
848 Self::State => "commit plan node state is invalid",
849 Self::Limit => "commit plan exceeds configured bounds",
850 Self::TrailingBytes => "commit plan contains trailing bytes",
851 })
852 }
853}
854
855impl Error for PlanDecodeError {}