Skip to main content

saddle_core/
bounded_diagnostic.rs

1//! Allocation-free source facts for admitted execution. Not execution authority.
2use crate::{CaptureSite, DiagnosticCategory, DiagnosticCode, DiagnosticStage};
3use serde::{Serialize, Serializer};
4
5/// Audited metadata only, never arbitrary error messages or business values.
6#[derive(Clone, Copy)]
7pub struct InlineDiagnosticText {
8    bytes: [u8; 192],
9    len: u8,
10    truncated: bool,
11    redacted: bool,
12}
13impl InlineDiagnosticText {
14    pub fn metadata(value: &str) -> Self {
15        let mut out = Self {
16            bytes: [0; 192],
17            len: 0,
18            truncated: false,
19            redacted: false,
20        };
21        let mut len = value.len().min(192);
22        while !value.is_char_boundary(len) {
23            len -= 1;
24        }
25        out.truncated = len < value.len();
26        if !value[..len]
27            .chars()
28            .all(|c| c.is_alphanumeric() || "_:<>[],(); &*.-".contains(c))
29        {
30            out.redacted = true;
31            return out;
32        }
33        out.bytes[..len].copy_from_slice(&value.as_bytes()[..len]);
34        out.len = len as u8;
35        out.truncated = len < value.len();
36        out
37    }
38}
39impl Serialize for InlineDiagnosticText {
40    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
41        use serde::ser::SerializeStruct;
42        let mut value = serializer.serialize_struct("InlineDiagnosticText", 3)?;
43        value.serialize_field(
44            "value",
45            std::str::from_utf8(&self.bytes[..self.len as usize]).unwrap_or(""),
46        )?;
47        value.serialize_field("truncated", &self.truncated)?;
48        value.serialize_field("redacted", &self.redacted)?;
49        value.end()
50    }
51}
52
53#[derive(Clone, Copy, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum DiagnosticFactUnavailable {
56    OpaqueSource,
57    MetadataUnavailable,
58    NotApplicable,
59}
60
61/// Membership must come from the generated response registry, not an error text.
62#[derive(Clone, Copy, Serialize)]
63pub struct RegisteredDiagnosticCode(&'static str);
64impl RegisteredDiagnosticCode {
65    pub fn from_registered(code: &'static str, registry: &[&'static str]) -> Option<Self> {
66        (!code.is_empty()
67            && code.len() <= 128
68            && code
69                .bytes()
70                .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
71            && registry.len() <= 256
72            && registry.contains(&code))
73        .then_some(Self(code))
74    }
75}
76
77/// Fixed source facts. Static codes must come from the component's closed mapping.
78#[derive(Clone, Copy, Serialize)]
79pub struct BoundedDiagnosticCause {
80    stage: DiagnosticStage,
81    code: DiagnosticCode,
82    io_kind: Option<DiagnosticCode>,
83    os_code: Option<i32>,
84    database_code: Option<u32>,
85    sqlstate: Option<InlineDiagnosticText>,
86    column_index: Option<u64>,
87    column_count: Option<u64>,
88    target_rust_type: Option<InlineDiagnosticText>,
89    actual_db_type: Option<InlineDiagnosticText>,
90    object: Option<InlineDiagnosticText>,
91    unavailable: Option<DiagnosticFactUnavailable>,
92    target_type_unavailable: Option<DiagnosticFactUnavailable>,
93    actual_type_unavailable: Option<DiagnosticFactUnavailable>,
94}
95impl BoundedDiagnosticCause {
96    pub fn new(stage: DiagnosticStage, code: DiagnosticCode) -> Self {
97        Self {
98            stage,
99            code,
100            io_kind: None,
101            os_code: None,
102            database_code: None,
103            sqlstate: None,
104            column_index: None,
105            column_count: None,
106            target_rust_type: None,
107            actual_db_type: None,
108            object: None,
109            unavailable: None,
110            target_type_unavailable: Some(DiagnosticFactUnavailable::MetadataUnavailable),
111            actual_type_unavailable: Some(DiagnosticFactUnavailable::MetadataUnavailable),
112        }
113    }
114    pub fn with_system(mut self, kind: DiagnosticCode, os: Option<i32>) -> Self {
115        self.io_kind = Some(kind);
116        self.os_code = os;
117        self
118    }
119    pub fn with_database_code(mut self, code: u32) -> Self {
120        self.database_code = Some(code);
121        self
122    }
123    pub fn with_sqlstate(mut self, state: &str) -> Self {
124        self.sqlstate = (state.len() == 5
125            && state
126                .bytes()
127                .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()))
128        .then(|| InlineDiagnosticText::metadata(state));
129        self
130    }
131    pub fn with_column(mut self, index: Option<u64>, count: Option<u64>) -> Self {
132        self.column_index = index;
133        self.column_count = count;
134        self
135    }
136    pub fn with_types(
137        mut self,
138        target: Option<InlineDiagnosticText>,
139        actual: Option<InlineDiagnosticText>,
140        unavailable: Option<DiagnosticFactUnavailable>,
141    ) -> Self {
142        self.target_rust_type = target;
143        self.actual_db_type = actual;
144        self.unavailable = unavailable;
145        self.target_type_unavailable = if target.is_some() {
146            None
147        } else {
148            Some(unavailable.unwrap_or(DiagnosticFactUnavailable::MetadataUnavailable))
149        };
150        self.actual_type_unavailable = if actual.is_some() {
151            None
152        } else {
153            Some(unavailable.unwrap_or(DiagnosticFactUnavailable::MetadataUnavailable))
154        };
155        self
156    }
157    pub fn with_object(mut self, object: InlineDiagnosticText) -> Self {
158        self.object = Some(object);
159        self
160    }
161}
162
163/// Four fixed causes plus explicit overflow. Capture never invokes Backtrace or
164/// an arbitrary source formatter. Retain alongside the original Result, not in it.
165#[derive(Serialize)]
166pub struct BoundedDiagnostic {
167    diagnostic_id: u64,
168    primary_diagnostic_id: Option<u64>,
169    category: DiagnosticCategory,
170    capture_site: CaptureSite,
171    origin_file: &'static str,
172    origin_line: u32,
173    origin_column: u32,
174    causes: [Option<BoundedDiagnosticCause>; 4],
175    cause_count: usize,
176    omitted_causes: u64,
177    stack_status: &'static str,
178}
179impl BoundedDiagnostic {
180    #[track_caller]
181    pub fn capture(
182        category: DiagnosticCategory,
183        site: CaptureSite,
184        cause: BoundedDiagnosticCause,
185    ) -> Self {
186        let origin = std::panic::Location::caller();
187        let file = origin
188            .file()
189            .rsplit("/crates/")
190            .next()
191            .unwrap_or(origin.file());
192        let file = if file.starts_with('/') || file.contains('\\') {
193            file.rsplit(['/', '\\']).next().unwrap_or("unknown")
194        } else {
195            file
196        };
197        Self {
198            diagnostic_id: crate::diagnostic::NEXT_ID
199                .fetch_add(1, std::sync::atomic::Ordering::Relaxed),
200            primary_diagnostic_id: None,
201            category,
202            capture_site: site,
203            origin_file: file,
204            origin_line: origin.line(),
205            origin_column: origin.column(),
206            causes: [Some(cause), None, None, None],
207            cause_count: 1,
208            omitted_causes: 0,
209            stack_status: "unavailable_bounded_capture",
210        }
211    }
212    pub fn wrap(mut self, cause: BoundedDiagnosticCause) -> Self {
213        if self.cause_count == self.causes.len() {
214            self.omitted_causes = self.omitted_causes.saturating_add(1);
215        } else {
216            self.causes.copy_within(0..self.cause_count, 1);
217            self.causes[0] = Some(cause);
218            self.cause_count += 1;
219        }
220        self
221    }
222    pub fn during_cleanup_of(mut self, primary: &Self) -> Self {
223        self.primary_diagnostic_id = Some(primary.diagnostic_id);
224        self
225    }
226    pub fn id(&self) -> u64 {
227        self.diagnostic_id
228    }
229    pub fn occurrence(&self) -> DiagnosticOccurrence {
230        DiagnosticOccurrence {
231            diagnostic_id: self.diagnostic_id,
232            primary_diagnostic_id: self.primary_diagnostic_id,
233        }
234    }
235}
236
237/// Read-only correlation, not delivery confirmation or execution authority.
238///
239/// ```compile_fail
240/// use saddle_core::DiagnosticOccurrence;
241/// let forged = DiagnosticOccurrence { diagnostic_id: 1, primary_diagnostic_id: None };
242/// ```
243#[derive(Clone, Copy, Serialize)]
244pub struct DiagnosticOccurrence {
245    diagnostic_id: u64,
246    primary_diagnostic_id: Option<u64>,
247}
248impl DiagnosticOccurrence {
249    pub(crate) const fn source_id(&self) -> u64 {
250        self.diagnostic_id
251    }
252    pub(crate) fn from_diagnostic(diagnostic: &crate::Diagnostic) -> Self {
253        Self {
254            diagnostic_id: diagnostic.id(),
255            primary_diagnostic_id: diagnostic.primary_id_for_projection(),
256        }
257    }
258}
259
260#[derive(Clone, Copy, Serialize)]
261#[serde(rename_all = "snake_case")]
262pub enum OperationOutcome {
263    Succeeded,
264    Rejected,
265    Failed,
266    Cancelled,
267    TimedOut,
268    Panicked,
269    Unknown,
270}
271#[derive(Clone, Copy, Serialize)]
272#[serde(rename_all = "snake_case")]
273pub enum PhysicalDispositionFact {
274    NotUsed,
275    Returned,
276    Discarded,
277    Unknown,
278}
279#[derive(Clone, Copy, Serialize)]
280#[serde(rename_all = "snake_case")]
281pub enum BusinessOutcome {
282    Success,
283    Failure,
284    Unknown,
285}
286#[derive(Clone, Copy, Serialize)]
287#[serde(rename_all = "snake_case")]
288pub enum ResponseDelivery {
289    NotStarted,
290    Partial,
291    LocalWriteComplete,
292    Failed,
293    Cancelled,
294    TimedOut,
295    Unknown,
296}
297#[derive(Clone, Copy, Serialize)]
298#[serde(rename_all = "snake_case")]
299pub enum CleanupOutcome {
300    NotRun,
301    Succeeded,
302    Failed,
303    Unknown,
304}
305
306/// Each domain reports only facts it owns; defaults are deliberately unknown.
307/// These are observations, not proof of delivery, finalization or execution.
308#[derive(Serialize)]
309pub struct DiagnosticOutcomeAxes {
310    pub operation: OperationOutcome,
311    pub physical: PhysicalDispositionFact,
312    pub business: BusinessOutcome,
313    pub business_code: Option<RegisteredDiagnosticCode>,
314    pub delivery: ResponseDelivery,
315    pub bytes_written: Option<u64>,
316    pub cleanup: CleanupOutcome,
317}
318impl Default for DiagnosticOutcomeAxes {
319    fn default() -> Self {
320        Self {
321            operation: OperationOutcome::Unknown,
322            physical: PhysicalDispositionFact::Unknown,
323            business: BusinessOutcome::Unknown,
324            business_code: None,
325            delivery: ResponseDelivery::Unknown,
326            bytes_written: None,
327            cleanup: CleanupOutcome::Unknown,
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    #[test]
336    fn bounded_causes_primary_cleanup_metadata_and_business_code() {
337        let cause = BoundedDiagnosticCause::new(
338            DiagnosticStage::RequestDb,
339            DiagnosticCode::new("db.decode").unwrap(),
340        )
341        .with_database_code(1064)
342        .with_sqlstate("42000")
343        .with_types(
344            None,
345            Some(InlineDiagnosticText::metadata("VARCHAR")),
346            Some(DiagnosticFactUnavailable::OpaqueSource),
347        );
348        let mut primary = BoundedDiagnostic::capture(
349            DiagnosticCategory::UnexpectedError,
350            CaptureSite::FirstObserved,
351            cause,
352        );
353        let id = primary.id();
354        for _ in 0..6 {
355            primary = primary.wrap(cause);
356        }
357        let p = serde_json::to_value(&primary).unwrap();
358        assert_eq!(p["diagnostic_id"], id);
359        assert_eq!(p["omitted_causes"], 3);
360        assert_eq!(p["causes"][0]["sqlstate"]["value"], "42000");
361        assert_eq!(p["causes"][0]["target_type_unavailable"], "opaque_source");
362        assert!(p["causes"][0]["actual_type_unavailable"].is_null());
363        let cleanup = BoundedDiagnostic::capture(
364            DiagnosticCategory::UnexpectedError,
365            CaptureSite::FirstObserved,
366            cause,
367        )
368        .during_cleanup_of(&primary);
369        assert_ne!(cleanup.id(), id);
370        assert_eq!(
371            serde_json::to_value(cleanup).unwrap()["primary_diagnostic_id"],
372            id
373        );
374        let secret =
375            serde_json::to_value(InlineDiagnosticText::metadata("mysql://SECRET@host")).unwrap();
376        assert_eq!(secret["redacted"], true);
377        assert!(!secret.to_string().contains("SECRET"));
378        let long = serde_json::to_value(InlineDiagnosticText::metadata(&"界".repeat(100))).unwrap();
379        assert_eq!(long["truncated"], true);
380        assert!(
381            RegisteredDiagnosticCode::from_registered("INVALID_VALUE", &["INVALID_VALUE"])
382                .is_some()
383        );
384        assert!(RegisteredDiagnosticCode::from_registered("FOREIGN", &["INVALID_VALUE"]).is_none());
385        assert!(
386            RegisteredDiagnosticCode::from_registered("SECRET@host", &["SECRET@host"]).is_none()
387        );
388    }
389}