Skip to main content

palisade_errors/
convenience.rs

1//! Convenience macros for creating errors with format strings.
2//!
3//! # Security Model
4//!
5//! These macros enforce compile-time safety to prevent untrusted data from
6//! leaking into error messages without explicit sanitization.
7//!
8//! # Rules
9//!
10//! 1. **Operation names MUST be string literals** - prevents dynamic injection
11//! 2. **Format strings MUST be string literals** - prevents format string attacks
12//! 3. **Format arguments must be sanitized via `sanitized!()` wrapper** - bounded length
13//!
14//! # Usage
15//!
16//! ```rust
17//! # use palisade_errors::{config_err, definitions, sanitized};
18//! let line_num = 42;
19//! // ✓ CORRECT: Format string is literal, variable is sanitized
20//! let err = config_err!(
21//!     &definitions::CFG_PARSE_FAILED,
22//!     "validate",
23//!     "Invalid value at line {}",
24//!     sanitized!(line_num)
25//! );
26//! ```
27//!
28//! ```rust,compile_fail
29//! # use palisade_errors::{config_err, definitions};
30//! let user_input = "attacker data";
31//! // ✗ COMPILE ERROR: Operation must be a literal
32//! let err = config_err!(&definitions::CFG_PARSE_FAILED, user_input, "Failed");
33//! ```
34//!
35//! ```rust,compile_fail
36//! # use palisade_errors::{config_err, definitions};
37//! let unsanitized = "oops";
38//! // ✗ COMPILE ERROR: Args must be wrapped in sanitized!()
39//! let err = config_err!(&definitions::CFG_PARSE_FAILED, "op", "{}", unsanitized);
40//! ```
41//!
42//! ## Sanitization
43//!
44//! The `sanitized!()` macro truncates strings to prevent DoS via massive error messages
45//! and ensures all format arguments are bounded in length.
46//!
47//! # Security Properties
48//!
49//! This module implements several security principles to make the error macro system dumb-proof and secure:
50//! - **Compile-time Literal Enforcement**: Requires operation names, details, and format strings to be string literals, preventing runtime injection or format string vulnerabilities.
51//! - **Mandatory Sanitization for Dynamic Data**: Dynamic arguments must be explicitly wrapped in `sanitized!()` macro. This prevents accidental inclusion of unsanitized data.
52//! - **Length Bounding and Truncation**: Sanitized values are strictly limited to 256 characters to prevent DoS through oversized error messages or logs.
53//! - **UTF-8 Boundary Respect**: Truncation always occurs at valid character boundaries to avoid creating invalid strings that could cause downstream parsing errors.
54//! - **Control Character Neutralization**: Non-printable control characters are replaced with '?' to prevent log injection, formatting disruption, or terminal escape sequence attacks.
55//! - **Sensitive Data Isolation**: Sensitive information (e.g., passwords, keys) must use dedicated `_sensitive` macros and is isolated to internal logs only—never exposed in external error messages.
56//! - **No External Sensitive Logging**: Sensitive data is structurally separated and cannot be accidentally included in public-facing error details by convention.
57//! - **Pure Macro Expansion**: Macros expand to pure expressions without side effects, I/O, or runtime dependencies beyond standard library.
58//! - **Defense in Depth**: Multiple layers including literal requirements, sanitization, and separation ensure even if one layer fails, others protect.
59//! - **Fail-Safe Defaults**: Invalid UTF-8 or truncation failures default to safe placeholders like "[INVALID_UTF8]" instead of panicking or leaking.
60//! - **No Side Channels**: Sanitization is deterministic and linear-time relative to input length (up to bound), avoiding attacker-controlled amplification.
61//! - **Dumb-Proof Design**: By requiring explicit `sanitized!()` for args and literals for formats, accidental misuse (e.g., logging raw sensitive data externally) fails at compile time or produces safe output.
62//!
63//! Note: While format! allocates, this is acceptable for error paths. For hot paths, consider pre-formatted strings.
64
65// ============================================================================
66// Sanitization Utilities
67// ============================================================================
68
69/// Maximum length for sanitized strings in error messages.
70///
71/// This prevents DoS attacks via extremely long error messages while still
72/// allowing enough context for debugging.
73pub const MAX_SANITIZED_LEN: usize = 256;
74
75/// Sanitize untrusted input for inclusion in error messages.
76///
77/// # Behavior
78/// - Truncates strings to MAX_SANITIZED_LEN characters, respecting UTF-8 boundaries.
79/// - Replaces control characters with '?' to prevent log injection or formatting issues.
80/// - Handles non-string types by converting to string first.
81/// - For fully control-char inputs exceeding length, uses "[INVALID_INPUT]".
82///
83/// # Allocation
84/// - Allocates a new String for the sanitized output.
85///
86/// # Example
87///
88/// ```rust
89/// # use palisade_errors::sanitized;
90/// let long = "A".repeat(300);
91/// let san = sanitized!(long);
92/// assert!(san.len() <= 256 + 13);
93/// assert!(san.ends_with("[TRUNCATED]"));
94/// ```
95#[macro_export]
96macro_rules! sanitized {
97    ($expr:expr) => {{
98        let original = $expr.to_string();
99        let max_len = $crate::convenience::MAX_SANITIZED_LEN;
100        
101        let mut s = String::with_capacity(max_len.min(original.len()));
102        let mut len = 0;
103        let mut truncated = false;
104        let mut saw_non_control = false;
105        let mut in_escape = false;
106        
107        for c in original.chars() {
108            if in_escape {
109                if c == 'm' {
110                    in_escape = false;
111                }
112                continue;
113            }
114
115            if c == '\u{1b}' {
116                in_escape = true;
117                let replacement = '?';
118                let char_len = replacement.len_utf8();
119                if len + char_len > max_len {
120                    truncated = true;
121                    break;
122                }
123                s.push(replacement);
124                len += char_len;
125                continue;
126            }
127
128            let replacement = if c.is_control() { '?' } else { c };
129            if !c.is_control() {
130                saw_non_control = true;
131            }
132            let char_len = replacement.len_utf8();
133            
134            if len + char_len > max_len {
135                truncated = true;
136                break;
137            }
138            
139            s.push(replacement);
140            len += char_len;
141        }
142        
143        if !saw_non_control {
144            s = String::from("[INVALID_INPUT]");
145        } else if truncated {
146            // 13 is length of "...[TRUNCATED]"
147            let mut new_len = max_len.saturating_sub(13);
148            while new_len > 0 && !s.is_char_boundary(new_len) {
149                new_len -= 1;
150            }
151            if len > new_len {
152                s.truncate(new_len);
153            }
154            if !s.is_empty() {
155                s.push_str("...[TRUNCATED]");
156            } else {
157                s = String::from("[INVALID_INPUT]");
158            }
159        }
160        
161        s
162    }};
163}
164
165// ============================================================================
166// Internal Helper Macro
167// ============================================================================
168
169#[macro_export]
170macro_rules! create_lie_error {
171    ($prefix:literal, $code:expr, $op:literal, $details:expr) => {
172        {
173            let details = $details;
174            let internal = format!("{} op '{}': {}", $prefix, $op, details);
175            $crate::DualContextError::with_lie(details, internal, $code.category())
176        }
177    };
178}
179
180// ============================================================================
181// Error Creation Macros
182// ============================================================================
183
184/// Create a configuration error with compile-time literal enforcement.
185///
186/// Uses DualContextError::with_lie for public deception.
187///
188/// # Arguments
189/// - `$code`: &ErrorCode (expression)
190/// - `$op`: Operation name (string literal)
191/// - `$details`: Public details (string literal or format literal)
192/// - `$args`: Optional sanitized arguments (must use sanitized!())
193///
194/// # Security
195/// - Public: Deceptive lie from $details
196/// - Internal: Diagnostic with operation context
197///
198/// # Example
199///
200/// ```rust
201/// # use palisade_errors::{config_err, definitions, sanitized};
202/// let value = 42;
203/// let err = config_err!(
204///     &definitions::CFG_INVALID_VALUE,
205///     "validate_threshold",
206///     "Invalid configuration value: {}",
207///     sanitized!(value)
208/// );
209/// ```
210#[macro_export]
211macro_rules! config_err {
212    ($code:expr, $op:literal, $details:literal) => {
213        $crate::create_lie_error!("Configuration", $code, $op, $details)
214    };
215    ($code:expr, $op:literal, $fmt:literal $(, sanitized!($arg:expr))+ $(,)?) => {
216        {
217            let details = format!($fmt $(, $crate::sanitized!($arg))+);
218            $crate::create_lie_error!("Configuration", $code, $op, details)
219        }
220    };
221}
222
223/// Create a configuration error with sensitive internal context.
224///
225/// # Arguments
226/// - `$code`: &ErrorCode
227/// - `$op`: Operation literal
228/// - `$public`: Public deceptive literal or format
229/// - `$sensitive`: Sensitive data for internal only (recommend sanitizing)
230///
231/// # Security
232/// - Public: Lie from $public
233/// - Internal: Sensitive with operation
234///
235/// # Example
236///
237/// ```rust
238/// # use palisade_errors::{config_err_sensitive, definitions, sanitized};
239/// let secret = "key";
240/// let err = config_err_sensitive!(
241///     &definitions::CFG_VALIDATION_FAILED,
242///     "auth",
243///     "Configuration invalid",
244///     sanitized!(secret)
245/// );
246/// ```
247#[macro_export]
248macro_rules! config_err_sensitive {
249    ($code:expr, $op:literal, $public:literal, $sensitive:expr) => {
250        $crate::DualContextError::with_lie_and_sensitive(
251            $public,
252            format!("Operation '{}': [SENSITIVE] {}", $op, $sensitive),
253            $code.category(),
254        )
255    };
256    ($code:expr, $op:literal, $fmt:literal, $sensitive:expr $(, sanitized!($arg:expr))+ $(,)?) => {
257        $crate::DualContextError::with_lie_and_sensitive(
258            format!($fmt $(, $crate::sanitized!($arg))+),
259            format!("Operation '{}': [SENSITIVE] {}", $op, $sensitive),
260            $code.category(),
261        )
262    };
263}
264
265/// Create a deployment error.
266///
267/// Maps to Deployment category, uses with_lie.
268#[macro_export]
269macro_rules! deployment_err {
270    ($code:expr, $op:literal, $details:literal) => {
271        $crate::create_lie_error!("Deployment", $code, $op, $details)
272    };
273    ($code:expr, $op:literal, $fmt:literal $(, sanitized!($arg:expr))+ $(,)?) => {
274        {
275            let details = format!($fmt $(, $crate::sanitized!($arg))+);
276            $crate::create_lie_error!("Deployment", $code, $op, details)
277        }
278    };
279}
280
281/// Create a telemetry error.
282///
283/// Maps to Monitoring category.
284#[macro_export]
285macro_rules! telemetry_err {
286    ($code:expr, $op:literal, $details:literal) => {
287        $crate::create_lie_error!("Telemetry", $code, $op, $details)
288    };
289    ($code:expr, $op:literal, $fmt:literal $(, sanitized!($arg:expr))+ $(,)?) => {
290        {
291            let details = format!($fmt $(, $crate::sanitized!($arg))+);
292            $crate::create_lie_error!("Telemetry", $code, $op, details)
293        }
294    };
295}
296
297/// Create a correlation error.
298///
299/// Maps to Analysis category.
300#[macro_export]
301macro_rules! correlation_err {
302    ($code:expr, $op:literal, $details:literal) => {
303        $crate::create_lie_error!("Correlation", $code, $op, $details)
304    };
305    ($code:expr, $op:literal, $fmt:literal $(, sanitized!($arg:expr))+ $(,)?) => {
306        {
307            let details = format!($fmt $(, $crate::sanitized!($arg))+);
308            $crate::create_lie_error!("Correlation", $code, $op, details)
309        }
310    };
311}
312
313/// Create a response error.
314///
315/// Maps to Response category.
316#[macro_export]
317macro_rules! response_err {
318    ($code:expr, $op:literal, $details:literal) => {
319        $crate::create_lie_error!("Response", $code, $op, $details)
320    };
321    ($code:expr, $op:literal, $fmt:literal $(, sanitized!($arg:expr))+ $(,)?) => {
322        {
323            let details = format!($fmt $(, $crate::sanitized!($arg))+);
324            $crate::create_lie_error!("Response", $code, $op, details)
325        }
326    };
327}
328
329/// Create a logging error.
330///
331/// Maps to Audit category.
332#[macro_export]
333macro_rules! logging_err {
334    ($code:expr, $op:literal, $details:literal) => {
335        $crate::create_lie_error!("Logging", $code, $op, $details)
336    };
337    ($code:expr, $op:literal, $fmt:literal $(, sanitized!($arg:expr))+ $(,)?) => {
338        {
339            let details = format!($fmt $(, $crate::sanitized!($arg))+);
340            $crate::create_lie_error!("Logging", $code, $op, details)
341        }
342    };
343}
344
345/// Create a platform error.
346///
347/// Maps to System category.
348#[macro_export]
349macro_rules! platform_err {
350    ($code:expr, $op:literal, $details:literal) => {
351        $crate::create_lie_error!("Platform", $code, $op, $details)
352    };
353    ($code:expr, $op:literal, $fmt:literal $(, sanitized!($arg:expr))+ $(,)?) => {
354        {
355            let details = format!($fmt $(, $crate::sanitized!($arg))+);
356            $crate::create_lie_error!("Platform", $code, $op, details)
357        }
358    };
359}
360
361/// Create an I/O error.
362///
363/// Maps to IO category.
364#[macro_export]
365macro_rules! io_err {
366    ($code:expr, $op:literal, $details:literal) => {
367        $crate::create_lie_error!("IO", $code, $op, $details)
368    };
369    ($code:expr, $op:literal, $fmt:literal $(, sanitized!($arg:expr))+ $(,)?) => {
370        {
371            let details = format!($fmt $(, $crate::sanitized!($arg))+);
372            $crate::create_lie_error!("IO", $code, $op, details)
373        }
374    };
375}
376
377/// Define error codes with minimal boilerplate.
378///
379/// # Example
380///
381/// ```rust
382/// # use palisade_errors::{define_error_code, OperationCategory, namespaces};
383/// define_error_code!(
384///     CFG_PARSE_FAILED,
385///     &namespaces::CFG,
386///     100,
387///     OperationCategory::Configuration,
388///     350
389/// );
390/// ```
391#[macro_export]
392macro_rules! define_error_code {
393    ($name:ident, $namespace:expr, $code:expr, $category:expr, $impact:expr) => {
394        pub const $name: $crate::ErrorCode = $crate::ErrorCode::const_new(
395            $namespace,
396            $code,
397            $category,
398            $crate::ImpactScore::new($impact),
399        );
400    };
401}
402
403/// Define multiple error codes within the same namespace.
404///
405/// # Example
406///
407/// ```rust
408/// # use palisade_errors::{define_error_codes, OperationCategory, namespaces};
409/// define_error_codes! {
410///     &namespaces::CFG, OperationCategory::Configuration => {
411///         CFG_PARSE_FAILED = (100, 350),
412///         CFG_VALIDATION_FAILED = (101, 250),
413///     }
414/// }
415/// ```
416#[macro_export]
417macro_rules! define_error_codes {
418    ($namespace:expr, $category:expr => { $( $name:ident = ($code:expr, $impact:expr) ),+ $(,)? }) => {
419        $(
420            $crate::define_error_code!($name, $namespace, $code, $category, $impact);
421        )+
422    };
423}
424
425// ============================================================================
426// Tests
427// ============================================================================
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432    use crate::definitions;
433    use crate::SocAccess;
434
435    #[test]
436    fn test_literal_enforcement_compiles() {
437        // These should compile
438        let _err1 = config_err!(&definitions::CFG_PARSE_FAILED, "test_op", "test details");
439        let line = 42;
440        let _err2 = config_err!(&definitions::CFG_PARSE_FAILED, "test_op", "line {}", sanitized!(line));
441    }
442
443    #[test]
444    fn sanitized_macro_truncates_long_strings() {
445        let long_string = "A".repeat(1000);
446        let sanitized = sanitized!(long_string);
447        
448        assert!(sanitized.len() <= MAX_SANITIZED_LEN + 13); // "...[TRUNCATED]"
449        assert!(sanitized.contains("[TRUNCATED]"));
450    }
451    
452    #[test]
453    fn sanitized_macro_preserves_short_strings() {
454        let short_string = "short";
455        let sanitized = sanitized!(short_string);
456        
457        assert_eq!(sanitized, "short");
458    }
459    
460    #[test]
461    fn sanitized_macro_respects_utf8_boundaries() {
462        let emoji = "🔥".repeat(100);
463        let sanitized = sanitized!(emoji);
464        
465        assert!(std::str::from_utf8(sanitized.as_bytes()).is_ok());
466    }
467    
468    #[test]
469    fn sanitized_macro_replaces_control_chars() {
470        let input = "hello\nworld\t\x07";
471        let sanitized = sanitized!(input);
472        
473        assert_eq!(sanitized, "hello?world??");
474    }
475    
476    #[test]
477    fn sanitized_macro_handles_invalid_start() {
478        let input = "\x07".repeat(300);
479        let sanitized = sanitized!(input);
480        
481        assert_eq!(sanitized, "[INVALID_INPUT]");
482    }
483    
484    #[test]
485    fn sanitized_macro_works_with_numbers() {
486        let num = 42;
487        let sanitized = sanitized!(num);
488        
489        assert_eq!(sanitized, "42");
490    }
491    
492    #[test]
493    fn error_macros_with_sanitized_args() {
494        let value = "untrusted".repeat(100);
495        let err = config_err!(
496            &definitions::CFG_INVALID_VALUE,
497            "validate",
498            "Invalid value: {}",
499            sanitized!(value)
500        );
501        
502        assert!(err.external_message().len() <= MAX_SANITIZED_LEN + 20); // "Invalid value: " + truncated
503    }
504    
505    #[test]
506    fn config_err_sensitive_with_sanitization() {
507        let password = "secret123";
508        let err = config_err_sensitive!(
509            &definitions::CFG_VALIDATION_FAILED,
510            "auth",
511            "Auth failed",
512            sanitized!(format!("pwd_len={}", password.len()))
513        );
514        
515        assert_eq!(err.external_message(), "Auth failed");
516        let access = SocAccess::acquire();
517        let sensitive = err.internal().expose_sensitive(&access).unwrap();
518        assert!(sensitive.contains("pwd_len=9"));
519    }
520    
521    #[test]
522    fn all_error_macros_compile() {
523        let val = "test";
524        
525        let _e1 = config_err!(&definitions::CFG_PARSE_FAILED, "op", "details");
526        let _e2 = deployment_err!(&definitions::DCP_DEPLOY_FAILED, "op", "details");
527        let _e3 = telemetry_err!(&definitions::TEL_INIT_FAILED, "op", "details");
528        let _e4 = correlation_err!(&definitions::COR_RULE_EVAL_FAILED, "op", "details");
529        let _e5 = response_err!(&definitions::RSP_EXEC_FAILED, "op", "details");
530        let _e6 = logging_err!(&definitions::LOG_WRITE_FAILED, "op", "details");
531        let _e7 = platform_err!(&definitions::PLT_UNSUPPORTED, "op", "details");
532        let _e8 = io_err!(&definitions::IO_TIMEOUT, "op", "details");
533        
534        // With format
535        let _e9 = config_err!(&definitions::CFG_PARSE_FAILED, "op", "val: {}", sanitized!(val));
536    }
537    
538    #[test]
539    fn macros_accept_trailing_comma() {
540        let value = 42;
541        let _err = config_err!(
542            &definitions::CFG_PARSE_FAILED,
543            "test",
544            "Value: {}",
545            sanitized!(value),
546        );
547    }
548    
549    #[test]
550    fn sanitized_with_pathological_utf8() {
551        let s = "Ñ".repeat(128); // Each 2 bytes, total 256 bytes
552        let sanitized = sanitized!(s);
553        
554        assert_eq!(sanitized.len(), 256);
555        assert!(std::str::from_utf8(sanitized.as_bytes()).is_ok());
556    }
557    
558    #[test]
559    fn sanitized_with_mixed_controls() {
560        let s = "normal\x1b[0m escape \r\n sequence";
561        let sanitized = sanitized!(s);
562        assert_eq!(sanitized, "normal? escape ?? sequence");
563    }
564}