obj/error.rs
1//! Error-code enum + `obj_strerror`.
2//!
3//! Every fallible C entry point returns [`obj_error_t`] — a signed
4//! `int32_t` because C99 has no `uint32_t`-typed enum constants in
5//! the language proper (cbindgen emits a typedef + a set of
6//! `#define`-style constants).
7//!
8//! The mapping from [`obj_engine::Error`] to one of these codes lives in
9//! [`error_to_code`]. The match is exhaustive: if [`obj_engine::Error`]
10//! gains a variant, the build fails until the mapping is updated —
11//! power-of-ten Rule 7 plus a `#[non_exhaustive]` catch-all that
12//! collapses unknown variants into the most defensive ([`OBJ_ERR_CORRUPTION`]).
13
14use core::ffi::c_char;
15
16/// Numeric type carried by `obj_error_t`. Aliased so the cbindgen
17/// header emits `typedef int32_t obj_error_t;` rather than a bare
18/// `int32_t` everywhere.
19pub type obj_error_t = i32;
20
21/// Operation succeeded.
22pub const OBJ_OK: obj_error_t = 0;
23
24/// Caller passed an invalid argument: a null handle / out-pointer
25/// where one is required, a non-UTF-8 path or collection name, a
26/// length that exceeds the documented maximum, etc.
27pub const OBJ_ERR_INVALID_ARG: obj_error_t = 1;
28
29/// Underlying I/O / syscall failure: file open, read, write, fsync,
30/// directory enumeration, lock acquisition syscall.
31pub const OBJ_ERR_IO: obj_error_t = 2;
32
33/// Content-level corruption: a page CRC mismatch, malformed
34/// catalog row, schema-version-from-future, B-tree invariant
35/// violated, etc.
36pub const OBJ_ERR_CORRUPTION: obj_error_t = 3;
37
38/// A lock could not be acquired inside the configured timeout —
39/// another transaction holds the writer mutex / cross-process
40/// writer byte, or the in-process pager mutex was poisoned.
41pub const OBJ_ERR_BUSY: obj_error_t = 4;
42
43/// The requested record / index entry / collection was not found.
44/// Iterators also return this when exhausted (see
45/// `obj_iter_next` docs).
46pub const OBJ_ERR_NOT_FOUND: obj_error_t = 5;
47
48/// An integrity check completed and found violations. Distinct
49/// from [`OBJ_ERR_CORRUPTION`] which is the hard fail-closed shape
50/// emitted by individual operations; [`OBJ_ERR_INTEGRITY`] is the
51/// soft "the report has failures" shape returned by aggregate
52/// diagnostic calls.
53pub const OBJ_ERR_INTEGRITY: obj_error_t = 6;
54
55/// Operation is not supported in this build / for this database
56/// (e.g. `obj_backup_to` on an in-memory database, an attempt to
57/// mutate an attached read-only attachment, an unimplemented
58/// schema migration).
59pub const OBJ_ERR_UNSUPPORTED: obj_error_t = 7;
60
61/// A C string handed across the FFI boundary was not valid UTF-8.
62/// Distinct from [`OBJ_ERR_INVALID_ARG`] because UTF-8 issues are
63/// frequent enough on user paths / index names to merit their own
64/// signal.
65pub const OBJ_ERR_UTF8: obj_error_t = 8;
66
67/// Highest reserved code in the obj-defined range. Future error
68/// codes must stay `<= OBJ_ERR_RESERVED_MAX`; this allows
69/// downstream callers to reliably distinguish obj-emitted codes
70/// from any they may layer on top.
71pub const OBJ_ERR_RESERVED_MAX: obj_error_t = 1023;
72
73/// Static null-terminated message used by [`obj_strerror`]. The
74/// strings carry an embedded `\0` terminator so they can be
75/// returned as `*const c_char` directly.
76struct ErrorEntry {
77 /// The associated code.
78 code: obj_error_t,
79 /// NUL-terminated UTF-8 bytes.
80 msg: &'static [u8],
81}
82
83const ERROR_TABLE: &[ErrorEntry] = &[
84 ErrorEntry {
85 code: OBJ_OK,
86 msg: b"OK\0",
87 },
88 ErrorEntry {
89 code: OBJ_ERR_INVALID_ARG,
90 msg: b"invalid argument\0",
91 },
92 ErrorEntry {
93 code: OBJ_ERR_IO,
94 msg: b"i/o error\0",
95 },
96 ErrorEntry {
97 code: OBJ_ERR_CORRUPTION,
98 msg: b"corruption detected\0",
99 },
100 ErrorEntry {
101 code: OBJ_ERR_BUSY,
102 msg: b"lock busy\0",
103 },
104 ErrorEntry {
105 code: OBJ_ERR_NOT_FOUND,
106 msg: b"not found\0",
107 },
108 ErrorEntry {
109 code: OBJ_ERR_INTEGRITY,
110 msg: b"integrity check failed\0",
111 },
112 ErrorEntry {
113 code: OBJ_ERR_UNSUPPORTED,
114 msg: b"operation not supported\0",
115 },
116 ErrorEntry {
117 code: OBJ_ERR_UTF8,
118 msg: b"invalid UTF-8\0",
119 },
120];
121
122/// Default fallback when [`obj_strerror`] is called with an
123/// unknown code. The leading `unknown:` prefix is stable enough
124/// for grep-driven diagnostics.
125const UNKNOWN_ERR: &[u8] = b"unknown obj_error_t\0";
126
127/// Map an [`obj_engine::Error`] variant onto an [`obj_error_t`] code.
128///
129/// The match is exhaustive over the public `obj_engine::Error` enum at
130/// compile time so any future variant addition fails to compile
131/// here, forcing the C-ABI mapping to stay in lockstep — power-of-
132/// ten Rule 7.
133///
134/// `#[non_exhaustive]` on `obj_engine::Error` means we must keep a final
135/// catch-all arm. The defensive choice is [`OBJ_ERR_CORRUPTION`]:
136/// callers who treat unknown codes as "stop and investigate" stay
137/// safe even if obj-core grows a new fail-closed variant before
138/// the mapping is updated.
139#[must_use]
140// Clippy `match_same_arms` would collapse the explicit per-
141// variant arms into the wildcard, but the explicit structure
142// IS the design — each new `obj_engine::Error` variant should fail to
143// compile here until a maintainer decides where it slots into the
144// C-ABI taxonomy. Power-of-ten Rule 7.
145#[allow(clippy::match_same_arms)] // explicit variant enumeration is the design.
146pub(crate) fn error_to_code(err: &obj_engine::Error) -> obj_error_t {
147 use obj_engine::Error;
148 match err {
149 Error::Io(_) => OBJ_ERR_IO,
150 Error::Corruption { .. }
151 | Error::WalCorruption { .. }
152 | Error::InvalidFormat { .. }
153 | Error::BTreeDepthExceeded { .. }
154 | Error::BTreeInvariantViolated { .. }
155 | Error::CollectionIdMismatch { .. } => OBJ_ERR_CORRUPTION,
156 Error::InvalidArgument(_)
157 | Error::BTreeKeyTooLarge { .. }
158 | Error::BTreeValueTooLarge { .. }
159 | Error::BTreeKeyExists
160 | Error::BTreeScanLimitExceeded { .. }
161 | Error::DocumentTooLarge { .. }
162 | Error::SortBufferExceeded { .. }
163 | Error::SortKeyEncode { .. }
164 | Error::DistinctCountExceeded { .. }
165 | Error::IndexFieldMissing { .. }
166 | Error::IndexFieldTypeMismatch { .. }
167 | Error::IndexKindMismatch { .. }
168 | Error::IndexKeyPathsMismatch { .. }
169 | Error::UniqueConstraintViolation { .. }
170 | Error::EachIndexTooLarge { .. }
171 | Error::SchemaTypeMismatch { .. }
172 | Error::SchemaDepthExceeded { .. }
173 | Error::DynamicPathNotMap { .. } => OBJ_ERR_INVALID_ARG,
174 Error::Codec(_) => OBJ_ERR_CORRUPTION,
175 Error::Busy { .. } => OBJ_ERR_BUSY,
176 Error::CollectionNotFound { .. }
177 | Error::DocumentNotFound { .. }
178 | Error::IndexNotFound { .. }
179 | Error::IndexNotUnique { .. }
180 | Error::CollectionNamespaceUnknown { .. } => OBJ_ERR_NOT_FOUND,
181 Error::CollectionAlreadyExists { .. }
182 | Error::IdSpaceExhausted { .. }
183 | Error::ReadOnly { .. }
184 | Error::SchemaMigrationNotImplemented { .. }
185 | Error::SchemaVersionFromFuture { .. }
186 | Error::SchemaNotRegistered { .. }
187 | Error::BackupNotSupportedForMemoryPager
188 | Error::AttachedDatabaseIsReadOnly { .. } => OBJ_ERR_UNSUPPORTED,
189 Error::BackupDestinationExists { .. }
190 | Error::AttachmentAlreadyExists { .. }
191 | Error::AttachmentNotReadable { .. } => OBJ_ERR_IO,
192 // `Error` is `#[non_exhaustive]`; future variants collapse
193 // to the most defensive code so a C caller still sees
194 // "this is bad, stop" rather than `OBJ_OK`.
195 _ => OBJ_ERR_CORRUPTION,
196 }
197}
198
199/// Return a static C string describing `code`. Never returns NULL.
200///
201/// # Safety
202///
203/// The returned pointer points into the read-only data segment of
204/// the loaded library; it has `'static` lifetime and MUST NOT be
205/// freed. It is safe to read until the dynamic library is
206/// unloaded.
207#[no_mangle]
208pub unsafe extern "C" fn obj_strerror(code: obj_error_t) -> *const c_char {
209 for entry in ERROR_TABLE {
210 if entry.code == code {
211 return entry.msg.as_ptr().cast::<c_char>();
212 }
213 }
214 UNKNOWN_ERR.as_ptr().cast::<c_char>()
215}