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, go, html, java, javascript, json, python, ruby, 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
47// -- html --
48
49display_fn! {
50    /// zero-allocation display wrapper for [`for_html`](crate::for_html).
51    display_html => html::write_html
52}
53
54display_fn! {
55    /// zero-allocation display wrapper for [`for_html_content`](crate::for_html_content).
56    display_html_content => html::write_html_content
57}
58
59display_fn! {
60    /// zero-allocation display wrapper for [`for_html_attribute`](crate::for_html_attribute).
61    display_html_attribute => html::write_html_attribute
62}
63
64display_fn! {
65    /// zero-allocation display wrapper for
66    /// [`for_html_unquoted_attribute`](crate::for_html_unquoted_attribute).
67    display_html_unquoted_attribute => html::write_html_unquoted_attribute
68}
69
70// -- xml --
71
72display_fn! {
73    /// zero-allocation display wrapper for [`for_xml`](crate::for_xml).
74    display_xml => xml::write_xml
75}
76
77display_fn! {
78    /// zero-allocation display wrapper for [`for_xml_content`](crate::for_xml_content).
79    display_xml_content => xml::write_xml_content
80}
81
82display_fn! {
83    /// zero-allocation display wrapper for [`for_xml_attribute`](crate::for_xml_attribute).
84    display_xml_attribute => xml::write_xml_attribute
85}
86
87display_fn! {
88    /// zero-allocation display wrapper for [`for_xml_comment`](crate::for_xml_comment).
89    display_xml_comment => xml::write_xml_comment
90}
91
92display_fn! {
93    /// zero-allocation display wrapper for [`for_cdata`](crate::for_cdata).
94    display_cdata => xml::write_cdata
95}
96
97display_fn! {
98    /// zero-allocation display wrapper for [`for_xml11`](crate::for_xml11).
99    display_xml11 => xml::write_xml11
100}
101
102display_fn! {
103    /// zero-allocation display wrapper for [`for_xml11_content`](crate::for_xml11_content).
104    display_xml11_content => xml::write_xml11_content
105}
106
107display_fn! {
108    /// zero-allocation display wrapper for [`for_xml11_attribute`](crate::for_xml11_attribute).
109    display_xml11_attribute => xml::write_xml11_attribute
110}
111
112// -- javascript --
113
114display_fn! {
115    /// zero-allocation display wrapper for [`for_javascript`](crate::for_javascript).
116    display_javascript => javascript::write_javascript
117}
118
119display_fn! {
120    /// zero-allocation display wrapper for
121    /// [`for_javascript_attribute`](crate::for_javascript_attribute).
122    display_javascript_attribute => javascript::write_javascript_attribute
123}
124
125display_fn! {
126    /// zero-allocation display wrapper for
127    /// [`for_javascript_block`](crate::for_javascript_block).
128    display_javascript_block => javascript::write_javascript_block
129}
130
131display_fn! {
132    /// zero-allocation display wrapper for
133    /// [`for_javascript_source`](crate::for_javascript_source).
134    display_javascript_source => javascript::write_javascript_source
135}
136
137display_fn! {
138    /// zero-allocation display wrapper for [`for_js_template`](crate::for_js_template).
139    display_js_template => javascript::write_js_template
140}
141
142// -- css --
143
144display_fn! {
145    /// zero-allocation display wrapper for [`for_css_string`](crate::for_css_string).
146    display_css_string => css::write_css_string
147}
148
149display_fn! {
150    /// zero-allocation display wrapper for [`for_css_url`](crate::for_css_url).
151    display_css_url => css::write_css_url
152}
153
154// -- uri --
155
156display_fn! {
157    /// zero-allocation display wrapper for [`for_uri_component`](crate::for_uri_component).
158    display_uri_component => uri::write_uri_component
159}
160
161// -- json --
162
163display_fn! {
164    /// zero-allocation display wrapper for [`for_json`](crate::for_json).
165    display_json => json::write_json
166}
167
168// -- java --
169
170display_fn! {
171    /// zero-allocation display wrapper for [`for_java`](crate::for_java).
172    display_java => java::write_java
173}
174
175// -- go --
176
177display_fn! {
178    /// zero-allocation display wrapper for [`for_go_string`](crate::for_go_string).
179    display_go_string => go::write_go_string
180}
181
182display_fn! {
183    /// zero-allocation display wrapper for [`for_go_char`](crate::for_go_char).
184    display_go_char => go::write_go_char
185}
186
187display_fn! {
188    /// zero-allocation display wrapper for [`for_go_byte_string`](crate::for_go_byte_string).
189    display_go_byte_string => go::write_go_byte_string
190}
191
192// -- rust --
193
194display_fn! {
195    /// zero-allocation display wrapper for [`for_rust_string`](crate::for_rust_string).
196    display_rust_string => rust::write_rust_string
197}
198
199display_fn! {
200    /// zero-allocation display wrapper for [`for_rust_char`](crate::for_rust_char).
201    display_rust_char => rust::write_rust_char
202}
203
204display_fn! {
205    /// zero-allocation display wrapper for
206    /// [`for_rust_byte_string`](crate::for_rust_byte_string).
207    display_rust_byte_string => rust::write_rust_byte_string
208}
209
210// -- ruby --
211
212display_fn! {
213    /// zero-allocation display wrapper for [`for_ruby_string`](crate::for_ruby_string).
214    display_ruby_string => ruby::write_ruby_string
215}
216
217// -- python --
218
219display_fn! {
220    /// zero-allocation display wrapper for [`for_python_string`](crate::for_python_string).
221    display_python_string => python::write_python_string
222}
223
224display_fn! {
225    /// zero-allocation display wrapper for [`for_python_bytes`](crate::for_python_bytes).
226    display_python_bytes => python::write_python_bytes
227}
228
229display_fn! {
230    /// zero-allocation display wrapper for
231    /// [`for_python_raw_string`](crate::for_python_raw_string).
232    display_python_raw_string => python::write_python_raw_string
233}
234
235// -- sql --
236
237display_fn! {
238    /// zero-allocation display wrapper for [`for_sql`](crate::for_sql).
239    display_sql => sql::write_sql
240}
241
242display_fn! {
243    /// zero-allocation display wrapper for [`for_sql_backslash`](crate::for_sql_backslash).
244    display_sql_backslash => sql::write_sql_backslash
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    // verify that every display_* wrapper produces identical output to its for_* counterpart.
252
253    macro_rules! display_matches_for {
254        ($name:ident, $display_fn:ident, $for_fn:path) => {
255            #[test]
256            fn $name() {
257                for input in [
258                    "",
259                    "hello",
260                    "<script>alert('xss')</script>",
261                    "café",
262                    "世界",
263                    "😀",
264                    "a&b<c>d\"e'f",
265                    "\x00\x01\x1F\x7F",
266                    "\t\n\r",
267                    "\u{0080}\u{009F}",
268                    "\u{2028}\u{2029}",
269                    "a\\b/c",
270                    "key=val&foo=bar",
271                    "`${inject}`",
272                ] {
273                    assert_eq!(
274                        format!("{}", $display_fn(input)),
275                        $for_fn(input),
276                        "mismatch for {:?} on {}",
277                        input,
278                        stringify!($display_fn),
279                    );
280                }
281            }
282        };
283    }
284
285    // html
286    display_matches_for!(html, display_html, crate::for_html);
287    display_matches_for!(html_content, display_html_content, crate::for_html_content);
288    display_matches_for!(
289        html_attribute,
290        display_html_attribute,
291        crate::for_html_attribute
292    );
293    display_matches_for!(
294        html_unquoted_attribute,
295        display_html_unquoted_attribute,
296        crate::for_html_unquoted_attribute
297    );
298
299    // xml
300    display_matches_for!(xml, display_xml, crate::for_xml);
301    display_matches_for!(xml_content, display_xml_content, crate::for_xml_content);
302    display_matches_for!(
303        xml_attribute,
304        display_xml_attribute,
305        crate::for_xml_attribute
306    );
307    display_matches_for!(xml_comment, display_xml_comment, crate::for_xml_comment);
308    display_matches_for!(cdata, display_cdata, crate::for_cdata);
309    display_matches_for!(xml11, display_xml11, crate::for_xml11);
310    display_matches_for!(
311        xml11_content,
312        display_xml11_content,
313        crate::for_xml11_content
314    );
315    display_matches_for!(
316        xml11_attribute,
317        display_xml11_attribute,
318        crate::for_xml11_attribute
319    );
320
321    // javascript
322    display_matches_for!(javascript, display_javascript, crate::for_javascript);
323    display_matches_for!(
324        javascript_attribute,
325        display_javascript_attribute,
326        crate::for_javascript_attribute
327    );
328    display_matches_for!(
329        javascript_block,
330        display_javascript_block,
331        crate::for_javascript_block
332    );
333    display_matches_for!(
334        javascript_source,
335        display_javascript_source,
336        crate::for_javascript_source
337    );
338    display_matches_for!(js_template, display_js_template, crate::for_js_template);
339
340    // css
341    display_matches_for!(css_string, display_css_string, crate::for_css_string);
342    display_matches_for!(css_url, display_css_url, crate::for_css_url);
343
344    // uri
345    display_matches_for!(
346        uri_component,
347        display_uri_component,
348        crate::for_uri_component
349    );
350
351    // json
352    display_matches_for!(json, display_json, crate::for_json);
353
354    // java
355    display_matches_for!(java, display_java, crate::for_java);
356
357    // go
358    display_matches_for!(go_string, display_go_string, crate::for_go_string);
359    display_matches_for!(go_char, display_go_char, crate::for_go_char);
360    display_matches_for!(
361        go_byte_string,
362        display_go_byte_string,
363        crate::for_go_byte_string
364    );
365
366    // rust
367    display_matches_for!(rust_string, display_rust_string, crate::for_rust_string);
368    display_matches_for!(rust_char, display_rust_char, crate::for_rust_char);
369    display_matches_for!(
370        rust_byte_string,
371        display_rust_byte_string,
372        crate::for_rust_byte_string
373    );
374
375    // ruby
376    display_matches_for!(ruby_string, display_ruby_string, crate::for_ruby_string);
377
378    // python
379    display_matches_for!(
380        python_string,
381        display_python_string,
382        crate::for_python_string
383    );
384    display_matches_for!(python_bytes, display_python_bytes, crate::for_python_bytes);
385    display_matches_for!(
386        python_raw_string,
387        display_python_raw_string,
388        crate::for_python_raw_string
389    );
390
391    // sql
392    display_matches_for!(sql, display_sql, crate::for_sql);
393    display_matches_for!(
394        sql_backslash,
395        display_sql_backslash,
396        crate::for_sql_backslash
397    );
398
399    // -- usage pattern tests --
400
401    #[test]
402    fn inline_format_html() {
403        let input = "<b>bold</b>";
404        let result = format!("<p>{}</p>", display_html(input));
405        assert_eq!(result, "<p>&lt;b&gt;bold&lt;/b&gt;</p>");
406    }
407
408    #[test]
409    fn inline_format_nested_contexts() {
410        let query = "hello world & goodbye";
411        let href = format!("/search?q={}", display_uri_component(query));
412        let attr = format!(r#"<a href="{}">"#, display_html_attribute(&href));
413        assert!(attr.contains("/search?q=hello%20world%20%26%20goodbye"));
414    }
415
416    #[test]
417    fn write_macro_integration() {
418        use std::fmt::Write;
419        let mut buf = String::new();
420        write!(buf, "<p>{}</p>", display_html("a & b")).unwrap();
421        assert_eq!(buf, "<p>a &amp; b</p>");
422    }
423
424    #[test]
425    fn display_wrapper_is_reusable() {
426        let wrapper = display_html("<b>");
427        let first = format!("{wrapper}");
428        let second = format!("{wrapper}");
429        assert_eq!(first, second);
430        assert_eq!(first, "&lt;b&gt;");
431    }
432}