Skip to content

Commit 586b549

Browse files
feat: fallible
1 parent fd12e6e commit 586b549

File tree

5 files changed

+358
-101
lines changed

5 files changed

+358
-101
lines changed

confidence-resolver/src/err.rs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
use alloc::string::{String, ToString};
2+
use core::panic::Location;
3+
4+
/// A minimal error type suitable as a replacement for runtime panics.
5+
///
6+
/// - Its only state is a 48‑bit code intended to be unique per call site or tag.
7+
/// - Renders as an 8‑char base64url code (no padding).
8+
/// - Use `ErrorCode::from_location()` (#[track_caller]) to derive a code from the call site, or
9+
/// `ErrorCode::from_tag("module.feature.case")` for a stable tag.
10+
/// - Typical internal use: return `Fallible<T>` (alias for `Result<T, ErrorCode>`) and propagate with `?`.
11+
/// - At API boundaries that return `Result<T, String>`, `?` works via `From<ErrorCode> for String`
12+
/// and renders as `internal error [XXXXXXXX]`.
13+
/// - `Option<T>` or `Result<T,_>` can be converted to `Fallible<T>` via `.or_fail()` See `OrFailExt`
14+
///
15+
/// Note: We do not (yet) ship a code→location/tag table; that can be generated in a separate build if needed.
16+
///
17+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
18+
pub struct ErrorCode([u8; 6]);
19+
20+
impl ErrorCode {
21+
pub const fn new(code: u64) -> Self {
22+
let b = code.to_le_bytes();
23+
ErrorCode([b[0], b[1], b[2], b[3], b[4], b[5]])
24+
}
25+
26+
/// Builds a code from a stable tag string.
27+
///
28+
/// - Useful when you want stability across refactors (line moves).
29+
/// - Keep tags short and unique within the crate; consider a CI check for duplicates.
30+
pub const fn from_tag(tag: &str) -> Self {
31+
ErrorCode::new(fnv1a64([tag.as_bytes()]))
32+
}
33+
34+
/// Builds a code from the caller’s file/line/column.
35+
///
36+
/// - Uses `#[track_caller]` so the code reflects the call site (moves if lines change).
37+
/// - Prefer this where you’d otherwise `panic!`.
38+
#[track_caller]
39+
pub const fn from_location() -> Self {
40+
let loc = Location::caller();
41+
let parts = [
42+
loc.file().as_bytes(),
43+
&loc.line().to_le_bytes(),
44+
&loc.column().to_le_bytes(),
45+
];
46+
ErrorCode::new(fnv1a64(parts))
47+
}
48+
49+
/// Returns the 8‑character base64url encoding of the 48‑bit code (no padding).
50+
pub fn b64(self) -> [u8; 8] {
51+
// load to 48-bit value
52+
let mut v: u64 = 0;
53+
for b in self.0 {
54+
v = (v << 8) | (b as u64);
55+
}
56+
57+
// emit 8 sextets (MSB first)
58+
core::array::from_fn(|i| {
59+
let shift = 42 - i * 6;
60+
let sextet = ((v >> shift) & 0x3F) as u8;
61+
b64u6(sextet)
62+
})
63+
}
64+
65+
/// Returns the raw 48‑bit code in the low bits of a `u64` (little‑endian packing).
66+
pub fn code(self) -> u64 {
67+
let mut b = [0u8; 8];
68+
b[..6].copy_from_slice(&self.0);
69+
u64::from_le_bytes(b)
70+
}
71+
72+
pub fn b64_str(self) -> impl core::fmt::Display {
73+
struct D([u8; 8]);
74+
impl core::fmt::Display for D {
75+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
76+
// safe because base 64 is ASCII so identical bytes in utf8
77+
write!(f, "{}", unsafe { core::str::from_utf8_unchecked(&self.0) })
78+
}
79+
}
80+
D(self.b64())
81+
}
82+
}
83+
84+
impl From<ErrorCode> for String {
85+
fn from(e: ErrorCode) -> String {
86+
e.to_string()
87+
}
88+
}
89+
90+
/// Convenience alias for `Result<T, ErrorCode>` used in internal APIs.
91+
pub type Fallible<T> = core::result::Result<T, ErrorCode>;
92+
93+
/// Extension methods to collapse `Option<T>`/`Result<T, E>` into `Fallible<T>`.
94+
///
95+
/// - `Option<T>::or_fail()` → code from call site if `None`.
96+
/// - `Result<T, E>::or_fail()` → maps any `Err(E)` to a call‑site code.
97+
/// Prefer plain `?` when the error is already `ErrorCode`.
98+
pub trait OrFailExt<T> {
99+
#[track_caller]
100+
fn or_fail(self) -> Fallible<T>;
101+
}
102+
103+
/// Macro: derive an `ErrorCode` from a module‑qualified tag.
104+
///
105+
/// - Usage: `module_err!(":subsystem.case")`
106+
/// - Expands to `ErrorCode::from_tag(concat!(module_path!(), tag))`.
107+
/// - Returns an `ErrorCode` value (not a `Result`); use with `ok_or(...)` / `map_err(...)`.
108+
///
109+
/// Examples:
110+
/// ```rust
111+
/// let code = module_err!(":gzip.crc_mismatch");
112+
/// let crc = buf.get(..4).ok_or(module_err!(":gzip.truncated_crc"))?;
113+
/// ```
114+
#[macro_export]
115+
macro_rules! module_err {
116+
($tag:literal) => {
117+
$crate::ErrorCode::from_tag(concat!(module_path!(), $tag))
118+
};
119+
}
120+
121+
/// Macro: early‑return with `Err(ErrorCode)`.
122+
///
123+
/// Forms:
124+
/// - `fail!()` → `return Err(ErrorCode::from_location())`
125+
/// - `fail!(":tag")` → `return Err(module_err!(":tag"))`
126+
///
127+
/// Notes:
128+
/// - Add a semicolon at the call site when used as a statement: `fail!();`.
129+
/// - Prefer `.or_fail()?` on `Option`/`Result` when you’re not immediately returning.
130+
///
131+
/// Examples:
132+
/// ```rust
133+
/// if bad_magic { fail!(":parser.bad_magic"); }
134+
/// let v = maybe_val.or_fail()?; // alternative for Option
135+
/// ```
136+
#[macro_export]
137+
macro_rules! fail {
138+
() => {
139+
return Err($crate::ErrorCode::from_location())
140+
};
141+
($tag:literal) => {
142+
return Err($crate::module_err!($tag))
143+
};
144+
}
145+
146+
impl<T> OrFailExt<T> for Option<T> {
147+
#[track_caller]
148+
fn or_fail(self) -> Fallible<T> {
149+
self.ok_or(ErrorCode::from_location())
150+
}
151+
}
152+
153+
impl<T, E> OrFailExt<T> for Result<T, E> {
154+
#[track_caller]
155+
fn or_fail(self) -> Fallible<T> {
156+
self.map_err(|_| ErrorCode::from_location())
157+
}
158+
}
159+
160+
impl core::fmt::Display for ErrorCode {
161+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
162+
write!(f, "internal error [{}]", self.b64_str())
163+
}
164+
}
165+
166+
#[allow(clippy::indexing_slicing)]
167+
const fn fnv1a64<const N: usize>(parts: [&[u8]; N]) -> u64 {
168+
const FNV64_INIT: u64 = 0xCBF2_9CE4_8422_2325;
169+
const FNV64_PRIME: u64 = 0x1000_0000_01B3;
170+
171+
let mut h = FNV64_INIT;
172+
let mut i = 0;
173+
while i < N {
174+
let b = parts[i];
175+
let mut j = 0;
176+
while j < b.len() {
177+
h ^= b[j] as u64;
178+
h = h.wrapping_mul(FNV64_PRIME);
179+
j += 1;
180+
}
181+
i += 1;
182+
}
183+
h
184+
}
185+
186+
#[inline]
187+
fn b64u6(x: u8) -> u8 {
188+
match x {
189+
0..=25 => b'A' + x,
190+
26..=51 => b'a' + (x - 26),
191+
52..=61 => b'0' + (x - 52),
192+
62 => b'-',
193+
_ => b'_', // 63
194+
}
195+
}
196+
197+
#[cfg(test)]
198+
mod tests {
199+
use super::*;
200+
201+
#[test]
202+
fn display() {
203+
let e = ErrorCode::from_location();
204+
let s = e.to_string();
205+
assert!(s.starts_with("internal error ["));
206+
}
207+
208+
#[test]
209+
fn different_call_sites_differ() {
210+
let a = ErrorCode::from_location();
211+
let b = ErrorCode::from_location(); // different line ⇒ different site
212+
assert_ne!(a, b);
213+
}
214+
}

confidence-resolver/src/gzip.rs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,61 @@
22
// use std::io::{BufRead, Error, ErrorKind, Read, Result, Write};
33
// use std::time;
44
use alloc::vec::Vec;
5-
use bytes::Buf;
65
use miniz_oxide::inflate::decompress_to_vec;
76

7+
use crate::err::{Fallible, OrFailExt};
8+
use crate::fail;
9+
810
const FHCRC: u8 = 1 << 1;
911
const FEXTRA: u8 = 1 << 2;
1012
const FNAME: u8 = 1 << 3;
1113
const FCOMMENT: u8 = 1 << 4;
1214
const FRESERVED: u8 = 1 << 5 | 1 << 6 | 1 << 7;
1315

14-
pub fn decompress_gz(buffer: &[u8]) -> Result<Vec<u8>, &'static str> {
16+
pub fn decompress_gz(buffer: &[u8]) -> Fallible<Vec<u8>> {
1517
let [m0, m1, cm, flags, ..] = *buffer else {
16-
return Err("truncated header");
18+
fail!();
1719
};
1820
// let header : &[u8; 4] = buffer.get(0..4).ok_or("truncated header")?.try_into().map_err(|_| "err")?;
1921
if m0 != 0x1f || m1 != 0x8b {
20-
return Err("invalid magic number");
22+
fail!("invalid magic number");
2123
}
2224
if cm != 8 {
23-
return Err("invalid compression method");
25+
fail!("invalid compression method");
2426
}
2527
if flags & FRESERVED != 0 {
26-
return Err("invalid flags");
28+
fail!("invalid flags");
2729
}
2830
if flags & FEXTRA != 0 {
29-
return Err("extra data not supported");
31+
fail!("extra data not supported");
3032
}
3133
if flags & FNAME != 0 {
32-
return Err("filename not supported");
34+
fail!("filename not supported");
3335
}
3436
if flags & FCOMMENT != 0 {
35-
return Err("comment not supported");
37+
fail!("comment not supported");
3638
}
3739
if flags & FHCRC != 0 {
38-
return Err("crc not supported");
40+
fail!("crc not supported");
3941
}
4042
let trailer_start = buffer.len() - 8;
41-
let crc_bytes = buffer.get(trailer_start..trailer_start + 4).ok_or("truncated crc")?;
42-
let crc = u32::from_le_bytes(crc_bytes.try_into().map_err(|_| "err")?);
43+
let crc_bytes = buffer.get(trailer_start..trailer_start + 4).or_fail()?;
44+
let crc = u32::from_le_bytes(crc_bytes.try_into().or_fail()?);
4345
let isize = u32::from_le_bytes(
44-
buffer.get(trailer_start + 4..trailer_start + 8).ok_or("err")?
46+
buffer
47+
.get(trailer_start + 4..trailer_start + 8)
48+
.or_fail()?
4549
.try_into()
46-
.map_err(|_| "err")?,
50+
.or_fail()?,
4751
);
48-
let compressed_bytes = buffer.get(10..trailer_start).ok_or("truncated data")?;
49-
let data = decompress_to_vec(compressed_bytes).map_err(|_| "failed to decompress")?;
52+
let compressed_bytes = buffer.get(10..trailer_start).or_fail()?;
53+
let data = decompress_to_vec(compressed_bytes).or_fail()?;
5054
if isize != data.len() as u32 {
51-
return Err("invalid isize");
55+
fail!("invalid data length");
5256
}
5357
let crc_calc = crc32fast::hash(&data);
5458
if crc_calc != crc {
55-
return Err("crc mismatch");
59+
fail!("crc mismatch");
5660
}
5761
Ok(data)
5862
}

0 commit comments

Comments
 (0)