fob_cli/dev/
error_overlay.rs

1//! Error overlay generator for development mode.
2//!
3//! Creates an HTML error page displayed in the browser when builds fail.
4//! Auto-dismisses and reloads when the next build succeeds.
5
6/// Generate an HTML error overlay page.
7///
8/// Creates a styled error page that displays build errors in the browser.
9/// The overlay includes:
10/// - Dark theme for reduced eye strain
11/// - Properly escaped error messages
12/// - Retry button to manually trigger rebuild
13///
14/// # Arguments
15///
16/// * `error` - Error message to display
17///
18/// # Returns
19///
20/// HTML string ready to serve, or an error if generation fails
21///
22/// # Security
23///
24/// - HTML-escapes error messages to prevent XSS
25/// - No inline JavaScript execution from error content
26/// - CSP-compatible (no eval, unsafe-inline limited to style)
27pub fn generate_error_overlay(error: &str) -> Result<String, String> {
28    use fob_gen::{Allocator, HtmlBuilder};
29
30    let allocator = Allocator::default();
31    let html_builder = HtmlBuilder::new(allocator);
32
33    html_builder
34        .error_overlay(error)
35        .map_err(|e| format!("Failed to generate error overlay: {}", e))
36}
37
38/// HTML-escape a string to prevent XSS attacks.
39///
40/// Escapes the following characters:
41/// - `&` -> `&amp;`
42/// - `<` -> `&lt;`
43/// - `>` -> `&gt;`
44/// - `"` -> `&quot;`
45/// - `'` -> `&#x27;`
46///
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    /// Escape HTML special characters to prevent XSS attacks.
52    ///
53    /// Converts the following characters:
54    /// - `&` -> `&amp;`
55    /// - `<` -> `&lt;`
56    /// - `>` -> `&gt;`
57    /// - `"` -> `&quot;`
58    /// - `'` -> `&#x27;`
59    ///
60    /// # Security
61    ///
62    /// This is critical for preventing XSS when displaying error messages
63    /// that might contain user input or file paths with special characters.
64    fn html_escape(s: &str) -> String {
65        s.chars()
66            .map(|c| match c {
67                '&' => "&amp;".to_string(),
68                '<' => "&lt;".to_string(),
69                '>' => "&gt;".to_string(),
70                '"' => "&quot;".to_string(),
71                '\'' => "&#x27;".to_string(),
72                _ => c.to_string(),
73            })
74            .collect()
75    }
76
77    #[test]
78    fn test_html_escape_ampersand() {
79        assert_eq!(html_escape("a & b"), "a &amp; b");
80    }
81
82    #[test]
83    fn test_html_escape_angle_brackets() {
84        assert_eq!(html_escape("<script>"), "&lt;script&gt;");
85    }
86
87    #[test]
88    fn test_html_escape_quotes() {
89        assert_eq!(
90            html_escape(r#"He said "hello""#),
91            "He said &quot;hello&quot;"
92        );
93        assert_eq!(html_escape("It's working"), "It&#x27;s working");
94    }
95
96    #[test]
97    fn test_html_escape_combined() {
98        let input = r#"Error in <Component attr="value" & 'test'>"#;
99        let expected =
100            r#"Error in &lt;Component attr=&quot;value&quot; &amp; &#x27;test&#x27;&gt;"#;
101        assert_eq!(html_escape(input), expected);
102    }
103
104    #[test]
105    fn test_html_escape_no_special_chars() {
106        let input = "Normal error message";
107        assert_eq!(html_escape(input), input);
108    }
109
110    #[test]
111    fn test_generate_error_overlay_contains_escaped_error() {
112        let error = "<script>alert('xss')</script>";
113        let html = generate_error_overlay(error).expect("HTML generation should succeed");
114
115        // Should contain escaped version
116        assert!(html.contains("&lt;script&gt;"));
117        assert!(!html.contains("<script>alert"));
118    }
119
120    #[test]
121    fn test_generate_error_overlay_structure() {
122        let html = generate_error_overlay("Test error").expect("HTML generation should succeed");
123
124        // Check required elements
125        assert!(html.contains("<!DOCTYPE html>"));
126        assert!(html.contains("Build Error"));
127        assert!(html.contains("Test error"));
128        assert!(html.contains("Retry Build"));
129        assert!(html.contains("/__fob_sse__"));
130    }
131}