Skip to main content

contextual_encoder/
display.rs

1//! zero-allocation [`Display`](std::fmt::Display) wrappers for all encoding
2//! contexts.
3//!
4//! every `for_*` function allocates a `String`. when embedding encoded output
5//! in a larger format string (e.g., `format!("<p>{}</p>", for_html(s))`), the
6//! intermediate string is immediately consumed and discarded — a wasted
7//! allocation.
8//!
9//! the `display_*` functions return lightweight wrappers that implement
10//! [`Display`](std::fmt::Display) by delegating to the corresponding `write_*`
11//! function. this enables zero-allocation inline formatting:
12//!
13//! ```
14//! use contextual_encoder::display_html;
15//!
16//! let user_input = "<script>alert('xss')</script>";
17//! // one allocation (the final String), zero intermediate allocations
18//! let output = format!("<p>{}</p>", display_html(user_input));
19//! assert!(output.contains("&lt;script&gt;"));
20//! ```
21//!
22//! each `display_*` wrapper encodes identically to its `for_*` / `write_*`
23//! counterpart. see the corresponding `for_*` function for encoding rules.
24
25use std::fmt;
26
27use crate::{css, html, javascript, json, rust, sql, uri, xml};
28
29macro_rules! display_fn {
30    (
31        $(#[$meta:meta])*
32        $name:ident => $module:ident :: $write_fn:ident
33    ) => {
34        $(#[$meta])*
35        pub fn $name(input: &str) -> impl fmt::Display + '_ {
36            struct W<'a>(&'a str);
37            impl fmt::Display for W<'_> {
38                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39                    $module::$write_fn(f, self.0)
40                }
41            }
42            W(input)
43        }
44    };
45}
46
47display_fn! {
48    /// zero-allocation display wrapper for [`for_html`](crate::for_html).
49    display_html => html::write_html
50}
51
52display_fn! {
53    /// zero-allocation display wrapper for [`for_html_content`](crate::for_html_content).
54    display_html_content => html::write_html_content
55}
56
57display_fn! {
58    /// zero-allocation display wrapper for [`for_html_attribute`](crate::for_html_attribute).
59    display_html_attribute => html::write_html_attribute
60}
61
62display_fn! {
63    /// zero-allocation display wrapper for
64    /// [`for_html_unquoted_attribute`](crate::for_html_unquoted_attribute).
65    display_html_unquoted_attribute => html::write_html_unquoted_attribute
66}
67
68display_fn! {
69    /// zero-allocation display wrapper for [`for_xml`](crate::for_xml).
70    display_xml => xml::write_xml
71}
72
73display_fn! {
74    /// zero-allocation display wrapper for [`for_xml_content`](crate::for_xml_content).
75    display_xml_content => xml::write_xml_content
76}
77
78display_fn! {
79    /// zero-allocation display wrapper for [`for_xml_attribute`](crate::for_xml_attribute).
80    display_xml_attribute => xml::write_xml_attribute
81}
82
83display_fn! {
84    /// zero-allocation display wrapper for [`for_xml_comment`](crate::for_xml_comment).
85    display_xml_comment => xml::write_xml_comment
86}
87
88display_fn! {
89    /// zero-allocation display wrapper for [`for_cdata`](crate::for_cdata).
90    display_cdata => xml::write_cdata
91}
92
93display_fn! {
94    /// zero-allocation display wrapper for [`for_xml11`](crate::for_xml11).
95    display_xml11 => xml::write_xml11
96}
97
98display_fn! {
99    /// zero-allocation display wrapper for [`for_xml11_content`](crate::for_xml11_content).
100    display_xml11_content => xml::write_xml11_content
101}
102
103display_fn! {
104    /// zero-allocation display wrapper for [`for_xml11_attribute`](crate::for_xml11_attribute).
105    display_xml11_attribute => xml::write_xml11_attribute
106}
107
108display_fn! {
109    /// zero-allocation display wrapper for [`for_javascript`](crate::for_javascript).
110    display_javascript => javascript::write_javascript
111}
112
113display_fn! {
114    /// zero-allocation display wrapper for
115    /// [`for_javascript_attribute`](crate::for_javascript_attribute).
116    display_javascript_attribute => javascript::write_javascript_attribute
117}
118
119display_fn! {
120    /// zero-allocation display wrapper for
121    /// [`for_javascript_block`](crate::for_javascript_block).
122    display_javascript_block => javascript::write_javascript_block
123}
124
125display_fn! {
126    /// zero-allocation display wrapper for
127    /// [`for_javascript_source`](crate::for_javascript_source).
128    display_javascript_source => javascript::write_javascript_source
129}
130
131display_fn! {
132    /// zero-allocation display wrapper for [`for_js_template`](crate::for_js_template).
133    display_js_template => javascript::write_js_template
134}
135
136display_fn! {
137    /// zero-allocation display wrapper for [`for_css_string`](crate::for_css_string).
138    display_css_string => css::write_css_string
139}
140
141display_fn! {
142    /// zero-allocation display wrapper for [`for_css_url`](crate::for_css_url).
143    display_css_url => css::write_css_url
144}
145
146display_fn! {
147    /// zero-allocation display wrapper for [`for_uri_component`](crate::for_uri_component).
148    display_uri_component => uri::write_uri_component
149}
150
151display_fn! {
152    /// zero-allocation display wrapper for [`for_uri_path`](crate::for_uri_path).
153    display_uri_path => uri::write_uri_path
154}
155
156display_fn! {
157    /// zero-allocation display wrapper for
158    /// [`for_form_urlencoded`](crate::for_form_urlencoded).
159    display_form_urlencoded => uri::write_form_urlencoded
160}
161
162display_fn! {
163    /// zero-allocation display wrapper for [`for_json`](crate::for_json).
164    display_json => json::write_json
165}
166
167display_fn! {
168    /// zero-allocation display wrapper for [`for_rust_string`](crate::for_rust_string).
169    display_rust_string => rust::write_rust_string
170}
171
172display_fn! {
173    /// zero-allocation display wrapper for [`for_rust_char`](crate::for_rust_char).
174    display_rust_char => rust::write_rust_char
175}
176
177display_fn! {
178    /// zero-allocation display wrapper for
179    /// [`for_rust_byte_string`](crate::for_rust_byte_string).
180    display_rust_byte_string => rust::write_rust_byte_string
181}
182
183display_fn! {
184    /// zero-allocation display wrapper for [`for_sql`](crate::for_sql).
185    display_sql => sql::write_sql
186}
187
188display_fn! {
189    /// zero-allocation display wrapper for [`for_sql_backslash`](crate::for_sql_backslash).
190    display_sql_backslash => sql::write_sql_backslash
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    // verify that every display_* wrapper produces identical output to its for_* counterpart.
198
199    macro_rules! display_matches_for {
200        ($name:ident, $display_fn:ident, $for_fn:path) => {
201            #[test]
202            fn $name() {
203                for input in [
204                    "",
205                    "hello",
206                    "<script>alert('xss')</script>",
207                    "café",
208                    "世界",
209                    "😀",
210                    "a&b<c>d\"e'f",
211                    "\x00\x01\x1F\x7F",
212                    "\t\n\r",
213                    "\u{0080}\u{009F}",
214                    "\u{2028}\u{2029}",
215                    "a\\b/c",
216                    "key=val&foo=bar",
217                    "`${inject}`",
218                ] {
219                    assert_eq!(
220                        format!("{}", $display_fn(input)),
221                        $for_fn(input),
222                        "mismatch for {:?} on {}",
223                        input,
224                        stringify!($display_fn),
225                    );
226                }
227            }
228        };
229    }
230
231    // html
232    display_matches_for!(html, display_html, crate::for_html);
233    display_matches_for!(html_content, display_html_content, crate::for_html_content);
234    display_matches_for!(
235        html_attribute,
236        display_html_attribute,
237        crate::for_html_attribute
238    );
239    display_matches_for!(
240        html_unquoted_attribute,
241        display_html_unquoted_attribute,
242        crate::for_html_unquoted_attribute
243    );
244
245    // xml
246    display_matches_for!(xml, display_xml, crate::for_xml);
247    display_matches_for!(xml_content, display_xml_content, crate::for_xml_content);
248    display_matches_for!(
249        xml_attribute,
250        display_xml_attribute,
251        crate::for_xml_attribute
252    );
253    display_matches_for!(xml_comment, display_xml_comment, crate::for_xml_comment);
254    display_matches_for!(cdata, display_cdata, crate::for_cdata);
255    display_matches_for!(xml11, display_xml11, crate::for_xml11);
256    display_matches_for!(
257        xml11_content,
258        display_xml11_content,
259        crate::for_xml11_content
260    );
261    display_matches_for!(
262        xml11_attribute,
263        display_xml11_attribute,
264        crate::for_xml11_attribute
265    );
266
267    // javascript
268    display_matches_for!(javascript, display_javascript, crate::for_javascript);
269    display_matches_for!(
270        javascript_attribute,
271        display_javascript_attribute,
272        crate::for_javascript_attribute
273    );
274    display_matches_for!(
275        javascript_block,
276        display_javascript_block,
277        crate::for_javascript_block
278    );
279    display_matches_for!(
280        javascript_source,
281        display_javascript_source,
282        crate::for_javascript_source
283    );
284    display_matches_for!(js_template, display_js_template, crate::for_js_template);
285
286    // css
287    display_matches_for!(css_string, display_css_string, crate::for_css_string);
288    display_matches_for!(css_url, display_css_url, crate::for_css_url);
289
290    // uri
291    display_matches_for!(
292        uri_component,
293        display_uri_component,
294        crate::for_uri_component
295    );
296    display_matches_for!(uri_path, display_uri_path, crate::for_uri_path);
297    display_matches_for!(
298        form_urlencoded,
299        display_form_urlencoded,
300        crate::for_form_urlencoded
301    );
302
303    // json
304    display_matches_for!(json, display_json, crate::for_json);
305
306    // rust
307    display_matches_for!(rust_string, display_rust_string, crate::for_rust_string);
308    display_matches_for!(rust_char, display_rust_char, crate::for_rust_char);
309    display_matches_for!(
310        rust_byte_string,
311        display_rust_byte_string,
312        crate::for_rust_byte_string
313    );
314
315    // sql
316    display_matches_for!(sql, display_sql, crate::for_sql);
317    display_matches_for!(
318        sql_backslash,
319        display_sql_backslash,
320        crate::for_sql_backslash
321    );
322
323    // -- usage pattern tests --
324
325    #[test]
326    fn inline_format_html() {
327        let input = "<b>bold</b>";
328        let result = format!("<p>{}</p>", display_html(input));
329        assert_eq!(result, "<p>&lt;b&gt;bold&lt;/b&gt;</p>");
330    }
331
332    #[test]
333    fn inline_format_nested_contexts() {
334        let query = "hello world & goodbye";
335        let href = format!("/search?q={}", display_uri_component(query));
336        let attr = format!(r#"<a href="{}">"#, display_html_attribute(&href));
337        assert!(attr.contains("/search?q=hello%20world%20%26%20goodbye"));
338    }
339
340    #[test]
341    fn write_macro_integration() {
342        use std::fmt::Write;
343        let mut buf = String::new();
344        write!(buf, "<p>{}</p>", display_html("a & b")).unwrap();
345        assert_eq!(buf, "<p>a &amp; b</p>");
346    }
347
348    #[test]
349    fn display_wrapper_is_reusable() {
350        let wrapper = display_html("<b>");
351        let first = format!("{wrapper}");
352        let second = format!("{wrapper}");
353        assert_eq!(first, second);
354        assert_eq!(first, "&lt;b&gt;");
355    }
356}