Skip to main content

pptxboss_core/
xml.rs

1//! A pull tokenizer for the XML subset Office Open XML parts use.
2//!
3//! Parts are UTF-8 (a byte order mark is skipped), namespace-aware, and
4//! carry no DTD. The tokenizer borrows every name and text run from the
5//! input, resolves namespace prefixes against a small table of the
6//! namespaces the reader knows (Transitional and Strict URIs map to the
7//! same [`Ns`]), skips comments and processing instructions, and hands
8//! back CDATA as text. Character references, the five predefined
9//! entities and the `_xHHHH_` escape convention are decoded by
10//! [`unescape_into`], only when a text run contains them.
11//!
12//! Well-formedness is checked as far as the tokenizer must to make
13//! progress: mismatched or unterminated tags are errors. Everything else
14//! is lenient by default so a damaged slide still yields its text; the
15//! verifier runs [`well_formed`] for the full check.
16
17use std::sync::OnceLock;
18
19use memchr::{memchr, memchr2, memchr3, memmem};
20
21fn xmlns_finder() -> &'static memmem::Finder<'static> {
22    static FINDER: OnceLock<memmem::Finder<'static>> = OnceLock::new();
23    FINDER.get_or_init(|| memmem::Finder::new(b"xmlns"))
24}
25
26/// A namespace the reader recognizes.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub enum Ns {
29    /// No namespace (unprefixed attribute, or unprefixed element without a default namespace).
30    None,
31    /// PresentationML main.
32    Pml,
33    /// DrawingML main.
34    Dml,
35    /// Office document relationship references (`r:id`, `r:embed`).
36    Rel,
37    /// OPC package relationships (`.rels` parts).
38    PkgRel,
39    /// OPC content types stream.
40    ContentTypes,
41    /// Markup compatibility and extensibility.
42    Mce,
43    /// OPC core properties.
44    Cp,
45    /// Dublin Core elements.
46    Dc,
47    /// Dublin Core terms.
48    Dcterms,
49    /// XML Schema instance.
50    Xsi,
51    /// Extended file properties (`docProps/app.xml`).
52    Ep,
53    /// Variant types used by extended and custom properties.
54    Vt,
55    /// DrawingML charts.
56    Chart,
57    /// The 2014 extended charts (`cx:`).
58    ChartEx,
59    /// DrawingML diagrams.
60    Dgm,
61    /// DrawingML pictures.
62    Pic,
63    /// The `xml:` namespace.
64    Xml,
65    /// PowerPoint 2010 extensions (`p14:`), among them sections.
66    P14,
67    /// PowerPoint 2018 extensions (`p188:`): threaded comments and their authors.
68    P188,
69    /// Any other namespace, numbered per reader in order of first sight.
70    Other(u16),
71}
72
73/// Which family of namespace URIs a document uses.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum Conformance {
76    Transitional,
77    Strict,
78}
79
80const KNOWN: &[(&[u8], Ns, Conformance)] = &[
81    (
82        b"http://schemas.openxmlformats.org/presentationml/2006/main",
83        Ns::Pml,
84        Conformance::Transitional,
85    ),
86    (
87        b"http://purl.oclc.org/ooxml/presentationml/main",
88        Ns::Pml,
89        Conformance::Strict,
90    ),
91    (
92        b"http://schemas.openxmlformats.org/drawingml/2006/main",
93        Ns::Dml,
94        Conformance::Transitional,
95    ),
96    (
97        b"http://purl.oclc.org/ooxml/drawingml/main",
98        Ns::Dml,
99        Conformance::Strict,
100    ),
101    (
102        b"http://schemas.openxmlformats.org/officeDocument/2006/relationships",
103        Ns::Rel,
104        Conformance::Transitional,
105    ),
106    (
107        b"http://purl.oclc.org/ooxml/officeDocument/relationships",
108        Ns::Rel,
109        Conformance::Strict,
110    ),
111    (
112        b"http://schemas.openxmlformats.org/package/2006/relationships",
113        Ns::PkgRel,
114        Conformance::Transitional,
115    ),
116    (
117        b"http://schemas.openxmlformats.org/package/2006/content-types",
118        Ns::ContentTypes,
119        Conformance::Transitional,
120    ),
121    (
122        b"http://schemas.openxmlformats.org/markup-compatibility/2006",
123        Ns::Mce,
124        Conformance::Transitional,
125    ),
126    (
127        b"http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
128        Ns::Cp,
129        Conformance::Transitional,
130    ),
131    (
132        b"http://purl.org/dc/elements/1.1/",
133        Ns::Dc,
134        Conformance::Transitional,
135    ),
136    (
137        b"http://purl.org/dc/terms/",
138        Ns::Dcterms,
139        Conformance::Transitional,
140    ),
141    (
142        b"http://www.w3.org/2001/XMLSchema-instance",
143        Ns::Xsi,
144        Conformance::Transitional,
145    ),
146    (
147        b"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties",
148        Ns::Ep,
149        Conformance::Transitional,
150    ),
151    (
152        b"http://purl.oclc.org/ooxml/officeDocument/extendedProperties",
153        Ns::Ep,
154        Conformance::Strict,
155    ),
156    (
157        b"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes",
158        Ns::Vt,
159        Conformance::Transitional,
160    ),
161    (
162        b"http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes",
163        Ns::Vt,
164        Conformance::Strict,
165    ),
166    (
167        b"http://schemas.openxmlformats.org/drawingml/2006/chart",
168        Ns::Chart,
169        Conformance::Transitional,
170    ),
171    (
172        b"http://purl.oclc.org/ooxml/drawingml/chart",
173        Ns::Chart,
174        Conformance::Strict,
175    ),
176    (
177        b"http://schemas.openxmlformats.org/drawingml/2006/diagram",
178        Ns::Dgm,
179        Conformance::Transitional,
180    ),
181    (
182        b"http://purl.oclc.org/ooxml/drawingml/diagram",
183        Ns::Dgm,
184        Conformance::Strict,
185    ),
186    (
187        b"http://schemas.openxmlformats.org/drawingml/2006/picture",
188        Ns::Pic,
189        Conformance::Transitional,
190    ),
191    (
192        b"http://purl.oclc.org/ooxml/drawingml/picture",
193        Ns::Pic,
194        Conformance::Strict,
195    ),
196    (
197        b"http://www.w3.org/XML/1998/namespace",
198        Ns::Xml,
199        Conformance::Transitional,
200    ),
201    (
202        b"http://schemas.microsoft.com/office/powerpoint/2010/main",
203        Ns::P14,
204        Conformance::Transitional,
205    ),
206    (
207        b"http://schemas.microsoft.com/office/drawing/2014/chartex",
208        Ns::ChartEx,
209        Conformance::Transitional,
210    ),
211    (
212        b"http://schemas.microsoft.com/office/powerpoint/2018/8/main",
213        Ns::P188,
214        Conformance::Transitional,
215    ),
216];
217
218/// The URI of a namespace known to the reader, Transitional form.
219pub fn transitional_uri(ns: Ns) -> Option<&'static str> {
220    KNOWN
221        .iter()
222        .find(|(_, known, conformance)| *known == ns && *conformance == Conformance::Transitional)
223        .and_then(|(uri, _, _)| std::str::from_utf8(uri).ok())
224}
225
226/// A tokenizer error with the byte offset it was found at.
227#[derive(Clone, Debug, PartialEq, Eq)]
228pub struct XmlError {
229    pub offset: usize,
230    pub msg: &'static str,
231}
232
233impl std::fmt::Display for XmlError {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        write!(f, "{} at byte {}", self.msg, self.offset)
236    }
237}
238
239impl std::error::Error for XmlError {}
240
241type XmlResult<T> = std::result::Result<T, XmlError>;
242
243/// A resolved element or attribute name.
244#[derive(Clone, Copy, Debug, PartialEq, Eq)]
245pub struct Name<'a> {
246    pub ns: Ns,
247    pub prefix: &'a [u8],
248    pub local: &'a [u8],
249}
250
251impl<'a> Name<'a> {
252    /// True for the element `ns:local`.
253    #[inline]
254    pub fn is(&self, ns: Ns, local: &[u8]) -> bool {
255        self.ns == ns && self.local == local
256    }
257
258    /// The name as written, `prefix:local` or `local`.
259    pub fn qualified(&self) -> String {
260        match self.prefix.is_empty() {
261            true => String::from_utf8_lossy(self.local).into_owned(),
262            false => format!(
263                "{}:{}",
264                String::from_utf8_lossy(self.prefix),
265                String::from_utf8_lossy(self.local)
266            ),
267        }
268    }
269}
270
271/// A start tag.
272#[derive(Clone, Copy, Debug)]
273pub struct Start<'a> {
274    pub name: Name<'a>,
275    /// Bytes between the element name and the closing `>` or `/>`.
276    pub raw_attrs: &'a [u8],
277    pub self_closing: bool,
278    /// Byte offset of the `<`.
279    pub offset: usize,
280}
281
282/// One attribute with its raw (still escaped) value.
283#[derive(Clone, Copy, Debug, PartialEq, Eq)]
284pub struct Attr<'a> {
285    pub name: Name<'a>,
286    pub raw_value: &'a [u8],
287}
288
289/// One token of the document.
290#[derive(Clone, Copy, Debug)]
291pub enum Event<'a> {
292    Start(Start<'a>),
293    /// An end tag, or the synthesized end of a self-closing element.
294    End(Name<'a>),
295    /// Character data, still escaped unless it came from a CDATA section.
296    Text {
297        raw: &'a [u8],
298        cdata: bool,
299    },
300    Eof,
301}
302
303#[derive(Clone, Copy)]
304struct Scope<'a> {
305    depth: usize,
306    prefix: &'a [u8],
307    ns: Ns,
308}
309
310/// The innermost namespace bound to a prefix: the prefix bytes and the namespace.
311type Binding<'a> = Option<(&'a [u8], Ns)>;
312
313/// An element on the open stack: its resolved name and the name bytes as written.
314#[derive(Clone, Copy)]
315struct Open<'a> {
316    name: Name<'a>,
317    qname: &'a [u8],
318}
319
320/// The pull tokenizer.
321pub struct Reader<'a> {
322    data: &'a [u8],
323    pos: usize,
324    open: Vec<Open<'a>>,
325    scopes: Vec<Scope<'a>>,
326    /// The innermost binding for each prefix first byte; the common case
327    /// resolves with one table lookup and one short compare.
328    first_byte: Box<[Binding<'a>; 256]>,
329    default_ns: Ns,
330    pending_end: Option<Name<'a>>,
331    other: Vec<&'a [u8]>,
332    saw_transitional: bool,
333    saw_strict: bool,
334    strict: bool,
335    root_closed: bool,
336}
337
338impl<'a> Reader<'a> {
339    pub fn new(data: &'a [u8]) -> Self {
340        let pos = match data.starts_with(&[0xef, 0xbb, 0xbf]) {
341            true => 3,
342            false => 0,
343        };
344        Self {
345            data,
346            pos,
347            open: Vec::with_capacity(16),
348            scopes: Vec::with_capacity(8),
349            first_byte: Box::new([None; 256]),
350            default_ns: Ns::None,
351            pending_end: None,
352            other: Vec::new(),
353            saw_transitional: false,
354            saw_strict: false,
355            strict: false,
356            root_closed: false,
357        }
358    }
359
360    /// Enables the full well-formedness checks the verifier needs.
361    pub fn strict(mut self) -> Self {
362        self.strict = true;
363        self
364    }
365
366    /// Number of currently open elements.
367    #[inline]
368    pub fn depth(&self) -> usize {
369        self.open.len()
370    }
371
372    /// Current byte offset.
373    #[inline]
374    pub fn offset(&self) -> usize {
375        self.pos
376    }
377
378    /// True once a Transitional namespace URI has been declared.
379    pub fn saw_transitional(&self) -> bool {
380        self.saw_transitional
381    }
382
383    /// True once a Strict namespace URI has been declared.
384    pub fn saw_strict(&self) -> bool {
385        self.saw_strict
386    }
387
388    /// The URI behind an [`Ns::Other`] index.
389    pub fn other_uri(&self, index: u16) -> Option<&'a [u8]> {
390        self.other.get(usize::from(index)).copied()
391    }
392
393    /// Resolves a prefix against the namespaces in scope. `xml` is always bound.
394    #[inline]
395    pub fn resolve(&self, prefix: &[u8]) -> Ns {
396        let Some(&first) = prefix.first() else {
397            return self.default_ns;
398        };
399        if let Some((bound, ns)) = self.first_byte[usize::from(first)] {
400            if bound == prefix {
401                return ns;
402            }
403        }
404        self.resolve_slow(prefix)
405    }
406
407    fn resolve_slow(&self, prefix: &[u8]) -> Ns {
408        if let Some(scope) = self
409            .scopes
410            .iter()
411            .rev()
412            .find(|scope| scope.prefix == prefix)
413        {
414            return scope.ns;
415        }
416        match prefix {
417            b"xml" => Ns::Xml,
418            _ => Ns::None,
419        }
420    }
421
422    fn bind(&mut self, prefix: &'a [u8], ns: Ns) {
423        match prefix.first() {
424            None => self.default_ns = ns,
425            Some(&first) => self.first_byte[usize::from(first)] = Some((prefix, ns)),
426        }
427    }
428
429    /// Recomputes the fast binding for `prefix` after its scope was popped.
430    fn rebind(&mut self, prefix: &[u8]) {
431        let remaining = self
432            .scopes
433            .iter()
434            .rev()
435            .find(|scope| match prefix.first() {
436                None => scope.prefix.is_empty(),
437                Some(&first) => scope.prefix.first() == Some(&first),
438            })
439            .map(|scope| (scope.prefix, scope.ns));
440        match prefix.first() {
441            None => self.default_ns = remaining.map_or(Ns::None, |(_, ns)| ns),
442            Some(&first) => self.first_byte[usize::from(first)] = remaining,
443        }
444    }
445
446    /// The next token. This is a pull parser, not an iterator: the end of
447    /// input is an event, and errors end the stream.
448    #[allow(clippy::should_implement_trait)]
449    pub fn next(&mut self) -> XmlResult<Event<'a>> {
450        if let Some(name) = self.pending_end.take() {
451            self.finish_open();
452            return Ok(Event::End(name));
453        }
454        loop {
455            if self.pos >= self.data.len() {
456                if !self.open.is_empty() {
457                    return Err(XmlError {
458                        offset: self.pos,
459                        msg: "unexpected end of document inside an element",
460                    });
461                }
462                return Ok(Event::Eof);
463            }
464            if self.data[self.pos] != b'<' {
465                if self.open.is_empty() {
466                    self.skip_outer_whitespace()?;
467                    continue;
468                }
469                return self.text();
470            }
471            let tag_start = self.pos;
472            let Some(&kind) = self.data.get(tag_start + 1) else {
473                return Err(XmlError {
474                    offset: tag_start,
475                    msg: "unterminated tag",
476                });
477            };
478            match kind {
479                b'/' => return self.end_tag(tag_start),
480                b'?' => {
481                    self.skip_past(tag_start + 2, b"?>", "unterminated processing instruction")?
482                }
483                b'!' => {
484                    if self.data[tag_start + 1..].starts_with(b"!--") {
485                        self.skip_past(tag_start + 4, b"-->", "unterminated comment")?;
486                        continue;
487                    }
488                    if self.data[tag_start + 1..].starts_with(b"![CDATA[") {
489                        let body_start = tag_start + 9;
490                        let end =
491                            memmem::find(&self.data[body_start..], b"]]>").ok_or(XmlError {
492                                offset: tag_start,
493                                msg: "unterminated CDATA section",
494                            })?;
495                        self.pos = body_start + end + 3;
496                        return Ok(Event::Text {
497                            raw: &self.data[body_start..body_start + end],
498                            cdata: true,
499                        });
500                    }
501                    if self.data[tag_start + 1..].starts_with(b"!DOCTYPE") {
502                        return Err(XmlError {
503                            offset: tag_start,
504                            msg: "DTD declarations are not allowed in package parts",
505                        });
506                    }
507                    return Err(XmlError {
508                        offset: tag_start,
509                        msg: "unrecognized markup declaration",
510                    });
511                }
512                _ => return self.start_tag(tag_start),
513            }
514        }
515    }
516
517    /// Consumes everything up to and including the end tag matching the
518    /// most recent start tag, without resolving names or namespaces.
519    pub fn skip_element(&mut self) -> XmlResult<()> {
520        let target = self.open.len().saturating_sub(1);
521        if self.pending_end.take().is_some() {
522            if self.open.is_empty() {
523                return Err(XmlError {
524                    offset: self.pos,
525                    msg: "nothing to skip",
526                });
527            }
528            self.finish_open();
529            return Ok(());
530        }
531        loop {
532            let pos = self.pos;
533            let Some(rel) = memchr(b'<', &self.data[pos..]) else {
534                return Err(XmlError {
535                    offset: pos,
536                    msg: "unexpected end of document inside an element",
537                });
538            };
539            let tag_start = pos + rel;
540            let kind = *self.data.get(tag_start + 1).ok_or(XmlError {
541                offset: tag_start,
542                msg: "unterminated tag",
543            })?;
544            match kind {
545                b'/' => {
546                    let fast = self.open.last().and_then(|open| {
547                        let end = tag_start + 2 + open.qname.len();
548                        let hit = self.data.get(tag_start + 2..end) == Some(open.qname)
549                            && self.data.get(end) == Some(&b'>');
550                        hit.then_some(end + 1)
551                    });
552                    match fast {
553                        Some(after) => {
554                            self.pos = after;
555                            self.finish_open();
556                        }
557                        None => {
558                            let close = memchr(b'>', &self.data[tag_start..]).ok_or(XmlError {
559                                offset: tag_start,
560                                msg: "unterminated end tag",
561                            })?;
562                            self.pos = tag_start + close + 1;
563                            let qname = trim_ascii(&self.data[tag_start + 2..tag_start + close]);
564                            self.close(qname, tag_start)?;
565                        }
566                    }
567                    if self.open.len() == target {
568                        return Ok(());
569                    }
570                }
571                b'?' => {
572                    self.skip_past(tag_start + 2, b"?>", "unterminated processing instruction")?
573                }
574                b'!' => {
575                    if self.data[tag_start + 1..].starts_with(b"!--") {
576                        self.skip_past(tag_start + 4, b"-->", "unterminated comment")?;
577                    } else if self.data[tag_start + 1..].starts_with(b"![CDATA[") {
578                        self.skip_past(tag_start + 9, b"]]>", "unterminated CDATA section")?;
579                    } else {
580                        return Err(XmlError {
581                            offset: tag_start,
582                            msg: "unrecognized markup declaration",
583                        });
584                    }
585                }
586                _ => {
587                    let (name_end, _, self_closing, _) = self.scan_start(tag_start)?;
588                    if !self_closing {
589                        let qname = &self.data[tag_start + 1..name_end];
590                        let name = Name {
591                            ns: Ns::None,
592                            prefix: &[],
593                            local: qname,
594                        };
595                        self.open.push(Open { name, qname });
596                    }
597                }
598            }
599        }
600    }
601
602    /// Reads the text content of the current element up to its end tag,
603    /// appending decoded characters to `out`; nested elements contribute
604    /// their text too.
605    pub fn text_content(&mut self, out: &mut String) -> XmlResult<()> {
606        let target = self.open.len().saturating_sub(1);
607        loop {
608            match self.next()? {
609                Event::Text { raw, cdata } => match cdata {
610                    true => out.push_str(&String::from_utf8_lossy(raw)),
611                    false => unescape_into(raw, out),
612                },
613                Event::End(_) if self.open.len() == target => return Ok(()),
614                Event::Eof => return Ok(()),
615                _ => {}
616            }
617        }
618    }
619
620    /// The raw value of attribute `ns:local` on `start`, if present.
621    pub fn attr(&self, start: &Start<'a>, ns: Ns, local: &[u8]) -> Option<&'a [u8]> {
622        self.attrs(start)
623            .find(|attr| attr.name.ns == ns && attr.name.local == local)
624            .map(|attr| attr.raw_value)
625    }
626
627    /// The attributes of `start`, resolved against the namespaces in scope.
628    pub fn attrs(&self, start: &Start<'a>) -> Attrs<'_, 'a> {
629        Attrs {
630            reader: self,
631            raw: start.raw_attrs,
632            pos: 0,
633        }
634    }
635
636    fn text(&mut self) -> XmlResult<Event<'a>> {
637        let start = self.pos;
638        let end = memchr(b'<', &self.data[start..])
639            .map(|rel| start + rel)
640            .unwrap_or(self.data.len());
641        self.pos = end;
642        let raw = &self.data[start..end];
643        if self.strict {
644            if let Some(offset) = invalid_char_offset(raw) {
645                return Err(XmlError {
646                    offset: start + offset,
647                    msg: "character not allowed in XML",
648                });
649            }
650            if let Some(rel) = memmem::find(raw, b"]]>") {
651                return Err(XmlError {
652                    offset: start + rel,
653                    msg: "']]>' is not allowed in character data",
654                });
655            }
656        }
657        Ok(Event::Text { raw, cdata: false })
658    }
659
660    fn skip_outer_whitespace(&mut self) -> XmlResult<()> {
661        let start = self.pos;
662        let end = memchr(b'<', &self.data[start..])
663            .map(|rel| start + rel)
664            .unwrap_or(self.data.len());
665        if !self.data[start..end]
666            .iter()
667            .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
668        {
669            return Err(XmlError {
670                offset: start,
671                msg: "text outside the root element",
672            });
673        }
674        self.pos = end;
675        Ok(())
676    }
677
678    fn skip_past(&mut self, from: usize, needle: &[u8], msg: &'static str) -> XmlResult<()> {
679        let rel =
680            memmem::find(&self.data[from.min(self.data.len())..], needle).ok_or(XmlError {
681                offset: self.pos,
682                msg,
683            })?;
684        self.pos = from + rel + needle.len();
685        Ok(())
686    }
687
688    /// Scans a start tag beginning at `tag_start`; returns the end of the
689    /// name, the end of the attribute region and whether the tag is
690    /// self-closing, and leaves `pos` after the tag.
691    fn scan_start(&mut self, tag_start: usize) -> XmlResult<(usize, usize, bool, Option<usize>)> {
692        let data = self.data;
693        let name_start = tag_start + 1;
694        let mut i = name_start;
695        let mut colon = None;
696        while i < data.len() {
697            let byte = data[i];
698            if NAME_END[usize::from(byte)] {
699                break;
700            }
701            if byte == b':' && colon.is_none() {
702                colon = Some(i);
703            }
704            i += 1;
705        }
706        if i == name_start {
707            return Err(XmlError {
708                offset: tag_start,
709                msg: "element name expected",
710            });
711        }
712        let name_end = i;
713        if !self.strict {
714            if let Some(rel) = first_unquoted_gt(&data[name_end..]) {
715                let gt = name_end + rel;
716                self.pos = gt + 1;
717                let self_closing = gt > name_end && data[gt - 1] == b'/';
718                let attrs_end = match self_closing {
719                    true => gt - 1,
720                    false => gt,
721                };
722                return Ok((name_end, attrs_end, self_closing, colon));
723            }
724        }
725        loop {
726            while i < data.len() && matches!(data[i], b' ' | b'\t' | b'\r' | b'\n') {
727                i += 1;
728            }
729            match data.get(i) {
730                None => {
731                    return Err(XmlError {
732                        offset: tag_start,
733                        msg: "unterminated start tag",
734                    })
735                }
736                Some(b'>') => {
737                    self.pos = i + 1;
738                    return Ok((name_end, i, false, colon));
739                }
740                Some(b'/') => {
741                    if data.get(i + 1) != Some(&b'>') {
742                        return Err(XmlError {
743                            offset: i,
744                            msg: "'/' must be followed by '>'",
745                        });
746                    }
747                    self.pos = i + 2;
748                    return Ok((name_end, i, true, colon));
749                }
750                Some(_) => {
751                    let eq = memchr3(b'=', b'>', b'/', &data[i..])
752                        .map(|rel| i + rel)
753                        .ok_or(XmlError {
754                            offset: i,
755                            msg: "unterminated start tag",
756                        })?;
757                    if data[eq] != b'=' {
758                        if self.strict {
759                            return Err(XmlError {
760                                offset: i,
761                                msg: "attribute without a value",
762                            });
763                        }
764                        i = eq;
765                        continue;
766                    }
767                    let mut v = eq + 1;
768                    while v < data.len() && matches!(data[v], b' ' | b'\t' | b'\r' | b'\n') {
769                        v += 1;
770                    }
771                    let quote = *data.get(v).ok_or(XmlError {
772                        offset: v,
773                        msg: "unterminated start tag",
774                    })?;
775                    if quote != b'"' && quote != b'\'' {
776                        return Err(XmlError {
777                            offset: v,
778                            msg: "attribute value must be quoted",
779                        });
780                    }
781                    let close =
782                        memchr(quote, &data[v + 1..])
783                            .map(|rel| v + 1 + rel)
784                            .ok_or(XmlError {
785                                offset: v,
786                                msg: "unterminated attribute value",
787                            })?;
788                    if self.strict && memchr(b'<', &data[v + 1..close]).is_some() {
789                        return Err(XmlError {
790                            offset: v,
791                            msg: "'<' is not allowed in an attribute value",
792                        });
793                    }
794                    i = close + 1;
795                }
796            }
797        }
798    }
799
800    fn start_tag(&mut self, tag_start: usize) -> XmlResult<Event<'a>> {
801        let (name_end, attrs_end, self_closing, colon) = self.scan_start(tag_start)?;
802        let raw_attrs = &self.data[name_end..attrs_end];
803        let qname = &self.data[tag_start + 1..name_end];
804        let (prefix, local): (&'a [u8], &'a [u8]) = match colon {
805            Some(colon) => (
806                &self.data[tag_start + 1..colon],
807                &self.data[colon + 1..name_end],
808            ),
809            None => (&[], qname),
810        };
811        if self.strict {
812            let valid = is_ncname(local) && (prefix.is_empty() || is_ncname(prefix));
813            if !valid {
814                return Err(XmlError {
815                    offset: tag_start,
816                    msg: "invalid element name",
817                });
818            }
819            if self.open.is_empty() && self.root_closed {
820                return Err(XmlError {
821                    offset: tag_start,
822                    msg: "more than one root element",
823                });
824            }
825        }
826        let depth = self.open.len() + 1;
827        if raw_attrs.len() >= 6 && xmlns_finder().find(raw_attrs).is_some() {
828            self.declare_namespaces(raw_attrs, depth, tag_start)?;
829        }
830        let ns = self.resolve(prefix);
831        if self.strict && ns == Ns::None && !prefix.is_empty() {
832            return Err(XmlError {
833                offset: tag_start,
834                msg: "undeclared namespace prefix",
835            });
836        }
837        let name = Name { ns, prefix, local };
838        self.open.push(Open { name, qname });
839        if self_closing {
840            self.pending_end = Some(name);
841        }
842        Ok(Event::Start(Start {
843            name,
844            raw_attrs,
845            self_closing,
846            offset: tag_start,
847        }))
848    }
849
850    fn declare_namespaces(
851        &mut self,
852        raw_attrs: &'a [u8],
853        depth: usize,
854        tag_start: usize,
855    ) -> XmlResult<()> {
856        let mut seen_default = false;
857        for attr in (RawAttrs {
858            raw: raw_attrs,
859            pos: 0,
860        }) {
861            let (prefix, local) = split_qname(attr.0);
862            let is_default = prefix.is_empty() && local == b"xmlns";
863            let is_prefixed = prefix == b"xmlns";
864            if !is_default && !is_prefixed {
865                continue;
866            }
867            let declared_prefix: &'a [u8] = match is_default {
868                true => b"",
869                false => local,
870            };
871            if self.strict {
872                if is_default && seen_default {
873                    return Err(XmlError {
874                        offset: tag_start,
875                        msg: "duplicate default namespace declaration",
876                    });
877                }
878                if self
879                    .scopes
880                    .iter()
881                    .any(|scope| scope.depth == depth && scope.prefix == declared_prefix)
882                {
883                    return Err(XmlError {
884                        offset: tag_start,
885                        msg: "duplicate namespace declaration",
886                    });
887                }
888            }
889            seen_default |= is_default;
890            let ns = self.intern(attr.1);
891            self.scopes.push(Scope {
892                depth,
893                prefix: declared_prefix,
894                ns,
895            });
896            self.bind(declared_prefix, ns);
897        }
898        Ok(())
899    }
900
901    fn intern(&mut self, uri: &'a [u8]) -> Ns {
902        if uri.is_empty() {
903            return Ns::None;
904        }
905        if let Some((_, ns, conformance)) = KNOWN.iter().find(|(known, _, _)| *known == uri) {
906            let class_specific = matches!(
907                ns,
908                Ns::Pml | Ns::Dml | Ns::Rel | Ns::Ep | Ns::Vt | Ns::Chart | Ns::Dgm | Ns::Pic
909            );
910            match conformance {
911                Conformance::Transitional if class_specific => self.saw_transitional = true,
912                Conformance::Strict => self.saw_strict = true,
913                _ => {}
914            }
915            return *ns;
916        }
917        if let Some(index) = self.other.iter().position(|known| *known == uri) {
918            return Ns::Other(index as u16);
919        }
920        self.other.push(uri);
921        Ns::Other((self.other.len() - 1) as u16)
922    }
923
924    fn end_tag(&mut self, tag_start: usize) -> XmlResult<Event<'a>> {
925        if let Some(open) = self.open.last().copied() {
926            let end = tag_start + 2 + open.qname.len();
927            let matches_open = self.data.get(tag_start + 2..end) == Some(open.qname)
928                && self.data.get(end) == Some(&b'>');
929            if matches_open {
930                self.pos = end + 1;
931                self.finish_open();
932                return Ok(Event::End(open.name));
933            }
934        }
935        let close = memchr(b'>', &self.data[tag_start..])
936            .map(|rel| tag_start + rel)
937            .ok_or(XmlError {
938                offset: tag_start,
939                msg: "unterminated end tag",
940            })?;
941        let qname = trim_ascii(&self.data[tag_start + 2..close]);
942        self.pos = close + 1;
943        let name = self.close(qname, tag_start)?;
944        Ok(Event::End(name))
945    }
946
947    /// Pops the open element, which must be named `qname` as written.
948    fn close(&mut self, qname: &[u8], offset: usize) -> XmlResult<Name<'a>> {
949        let Some(open) = self.open.last().copied() else {
950            return Err(XmlError {
951                offset,
952                msg: "end tag without a start tag",
953            });
954        };
955        if open.qname != qname {
956            return Err(XmlError {
957                offset,
958                msg: "end tag does not match the open element",
959            });
960        }
961        self.finish_open();
962        Ok(open.name)
963    }
964
965    /// Pops the open element and the namespaces it declared.
966    #[inline]
967    fn finish_open(&mut self) {
968        self.open.pop();
969        self.pop_scopes();
970        self.root_closed |= self.open.is_empty();
971    }
972
973    #[inline]
974    fn pop_scopes(&mut self) {
975        let depth = self.open.len() + 1;
976        while self.scopes.last().is_some_and(|scope| scope.depth == depth) {
977            let Some(scope) = self.scopes.pop() else {
978                return;
979            };
980            self.rebind(scope.prefix);
981        }
982    }
983}
984
985/// Iterator over the attributes of a start tag, resolved against the
986/// reader's namespaces in scope.
987pub struct Attrs<'r, 'a> {
988    reader: &'r Reader<'a>,
989    raw: &'a [u8],
990    pos: usize,
991}
992
993impl<'r, 'a> Iterator for Attrs<'r, 'a> {
994    type Item = Attr<'a>;
995
996    fn next(&mut self) -> Option<Attr<'a>> {
997        let (qname, raw_value, next) = next_raw_attr(self.raw, self.pos)?;
998        self.pos = next;
999        let (prefix, local) = split_qname(qname);
1000        let ns = match prefix.is_empty() {
1001            true => Ns::None,
1002            false => self.reader.resolve(prefix),
1003        };
1004        Some(Attr {
1005            name: Name { ns, prefix, local },
1006            raw_value,
1007        })
1008    }
1009}
1010
1011struct RawAttrs<'a> {
1012    raw: &'a [u8],
1013    pos: usize,
1014}
1015
1016impl<'a> Iterator for RawAttrs<'a> {
1017    type Item = (&'a [u8], &'a [u8]);
1018
1019    fn next(&mut self) -> Option<Self::Item> {
1020        let (qname, value, next) = next_raw_attr(self.raw, self.pos)?;
1021        self.pos = next;
1022        Some((qname, value))
1023    }
1024}
1025
1026/// Scans one `name="value"` pair starting at `pos`; returns (name, raw value, position after).
1027fn next_raw_attr(raw: &[u8], mut pos: usize) -> Option<(&[u8], &[u8], usize)> {
1028    while pos < raw.len() && matches!(raw[pos], b' ' | b'\t' | b'\r' | b'\n') {
1029        pos += 1;
1030    }
1031    if pos >= raw.len() {
1032        return None;
1033    }
1034    let eq = memchr(b'=', &raw[pos..]).map(|rel| pos + rel)?;
1035    let name = trim_ascii(&raw[pos..eq]);
1036    let mut v = eq + 1;
1037    while v < raw.len() && matches!(raw[v], b' ' | b'\t' | b'\r' | b'\n') {
1038        v += 1;
1039    }
1040    let quote = *raw.get(v)?;
1041    let close = memchr(quote, &raw[v + 1..]).map(|rel| v + 1 + rel)?;
1042    Some((name, &raw[v + 1..close], close + 1))
1043}
1044
1045/// The offset of the first `>` outside a quoted attribute value, or None
1046/// when a quote is left open before any such `>`.
1047fn first_unquoted_gt(data: &[u8]) -> Option<usize> {
1048    let mut i = 0;
1049    loop {
1050        let at = i + memchr3(b'>', b'"', b'\'', &data[i..])?;
1051        let byte = data[at];
1052        if byte == b'>' {
1053            return Some(at);
1054        }
1055        let close = memchr(byte, &data[at + 1..])?;
1056        i = at + 1 + close + 1;
1057    }
1058}
1059
1060/// Bytes that end an element name inside a start tag.
1061static NAME_END: [bool; 256] = {
1062    let mut table = [false; 256];
1063    table[b' ' as usize] = true;
1064    table[b'\t' as usize] = true;
1065    table[b'\r' as usize] = true;
1066    table[b'\n' as usize] = true;
1067    table[b'/' as usize] = true;
1068    table[b'>' as usize] = true;
1069    table
1070};
1071
1072#[inline]
1073fn split_qname(qname: &[u8]) -> (&[u8], &[u8]) {
1074    match memchr(b':', qname) {
1075        Some(colon) => (&qname[..colon], &qname[colon + 1..]),
1076        None => (&[], qname),
1077    }
1078}
1079
1080fn trim_ascii(bytes: &[u8]) -> &[u8] {
1081    let start = bytes
1082        .iter()
1083        .position(|byte| !byte.is_ascii_whitespace())
1084        .unwrap_or(bytes.len());
1085    let end = bytes
1086        .iter()
1087        .rposition(|byte| !byte.is_ascii_whitespace())
1088        .map_or(start, |i| i + 1);
1089    &bytes[start..end.max(start)]
1090}
1091
1092/// Whether `name` is an XML NCName (ASCII rules; non-ASCII bytes are accepted).
1093pub fn is_ncname(name: &[u8]) -> bool {
1094    let Some(&first) = name.first() else {
1095        return false;
1096    };
1097    let start_ok = first.is_ascii_alphabetic() || first == b'_' || first >= 0x80;
1098    start_ok
1099        && name.iter().all(|&byte| {
1100            byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.') || byte >= 0x80
1101        })
1102}
1103
1104/// Offset of the first byte that XML 1.0 forbids in character data
1105/// (controls other than tab, newline and carriage return), if any.
1106pub fn invalid_char_offset(raw: &[u8]) -> Option<usize> {
1107    raw.iter()
1108        .position(|&byte| byte < 0x20 && !matches!(byte, b'\t' | b'\n' | b'\r'))
1109}
1110
1111/// Decodes character references, the five predefined entities and
1112/// `_xHHHH_` escapes from `raw`, appending to `out`. Unknown entities and
1113/// malformed references are kept literally; invalid UTF-8 is replaced.
1114pub fn unescape_into(raw: &[u8], out: &mut String) {
1115    let needs_work = memchr2(b'&', b'_', raw).is_some();
1116    if !needs_work {
1117        push_lossy(raw, out);
1118        return;
1119    }
1120    let mut rest = raw;
1121    while let Some(rel) = memchr2(b'&', b'_', rest) {
1122        push_lossy(&rest[..rel], out);
1123        rest = &rest[rel..];
1124        match rest[0] {
1125            b'&' => {
1126                let Some(semi) = memchr(b';', &rest[..rest.len().min(12)]) else {
1127                    out.push('&');
1128                    rest = &rest[1..];
1129                    continue;
1130                };
1131                let entity = &rest[1..semi];
1132                match decode_entity(entity) {
1133                    Some(ch) => {
1134                        out.push(ch);
1135                        rest = &rest[semi + 1..];
1136                    }
1137                    None => {
1138                        out.push('&');
1139                        rest = &rest[1..];
1140                    }
1141                }
1142            }
1143            _ => match decode_x_escape(rest) {
1144                Some(ch) => {
1145                    out.push(ch);
1146                    rest = &rest[7..];
1147                }
1148                None => {
1149                    out.push('_');
1150                    rest = &rest[1..];
1151                }
1152            },
1153        }
1154    }
1155    push_lossy(rest, out);
1156}
1157
1158/// Decodes only XML entities and character references, leaving `_xHHHH_`
1159/// sequences untouched (for attribute values such as part names).
1160pub fn unescape_attr(raw: &[u8]) -> String {
1161    let mut out = String::with_capacity(raw.len());
1162    if memchr(b'&', raw).is_none() {
1163        push_lossy(raw, &mut out);
1164        return out;
1165    }
1166    let mut rest = raw;
1167    while let Some(rel) = memchr(b'&', rest) {
1168        push_lossy(&rest[..rel], &mut out);
1169        rest = &rest[rel..];
1170        let decoded = memchr(b';', &rest[..rest.len().min(12)])
1171            .and_then(|semi| decode_entity(&rest[1..semi]).map(|ch| (ch, semi)));
1172        match decoded {
1173            Some((ch, semi)) => {
1174                out.push(ch);
1175                rest = &rest[semi + 1..];
1176            }
1177            None => {
1178                out.push('&');
1179                rest = &rest[1..];
1180            }
1181        }
1182    }
1183    push_lossy(rest, &mut out);
1184    out
1185}
1186
1187fn decode_entity(entity: &[u8]) -> Option<char> {
1188    match entity {
1189        b"lt" => Some('<'),
1190        b"gt" => Some('>'),
1191        b"amp" => Some('&'),
1192        b"quot" => Some('"'),
1193        b"apos" => Some('\''),
1194        _ => {
1195            let digits = entity.strip_prefix(b"#")?;
1196            let code = match digits.strip_prefix(b"x") {
1197                Some(hex) => u32::from_str_radix(std::str::from_utf8(hex).ok()?, 16).ok()?,
1198                None => std::str::from_utf8(digits).ok()?.parse::<u32>().ok()?,
1199            };
1200            char::from_u32(code)
1201        }
1202    }
1203}
1204
1205fn decode_x_escape(rest: &[u8]) -> Option<char> {
1206    if rest.len() < 7 || rest[1] != b'x' || rest[6] != b'_' {
1207        return None;
1208    }
1209    let hex = std::str::from_utf8(&rest[2..6]).ok()?;
1210    let code = u32::from_str_radix(hex, 16).ok()?;
1211    char::from_u32(code)
1212}
1213
1214#[inline]
1215fn push_lossy(bytes: &[u8], out: &mut String) {
1216    match std::str::from_utf8(bytes) {
1217        Ok(text) => out.push_str(text),
1218        Err(_) => out.push_str(&String::from_utf8_lossy(bytes)),
1219    }
1220}
1221
1222/// Runs the strict tokenizer over a whole part, returning the first error.
1223pub fn well_formed(data: &[u8]) -> XmlResult<()> {
1224    let mut reader = Reader::new(data).strict();
1225    let mut saw_root = false;
1226    loop {
1227        match reader.next()? {
1228            Event::Start(_) => saw_root = true,
1229            Event::Eof => break,
1230            _ => {}
1231        }
1232    }
1233    match saw_root {
1234        true => Ok(()),
1235        false => Err(XmlError {
1236            offset: data.len(),
1237            msg: "no root element",
1238        }),
1239    }
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244    use super::*;
1245
1246    const SLIDE: &[u8] = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1247<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="Title 1"/><p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr><p:txBody><a:bodyPr/><a:p><a:r><a:rPr lang="en-US"><a:hlinkClick r:id="rId2"/></a:rPr><a:t>Hello &amp; welcome</a:t></a:r><a:br/><a:r><a:t><![CDATA[a < b]]></a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>"#;
1248
1249    fn collect(data: &[u8]) -> Vec<String> {
1250        let mut reader = Reader::new(data);
1251        let mut out = Vec::new();
1252        loop {
1253            match reader.next().unwrap() {
1254                Event::Start(start) => out.push(format!(
1255                    "<{:?}:{}{}",
1256                    start.name.ns,
1257                    String::from_utf8_lossy(start.name.local),
1258                    if start.self_closing { "/" } else { "" }
1259                )),
1260                Event::End(name) => out.push(format!("</{}", String::from_utf8_lossy(name.local))),
1261                Event::Text { raw, cdata } => {
1262                    let mut text = String::new();
1263                    match cdata {
1264                        true => text.push_str(std::str::from_utf8(raw).unwrap()),
1265                        false => unescape_into(raw, &mut text),
1266                    }
1267                    out.push(format!("'{text}'"));
1268                }
1269                Event::Eof => return out,
1270            }
1271        }
1272    }
1273
1274    #[test]
1275    fn resolves_prefixes_and_emits_ends_for_self_closing_tags() {
1276        let events = collect(SLIDE);
1277        assert_eq!(events[0], "<Pml:sld");
1278        assert!(events.contains(&"<Pml:cNvPr/".to_string()));
1279        assert!(events.contains(&"<Dml:bodyPr/".to_string()));
1280        assert!(events.contains(&"'Hello & welcome'".to_string()));
1281        assert!(events.contains(&"'a < b'".to_string()));
1282        let ends = events
1283            .iter()
1284            .filter(|event| event.starts_with("</"))
1285            .count();
1286        let starts = events
1287            .iter()
1288            .filter(|event| event.starts_with('<') && !event.starts_with("</"))
1289            .count();
1290        assert_eq!(starts, ends);
1291        assert_eq!(events.last().unwrap(), "</sld");
1292    }
1293
1294    #[test]
1295    fn attributes_resolve_namespaces_and_default_namespace_does_not_apply_to_them() {
1296        let mut reader = Reader::new(SLIDE);
1297        loop {
1298            if let Event::Start(start) = reader.next().unwrap() {
1299                if start.name.is(Ns::Dml, b"hlinkClick") {
1300                    assert_eq!(reader.attr(&start, Ns::Rel, b"id"), Some(&b"rId2"[..]));
1301                    assert_eq!(reader.attr(&start, Ns::None, b"id"), None);
1302                    break;
1303                }
1304            }
1305        }
1306        let data = br#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/></Types>"#;
1307        let mut reader = Reader::new(data);
1308        let Event::Start(types) = reader.next().unwrap() else {
1309            panic!()
1310        };
1311        assert!(types.name.is(Ns::ContentTypes, b"Types"));
1312        let Event::Start(default) = reader.next().unwrap() else {
1313            panic!()
1314        };
1315        assert!(default.name.is(Ns::ContentTypes, b"Default"));
1316        let attrs: Vec<_> = reader.attrs(&default).collect();
1317        assert_eq!(attrs.len(), 2);
1318        assert_eq!(attrs[0].name.ns, Ns::None);
1319        assert_eq!(attrs[0].name.local, b"Extension");
1320        assert_eq!(attrs[1].raw_value, b"application/xml");
1321    }
1322
1323    #[test]
1324    fn lenient_start_tags_keep_mixed_quotes_and_quoted_angle_brackets() {
1325        let data = br#"<a x="it's" y=">" z='say "hi" &gt; there'>text</a>"#;
1326        let mut reader = Reader::new(data);
1327        let Event::Start(start) = reader.next().unwrap() else {
1328            panic!()
1329        };
1330        assert!(!start.self_closing);
1331        let attrs: Vec<_> = reader.attrs(&start).collect();
1332        assert_eq!(attrs.len(), 3);
1333        assert_eq!(attrs[0].raw_value, b"it's");
1334        assert_eq!(attrs[1].raw_value, b">");
1335        assert_eq!(attrs[2].raw_value, br#"say "hi" &gt; there"#);
1336        let Event::Text { raw, .. } = reader.next().unwrap() else {
1337            panic!()
1338        };
1339        assert_eq!(raw, b"text");
1340        assert!(matches!(reader.next().unwrap(), Event::End(_)));
1341        let mut reader = Reader::new(br#"<a x="it's" y=">"/>"#);
1342        let Event::Start(start) = reader.next().unwrap() else {
1343            panic!()
1344        };
1345        assert!(start.self_closing);
1346        assert_eq!(reader.attrs(&start).count(), 2);
1347        assert!(matches!(reader.next().unwrap(), Event::End(_)));
1348        assert!(matches!(reader.next().unwrap(), Event::Eof));
1349        assert_eq!(first_unquoted_gt(br#"x="unclosed>"#), None);
1350    }
1351
1352    #[test]
1353    fn strict_namespace_uris_map_to_the_same_ids() {
1354        let data = br#"<p:sld xmlns:p="http://purl.oclc.org/ooxml/presentationml/main" xmlns:a="http://purl.oclc.org/ooxml/drawingml/main"><a:t>x</a:t></p:sld>"#;
1355        let mut reader = Reader::new(data);
1356        let Event::Start(sld) = reader.next().unwrap() else {
1357            panic!()
1358        };
1359        assert_eq!(sld.name.ns, Ns::Pml);
1360        let Event::Start(t) = reader.next().unwrap() else {
1361            panic!()
1362        };
1363        assert_eq!(t.name.ns, Ns::Dml);
1364        assert!(reader.saw_strict());
1365        assert!(!reader.saw_transitional());
1366    }
1367
1368    #[test]
1369    fn unknown_namespaces_are_numbered_and_scoped() {
1370        let data =
1371            br#"<r xmlns="urn:a"><x xmlns="urn:b"><y/></x><z xmlns:q="urn:b"><q:w/></z></r>"#;
1372        let mut reader = Reader::new(data);
1373        let mut seen = Vec::new();
1374        loop {
1375            match reader.next().unwrap() {
1376                Event::Start(start) => seen.push((
1377                    String::from_utf8_lossy(start.name.local).into_owned(),
1378                    start.name.ns,
1379                )),
1380                Event::Eof => break,
1381                _ => {}
1382            }
1383        }
1384        assert_eq!(seen[0], ("r".into(), Ns::Other(0)));
1385        assert_eq!(seen[1], ("x".into(), Ns::Other(1)));
1386        assert_eq!(seen[2], ("y".into(), Ns::Other(1)));
1387        assert_eq!(seen[3], ("z".into(), Ns::Other(0)));
1388        assert_eq!(seen[4], ("w".into(), Ns::Other(1)));
1389        assert_eq!(reader.other_uri(1), Some(&b"urn:b"[..]));
1390    }
1391
1392    #[test]
1393    fn skip_element_consumes_the_subtree_and_keeps_depth_consistent() {
1394        let data = br#"<a><b x="1>2"><c/><!-- </b> --><d><![CDATA[</b>]]></d></b><e>after</e></a>"#;
1395        let mut reader = Reader::new(data);
1396        assert!(matches!(reader.next().unwrap(), Event::Start(_)));
1397        let Event::Start(b) = reader.next().unwrap() else {
1398            panic!()
1399        };
1400        assert_eq!(b.name.local, b"b");
1401        reader.skip_element().unwrap();
1402        assert_eq!(reader.depth(), 1);
1403        let Event::Start(e) = reader.next().unwrap() else {
1404            panic!()
1405        };
1406        assert_eq!(e.name.local, b"e");
1407        let mut text = String::new();
1408        reader.text_content(&mut text).unwrap();
1409        assert_eq!(text, "after");
1410        assert!(matches!(reader.next().unwrap(), Event::End(_)));
1411        assert!(matches!(reader.next().unwrap(), Event::Eof));
1412    }
1413
1414    #[test]
1415    fn skipping_a_self_closing_element_works() {
1416        let data = b"<a><b/><c>x</c></a>";
1417        let mut reader = Reader::new(data);
1418        reader.next().unwrap();
1419        reader.next().unwrap();
1420        reader.skip_element().unwrap();
1421        let Event::Start(c) = reader.next().unwrap() else {
1422            panic!()
1423        };
1424        assert_eq!(c.name.local, b"c");
1425    }
1426
1427    #[test]
1428    fn mismatched_and_unterminated_tags_are_errors() {
1429        assert!(Reader::new(b"<a></b>").next().is_ok());
1430        let mut reader = Reader::new(b"<a></b>");
1431        reader.next().unwrap();
1432        assert_eq!(
1433            reader.next().unwrap_err().msg,
1434            "end tag does not match the open element"
1435        );
1436        let mut reader = Reader::new(b"<a><b>");
1437        reader.next().unwrap();
1438        reader.next().unwrap();
1439        assert_eq!(
1440            reader.next().unwrap_err().msg,
1441            "unexpected end of document inside an element"
1442        );
1443        let mut reader = Reader::new(b"<a x='1'");
1444        assert_eq!(reader.next().unwrap_err().msg, "unterminated start tag");
1445        assert_eq!(
1446            Reader::new(b"<!DOCTYPE x><x/>").next().unwrap_err().msg,
1447            "DTD declarations are not allowed in package parts"
1448        );
1449    }
1450
1451    #[test]
1452    fn a_byte_order_mark_and_declaration_are_skipped() {
1453        let mut data = vec![0xef, 0xbb, 0xbf];
1454        data.extend_from_slice(b"<?xml version=\"1.0\"?>\n<x/>");
1455        let mut reader = Reader::new(&data);
1456        let Event::Start(x) = reader.next().unwrap() else {
1457            panic!()
1458        };
1459        assert_eq!(x.name.local, b"x");
1460    }
1461
1462    #[test]
1463    fn unescape_handles_entities_references_and_x_escapes() {
1464        let mut out = String::new();
1465        unescape_into(
1466            b"a &lt;b&gt; &amp; &quot;c&apos; &#65;&#x42; _x0009_tab &unknown; &amp _x00ZZ_ end",
1467            &mut out,
1468        );
1469        assert_eq!(out, "a <b> & \"c' AB \ttab &unknown; &amp _x00ZZ_ end");
1470        let mut out = String::new();
1471        unescape_into(b"plain text_with_underscores", &mut out);
1472        assert_eq!(out, "plain text_with_underscores");
1473        assert_eq!(
1474            unescape_attr(b"/ppt/a_x0041_.xml &amp; b"),
1475            "/ppt/a_x0041_.xml & b"
1476        );
1477        let mut out = String::new();
1478        unescape_into(&[b'o', b'k', 0xff, b'!'], &mut out);
1479        assert_eq!(out, "ok\u{fffd}!");
1480    }
1481
1482    #[test]
1483    fn well_formed_reports_violations_the_lenient_reader_tolerates() {
1484        assert!(well_formed(b"<a><b x=\"1\"/>text</a>").is_ok());
1485        assert_eq!(
1486            well_formed(b"<a>bad\x01char</a>").unwrap_err().msg,
1487            "character not allowed in XML"
1488        );
1489        assert_eq!(
1490            well_formed(b"<a x=\"<\"/>").unwrap_err().msg,
1491            "'<' is not allowed in an attribute value"
1492        );
1493        assert_eq!(
1494            well_formed(b"<p:a/>").unwrap_err().msg,
1495            "undeclared namespace prefix"
1496        );
1497        assert_eq!(
1498            well_formed(b"<1a/>").unwrap_err().msg,
1499            "invalid element name"
1500        );
1501        assert_eq!(well_formed(b"").unwrap_err().msg, "no root element");
1502        assert_eq!(
1503            well_formed(b"<a/><b/>").unwrap_err().msg,
1504            "more than one root element"
1505        );
1506        assert!(Reader::new(b"<p:a/>").next().is_ok());
1507        let mut lenient = Reader::new(b"<a/><b/>");
1508        lenient.next().unwrap();
1509        lenient.next().unwrap();
1510        assert!(matches!(lenient.next().unwrap(), Event::Start(_)));
1511    }
1512
1513    #[test]
1514    fn text_outside_root_is_rejected_but_whitespace_is_fine() {
1515        let mut reader = Reader::new(b"\n<a/>\n");
1516        assert!(matches!(reader.next().unwrap(), Event::Start(_)));
1517        assert!(matches!(reader.next().unwrap(), Event::End(_)));
1518        assert!(matches!(reader.next().unwrap(), Event::Eof));
1519        let mut reader = Reader::new(b"junk<a/>");
1520        assert_eq!(
1521            reader.next().unwrap_err().msg,
1522            "text outside the root element"
1523        );
1524    }
1525
1526    #[test]
1527    fn transitional_uri_lookup() {
1528        assert_eq!(
1529            transitional_uri(Ns::Pml),
1530            Some("http://schemas.openxmlformats.org/presentationml/2006/main")
1531        );
1532        assert_eq!(transitional_uri(Ns::Other(3)), None);
1533    }
1534}