Skip to main content

rupg_types/xml/
mod.rs

1//! The type `xml`: the input, output, receive and send functions of `xml.c` of PostgreSQL, with the check of libxml2 2.9.14 that PostgreSQL uses for a well formed value.
2//!
3//! PostgreSQL keeps the text of an `xml` value as it is, and only checks it. The checker in the modules below follows the parser of libxml2 with the options of PostgreSQL, so it accepts the same values and gives the same error text. It does not build a tree, because PostgreSQL does not keep one.
4
5mod build;
6mod content;
7mod ctxt;
8mod document;
9mod dtd;
10mod encodings;
11mod uri;
12
13use std::cell::RefCell;
14use std::rc::Rc;
15
16use rupg_common::SqlState;
17
18use crate::TypeError;
19use ctxt::{Report, cstr};
20
21pub use build::{
22    XmlStandalone, escape_xml, sql_name, xml_base64, xml_binhex, xml_comment, xml_concat,
23    xml_element, xml_name, xml_pi, xml_root, xml_text,
24};
25
26/// The error codes of `parse_xml_decl`.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28enum DeclError {
29    InvalidChar,
30    SpaceRequired,
31    StandaloneValue,
32    VersionMissing,
33    MissingEncoding,
34    XmldeclNotFinished,
35}
36
37impl DeclError {
38    /// `errdetail_for_xml_code`.
39    fn detail(self) -> &'static str {
40        match self {
41            DeclError::InvalidChar => "Invalid character value.",
42            DeclError::SpaceRequired => "Space required.",
43            DeclError::StandaloneValue => "standalone accepts only 'yes' or 'no'.",
44            DeclError::VersionMissing => "Malformed declaration: missing version.",
45            DeclError::MissingEncoding => "Missing encoding in text declaration.",
46            DeclError::XmldeclNotFinished => "Parsing XML declaration: '?>' expected.",
47        }
48    }
49}
50
51/// The parts of an XML declaration that `parse_xml_decl` reads.
52#[derive(Debug, Default)]
53struct Decl<'a> {
54    /// The length of the declaration, or 0 if there is none.
55    len: usize,
56    version: Option<&'a [u8]>,
57    encoding: Option<&'a [u8]>,
58    /// 1 for `yes`, 0 for `no` and -1 if it is not there.
59    standalone: i32,
60}
61
62/// `xmlIsBlank_ch`.
63fn is_space(c: u8) -> bool {
64    matches!(c, b' ' | b'\t' | b'\n' | b'\r')
65}
66
67/// The byte at `i`, or 0 after the end, as in a C string.
68fn at(s: &[u8], i: usize) -> u8 {
69    s.get(i).copied().unwrap_or(0)
70}
71
72/// `xmlGetUTF8Char` on at most 4 bytes. It gives -1 for a sequence that is not valid.
73fn utf8_char(s: &[u8]) -> i64 {
74    let len = s.len().min(4);
75    let b = |i: usize| i64::from(s[i]);
76    if len < 1 {
77        return -1;
78    }
79    let c = s[0];
80    if c & 0x80 == 0 {
81        return b(0);
82    }
83    if len < 2 || s[1] & 0xc0 != 0x80 {
84        return -1;
85    }
86    if c & 0xe0 != 0xe0 {
87        return (b(0) & 0x1f) << 6 | (b(1) & 0x3f);
88    }
89    if len < 3 || s[2] & 0xc0 != 0x80 {
90        return -1;
91    }
92    if c & 0xf0 != 0xf0 {
93        return (b(0) & 0x0f) << 12 | (b(1) & 0x3f) << 6 | (b(2) & 0x3f);
94    }
95    if c & 0xf8 != 0xf0 || len < 4 || s[3] & 0xc0 != 0x80 {
96        return -1;
97    }
98    (b(0) & 0x07) << 18 | (b(1) & 0x3f) << 12 | (b(2) & 0x3f) << 6 | (b(3) & 0x3f)
99}
100
101/// `PG_XMLISNAMECHAR`. The `_ch` forms of the libxml2 macros take each code point from 0xF8 up as a base char.
102fn is_name_char(c: i64) -> bool {
103    c.is_positive()
104        && (c >= 0xf8
105            || (0xc0..=0xd6).contains(&c)
106            || (0xd8..=0xf6).contains(&c)
107            || c == 0xb7
108            || u8::try_from(c)
109                .is_ok_and(|c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'-' | b'_' | b':')))
110}
111
112/// `parse_xml_decl`. The string ends at the first zero byte.
113fn parse_xml_decl(s: &[u8]) -> Result<Decl<'_>, DeclError> {
114    let s = cstr(s);
115    let mut decl = Decl { standalone: -1, ..Decl::default() };
116    let mut p = 0;
117    let skip = |p: &mut usize| {
118        while is_space(at(s, *p)) {
119            *p += 1;
120        }
121    };
122    let quoted = |p: &mut usize| -> Option<&[u8]> {
123        let q = at(s, *p);
124        if q != b'\'' && q != b'"' {
125            return None;
126        }
127        let end = s[*p + 1..].iter().position(|&c| c == q)? + *p + 1;
128        let value = &s[*p + 1..end];
129        *p = end + 1;
130        Some(value)
131    };
132    if s.starts_with(b"<?xml") && !is_name_char(utf8_char(&s[5..])) {
133        p = 5;
134        if !is_space(at(s, p)) {
135            return Err(DeclError::SpaceRequired);
136        }
137        skip(&mut p);
138        if !s[p..].starts_with(b"version") {
139            return Err(DeclError::VersionMissing);
140        }
141        p += 7;
142        skip(&mut p);
143        if at(s, p) != b'=' {
144            return Err(DeclError::VersionMissing);
145        }
146        p += 1;
147        skip(&mut p);
148        decl.version = Some(quoted(&mut p).ok_or(DeclError::VersionMissing)?);
149
150        let save = p;
151        skip(&mut p);
152        if s[p..].starts_with(b"encoding") {
153            if !is_space(at(s, save)) {
154                return Err(DeclError::SpaceRequired);
155            }
156            p += 8;
157            skip(&mut p);
158            if at(s, p) != b'=' {
159                return Err(DeclError::MissingEncoding);
160            }
161            p += 1;
162            skip(&mut p);
163            decl.encoding = Some(quoted(&mut p).ok_or(DeclError::MissingEncoding)?);
164        } else {
165            p = save;
166        }
167
168        let save = p;
169        skip(&mut p);
170        if s[p..].starts_with(b"standalone") {
171            if !is_space(at(s, save)) {
172                return Err(DeclError::SpaceRequired);
173            }
174            p += 10;
175            skip(&mut p);
176            if at(s, p) != b'=' {
177                return Err(DeclError::StandaloneValue);
178            }
179            p += 1;
180            skip(&mut p);
181            let rest = &s[p..];
182            if rest.starts_with(b"'yes'") || rest.starts_with(b"\"yes\"") {
183                decl.standalone = 1;
184                p += 5;
185            } else if rest.starts_with(b"'no'") || rest.starts_with(b"\"no\"") {
186                decl.standalone = 0;
187                p += 4;
188            } else {
189                return Err(DeclError::StandaloneValue);
190            }
191        } else {
192            p = save;
193        }
194
195        skip(&mut p);
196        if !s[p..].starts_with(b"?>") {
197            return Err(DeclError::XmldeclNotFinished);
198        }
199        p += 2;
200    }
201    if !s[..p].is_ascii() {
202        return Err(DeclError::InvalidChar);
203    }
204    decl.len = p;
205    Ok(decl)
206}
207
208/// `print_xml_decl` for a client in UTF-8, so with no encoding. It returns false if it writes nothing.
209fn print_xml_decl(out: &mut Vec<u8>, version: Option<&[u8]>, standalone: i32) -> bool {
210    if version.is_some_and(|v| v != b"1.0") || standalone != -1 {
211        out.extend_from_slice(b"<?xml version=\"");
212        out.extend_from_slice(version.unwrap_or(b"1.0"));
213        out.push(b'"');
214        match standalone {
215            1 => out.extend_from_slice(b" standalone=\"yes\""),
216            0 => out.extend_from_slice(b" standalone=\"no\""),
217            _ => {}
218        }
219        out.extend_from_slice(b"?>");
220        true
221    } else {
222        false
223    }
224}
225
226/// `xml_doctype_in_content`: true if the content is a sequence of blanks, comments and processing instructions before `<!DOCTYPE`.
227fn doctype_in_content(s: &[u8]) -> bool {
228    let find = |s: &[u8], from: usize, pat: &[u8]| {
229        s.get(from..)?.windows(pat.len()).position(|w| w == pat).map(|i| i + from)
230    };
231    let mut p = 0;
232    loop {
233        while is_space(at(s, p)) {
234            p += 1;
235        }
236        if at(s, p) != b'<' {
237            return false;
238        }
239        p += 1;
240        if at(s, p) == b'!' {
241            p += 1;
242            if s[p..].starts_with(b"DOCTYPE") {
243                return true;
244            }
245            if !s[p..].starts_with(b"--") {
246                return false;
247            }
248            match find(s, p + 2, b"--") {
249                Some(e) if at(s, e + 2) == b'>' => p = e + 3,
250                _ => return false,
251            }
252            continue;
253        }
254        if at(s, p) != b'?' {
255            return false;
256        }
257        p += 1;
258        match find(s, p, b"?>") {
259            Some(e) => p = e + 2,
260            None => return false,
261        }
262    }
263}
264
265/// The error of `xml_errsave`, with the text of libxml2 as the detail.
266fn parse_error(sqlstate: SqlState, message: &str, report: &Report) -> TypeError {
267    let mut error = TypeError::new(sqlstate, message.to_string());
268    if !report.err_buf.is_empty() {
269        error.detail = Some(String::from_utf8_lossy(&report.err_buf).into_owned());
270    }
271    error
272}
273
274/// `xml_parse` with the encoding UTF-8. `document` is true for `XMLOPTION_DOCUMENT`.
275fn xml_parse(s: &[u8], document: bool, report: &Rc<RefCell<Report>>) -> Result<(), TypeError> {
276    let s = cstr(s);
277    let mut as_document = document;
278    let mut count = 0;
279    if !document {
280        let decl = parse_xml_decl(s).map_err(|code| {
281            let mut error = TypeError::new(
282                SqlState::INVALID_XML_CONTENT,
283                "invalid XML content: invalid XML declaration".to_string(),
284            );
285            error.detail = Some(code.detail().to_string());
286            error
287        })?;
288        count = decl.len;
289        as_document = doctype_in_content(&s[count..]);
290    }
291    if as_document {
292        let well_formed = document::read_doc(s, report);
293        let report = report.borrow();
294        if !well_formed || report.err_occurred {
295            return Err(if document {
296                parse_error(SqlState::INVALID_XML_DOCUMENT, "invalid XML document", &report)
297            } else {
298                parse_error(SqlState::INVALID_XML_CONTENT, "invalid XML content", &report)
299            });
300        }
301    } else if count < s.len() {
302        let ret = document::balanced_chunk(&s[count..], report);
303        let report = report.borrow();
304        if ret != 0 || report.err_occurred {
305            return Err(parse_error(SqlState::INVALID_XML_CONTENT, "invalid XML content", &report));
306        }
307    }
308    Ok(())
309}
310
311/// `xml_in`, which checks that the text is a well formed document when `document` is true, or well formed content when it is false. The text ends at the first zero byte, as libxml2 sees it. `warn` gets the text of each warning of libxml2, which PostgreSQL reports with the SQLSTATE `01000` before the error.
312///
313/// # Errors
314///
315/// `2200M` for a document and `2200N` for content that is not well formed, with the messages of libxml2 as the detail.
316pub fn xml_in(text: &[u8], document: bool, warn: &mut dyn FnMut(String)) -> Result<(), TypeError> {
317    let report = Rc::new(RefCell::new(Report::default()));
318    let result = xml_parse(text, document, &report);
319    for warning in std::mem::take(&mut report.borrow_mut().warnings) {
320        warn(warning);
321    }
322    result
323}
324
325/// The encoding name in the XML declaration of the bytes of `xml_recv`, if there is one. A declaration that is not valid has no encoding here, because `xml_recv` does not check it.
326#[must_use]
327pub fn xml_encoding(bytes: &[u8]) -> Option<String> {
328    parse_xml_decl(bytes).ok()?.encoding.map(|e| String::from_utf8_lossy(e).into_owned())
329}
330
331/// `xml_out_internal` for a client in UTF-8: the text with a declaration only if it has a version that is not 1.0 or a standalone part. The encoding is not written. If the declaration does not parse, the text is written as it is and the result is the warning `XX001` of PostgreSQL.
332pub fn xml_out(text: &str, out: &mut Vec<u8>) -> Option<TypeError> {
333    let bytes = text.as_bytes();
334    match parse_xml_decl(bytes) {
335        Ok(decl) => {
336            let mut len = decl.len;
337            if !print_xml_decl(out, decl.version, decl.standalone) && at(bytes, len) == b'\n' {
338                len += 1;
339            }
340            out.extend_from_slice(&bytes[len..]);
341            None
342        }
343        Err(code) => {
344            out.extend_from_slice(bytes);
345            let mut error = TypeError::new(
346                SqlState::DATA_CORRUPTED,
347                "could not parse XML declaration in stored value".to_string(),
348            );
349            error.detail = Some(code.detail().to_string());
350            Some(error)
351        }
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    fn check(text: &str, document: bool) -> Result<Vec<String>, String> {
360        let mut warnings = Vec::new();
361        xml_in(text.as_bytes(), document, &mut |w| warnings.push(w))
362            .map(|()| warnings)
363            .map_err(|e| format!("{}: {}", e.message, e.detail.unwrap_or_default()))
364    }
365
366    fn out(text: &str) -> String {
367        let mut buf = Vec::new();
368        assert!(xml_out(text, &mut buf).is_none());
369        String::from_utf8(buf).unwrap()
370    }
371
372    #[test]
373    fn declarations() {
374        assert_eq!(parse_xml_decl(b"<a/>").map(|d| d.len), Ok(0));
375        assert_eq!(parse_xml_decl(b"<?xml-stylesheet?>").map(|d| d.len), Ok(0));
376        assert_eq!(parse_xml_decl(b"<?xmlversion='1.0'?>").map(|d| d.len), Ok(0));
377        assert_eq!(parse_xml_decl(b"<?xml version='1.0'?>").map(|d| d.len), Ok(21));
378        assert_eq!(parse_xml_decl(b"<?xml?>").map(|d| d.len), Err(DeclError::SpaceRequired));
379        assert_eq!(parse_xml_decl(b"<?xml ?>").map(|d| d.len), Err(DeclError::VersionMissing));
380        assert_eq!(
381            parse_xml_decl(b"<?xml version='1.0'encoding='x'?>").map(|d| d.len),
382            Err(DeclError::SpaceRequired)
383        );
384        assert_eq!(
385            parse_xml_decl(b"<?xml version='1.0' standalone='maybe'?>").map(|d| d.len),
386            Err(DeclError::StandaloneValue)
387        );
388        assert_eq!(
389            parse_xml_decl(b"<?xml version='1.0'").map(|d| d.len),
390            Err(DeclError::XmldeclNotFinished)
391        );
392        assert_eq!(
393            parse_xml_decl("<?xml version='\u{e9}'?>".as_bytes()).map(|d| d.len),
394            Err(DeclError::InvalidChar)
395        );
396        assert_eq!(
397            xml_encoding(b"<?xml version='1.0' encoding=\"LATIN1\"?>").as_deref(),
398            Some("LATIN1")
399        );
400    }
401
402    #[test]
403    fn input() {
404        assert_eq!(check("<a/>", false), Ok(vec![]));
405        assert_eq!(check("", false), Ok(vec![]));
406        assert_eq!(check("text <b>x</b> &amp;", false), Ok(vec![]));
407        assert!(check("<a", false).is_err_and(|e| e.starts_with("invalid XML content: ")));
408        assert!(check("<a/>", true).is_ok());
409        assert!(check("a", true).is_err_and(|e| e.starts_with("invalid XML document: ")));
410        assert!(check("<!DOCTYPE a><a/>", false).is_ok());
411        assert!(check("<?xml version=\"1.1\"?><a/>", true).is_ok_and(|w| w.len() == 1));
412    }
413
414    #[test]
415    fn output() {
416        assert_eq!(out("<a/>"), "<a/>");
417        assert_eq!(out("<?xml version=\"1.0\"?>\n<a/>"), "<a/>");
418        assert_eq!(
419            out("<?xml version='1.1' encoding='UTF-8'?><a/>"),
420            "<?xml version=\"1.1\"?><a/>"
421        );
422        assert_eq!(
423            out("<?xml version='1.0' standalone='no'?><a/>"),
424            "<?xml version=\"1.0\" standalone=\"no\"?><a/>"
425        );
426    }
427}