Skip to main content

wiremux_auth/profile/
error.rs

1//! Profile parse, refuse, and catalog errors.
2
3use std::io;
4use std::path::PathBuf;
5
6/// Failure from parse, refuse, or catalog load.
7#[derive(Debug, thiserror::Error)]
8pub enum ProfileError {
9    /// Filesystem read failed.
10    #[error("failed to read {}: {source}", path.display())]
11    Io {
12        /// Path that could not be read.
13        path: PathBuf,
14        /// Underlying I/O error.
15        #[source]
16        source: io::Error,
17    },
18    /// TOML/JSON syntax or type error.
19    #[error("failed to parse profile: {0}")]
20    Parse(String),
21    /// `schema_version` is greater than this crate understands.
22    #[error(
23        "unknown schema_version {found} (crate max {max}); upgrade wiremux or pin schema_version"
24    )]
25    SchemaVersion {
26        /// Version found in the document.
27        found: u32,
28        /// Highest version this crate accepts.
29        max: u32,
30    },
31    /// A required field is missing after env substitution.
32    #[error("{}", missing_required_field(.0))]
33    MissingField(&'static str),
34    /// A key name is reserved for code-exec (data-only profiles).
35    #[error("refused: forbidden key name `{0}`")]
36    ForbiddenKey(String),
37    /// Command interpolation in a non-hint field value.
38    #[error("refused: command interpolation in {field} ({trigger})")]
39    Interpolation {
40        /// Dotted field path.
41        field: String,
42        /// Syntax that triggered the refuse (`!`, `$(...)`, or backticks).
43        trigger: &'static str,
44    },
45    /// Disallowed scheme or non-loopback `http://` URL.
46    #[error(
47        "refused: disallowed URL in {field}: {} (https, or http only on 127.0.0.1 / localhost / ::1)",
48        crate::helpers::redact_url_origin(url)
49    )]
50    DisallowedUrl {
51        /// Dotted field path.
52        field: String,
53        /// The refused URL text.
54        url: String,
55    },
56    /// WASM or native-library path in a field value.
57    #[error("refused: native module path in {field}")]
58    NativeModule {
59        /// Dotted field path.
60        field: String,
61    },
62    /// No document in the catalog has this `id`.
63    #[error("{}; a .toml or .json path also works", not_found_message(id, known))]
64    NotFound {
65        /// Requested document id.
66        id: String,
67        /// Catalog ids from the same load (shipped ∪ user dir ∪ explicit file).
68        known: Vec<String>,
69    },
70}
71
72fn missing_required_field(field: &&str) -> String {
73    if *field == "schema_version" {
74        format!(
75            "missing required field schema_version (set to 1; crate max {})",
76            super::types::SCHEMA_VERSION_MAX
77        )
78    } else {
79        format!("missing required field {field}")
80    }
81}
82
83/// Catalog miss: known ids, plus a close-match hint when unique.
84pub fn not_found_message(id: &str, known: &[String]) -> String {
85    let listed = if known.is_empty() {
86        "(none)".to_string()
87    } else {
88        known.join(", ")
89    };
90    let mut msg = format!("profile `{id}` not found (known: {listed})");
91    let refs: Vec<&str> = known.iter().map(String::as_str).collect();
92    if let Some(suggest) = super::types::suggest_kebab(id, &refs) {
93        msg.push_str(&format!("; did you mean `{suggest}`"));
94    }
95    msg
96}