Skip to main content

autosar_data/
lexer.rs

1use super::AutosarDataError;
2use memchr::memchr;
3use std::path::PathBuf;
4use thiserror::Error;
5
6#[derive(Debug, Error, Eq, PartialEq, Clone, Copy)]
7#[non_exhaustive]
8/// `ArxmlLexerError` contains all errors that can occur while reading data
9pub enum ArxmlLexerError {
10    /// Incomplete data, closing '>' was not found
11    #[error("Incomplete data, closing '>' was not found")]
12    IncompleteData,
13
14    /// Invalid element: '<>'
15    #[error("Invalid element: '<>'")]
16    InvalidElement,
17
18    /// A processing instruction was started with '<?', but it did not end with '?>'
19    #[error("A processing instruction was started with '<?', but it did not end with '?>'")]
20    InvalidProcessingInstruction,
21
22    /// Invalid arxml header: The xml header of an arxml file must specify version="1.0" encoding="utf-8"
23    #[error("Invalid arxml header: The xml header of an arxml file must specify version=\"1.0\" encoding=\"utf-8\"")]
24    InvalidXmlHeader,
25
26    /// Invalid comment: Comments must start with '<!--' and end with '-->'
27    #[error("Invalid comment")]
28    InvalidComment,
29}
30
31#[derive(Debug)]
32pub(crate) enum ArxmlEvent<'a> {
33    ArxmlHeader(Option<bool>),
34    BeginElement(&'a [u8], &'a [u8]),
35    EndElement(&'a [u8]),
36    Characters(&'a [u8]),
37    Comment(&'a [u8]),
38    EndOfFile,
39}
40
41pub(crate) struct ArxmlLexer<'a> {
42    buffer: &'a [u8],
43    bufpos: usize,
44    line: usize,
45    deferred_end: Option<(usize, usize)>,
46    sourcefile: PathBuf,
47}
48
49impl<'a> ArxmlLexer<'a> {
50    pub(crate) fn new(buffer: &'a [u8], name: PathBuf) -> Self {
51        // skip the byte-order mark, if it is present
52        let bufpos = if buffer.len() > 3 && buffer[0] == 239 && buffer[1] == 187 && buffer[2] == 191 {
53            3
54        } else {
55            0
56        };
57        Self {
58            buffer,
59            bufpos,
60            line: 1,
61            deferred_end: None,
62            sourcefile: name,
63        }
64    }
65
66    fn read_characters(&mut self) -> (ArxmlEvent<'a>, bool) {
67        debug_assert!(self.bufpos < self.buffer.len());
68
69        // the start of the next element '<' is the end of this block of characters
70        let mut endpos = self.bufpos;
71        let mut all_whitespace = true;
72        while endpos < self.buffer.len() && self.buffer[endpos] != b'<' {
73            // count the lines directly in this loop; it's faster than calling count_lines and this loop is quite hot in the profile...
74            if !self.buffer[endpos].is_ascii_whitespace() {
75                all_whitespace = false;
76            } else if self.buffer[endpos] == b'\n' {
77                self.line += 1;
78            }
79            endpos += 1;
80        }
81        debug_assert!(endpos > self.bufpos);
82
83        let text = &self.buffer[self.bufpos..endpos];
84        self.bufpos = endpos;
85        (ArxmlEvent::Characters(text), all_whitespace)
86    }
87
88    fn read_element_start(&mut self, mut endpos: usize) -> ArxmlEvent<'a> {
89        debug_assert!(self.bufpos < self.buffer.len());
90        debug_assert!(endpos > self.bufpos + 1);
91        debug_assert!(self.buffer[self.bufpos] == b'<');
92
93        // this loop runs at most twice: once with the '>' found by next(), and - if that '>'
94        // turned out to be part of an attribute value - once with the real end of the element
95        let (text, elemname, attributes, is_end) = loop {
96            let (text, is_end) = if self.buffer[endpos - 1] == b'/' {
97                (&self.buffer[self.bufpos + 1..endpos - 1], true)
98            } else {
99                (&self.buffer[self.bufpos + 1..endpos], false)
100            };
101
102            let (elemname, attributes) = if let Some(splitpos) = text.iter().position(u8::is_ascii_whitespace) {
103                (&text[..splitpos], &text[splitpos + 1..])
104            } else {
105                (text, &text[0..0])
106            };
107
108            // xml permits an unescaped '>' inside a quoted attribute value, e.g. <FOO S="a > b">.
109            // Such a '>' terminated the search in next() too early, which shows up here as attribute
110            // text that ends in the middle of a quoted value.
111            let Some(quotechar) = unterminated_quote(attributes) else {
112                break (text, elemname, attributes, is_end);
113            };
114            let Some(real_endpos) = self.find_quoted_element_end(endpos, quotechar) else {
115                // The quoted value is not terminated, i.e. the input is not valid xml after all.
116                break (text, elemname, attributes, is_end);
117            };
118            endpos = real_endpos;
119        };
120
121        // this is a <element/>, so a EndElement event needs to be generated next
122        if is_end {
123            // calculate the position of the element name inside the text
124            self.deferred_end = Some((self.bufpos + 1, self.bufpos + 1 + elemname.len()));
125        }
126
127        self.line += count_lines(text);
128        self.bufpos = endpos + 1;
129        ArxmlEvent::BeginElement(elemname, attributes)
130    }
131
132    /// continue the search for the end of an element from inside a quoted attribute value
133    ///
134    /// Returns the position of the next '>' that is not inside a quoted attribute value,
135    /// or None if the element does not end in a way that is valid xml.
136    fn find_quoted_element_end(&self, startpos: usize, quotechar: u8) -> Option<usize> {
137        let mut quotechar = Some(quotechar);
138        for (idx, c) in self.buffer[startpos..].iter().enumerate() {
139            if *c == b'<' {
140                // '<' must be escaped everywhere in xml, so it can't be part of an attribute value.
141                // The element is simply malformed and the search should not run on into the following
142                // elements looking for a closing quote that doesn't exist.
143                return None;
144            } else if let Some(qc) = quotechar {
145                if *c == qc {
146                    quotechar = None;
147                }
148            } else if *c == b'>' {
149                return Some(startpos + idx);
150            } else if *c == b'"' || *c == b'\'' {
151                quotechar = Some(*c);
152            }
153        }
154        None
155    }
156
157    fn read_element_end(&mut self, endpos: usize) -> ArxmlEvent<'a> {
158        debug_assert!(self.bufpos < self.buffer.len());
159        debug_assert!(endpos > self.bufpos + 1);
160        debug_assert!(self.buffer[self.bufpos] == b'<');
161
162        let text = &self.buffer[self.bufpos + 2..endpos].trim_ascii_end();
163        self.bufpos = endpos + 1;
164
165        ArxmlEvent::EndElement(text)
166    }
167
168    fn read_xml_header(&mut self, endpos: usize) -> Option<Result<ArxmlEvent<'a>, AutosarDataError>> {
169        debug_assert!(self.bufpos < self.buffer.len());
170        debug_assert!(endpos > self.bufpos + 1);
171        debug_assert!(self.buffer[self.bufpos] == b'<');
172
173        if self.buffer[endpos - 1] != b'?' {
174            return Some(Err(self.error(ArxmlLexerError::InvalidProcessingInstruction)));
175        }
176
177        let text = &self.buffer[self.bufpos + 2..endpos - 1];
178        self.bufpos = endpos + 1;
179
180        let text_trimmed = text.trim_ascii();
181        let (elemname, mut rest) = if let Some(ws_pos) = text_trimmed.iter().position(|c| c.is_ascii_whitespace()) {
182            (&text_trimmed[..ws_pos], &text_trimmed[ws_pos..])
183        } else {
184            (text_trimmed, &text_trimmed[text_trimmed.len()..])
185        };
186
187        let result = if elemname == b"xml" {
188            let mut ver = &text[0..0];
189            let mut encoding = &text[0..0];
190            let mut standalone: Option<bool> = None;
191
192            let valid = loop {
193                rest = rest.trim_ascii_start();
194                if rest.is_empty() {
195                    break true;
196                }
197
198                let Some(eq_pos) = rest.iter().position(|c| *c == b'=') else {
199                    break false;
200                };
201
202                let attr_name = rest[..eq_pos].trim_ascii_end();
203                if attr_name.is_empty() || attr_name.iter().any(|c| c.is_ascii_whitespace()) {
204                    break false;
205                }
206
207                rest = rest[eq_pos + 1..].trim_ascii_start();
208                if rest.is_empty() || (rest[0] != b'"' && rest[0] != b'\'') {
209                    break false;
210                }
211
212                let quote = rest[0];
213                rest = &rest[1..];
214                let Some(end_quote_pos) = rest.iter().position(|c| *c == quote) else {
215                    break false;
216                };
217
218                let attr_val = &rest[..end_quote_pos];
219                rest = &rest[end_quote_pos + 1..];
220
221                if attr_name == b"version" {
222                    ver = attr_val;
223                } else if attr_name == b"encoding" {
224                    encoding = attr_val;
225                } else if attr_name == b"standalone" {
226                    standalone = Some(attr_val == b"yes");
227                }
228            };
229
230            if !valid
231                || ver != b"1.0"
232                || (encoding != b"utf-8" && encoding != b"UTF-8" && encoding != b"utf8" && encoding != b"UTF8")
233            {
234                Some(Err(self.error(ArxmlLexerError::InvalidXmlHeader)))
235            } else {
236                Some(Ok(ArxmlEvent::ArxmlHeader(standalone)))
237            }
238        } else {
239            None
240        };
241
242        self.line += count_lines(text);
243        result
244    }
245
246    fn read_comment(&mut self, endpos: usize) -> Result<ArxmlEvent<'a>, AutosarDataError> {
247        debug_assert!(self.bufpos < self.buffer.len());
248        debug_assert!(endpos > self.bufpos + 1);
249
250        let startpos = self.bufpos;
251        let text = &self.buffer[startpos..endpos];
252        self.bufpos = endpos + 1;
253
254        if text.len() < 6 || !text.starts_with(b"<!--") || !text.ends_with(b"--") {
255            return Err(AutosarDataError::LexerError {
256                filename: self.sourcefile.clone(),
257                line: self.line,
258                source: ArxmlLexerError::InvalidComment,
259            });
260        }
261        self.line += count_lines(text);
262        let comment = &self.buffer[startpos + 4..endpos - 2];
263        Ok(ArxmlEvent::Comment(comment))
264    }
265}
266
267impl ArxmlLexer<'_> {
268    pub(crate) fn next<'a>(&'a mut self) -> Result<(usize, ArxmlEvent<'a>), AutosarDataError> {
269        // if an <element/> was found, then a BeginElement event is returned first, and the EndElement is deferred and must be returned next
270        if let Some((startpos, endpos)) = self.deferred_end {
271            self.deferred_end = None;
272            Ok((self.line, ArxmlEvent::EndElement(&self.buffer[startpos..endpos])))
273        } else {
274            loop {
275                if self.bufpos == self.buffer.len() {
276                    break Ok((self.line, ArxmlEvent::EndOfFile));
277                } else if self.buffer[self.bufpos] == b'<' {
278                    // start of an <element> or </element> or <!--comment-->
279                    // find a '>' character
280                    let findpos = memchr(b'>', &self.buffer[self.bufpos + 1..])
281                        .ok_or_else(|| self.error(ArxmlLexerError::IncompleteData))?;
282                    // endpos may be the position of a '>' that is part of an attribute value, but the
283                    // call to read_element_start() will check for that and continue the search if necessary.
284                    let endpos = self.bufpos + findpos + 1;
285
286                    if endpos == self.bufpos + 1 {
287                        // string is "<>"
288                        return Err(self.error(ArxmlLexerError::InvalidElement));
289                    }
290
291                    // got a non-empty sequence of characters that starts with '<' and ends with '>'
292                    match self.buffer[self.bufpos + 1] {
293                        b'/' => {
294                            // second char is '/' -> EndElement
295                            return Ok((self.line, self.read_element_end(endpos)));
296                        }
297                        b'?' => {
298                            // second char is '?' -> xml header or processing instruction
299                            // processing instructions are ignored, read_xml_header returns None
300                            if let Some(result) = self.read_xml_header(endpos) {
301                                let value = result?;
302                                return Ok((self.line, value));
303                            }
304                        }
305                        b'!' => {
306                            // second char is '!' -> parse a comment
307                            // we found a '>' character, but comments are allowed to contain unquoted '<' and '>'
308                            // this means we need to make sure the end is actually '-->', not just '>'
309                            // XML conformance note: the Autosar standard defines a subset of XML and forbids
310                            // CDATA and DOCTYPE sections, so those don't need to be handled here.
311                            let mut comment_endpos = endpos;
312                            while comment_endpos < self.buffer.len()
313                                && !self.buffer[comment_endpos - 2..].starts_with(b"-->")
314                            {
315                                comment_endpos += 1;
316                            }
317                            if comment_endpos < self.buffer.len() {
318                                return self.read_comment(comment_endpos).map(|res| (self.line, res));
319                            } else {
320                                // hit the end of the input -> unclosed comment
321                                return Err(self.error(ArxmlLexerError::InvalidComment));
322                            }
323                        }
324                        _ => {
325                            // any other second char -> BeginElement
326                            return Ok((self.line, self.read_element_start(endpos)));
327                        }
328                    }
329                } else {
330                    // start of character sequence
331                    if let (ArxmlEvent::Characters(text), false) = self.read_characters() {
332                        // found a character sequence which is not all whitespace
333                        return Ok((self.line, ArxmlEvent::Characters(text)));
334                    }
335                }
336                // loop if:
337                // - a processing instruction was ignored
338                // - empty character data found (whitespace only)
339            }
340        }
341    }
342
343    fn error(&self, err: ArxmlLexerError) -> AutosarDataError {
344        AutosarDataError::LexerError {
345            filename: self.sourcefile.clone(),
346            line: self.line,
347            source: err,
348        }
349    }
350}
351
352/// check if the attribute text ends inside a quoted value
353///
354/// Returns the quote character of the unterminated value, or None if all quoted values are terminated.
355fn unterminated_quote(text: &[u8]) -> Option<u8> {
356    let mut quotechar = None;
357    for c in text {
358        if let Some(qc) = quotechar {
359            if *c == qc {
360                quotechar = None;
361            }
362        } else if *c == b'"' || *c == b'\'' {
363            quotechar = Some(*c);
364        }
365    }
366    quotechar
367}
368
369fn count_lines(text: &[u8]) -> usize {
370    text.iter().filter(|c| **c == b'\n').count()
371}
372
373#[cfg(test)]
374mod test {
375    use super::*;
376
377    #[test]
378    fn test_basic_functionality() {
379        let data =
380            b"<?xml version=\"1.0\" encoding=\"utf-8\"?><element attr=\"gggg\" attr3>contained characters</element>";
381        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
382        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::ArxmlHeader(None)))));
383        assert!(
384            matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, attrs))) if elem == b"element" && attrs.len() == 17)
385        );
386        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::Characters(text))) if text == b"contained characters"));
387        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::EndElement(elem))) if elem == b"element"));
388        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::EndOfFile))));
389    }
390
391    #[test]
392    fn skip_byte_order_mark() {
393        let data =
394            b"\xEF\xBB\xBF<?xml version=\"1.0\" encoding=\"utf-8\"?><element attr=\"gggg\" attr3>contained characters</element>";
395        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
396        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::ArxmlHeader(None)))));
397    }
398
399    #[test]
400    fn test_incomplete_data() {
401        let data = b"<element";
402        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
403        assert!(
404            matches!(lexer.next(), Err(AutosarDataError::LexerError {source, ..}) if source == ArxmlLexerError::IncompleteData)
405        );
406    }
407
408    #[test]
409    fn test_invalid_element() {
410        let data = b"<element><>";
411        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
412        assert!(lexer.next().is_ok());
413        assert!(
414            matches!(lexer.next(), Err(AutosarDataError::LexerError{source, ..}) if source == ArxmlLexerError::InvalidElement)
415        );
416    }
417
418    #[test]
419    fn test_invalid_processing_instruction() {
420        let data = b"<element><?what>";
421        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
422        assert!(lexer.next().is_ok());
423        assert!(
424            matches!(lexer.next(), Err(AutosarDataError::LexerError{source, ..}) if source == ArxmlLexerError::InvalidProcessingInstruction)
425        );
426    }
427
428    #[test]
429    fn test_comment() {
430        let data = b"<!-- foo--><element>";
431        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
432        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::Comment(_)))));
433        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(_elem, _attrs)))));
434    }
435
436    #[test]
437    fn test_invalid_comment() {
438        let data = b"<element><!-- foo>";
439        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
440        assert!(lexer.next().is_ok());
441        assert!(
442            matches!(lexer.next(), Err(AutosarDataError::LexerError{source, ..}) if source == ArxmlLexerError::InvalidComment)
443        );
444    }
445
446    #[test]
447    fn test_invalid_xml_header() {
448        let data = br#"<?xml version="1.0" encoding="cp1252"?>"#;
449        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
450        assert!(
451            matches!(lexer.next(), Err(AutosarDataError::LexerError{source, ..}) if source == ArxmlLexerError::InvalidXmlHeader)
452        );
453
454        let data = br#"<?xml ?>"#;
455        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
456        assert!(
457            matches!(lexer.next(), Err(AutosarDataError::LexerError{source, ..}) if source == ArxmlLexerError::InvalidXmlHeader)
458        );
459    }
460
461    #[test]
462    fn test_malformed_xml_header_attributes() {
463        // each of these breaks out of the attribute loop of read_xml_header in a different place
464        let malformed: &[&[u8]] = &[
465            // an attribute without a value: there is no '=' left in the remaining text
466            br#"<?xml version="1.0" encoding="utf-8" standalone?>"#,
467            // an empty attribute name
468            br#"<?xml ="1.0"?>"#,
469            // an attribute name containing whitespace
470            br#"<?xml version number="1.0"?>"#,
471            // an unquoted attribute value
472            br#"<?xml version=1.0?>"#,
473            // an attribute value whose closing quote is missing
474            br#"<?xml version="1.0?>"#,
475        ];
476        for data in malformed {
477            println!("checking: {}", String::from_utf8_lossy(data));
478            let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
479            assert!(
480                matches!(lexer.next(), Err(AutosarDataError::LexerError{source, ..}) if source == ArxmlLexerError::InvalidXmlHeader)
481            );
482        }
483    }
484
485    #[test]
486    fn test_xml_header_with_single_quotes_and_standalone() {
487        let data = br#"<?xml version='1.0' encoding='UTF8' standalone='yes'?><element>"#;
488        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
489        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::ArxmlHeader(Some(true))))));
490        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, _))) if elem == b"element"));
491    }
492
493    #[test]
494    fn test_processing_instruction_is_ignored() {
495        // a processing instruction which is not the xml header is skipped, and the lexer continues
496        // with the next event
497        let data = b"<?php echo 'hello'; ?><element>";
498        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
499        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, _))) if elem == b"element"));
500    }
501
502    #[test]
503    fn test_malformed_comment() {
504        // '<!' is treated as the start of a comment, but this is not one, even though it ends with '-->'
505        let data = b"<!DOCTYPE something -->";
506        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
507        assert!(
508            matches!(lexer.next(), Err(AutosarDataError::LexerError{source, ..}) if source == ArxmlLexerError::InvalidComment)
509        );
510
511        // a comment which is too short to contain both the opening and the closing marker
512        let data = b"<!-->";
513        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
514        assert!(
515            matches!(lexer.next(), Err(AutosarDataError::LexerError{source, ..}) if source == ArxmlLexerError::InvalidComment)
516        );
517    }
518
519    #[test]
520    fn traits() {
521        // ArxmlLexerError: Debug, Error, Eq, PartialEq, Clone
522        let err = ArxmlLexerError::IncompleteData;
523        let err2 = err;
524        assert_eq!(err, err2);
525        assert_eq!(format!("{err:#?}"), format!("{err2:#?}"));
526        assert_eq!(format!("{err}"), format!("{err2}"));
527
528        // ArxmlEvent: Debug
529        let event = ArxmlEvent::ArxmlHeader(None);
530        let _ = format!("{event:#?}");
531    }
532
533    /// github issue #15 - comments can contain '<' and '>'
534    #[test]
535    fn test_w3c_comment_example() {
536        let data = b"<!-- declarations for <head> & <body> -->";
537        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
538        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::Comment(_)))));
539    }
540
541    /// xml allows an unescaped '>' inside a quoted attribute value
542    #[test]
543    fn test_gt_in_attribute_value() {
544        // double quoted value containing '>'
545        let data = b"<element attr=\"a>b\">text</element>";
546        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
547        assert!(
548            matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, attrs))) if elem == b"element" && attrs == b"attr=\"a>b\"")
549        );
550        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::Characters(text))) if text == b"text"));
551        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::EndElement(elem))) if elem == b"element"));
552
553        // single quoted value containing '>' and '"'
554        let data = b"<element attr='a>\"b'>";
555        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
556        assert!(
557            matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, attrs))) if elem == b"element" && attrs == b"attr='a>\"b'")
558        );
559
560        // several attributes, only the last one contains '>'
561        let data = b"<element a=\"1\" b='2' c=\"3>4\" d=\"5\">";
562        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
563        assert!(
564            matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, attrs))) if elem == b"element" && attrs == b"a=\"1\" b='2' c=\"3>4\" d=\"5\"")
565        );
566
567        // empty element, whose attribute value contains "/>"
568        let data = b"<element attr=\"a/>b\"/>";
569        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
570        assert!(
571            matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, attrs))) if elem == b"element" && attrs == b"attr=\"a/>b\"")
572        );
573        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::EndElement(elem))) if elem == b"element"));
574        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::EndOfFile))));
575
576        // a newline inside the attribute value is still counted
577        let data = b"<element attr=\"a>\nb\"><x>";
578        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
579        assert!(matches!(lexer.next(), Ok((1, ArxmlEvent::BeginElement(_, _)))));
580        assert!(matches!(lexer.next(), Ok((2, ArxmlEvent::BeginElement(elem, _))) if elem == b"x"));
581    }
582
583    /// an attribute value that is never terminated: the element text is truncated at the first '>',
584    /// so that the parser can report the error and the following elements are still parsed
585    #[test]
586    fn test_unterminated_attribute_value() {
587        // the search for the end of the value stops at the '<' of the next element
588        let data = b"<element attr=\"abc>text</element>";
589        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
590        assert!(
591            matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, attrs))) if elem == b"element" && attrs == b"attr=\"abc")
592        );
593        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::Characters(text))) if text == b"text"));
594        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::EndElement(elem))) if elem == b"element"));
595
596        // the search for the end of the value runs to the end of the input
597        let data = b"<element attr=\"abc>text";
598        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
599        assert!(
600            matches!(lexer.next(), Ok((_, ArxmlEvent::BeginElement(elem, attrs))) if elem == b"element" && attrs == b"attr=\"abc")
601        );
602        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::Characters(text))) if text == b"text"));
603        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::EndOfFile))));
604    }
605
606    /// github issue #32 - extra spaces in the XML header should be tolerated
607    #[test]
608    fn test_xml_header_with_extra_spaces() {
609        let data = b"<?xml   version =   \"1.0\"   encoding =   \"utf-8\"   standalone = \"yes\"  ?>";
610        let mut lexer = ArxmlLexer::new(data, PathBuf::from("(buffer)"));
611        assert!(matches!(lexer.next(), Ok((_, ArxmlEvent::ArxmlHeader(Some(true))))));
612    }
613}