Skip to main content

shell_sanitize/
error.rs

1use std::borrow::Cow;
2use std::fmt;
3
4/// A single rule violation with context.
5#[derive(Debug, Clone, PartialEq, Eq)]
6#[non_exhaustive]
7pub struct RuleViolation {
8    /// Which rule detected the violation.
9    pub rule_name: &'static str,
10    /// Human-readable description of the violation.
11    pub message: String,
12    /// Byte offset in the input where the violation was found, if applicable.
13    pub position: Option<usize>,
14    /// The problematic fragment, if applicable.
15    pub fragment: Option<String>,
16}
17
18impl RuleViolation {
19    /// Create a new violation with the given rule name and message.
20    pub fn new(rule_name: &'static str, message: impl Into<String>) -> Self {
21        Self {
22            rule_name,
23            message: message.into(),
24            position: None,
25            fragment: None,
26        }
27    }
28
29    /// Set the byte position where the violation was detected.
30    pub fn at(mut self, position: usize) -> Self {
31        self.position = Some(position);
32        self
33    }
34
35    /// Set the problematic fragment.
36    pub fn with_fragment(mut self, fragment: impl Into<String>) -> Self {
37        self.fragment = Some(fragment.into());
38        self
39    }
40}
41
42impl fmt::Display for RuleViolation {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "[{}] {}", self.rule_name, self.message)?;
45        if let Some(pos) = self.position {
46            write!(f, " (at byte {})", pos)?;
47        }
48        if let Some(ref frag) = self.fragment {
49            write!(f, " fragment={:?}", frag)?;
50        }
51        Ok(())
52    }
53}
54
55/// Error returned when sanitization fails.
56///
57/// # Debug output
58///
59/// The `Debug` impl truncates the `input` field to 32 bytes and appends `…`
60/// to reduce the risk of leaking sensitive data (passwords, tokens, etc.)
61/// through `{:?}` logging. Use [`Display`](std::fmt::Display) for a safe, violations-only
62/// representation.
63#[derive(Clone, PartialEq, Eq)]
64#[non_exhaustive]
65pub struct SanitizeError {
66    /// The original input that failed sanitization.
67    pub input: String,
68    /// All violations found.
69    pub violations: Vec<RuleViolation>,
70}
71
72impl SanitizeError {
73    /// Create a new sanitization error.
74    pub fn new(input: impl Into<String>, violations: Vec<RuleViolation>) -> Self {
75        Self {
76            input: input.into(),
77            violations,
78        }
79    }
80}
81
82/// Max bytes of `input` shown in `Debug` output.
83const DEBUG_INPUT_TRUNCATE: usize = 32;
84
85impl fmt::Debug for SanitizeError {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        let truncated: Cow<'_, str> = if self.input.len() <= DEBUG_INPUT_TRUNCATE {
88            Cow::Borrowed(&self.input)
89        } else {
90            // Find a valid char boundary at or before the limit.
91            let end = (0..=DEBUG_INPUT_TRUNCATE)
92                .rev()
93                .find(|&i| self.input.is_char_boundary(i))
94                .unwrap_or(0);
95            Cow::Owned(format!("{}…", &self.input[..end]))
96        };
97
98        f.debug_struct("SanitizeError")
99            .field("input", &truncated)
100            .field("violations", &self.violations)
101            .finish()
102    }
103}
104
105impl fmt::Display for SanitizeError {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(
108            f,
109            "sanitization failed with {} violation(s)",
110            self.violations.len()
111        )?;
112        for v in &self.violations {
113            write!(f, "\n  - {}", v)?;
114        }
115        Ok(())
116    }
117}
118
119impl std::error::Error for SanitizeError {}