allow-core 0.2.0-rc.1

Core types and matching primitives for cargo-allow source exception policies.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
use std::fmt;
use std::ops::Range;
use std::path::Path;

/// One-based source location attached to a parse or validation diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoAllowErrorLocation {
    /// Source path when the caller had one; `None` means the input was an
    /// in-memory document without a known path.
    pub path: Option<String>,
    pub line: u32,
    pub column: u32,
}

/// Severity for a machine-readable diagnostic carried by a command error.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CargoAllowDiagnosticSeverity {
    Error,
    Warning,
    Info,
}

/// Structured validation or execution detail.
///
/// The fields are intentionally owned and optional so diagnostics can be
/// produced by policy, federation, import, and command layers without making
/// those layers depend on a parser-specific representation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoAllowDiagnostic {
    pub code: String,
    pub category: String,
    pub severity: CargoAllowDiagnosticSeverity,
    pub path: Option<String>,
    pub span: Option<CargoAllowErrorLocation>,
    pub entry_id: Option<String>,
    pub field: Option<String>,
    pub message: String,
    pub help: Option<String>,
    pub causes: Vec<String>,
}

impl CargoAllowDiagnostic {
    pub fn error(
        code: impl Into<String>,
        category: impl Into<String>,
        entry_id: Option<&str>,
        field: Option<&str>,
        message: impl Into<String>,
    ) -> Self {
        Self {
            code: code.into(),
            category: category.into(),
            severity: CargoAllowDiagnosticSeverity::Error,
            path: None,
            span: None,
            entry_id: entry_id.map(str::to_owned),
            field: field.map(str::to_owned),
            message: message.into(),
            help: None,
            causes: Vec::new(),
        }
    }
}

/// Structured kind for [`CargoAllowError`], enabling programmatic consumers
/// (CI tooling, sibling tools) to branch on error class instead of
/// string-matching the rendered message.
///
/// This enum is `#[non_exhaustive]` so new kinds can be added without a
/// breaking change for downstream library consumers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CargoAllowErrorKind {
    /// CLI usage error (bad flags, conflicting arguments).
    Usage,
    /// Invalid or missing configuration file/values.
    InvalidConfig,
    /// Invalid policy ledger (validation failure, parse error, unknown field).
    InvalidPolicy,
    /// Inventory discovery failure (git error, unreadable directory).
    Inventory,
    /// Scan failure (read error, parse error in a source file).
    Scan,
    /// Policy violation (check/diff gate failed).
    PolicyViolation,
    /// Artifact or write failure (receipt rendering, policy write).
    Artifact,
    /// Requested capability, platform, or tool contract is not supported.
    Unsupported,
    /// An external tool or process failed to provide usable evidence.
    InstrumentFailure,
    /// Internal invariant failure (should not happen).
    Internal,
    /// Unclassified — preserved for backward compatibility with `new()`.
    Unknown,
}

impl CargoAllowErrorKind {
    /// All error kinds currently defined by this version.
    ///
    /// The enum is non-exhaustive; callers must still handle future kinds.
    pub const ALL: &[Self] = &[
        Self::Usage,
        Self::InvalidConfig,
        Self::InvalidPolicy,
        Self::Inventory,
        Self::Scan,
        Self::PolicyViolation,
        Self::Artifact,
        Self::Unsupported,
        Self::InstrumentFailure,
        Self::Internal,
        Self::Unknown,
    ];

    /// Render the kind as a stable, lowercase identifier suitable for
    /// machine consumption (e.g. receipt `error.kind` fields).
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Usage => "usage",
            Self::InvalidConfig => "invalid_config",
            Self::InvalidPolicy => "invalid_policy",
            Self::Inventory => "inventory",
            Self::Scan => "scan",
            Self::PolicyViolation => "policy_violation",
            Self::Artifact => "artifact",
            Self::Unsupported => "unsupported",
            Self::InstrumentFailure => "instrument_failure",
            Self::Internal => "internal",
            Self::Unknown => "unknown",
        }
    }

    /// Return the stable machine-readable error code for this kind.
    ///
    /// Codes are part of the public contract and must not be reused for a
    /// different failure class. See `docs/error-codes.md` for the registry.
    pub const fn code(self) -> &'static str {
        match self {
            Self::Usage => "E0001_USAGE",
            Self::InvalidConfig => "E0002_INVALID_CONFIG",
            Self::InvalidPolicy => "E0003_INVALID_POLICY",
            Self::Inventory => "E0004_INVENTORY",
            Self::Scan => "E0005_SCAN",
            Self::PolicyViolation => "E0006_POLICY_VIOLATION",
            Self::Artifact => "E0007_ARTIFACT",
            Self::Internal => "E0008_INTERNAL",
            Self::Unknown => "E0009_UNKNOWN",
            Self::Unsupported => "E0010_UNSUPPORTED",
            Self::InstrumentFailure => "E0011_INSTRUMENT_FAILURE",
        }
    }
}

impl fmt::Display for CargoAllowErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Linked cause node for [`std::error::Error::source`] walks.
#[derive(Debug, Clone)]
struct CauseError {
    message: String,
    next: Option<Box<CauseError>>,
}

impl fmt::Display for CauseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for CauseError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.next
            .as_ref()
            .map(|next| next.as_ref() as &(dyn std::error::Error + 'static))
    }
}

/// The unified error type for the cargo-allow workspace.
///
/// Carries a structured [`CargoAllowErrorKind`], a human-readable message, and
/// an optional cause chain (rendered as a `caused by:` suffix in `Display`).
/// The `kind()` accessor lets programmatic consumers branch on error class
/// without string-matching.
#[derive(Debug, Clone)]
pub struct CargoAllowError {
    kind: CargoAllowErrorKind,
    message: String,
    location: Option<CargoAllowErrorLocation>,
    diagnostics: Vec<CargoAllowDiagnostic>,
    /// Rendered cause chain (each element is the `Display` of an underlying
    /// error). Stored as strings so the struct stays `Clone` + `PartialEq`.
    causes: Vec<String>,
    /// Linked cause chain for `Error::source` / `successors` walks.
    source: Option<Box<CauseError>>,
}

impl CargoAllowError {
    /// Create an error with [`CargoAllowErrorKind::Unknown`] (backward compat).
    pub fn new(message: impl Into<String>) -> Self {
        Self {
            kind: CargoAllowErrorKind::Unknown,
            message: message.into(),
            location: None,
            diagnostics: Vec::new(),
            causes: Vec::new(),
            source: None,
        }
    }

    /// Create an error with a structured kind.
    pub fn with_kind(kind: CargoAllowErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
            location: None,
            diagnostics: Vec::new(),
            causes: Vec::new(),
            source: None,
        }
    }

    /// Reclassify an existing error without discarding its structured metadata.
    ///
    /// Aggregation and adapter layers should use this when the surrounding
    /// contract provides a more precise kind. Rebuilding with [`Self::with_kind`]
    /// would lose locations, diagnostics, and causes.
    pub fn with_kind_preserving_metadata(mut self, kind: CargoAllowErrorKind) -> Self {
        self.kind = kind;
        self
    }

    /// Attach a cause (underlying error) to this error, returning a new value.
    ///
    /// The cause is rendered as a `caused by:` line in `Display` and linked for
    /// [`std::error::Error::source`] walks.
    pub fn with_cause(mut self, cause: &(impl std::error::Error + ?Sized)) -> Self {
        let message = cause.to_string();
        self.causes.push(message.clone());
        let node = Box::new(CauseError {
            message,
            next: None,
        });
        match self.source.as_mut() {
            None => self.source = Some(node),
            Some(head) => append_cause(head, node),
        }
        self
    }

    /// Prefix the human-readable message without discarding structured error
    /// metadata such as the kind, source location, diagnostics, or causes.
    ///
    /// Context layers should use this instead of rebuilding an error from its
    /// rendered string. Rebuilding loses information that machine consumers
    /// and editor integrations rely on.
    pub fn with_message_prefix(mut self, prefix: impl AsRef<str>) -> Self {
        let prefix = prefix.as_ref();
        if !prefix.is_empty() {
            self.message.insert_str(0, prefix);
        }
        self
    }

    /// Append a message suffix without discarding structured error metadata.
    ///
    /// Context layers should use this when adding remediation guidance after
    /// an existing message. Rebuilding an error from its rendered string loses
    /// metadata that machine consumers and editor integrations rely on.
    pub fn with_message_suffix(mut self, suffix: impl AsRef<str>) -> Self {
        let suffix = suffix.as_ref();
        if !suffix.is_empty() {
            self.message.push_str(suffix);
        }
        self
    }

    /// Rendered cause messages in attachment order (outermost first).
    pub fn causes(&self) -> &[String] {
        &self.causes
    }

    /// Attach one structured diagnostic detail, returning a new value.
    pub fn with_diagnostic(mut self, diagnostic: CargoAllowDiagnostic) -> Self {
        self.diagnostics.push(diagnostic);
        self
    }

    /// Attach multiple structured diagnostic details, returning a new value.
    pub fn with_diagnostics(
        mut self,
        diagnostics: impl IntoIterator<Item = CargoAllowDiagnostic>,
    ) -> Self {
        self.diagnostics.extend(diagnostics);
        self
    }

    /// Machine-readable details associated with this error.
    pub fn diagnostics(&self) -> &[CargoAllowDiagnostic] {
        &self.diagnostics
    }

    /// The structured error kind.
    pub fn kind(&self) -> CargoAllowErrorKind {
        self.kind
    }

    /// The stable machine-readable code for this error.
    pub fn code(&self) -> &'static str {
        self.kind.code()
    }

    /// Attach a one-based source location derived from a TOML byte span.
    ///
    /// TOML reports byte offsets. This conversion keeps the public error
    /// contract independent of the parser's error-display text and reports a
    /// character column suitable for editor and CI diagnostics.
    pub fn with_toml_span(
        mut self,
        path: Option<&Path>,
        source: &str,
        span: Option<Range<usize>>,
    ) -> Self {
        let Some(span) = span else {
            return self;
        };
        let prefix = source.get(..span.start).unwrap_or(source);
        let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
        let column = prefix
            .rsplit_once('\n')
            .map(|(_, line)| line.chars().count() + 1)
            .unwrap_or_else(|| prefix.chars().count() + 1);
        self.location = Some(CargoAllowErrorLocation {
            path: path.map(|value| value.display().to_string()),
            line: u32::try_from(line).unwrap_or(u32::MAX),
            column: u32::try_from(column).unwrap_or(u32::MAX),
        });
        for diagnostic in &mut self.diagnostics {
            diagnostic.path = path.map(|value| value.display().to_string());
            diagnostic.span = self.location.clone();
        }
        self
    }

    /// Structured source location, when the error originated from located
    /// input such as a TOML parse.
    pub fn location(&self) -> Option<&CargoAllowErrorLocation> {
        self.location.as_ref()
    }

    /// The human-readable message (without the cause chain).
    pub fn message(&self) -> &str {
        &self.message
    }
}

impl fmt::Display for CargoAllowError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message)?;
        for cause in &self.causes {
            write!(f, "\n  caused by: {cause}")?;
        }
        Ok(())
    }
}

impl std::error::Error for CargoAllowError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source
            .as_ref()
            .map(|cause| cause.as_ref() as &(dyn std::error::Error + 'static))
    }
}

/// Auto-convert `io::Error` so `?` works at IO call sites without manual
/// `map_err`. The kind is [`CargoAllowErrorKind::Unknown`]; callers that want
/// a specific kind (e.g. `Inventory`) should use `with_kind` explicitly.
impl From<std::io::Error> for CargoAllowError {
    fn from(e: std::io::Error) -> Self {
        let message = e.to_string();
        let mut err = CargoAllowError::with_kind(CargoAllowErrorKind::Unknown, message.clone());
        err.kind = match e.kind() {
            std::io::ErrorKind::NotFound => CargoAllowErrorKind::InvalidConfig,
            std::io::ErrorKind::PermissionDenied => CargoAllowErrorKind::Inventory,
            _ => CargoAllowErrorKind::Unknown,
        };
        err.message = message.clone();
        // Keep the IO error visible to `Error::source` walkers without
        // duplicating the same text under Display's `caused by:` lines.
        err.source = Some(Box::new(CauseError {
            message,
            next: None,
        }));
        err
    }
}

fn append_cause(head: &mut CauseError, node: Box<CauseError>) {
    match head.next.as_mut() {
        Some(next) => append_cause(next, node),
        None => head.next = Some(node),
    }
}

pub type CargoAllowResult<T> = Result<T, CargoAllowError>;

#[cfg(test)]
#[path = "error_tests.rs"]
mod tests;