Skip to main content

mkit_git_bridge/
error.rs

1//! Bridge error taxonomy.
2//!
3//! [`Refusal`] is the closed set of *policy* refusals from
4//! SPEC-GIT-BRIDGE (§4, §6.2, §7.1, §8, §12.1): the object or ref is
5//! valid mkit data that the v1 mapping deliberately does not
6//! translate. Everything else is a hard error.
7
8use mkit_core::Hash;
9use mkit_core::hash::to_hex;
10use std::fmt;
11
12/// A deliberate, spec'd refusal to translate (actionable; per-ref
13/// granularity is the caller's job).
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum Refusal {
17    /// Remix objects are not translated in v1 (SPEC-GIT-BRIDGE §8).
18    Remix { object: Hash },
19    /// Fixed-size chunked-blob manifests have no exact inverse
20    /// (SPEC-GIT-BRIDGE §4).
21    FixedSizeChunking { object: Hash, chunk_size: u32 },
22    /// Content-defined manifest a conformant mkit writer cannot have
23    /// produced (≤ threshold total size, or boundaries that differ
24    /// from the pinned `FastCDC` output) — it would not round-trip
25    /// (SPEC-GIT-BRIDGE §4).
26    NonCanonicalChunking { object: Hash, detail: &'static str },
27    /// Commit/tag timestamp exceeds `i64::MAX` (SPEC-GIT-BRIDGE §6.2).
28    TimestampOverflow { object: Hash, timestamp: u64 },
29    /// Tag object name contains bytes outside the mkit ref grammar
30    /// (SPEC-GIT-BRIDGE §7.1).
31    TagName { object: Hash },
32    /// Ref name is mkit-legal but git-illegal (SPEC-GIT-BRIDGE §12.1).
33    RefName { name: String, reason: &'static str },
34    /// Object prologue carries a schema version this mapping does not
35    /// cover (SPEC-GIT-BRIDGE §1.2).
36    SchemaVersion { object: Hash },
37    /// Import: submodule gitlink entry (SPEC-GIT-IMPORT §3.3). The
38    /// `object` is the zero-padded git tree sha1.
39    Gitlink { object: Hash, path: String },
40    /// Import: git tree-entry name mkit cannot store (SPEC-OBJECTS
41    /// §4.1 deserialize-time rules).
42    TreeEntryName { object: Hash, name: String },
43    /// Import: a git tree mode outside the pinned §3.3 table.
44    UnknownTreeMode { object: Hash, mode: String },
45    /// Import: a historic mode would normalize, but the state dir is
46    /// fork-mode (normalization breaks shared-SHA passthrough).
47    NormalizedModeInFork { object: Hash, mode: String },
48    /// Import: pre-1970 git timestamp (mkit timestamps are u64).
49    NegativeTimestamp { object: Hash, timestamp: i64 },
50    /// Import: structurally unparsable git object (SPEC-GIT-IMPORT
51    /// §3.2/§3.5 — refused per-ref, never coerced).
52    Unparsable { object: Hash, detail: String },
53    /// Import: blob over the 1 GiB per-file cap (SPEC-GIT-IMPORT §3.1).
54    BlobTooLarge { object: Hash, size: u64 },
55    /// Import: git tree with more entries than mkit's decode cap —
56    /// storing it would poison a signed object the store can never
57    /// read back (SPEC-GIT-IMPORT §3.3).
58    TooManyTreeEntries { object: Hash, count: usize },
59    /// Import: a tree entry's git mode contradicts the actual kind of
60    /// the object it names (e.g. mode 100644 → a commit). git tools
61    /// barely tolerate these; mkit's object model cannot represent
62    /// them (SPEC-GIT-IMPORT §3.3).
63    TreeEntryKind { object: Hash, name: String },
64    /// Import: the translated object cannot serialize under
65    /// SPEC-OBJECTS caps (oversize payload, illegal field) — refused
66    /// per-ref rather than failing the whole run.
67    Unrepresentable { object: Hash, detail: String },
68    /// Import: more than 1000 parents (`MAX_PARENTS`).
69    TooManyParents { object: Hash },
70    /// Import: author/tagger identity payload empty or over 4096.
71    AuthorPayload { object: Hash },
72    /// Import: tag→tag chain beyond the pinned depth (16).
73    TagChain { object: Hash },
74    /// Import: duplicate entry names after re-sorting to mkit order
75    /// (git-representable as file+dir of one name; undecodable here).
76    DuplicateTreeEntry { object: Hash },
77    /// Import: tree nesting beyond `MAX_TREE_DEPTH` (128).
78    TreeTooDeep { object: Hash },
79}
80
81impl fmt::Display for Refusal {
82    #[allow(clippy::too_many_lines)] // one arm per refusal; flat by design
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::Remix { object } => write!(
86                f,
87                "remix object {} is not translatable in bridge v1 (SPEC-GIT-BRIDGE §8)",
88                to_hex(object)
89            ),
90            Self::FixedSizeChunking { object, chunk_size } => write!(
91                f,
92                "chunked blob {} uses fixed-size chunking ({chunk_size}); only \
93                 content-defined manifests translate (SPEC-GIT-BRIDGE §4)",
94                to_hex(object)
95            ),
96            Self::NonCanonicalChunking { object, detail } => write!(
97                f,
98                "chunked blob {} cannot have been produced by a conformant \
99                 mkit writer ({detail}); refusing a non-round-trippable \
100                 translation (SPEC-GIT-BRIDGE §4)",
101                to_hex(object)
102            ),
103            Self::TimestampOverflow { object, timestamp } => write!(
104                f,
105                "object {} timestamp {timestamp} exceeds the git-representable range",
106                to_hex(object)
107            ),
108            Self::TagName { object } => write!(
109                f,
110                "tag object {} has a name outside the mkit ref grammar; \
111                 it cannot ride in a git tag header",
112                to_hex(object)
113            ),
114            Self::RefName { name, reason } => {
115                write!(f, "ref {name:?} is not a legal git ref name ({reason})")
116            }
117            Self::SchemaVersion { object } => write!(
118                f,
119                "object {} has a schema_version other than 1; bridge v1 maps schema 1 only",
120                to_hex(object)
121            ),
122            Self::Gitlink { object, path } => write!(
123                f,
124                "git tree {} contains a submodule gitlink at {path:?}; submodules are out \
125                 of scope (vendor the submodule, or exclude this ref) — SPEC-GIT-IMPORT §3.3",
126                &to_hex(object)[..40]
127            ),
128            Self::TreeEntryName { object, name } => write!(
129                f,
130                "git tree {} entry {name:?} is not a storable mkit name (SPEC-OBJECTS §4.1); \
131                 rename it upstream or exclude this ref",
132                &to_hex(object)[..40]
133            ),
134            Self::UnknownTreeMode { object, mode } => write!(
135                f,
136                "git tree {} carries mode {mode} outside the import mapping (SPEC-GIT-IMPORT §3.3)",
137                &to_hex(object)[..40]
138            ),
139            Self::NormalizedModeInFork { object, mode } => write!(
140                f,
141                "git tree {} carries historic mode {mode}, which would normalize lossily; \
142                 this state dir is fork-mode, where normalized trees cannot reproduce their \
143                 original sha1 — refusing (SPEC-GIT-IMPORT §3.3)",
144                &to_hex(object)[..40]
145            ),
146            Self::Unparsable { object, detail } => write!(
147                f,
148                "git object {} is structurally unparsable ({detail}); refused per SPEC-GIT-IMPORT §3.2",
149                &to_hex(object)[..40]
150            ),
151            Self::TooManyTreeEntries { object, count } => write!(
152                f,
153                "git tree {} has {count} entries, over mkit's decode cap (SPEC-GIT-IMPORT §3.3)",
154                &to_hex(object)[..40]
155            ),
156            Self::TreeEntryKind { object, name } => write!(
157                f,
158                "git tree {} entry {name:?} names an object of a kind its mode contradicts (SPEC-GIT-IMPORT §3.3)",
159                &to_hex(object)[..40]
160            ),
161            Self::Unrepresentable { object, detail } => write!(
162                f,
163                "git object {} does not serialize under SPEC-OBJECTS ({detail}); refused per SPEC-GIT-IMPORT §3",
164                &to_hex(object)[..40]
165            ),
166            Self::BlobTooLarge { object, size } => write!(
167                f,
168                "git blob {} is {size} bytes, over the 1 GiB per-file cap (SPEC-GIT-IMPORT §3.1)",
169                &to_hex(object)[..40]
170            ),
171            Self::NegativeTimestamp { object, timestamp } => write!(
172                f,
173                "git object {} has pre-1970 timestamp {timestamp}; mkit timestamps are unsigned",
174                &to_hex(object)[..40]
175            ),
176            Self::TooManyParents { object } => write!(
177                f,
178                "git commit {} has more than 1000 parents (MAX_PARENTS)",
179                &to_hex(object)[..40]
180            ),
181            Self::AuthorPayload { object } => write!(
182                f,
183                "git object {} has an author/tagger identity that is empty or over 4096 bytes",
184                &to_hex(object)[..40]
185            ),
186            Self::TagChain { object } => write!(
187                f,
188                "git tag {} heads a tag chain deeper than 16; refusing (SPEC-GIT-IMPORT §3.4)",
189                &to_hex(object)[..40]
190            ),
191            Self::DuplicateTreeEntry { object } => write!(
192                f,
193                "git tree {} contains entries whose names collide byte-equal in mkit \
194                 order (e.g. a file and a directory of one name); refusing",
195                &to_hex(object)[..40]
196            ),
197            Self::TreeTooDeep { object } => write!(
198                f,
199                "git tree {} nests deeper than 128 levels; refusing (matches mkit's \
200                 MAX_TREE_DEPTH defense)",
201                &to_hex(object)[..40]
202            ),
203        }
204    }
205}
206
207/// Unified bridge error.
208#[derive(Debug)]
209pub enum BridgeError {
210    /// A spec'd policy refusal (see [`Refusal`]).
211    Refused(Refusal),
212    /// Reading or decoding a source mkit object failed.
213    Source(String),
214    /// Reconstruction input is not a bridge-emitted git object
215    /// (missing/duplicate/unknown `mkit-*` headers, malformed body,
216    /// non-bridge mode bytes, …).
217    NotBridgeObject(String),
218    /// Reconstructed bytes failed an integrity check (BLAKE3 linkage
219    /// or round-trip mismatch).
220    Integrity(String),
221    /// Filesystem error from the loose-object writer or map cache.
222    Io(std::io::Error),
223}
224
225impl fmt::Display for BridgeError {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        match self {
228            Self::Refused(r) => write!(f, "refused: {r}"),
229            Self::Source(m) => write!(f, "source object: {m}"),
230            Self::NotBridgeObject(m) => write!(f, "not a bridge-emitted git object: {m}"),
231            Self::Integrity(m) => write!(f, "integrity: {m}"),
232            Self::Io(e) => write!(f, "io: {e}"),
233        }
234    }
235}
236
237impl std::error::Error for BridgeError {}
238
239impl From<std::io::Error> for BridgeError {
240    fn from(e: std::io::Error) -> Self {
241        Self::Io(e)
242    }
243}
244
245impl From<Refusal> for BridgeError {
246    fn from(r: Refusal) -> Self {
247        Self::Refused(r)
248    }
249}