oxixml_xml/parser/pi.rs
1//! Contains a parser for an XML processing instruction.
2
3use crate::errors::SyntaxError;
4use crate::parser::Parser;
5use crate::utils::{is_whitespace, memchr_iter};
6
7/// A parser that search a `?>` sequence in the slice.
8///
9/// To use a parser create an instance of parser and [`feed`] data into it.
10/// After successful search the parser will return [`Some`] with position where
11/// processing instruction is ended (the position after `?>`). If search was
12/// unsuccessful, a [`None`] will be returned. You typically would expect positive
13/// result of search, so that you should feed new data until you get it.
14///
15/// NOTE: after successful match the parser does not returned to the initial
16/// state and should not be used anymore. Create a new parser if you want to perform
17/// new search.
18///
19/// # Example
20///
21/// ```
22/// use oxixml_xml::parser::{Parser, PiParser};
23///
24/// let mut parser = PiParser::default();
25///
26/// // Parse `<?instruction with = 'some > and ?' inside?>and the text follow...`
27/// // splitted into three chunks
28/// assert_eq!(parser.feed(b"<?instruction"), None);
29/// // ...get new chunk of data
30/// assert_eq!(parser.feed(b" with = 'some > and ?"), None);
31/// // ...get another chunk of data
32/// assert_eq!(parser.feed(b"' inside?>and the text follow..."), Some(9));
33/// // ^ ^
34/// // 0 9
35/// ```
36///
37/// [`feed`]: Self::feed()
38#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
39pub struct PiParser(
40 /// A flag that indicates was the `bytes` in the previous attempt to find the
41 /// end ended with `?`.
42 pub bool,
43);
44
45impl Parser for PiParser {
46 /// Determines the end position of a processing instruction in the provided slice.
47 /// Processing instruction ends on the first occurrence of `?>` which cannot be
48 /// escaped.
49 ///
50 /// [Section 2.6]: Parameter entity references MUST NOT be recognized within
51 /// processing instructions, so parser do not search for them.
52 ///
53 /// Returns position after the `?>` or `None` if such sequence was not found.
54 ///
55 /// # Parameters
56 /// - `bytes`: a slice to find the end of a processing instruction.
57 /// Should contain text in ASCII-compatible encoding
58 ///
59 /// [Section 2.6]: https://www.w3.org/TR/xml11/#sec-pi
60 #[inline]
61 fn feed(&mut self, bytes: &[u8]) -> Option<usize> {
62 for i in memchr_iter(b'>', bytes) {
63 match i {
64 0 if self.0 => return Some(0),
65 // If the previous byte is `?`, then we found `?>`
66 i if i > 0 && bytes[i - 1] == b'?' => return Some(i),
67 _ => {}
68 }
69 }
70 self.0 = bytes.last().copied() == Some(b'?');
71 None
72 }
73
74 #[inline]
75 fn eof_error(self, content: &[u8]) -> SyntaxError {
76 // Check if content starts with "<?xml" followed by whitespace, '?' or end.
77 // This determines whether to report an unclosed XML declaration or PI.
78 //
79 // quick-xml 0.41 parity: the lexer works on bytes and therefore requires
80 // an ASCII-compatible encoding, so this discrimination does not fire for
81 // a document encoded in UTF-16 (or any other ASCII-incompatible
82 // encoding), which is reported as an unclosed PI instead. Such input has
83 // to be transcoded before it reaches the lexer, which is what
84 // [`DecodingReader`](crate::encoding::DecodingReader) is for. The
85 // behaviour is pinned by `unclosed_utf16_xml_declaration_is_a_pi` below.
86 let is_xml_decl = content.starts_with(b"<?xml")
87 && content
88 .get(5)
89 .is_none_or(|&b| is_whitespace(b) || b == b'?');
90 if is_xml_decl {
91 SyntaxError::UnclosedXmlDecl
92 } else {
93 SyntaxError::UnclosedPI
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 /// Returns `Ok(pos)` with the position in the buffer where processing
103 /// instruction is ended.
104 ///
105 /// Returns `Err(internal_state)` if parsing is not done yet.
106 fn parse_pi(bytes: &[u8], had_question_mark: bool) -> Result<usize, bool> {
107 let mut parser = PiParser(had_question_mark);
108 match parser.feed(bytes) {
109 Some(i) => Ok(i),
110 None => Err(parser.0),
111 }
112 }
113
114 // Comments shows which character was seen the last before calling `feed`.
115 // `x` means any character, pipe denotes start of the buffer that passed to `feed`
116 #[test]
117 fn pi() {
118 assert_eq!(parse_pi(b"", false), Err(false)); // x|
119 assert_eq!(parse_pi(b"", true), Err(false)); // ?|
120
121 assert_eq!(parse_pi(b"?", false), Err(true)); // x|?
122 assert_eq!(parse_pi(b"?", true), Err(true)); // ?|?
123
124 assert_eq!(parse_pi(b">", false), Err(false)); // x|>
125 assert_eq!(parse_pi(b">", true), Ok(0)); // ?|>
126
127 assert_eq!(parse_pi(b"?>", false), Ok(1)); // x|?>
128 assert_eq!(parse_pi(b"?>", true), Ok(1)); // ?|?>
129
130 assert_eq!(parse_pi(b">?>", false), Ok(2)); // x|>?>
131 assert_eq!(parse_pi(b">?>", true), Ok(0)); // ?|>?>
132 }
133
134 /// Pins the documented quick-xml 0.41 parity limitation of [`PiParser::eof_error`]:
135 /// the discrimination between an unclosed XML declaration and an unclosed
136 /// processing instruction is a byte comparison, so it only works for
137 /// ASCII-compatible encodings. UTF-16 input has to be transcoded with
138 /// [`DecodingReader`](crate::encoding::DecodingReader) first.
139 #[test]
140 fn unclosed_xml_declaration_needs_an_ascii_compatible_encoding() {
141 assert_eq!(
142 PiParser(false).eof_error(b"<?xml version='1.0'"),
143 SyntaxError::UnclosedXmlDecl
144 );
145 // The same declaration in UTF-16 LE is not recognized as one.
146 let utf16: Vec<u8> = "<?xml version='1.0'"
147 .encode_utf16()
148 .flat_map(u16::to_le_bytes)
149 .collect();
150 assert_eq!(PiParser(false).eof_error(&utf16), SyntaxError::UnclosedPI);
151 }
152}