1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
//! An XML writer that protects you from XML injections through type safety.  If
//! you forget to escape a string, your code just doesn't compile.  Contrary to
//! other XML writing libraries xmlsafe doesn't require you to escape everything:
//! you get to choose. Furthermore xmlsafe never panics and avoids allocations by
//! just writing to a `std::fmt::Write`.
//!
//! xmlsafe introduces three marker traits to mark the XML safety of `Display`
//! implementations. Please keep two things in mind:
//!
//! 1. Whenever you supply a string literal (`&'static str`), take care that it
//! is syntactically valid for the respective context.
//!
//! 2. Whenever you implement one of the marker traits, take care that you fulfill its
//! requirements.

//! # Example
//!
//! ```
//! use std::fmt::{Error, Write};
//! use xmlsafe::{XmlWriter, format_text, escape_text};
//!
//! fn write_greeting(w: XmlWriter, name: &str) -> Result<(), Error> {
//!     let mut w = w.open_start_tag("greeting")?.attr("id", 42)?.close()?;
//!     w.write(format_text!("Hello {}!", escape_text(name)))?;
//!     w.write_end_tag("greeting")?;
//!     Ok(())
//! }
//!
//! fn main() {
//!     let mut out = String::new();
//!     write_greeting(XmlWriter::new(&mut out), "Ferris").unwrap();
//!     assert_eq!(out, "<greeting id=\"42\">Hello Ferris!</greeting>");
//! }
//!
//! ```
//!
//! Note how the [`XmlWriter`] acts as a protective layer between the actual
//! write target (the String in our example) and the XML generation code.  Also
//! note that if we forgot the `escape_text` call, the example would not
//! compile.

/// Defines the types returned by the macros and functions.
#[doc(hidden)]
pub mod wrappers;

use std::{
    borrow::Cow,
    fmt::{Display, Error, Write},
};

use crate::wrappers::{EscapedAttValue, EscapedPcdata};

/// An XML writer that helps to prevent XML injections.
pub struct XmlWriter<'a>(&'a mut dyn Write);

/// Types whose `Display` implementation can be safely used as an XML name.
pub trait NameSafe: Display {}

/// Types whose `Display` implementation can be safely embedded between XML
/// tags.  Literal `<` and `&` characters must be escaped as `&lt;` and `&amp;`
/// respectively.
pub trait PcdataSafe: Display {}

/// Types whose `Display` implementation can be safely embedded in
/// double-quoted XML attribute values. Literal `"` and `&` characters must be
/// escaped as `&quot;` and `&amp;` respectively.
pub trait AttValueSafe: Display {}

impl<'a> XmlWriter<'a> {
    /// Creates a new `XmlWriter` from a `std::fmt::Write` implementation.
    pub fn new(writer: &mut impl Write) -> XmlWriter {
        XmlWriter(writer)
    }

    /// Writes a start tag.
    pub fn write_start_tag(&mut self, name: impl NameSafe) -> Result<(), Error> {
        write!(self.0, "<{}>", name)?;
        Ok(())
    }

    /// Opens a start tag returning an [`AttWriter`] making use of the typestate pattern.
    pub fn open_start_tag(self, name: impl NameSafe) -> Result<AttWriter<'a>, Error> {
        write!(self.0, "<{}", name)?;
        Ok(AttWriter(self))
    }

    /// Writes an end tag.
    pub fn write_end_tag(&mut self, name: impl NameSafe) -> Result<(), Error> {
        write!(self.0, "</{}>", name)?;
        Ok(())
    }

    /// Writes some PCDATA.
    pub fn write(&mut self, value: impl PcdataSafe) -> Result<(), Error> {
        self.0.write_fmt(format_args!("{}", value))?;
        Ok(())
    }

    /// Duplicates the `XmlWriter`, which is useful to delegate part of the XML
    /// generation to other functions (since `open_start_tag` requires `self`).
    pub fn duplicate(&mut self) -> XmlWriter {
        XmlWriter(self.0)
    }
}

/// An XML attribute writer returned by [`XmlWriter::open_start_tag`].
pub struct AttWriter<'a>(XmlWriter<'a>);

impl AttValueSafe for &'static str {}
impl NameSafe for &'static str {}
impl PcdataSafe for &'static str {}

macro_rules! primitive_impls {
    ($($type: ty),+) => {
        $(
            impl AttValueSafe for $type {}
            impl PcdataSafe for $type {}
        )+
    };
}

primitive_impls!(
    bool, char, f32, f64, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
);

impl<'a> AttWriter<'a> {
    /// Writes an attribute. Avoid writing two attributes with the same name or
    /// your XML will be invalid.
    pub fn write_attr(
        &mut self,
        name: impl NameSafe,
        value: impl AttValueSafe,
    ) -> Result<(), Error> {
        write!(self.0 .0, " {}=\"{}\"", name, value)?;
        Ok(())
    }

    /// Writes the given attribute and returns the Writer to allow method chaining.
    /// Avoid writing two attributes with the same name or your XML will be
    /// invalid.
    pub fn attr(mut self, name: impl NameSafe, value: impl AttValueSafe) -> Result<Self, Error> {
        self.write_attr(name, value)?;
        Ok(self)
    }

    /// Closes the opening tag, returning an XmlWriter you can use to write the element content.
    /// To produce a valid XML document you will need to close the tag after the content.
    pub fn close(self) -> Result<XmlWriter<'a>, Error> {
        self.0 .0.write_char('>')?;
        Ok(self.0)
    }

    /// Closes the element with a self-closing tag. Returns the XmlWriter so you
    /// can continue writing the document.
    pub fn close_empty(self) -> Result<XmlWriter<'a>, Error> {
        self.0 .0.write_str("/>")?;
        Ok(self.0)
    }
}

/// XML escape an untrusted string to make it `AttValueSafe`.
pub fn escape_att_value<'a, S: Into<Cow<'a, str>>>(input: S) -> EscapedAttValue<'a> {
    let input = input.into();
    fn is_trouble(c: char) -> bool {
        c == '"' || c == '&'
    }

    if input.contains(is_trouble) {
        let mut output = String::with_capacity(input.len());
        for c in input.chars() {
            match c {
                '"' => output.push_str("&quot;"),
                '&' => output.push_str("&amp;"),
                _ => output.push(c),
            }
        }
        EscapedAttValue(Cow::Owned(output))
    } else {
        EscapedAttValue(input)
    }
}

/// XML escape an untrusted string to make it `PcdataSafe`.
pub fn escape_text<'a, S: Into<Cow<'a, str>>>(input: S) -> EscapedPcdata<'a> {
    let input = input.into();
    fn is_trouble(c: char) -> bool {
        c == '<' || c == '&'
    }

    if input.contains(is_trouble) {
        let mut output = String::with_capacity(input.len());
        for c in input.chars() {
            match c {
                '<' => output.push_str("&lt;"),
                '&' => output.push_str("&amp;"),
                _ => output.push(c),
            }
        }
        EscapedPcdata(Cow::Owned(output))
    } else {
        EscapedPcdata(input)
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        escape_att_value, escape_text, format_att_value, format_text, AttValueSafe, PcdataSafe,
        XmlWriter,
    };

    #[test]
    fn open_tag_write_and_close_tag() {
        let mut out = String::new();
        let writer = XmlWriter::new(&mut out);
        let mut writer = writer
            .open_start_tag("hello-world")
            .unwrap()
            .attr("id", 333)
            .unwrap()
            .close()
            .unwrap();
        writer.write("some text").unwrap();
        writer.write_end_tag("hello-world").unwrap();
        assert_eq!(out, "<hello-world id=\"333\">some text</hello-world>");
    }

    #[test]
    fn test_escaping() {
        assert_eq!(
            escape_text("x < 5 > 3 && \"foo\" == 'bar'").to_string(),
            "x &lt; 5 > 3 &amp;&amp; \"foo\" == 'bar'"
        );
        assert_eq!(
            escape_att_value("x < 5 > 3 && \"foo\" == 'bar'").to_string(),
            "x < 5 > 3 &amp;&amp; &quot;foo&quot; == 'bar'"
        );
    }

    fn subroutine(mut x: XmlWriter) {
        x.write("hello from subroutine").unwrap();
    }

    #[test]
    fn test_duplicate() {
        let mut out = String::new();
        let mut writer = XmlWriter::new(&mut out);
        writer.write_start_tag("hello").unwrap();
        subroutine(writer.duplicate());
        writer.write_end_tag("hello").unwrap();
        assert_eq!(out, "<hello>hello from subroutine</hello>");
    }

    struct CustomPcdata;

    impl PcdataSafe for CustomPcdata {}

    impl std::fmt::Display for CustomPcdata {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            f.write_str("hello from display")
        }
    }

    #[test]
    fn test_format_text() {
        let mut out = String::new();
        let mut writer = XmlWriter::new(&mut out);
        writer
            .write(format_text!("> {} < {}", CustomPcdata, "test"))
            .unwrap();
        assert_eq!(out, "> hello from display < test");
    }

    struct CustomAttValue;

    impl AttValueSafe for CustomAttValue {}

    impl std::fmt::Display for CustomAttValue {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            f.write_str("hello from display")
        }
    }

    #[test]
    fn test_format_att_value() {
        let mut out = String::new();
        let writer = XmlWriter::new(&mut out);
        writer
            .open_start_tag("test")
            .unwrap()
            .attr(
                "foo",
                format_att_value!("> {} < {}", CustomAttValue, "test"),
            )
            .unwrap()
            .close()
            .unwrap();
        assert_eq!(out, "<test foo=\"> hello from display < test\">");
    }
}