git_closure/error.rs
1use std::io;
2
3use thiserror::Error;
4
5/// Canonical error type returned by `git-closure` library operations.
6#[derive(Debug, Error)]
7pub enum GitClosureError {
8 /// Any filesystem or OS-level I/O failure.
9 #[error("I/O error: {0}")]
10 Io(#[from] io::Error),
11 /// Snapshot syntax or semantic parse error.
12 #[error("parse error: {0}")]
13 Parse(String),
14 /// Top-level snapshot integrity mismatch.
15 ///
16 /// This compares the header `snapshot-hash` against the hash recomputed
17 /// from all parsed entries.
18 #[error("snapshot hash mismatch: expected {expected}, got {actual}")]
19 HashMismatch { expected: String, actual: String },
20 /// Per-file content digest mismatch.
21 ///
22 /// Unlike [`Self::HashMismatch`], this indicates one specific file entry
23 /// had bytes whose SHA-256 does not match its declared `:sha256` value.
24 #[error("content hash mismatch for {path}: expected {expected}, got {actual}")]
25 ContentHashMismatch {
26 /// Snapshot-relative path of the failing file.
27 path: String,
28 /// Declared digest from snapshot metadata.
29 expected: String,
30 /// Digest recomputed from decoded file content.
31 actual: String,
32 },
33 /// Per-file decoded byte-size mismatch.
34 #[error("size mismatch for {path}: metadata {expected}, decoded {actual}")]
35 SizeMismatch {
36 /// Snapshot-relative path of the failing file.
37 path: String,
38 /// Declared `:size` metadata value.
39 expected: u64,
40 /// Actual decoded content length in bytes.
41 actual: u64,
42 },
43 /// Path or symlink target failed safety checks.
44 #[error("unsafe path in snapshot: {0}")]
45 UnsafePath(String),
46 /// Required snapshot header is missing.
47 #[error("missing required header: {0}")]
48 MissingHeader(&'static str),
49 /// Legacy `format-hash` header encountered.
50 #[error("legacy format-hash header found; re-snapshot with current tool")]
51 LegacyHeader,
52 /// External command could not be started.
53 #[error("command '{command}' failed to spawn: {source}")]
54 CommandSpawnFailed {
55 /// Name of the command that failed.
56 command: &'static str,
57 /// Underlying process-spawn error.
58 #[source]
59 source: io::Error,
60 },
61 /// External command ran but returned a non-zero exit status.
62 #[error(
63 "command '{command}' exited with status {status}{stderr_suffix}",
64 stderr_suffix = format_command_stderr(stderr)
65 )]
66 CommandExitFailure {
67 /// Name of the command that failed.
68 command: &'static str,
69 /// Exit status rendered as text.
70 status: String,
71 /// Captured standard error output.
72 stderr: String,
73 },
74}
75
76fn format_command_stderr(stderr: &str) -> String {
77 if stderr.is_empty() {
78 String::new()
79 } else {
80 format!(":\n{stderr}")
81 }
82}