Skip to main content

stella_docx_kernel/
semantic.rs

1#![forbid(unsafe_code)]
2
3use std::collections::HashMap;
4
5use quick_xml::{
6    XmlVersion,
7    escape::resolve_predefined_entity,
8    events::{BytesStart, Event},
9    name::{Namespace, PrefixDeclaration, ResolveResult},
10    reader::NsReader,
11};
12use thiserror::Error;
13
14const WORDPROCESSING_TRANSITIONAL: &[u8] =
15    b"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
16const WORDPROCESSING_STRICT: &[u8] = b"http://purl.oclc.org/ooxml/wordprocessingml/main";
17const OFFICE_RELATIONSHIPS_TRANSITIONAL: &[u8] =
18    b"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
19const OFFICE_RELATIONSHIPS_STRICT: &[u8] =
20    b"http://purl.oclc.org/ooxml/officeDocument/relationships";
21const MARKUP_COMPATIBILITY_TRANSITIONAL: &[u8] =
22    b"http://schemas.openxmlformats.org/markup-compatibility/2006";
23const MARKUP_COMPATIBILITY_STRICT: &[u8] = b"http://purl.oclc.org/ooxml/markup-compatibility/main";
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct ScanLimits {
27    pub blocks: usize,
28    pub segments: usize,
29    pub depth: usize,
30    pub events: usize,
31    pub inline_context_copies: usize,
32}
33
34impl Default for ScanLimits {
35    fn default() -> Self {
36        Self {
37            blocks: 100_000,
38            segments: 1_000_000,
39            depth: 256,
40            events: 10_000_000,
41            inline_context_copies: 20_000_000,
42        }
43    }
44}
45
46#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
47pub struct ScanWork {
48    pub events: usize,
49    pub inline_context_copies: usize,
50}
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub enum RevisionKind {
54    Deletion,
55    Insertion,
56    MoveFrom,
57    MoveTo,
58}
59
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub enum InlineContext {
62    Hyperlink {
63        relationship_id: Option<String>,
64        anchor: Option<String>,
65    },
66    Revision(RevisionKind),
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum SegmentSource {
71    Break,
72    Tab,
73    Text,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct TextSegment {
78    pub start_utf16: usize,
79    pub end_utf16: usize,
80    pub source: SegmentSource,
81    pub contexts: Vec<InlineContext>,
82    pub xml_path: Vec<usize>,
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub enum BlockLocation {
87    Paragraph {
88        xml_path: Vec<usize>,
89    },
90    TableCellParagraph {
91        xml_path: Vec<usize>,
92        table_path: Vec<usize>,
93        row_path: Vec<usize>,
94        cell_path: Vec<usize>,
95    },
96    TextBoxParagraph {
97        xml_path: Vec<usize>,
98        text_box_path: Vec<usize>,
99    },
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct TextBlock {
104    pub text: String,
105    pub location: BlockLocation,
106    pub segments: Vec<TextSegment>,
107}
108
109#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
110pub struct PartCoverage {
111    pub hyperlink_segments: usize,
112    pub revision_segments: usize,
113    pub unsupported_alternate_content: usize,
114    pub unsupported_symbols: usize,
115    pub unsupported_field_instructions: usize,
116    pub work: ScanWork,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct PartScan {
121    pub blocks: Vec<TextBlock>,
122    pub coverage: PartCoverage,
123}
124
125#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
126pub enum ScanError {
127    #[error("WordprocessingML part is invalid XML")]
128    InvalidXml,
129    #[error("WordprocessingML part must not contain a document type declaration")]
130    DocumentTypeDeclaration,
131    #[error("WordprocessingML text is outside a paragraph")]
132    TextOutsideParagraph,
133    #[error("WordprocessingML part exceeds the configured nesting depth")]
134    TooDeep,
135    #[error("WordprocessingML part exceeds the configured block limit")]
136    TooManyBlocks,
137    #[error("WordprocessingML part exceeds the configured segment limit")]
138    TooManySegments,
139    #[error("WordprocessingML part exceeds the configured XML event limit")]
140    TooManyEvents,
141    #[error("WordprocessingML part exceeds the configured inline-context copy limit")]
142    TooManyInlineContextCopies,
143    #[error("WordprocessingML text length overflowed")]
144    TextLengthOverflow,
145    #[error("WordprocessingML part must be UTF-8 encoded")]
146    UnsupportedEncoding,
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150enum NamespaceKind {
151    MarkupCompatibility,
152    OfficeRelationships,
153    Other,
154    Wordprocessing,
155}
156
157fn namespace_kind(namespace: &ResolveResult<'_>) -> NamespaceKind {
158    let ResolveResult::Bound(Namespace(value)) = namespace else {
159        return NamespaceKind::Other;
160    };
161    if matches!(*value, WORDPROCESSING_TRANSITIONAL | WORDPROCESSING_STRICT) {
162        NamespaceKind::Wordprocessing
163    } else if matches!(
164        *value,
165        OFFICE_RELATIONSHIPS_TRANSITIONAL | OFFICE_RELATIONSHIPS_STRICT
166    ) {
167        NamespaceKind::OfficeRelationships
168    } else if matches!(
169        *value,
170        MARKUP_COMPATIBILITY_TRANSITIONAL | MARKUP_COMPATIBILITY_STRICT
171    ) {
172        NamespaceKind::MarkupCompatibility
173    } else {
174        NamespaceKind::Other
175    }
176}
177
178fn attribute(
179    reader: &NsReader<&[u8]>,
180    element: &BytesStart<'_>,
181    namespace: NamespaceKind,
182    local_name: &[u8],
183) -> Result<Option<String>, ScanError> {
184    for item in element.attributes() {
185        let item = item.map_err(|_| ScanError::InvalidXml)?;
186        let (resolved, name) = reader.resolver().resolve_attribute(item.key);
187        if namespace_kind(&resolved) == namespace && name.as_ref() == local_name {
188            return item
189                .decoded_and_normalized_value(XmlVersion::default(), reader.decoder())
190                .map(|value| Some(value.into_owned()))
191                .map_err(|_| ScanError::InvalidXml);
192        }
193    }
194    Ok(None)
195}
196
197fn unqualified_attribute(
198    reader: &NsReader<&[u8]>,
199    element: &BytesStart<'_>,
200    local_name: &[u8],
201) -> Result<Option<String>, ScanError> {
202    for item in element.attributes() {
203        let item = item.map_err(|_| ScanError::InvalidXml)?;
204        let (resolved, name) = reader.resolver().resolve_attribute(item.key);
205        if matches!(resolved, ResolveResult::Unbound) && name.as_ref() == local_name {
206            return item
207                .decoded_and_normalized_value(XmlVersion::default(), reader.decoder())
208                .map(|value| Some(value.into_owned()))
209                .map_err(|_| ScanError::InvalidXml);
210        }
211    }
212    Ok(None)
213}
214
215#[derive(Clone, Copy, Debug, Eq, PartialEq)]
216enum FrameKind {
217    AlternateContent { branch_selected: bool },
218    AlternateContentBranch,
219    Other,
220    Paragraph(usize),
221    Run,
222    Text,
223}
224
225struct Frame {
226    kind: FrameKind,
227    next_child: usize,
228    context_pushed: bool,
229    table_path: Option<Vec<usize>>,
230    row_path: Option<Vec<usize>>,
231    cell_path: Option<Vec<usize>>,
232    text_box_path: Option<Vec<usize>>,
233    content_suppressed: bool,
234}
235
236impl Frame {
237    const fn root() -> Self {
238        Self {
239            kind: FrameKind::Other,
240            next_child: 0,
241            context_pushed: false,
242            table_path: None,
243            row_path: None,
244            cell_path: None,
245            text_box_path: None,
246            content_suppressed: false,
247        }
248    }
249}
250
251struct BlockBuilder {
252    text: String,
253    utf16_len: usize,
254    location: BlockLocation,
255    segments: Vec<TextSegment>,
256}
257
258struct PendingText {
259    block_id: Option<usize>,
260    path: Vec<usize>,
261    value: String,
262}
263
264struct Scanner<F> {
265    frames: Vec<Frame>,
266    element_path: Vec<usize>,
267    builders: HashMap<usize, BlockBuilder>,
268    completed: HashMap<usize, TextBlock>,
269    coverage: PartCoverage,
270    active_contexts: Vec<InlineContext>,
271    limits: ScanLimits,
272    next_block_id: usize,
273    next_emit_id: usize,
274    segment_count: usize,
275    pending_text: Option<PendingText>,
276    root_seen: bool,
277    sink: F,
278}
279
280impl<F> Scanner<F>
281where
282    F: FnMut(TextBlock) -> Result<(), ScanError>,
283{
284    fn new(limits: ScanLimits, sink: F) -> Self {
285        Self {
286            frames: vec![Frame::root()],
287            element_path: Vec::new(),
288            builders: HashMap::new(),
289            completed: HashMap::new(),
290            coverage: PartCoverage::default(),
291            active_contexts: Vec::new(),
292            limits,
293            next_block_id: 0,
294            next_emit_id: 0,
295            segment_count: 0,
296            pending_text: None,
297            root_seen: false,
298            sink,
299        }
300    }
301
302    fn enter_element(&mut self) -> Result<(), ScanError> {
303        let parent = self.frames.last_mut().ok_or(ScanError::InvalidXml)?;
304        let child = parent.next_child;
305        parent.next_child = parent
306            .next_child
307            .checked_add(1)
308            .ok_or(ScanError::InvalidXml)?;
309        self.element_path.push(child);
310        Ok(())
311    }
312
313    fn current_block_id(&self) -> Option<usize> {
314        self.frames.iter().rev().find_map(|frame| match frame.kind {
315            FrameKind::Paragraph(block_id) => Some(block_id),
316            FrameKind::AlternateContent { .. }
317            | FrameKind::AlternateContentBranch
318            | FrameKind::Other
319            | FrameKind::Run
320            | FrameKind::Text => None,
321        })
322    }
323
324    fn inside_run(&self) -> bool {
325        self.frames
326            .iter()
327            .rev()
328            .any(|frame| frame.kind == FrameKind::Run)
329    }
330
331    fn contexts(&mut self) -> Result<Vec<InlineContext>, ScanError> {
332        let copies = self
333            .coverage
334            .work
335            .inline_context_copies
336            .checked_add(self.active_contexts.len())
337            .ok_or(ScanError::TooManyInlineContextCopies)?;
338        if copies > self.limits.inline_context_copies {
339            return Err(ScanError::TooManyInlineContextCopies);
340        }
341        self.coverage.work.inline_context_copies = copies;
342        Ok(self.active_contexts.clone())
343    }
344
345    fn record_event(&mut self) -> Result<(), ScanError> {
346        let events = self
347            .coverage
348            .work
349            .events
350            .checked_add(1)
351            .ok_or(ScanError::TooManyEvents)?;
352        if events > self.limits.events {
353            return Err(ScanError::TooManyEvents);
354        }
355        self.coverage.work.events = events;
356        Ok(())
357    }
358
359    fn nearest_path(&self, select: impl Fn(&Frame) -> Option<&Vec<usize>>) -> Option<Vec<usize>> {
360        self.frames.iter().rev().find_map(select).cloned()
361    }
362
363    fn record_coverage(&mut self, namespace: NamespaceKind, name: &[u8]) -> Result<(), ScanError> {
364        if namespace == NamespaceKind::MarkupCompatibility && name == b"AlternateContent" {
365            self.coverage.unsupported_alternate_content = self
366                .coverage
367                .unsupported_alternate_content
368                .checked_add(1)
369                .ok_or(ScanError::TooManySegments)?;
370        } else if namespace == NamespaceKind::Wordprocessing && name == b"sym" {
371            self.coverage.unsupported_symbols = self
372                .coverage
373                .unsupported_symbols
374                .checked_add(1)
375                .ok_or(ScanError::TooManySegments)?;
376        } else if namespace == NamespaceKind::Wordprocessing
377            && matches!(name, b"instrText" | b"fldSimple")
378        {
379            self.coverage.unsupported_field_instructions = self
380                .coverage
381                .unsupported_field_instructions
382                .checked_add(1)
383                .ok_or(ScanError::TooManySegments)?;
384        }
385        Ok(())
386    }
387
388    fn block_location(&self, path: &[usize]) -> BlockLocation {
389        self.nearest_path(|frame| frame.text_box_path.as_ref())
390            .map_or_else(
391                || {
392                    let table = self.nearest_path(|frame| frame.table_path.as_ref());
393                    let row = self.nearest_path(|frame| frame.row_path.as_ref());
394                    let cell = self.nearest_path(|frame| frame.cell_path.as_ref());
395                    if let (Some(table_path), Some(row_path), Some(cell_path)) = (table, row, cell)
396                    {
397                        BlockLocation::TableCellParagraph {
398                            xml_path: path.to_vec(),
399                            table_path,
400                            row_path,
401                            cell_path,
402                        }
403                    } else {
404                        BlockLocation::Paragraph {
405                            xml_path: path.to_vec(),
406                        }
407                    }
408                },
409                |text_box| BlockLocation::TextBoxParagraph {
410                    xml_path: path.to_vec(),
411                    text_box_path: text_box,
412                },
413            )
414    }
415
416    fn begin_paragraph(&mut self, path: &[usize]) -> Result<FrameKind, ScanError> {
417        if self.next_block_id >= self.limits.blocks {
418            return Err(ScanError::TooManyBlocks);
419        }
420        let block_id = self.next_block_id;
421        self.next_block_id = self
422            .next_block_id
423            .checked_add(1)
424            .ok_or(ScanError::TooManyBlocks)?;
425        self.builders.insert(
426            block_id,
427            BlockBuilder {
428                text: String::new(),
429                utf16_len: 0,
430                location: self.block_location(path),
431                segments: Vec::new(),
432            },
433        );
434        Ok(FrameKind::Paragraph(block_id))
435    }
436
437    fn inline_context(
438        reader: &NsReader<&[u8]>,
439        element: &BytesStart<'_>,
440        name: &[u8],
441    ) -> Result<Option<InlineContext>, ScanError> {
442        match name {
443            b"hyperlink" => Ok(Some(InlineContext::Hyperlink {
444                relationship_id: attribute(
445                    reader,
446                    element,
447                    NamespaceKind::OfficeRelationships,
448                    b"id",
449                )?,
450                anchor: attribute(reader, element, NamespaceKind::Wordprocessing, b"anchor")?,
451            })),
452            b"del" => Ok(Some(InlineContext::Revision(RevisionKind::Deletion))),
453            b"ins" => Ok(Some(InlineContext::Revision(RevisionKind::Insertion))),
454            b"moveFrom" => Ok(Some(InlineContext::Revision(RevisionKind::MoveFrom))),
455            b"moveTo" => Ok(Some(InlineContext::Revision(RevisionKind::MoveTo))),
456            _ => Ok(None),
457        }
458    }
459
460    fn alternate_choice_supported(
461        reader: &NsReader<&[u8]>,
462        element: &BytesStart<'_>,
463    ) -> Result<bool, ScanError> {
464        let Some(requires) = unqualified_attribute(reader, element, b"Requires")? else {
465            return Ok(false);
466        };
467        let mut prefixes = requires.split_ascii_whitespace().peekable();
468        if prefixes.peek().is_none() {
469            return Ok(false);
470        }
471        Ok(prefixes.all(|required_prefix| {
472            reader.resolver().bindings().any(|(prefix, namespace)| {
473                matches!(prefix, PrefixDeclaration::Named(value) if value == required_prefix.as_bytes())
474                    && matches!(
475                        namespace_kind(&ResolveResult::Bound(namespace)),
476                        NamespaceKind::OfficeRelationships | NamespaceKind::Wordprocessing
477                    )
478            })
479        }))
480    }
481
482    fn push_plain_frame(&mut self, kind: FrameKind, content_suppressed: bool) {
483        self.frames.push(Frame {
484            kind,
485            next_child: 0,
486            context_pushed: false,
487            table_path: None,
488            row_path: None,
489            cell_path: None,
490            text_box_path: None,
491            content_suppressed,
492        });
493    }
494
495    fn start_alternate_content(
496        &mut self,
497        reader: &NsReader<&[u8]>,
498        element: &BytesStart<'_>,
499        namespace: NamespaceKind,
500        name: &[u8],
501        parent_suppressed: bool,
502    ) -> Result<bool, ScanError> {
503        let parent_is_alternate_content = matches!(
504            self.frames.last().map(|frame| frame.kind),
505            Some(FrameKind::AlternateContent { .. })
506        );
507        if parent_is_alternate_content {
508            let eligible = namespace == NamespaceKind::MarkupCompatibility
509                && match name {
510                    b"Choice" => Self::alternate_choice_supported(reader, element)?,
511                    b"Fallback" => true,
512                    _ => false,
513                };
514            let parent = self.frames.last_mut().ok_or(ScanError::InvalidXml)?;
515            let FrameKind::AlternateContent { branch_selected } = &mut parent.kind else {
516                return Err(ScanError::InvalidXml);
517            };
518            let selected = !*branch_selected && eligible && !parent_suppressed;
519            if selected {
520                *branch_selected = true;
521            }
522            self.push_plain_frame(FrameKind::AlternateContentBranch, !selected);
523            return Ok(true);
524        }
525        if namespace == NamespaceKind::MarkupCompatibility && name == b"AlternateContent" {
526            self.push_plain_frame(
527                FrameKind::AlternateContent {
528                    branch_selected: false,
529                },
530                parent_suppressed,
531            );
532            return Ok(true);
533        }
534        Ok(false)
535    }
536
537    fn start(
538        &mut self,
539        reader: &NsReader<&[u8]>,
540        element: &BytesStart<'_>,
541    ) -> Result<(), ScanError> {
542        if self.frames.len() >= self.limits.depth {
543            return Err(ScanError::TooDeep);
544        }
545        if self.frames.len() == 1 {
546            if self.root_seen {
547                return Err(ScanError::InvalidXml);
548            }
549            self.root_seen = true;
550        }
551        self.enter_element()?;
552        let (resolved, local_name) = reader.resolver().resolve_element(element.name());
553        let namespace = namespace_kind(&resolved);
554        let name = local_name.as_ref();
555        self.record_coverage(namespace, name)?;
556
557        let parent_suppressed = self
558            .frames
559            .last()
560            .ok_or(ScanError::InvalidXml)?
561            .content_suppressed;
562        if self.start_alternate_content(reader, element, namespace, name, parent_suppressed)? {
563            return Ok(());
564        }
565        if parent_suppressed {
566            self.push_plain_frame(FrameKind::Other, true);
567            return Ok(());
568        }
569        if namespace != NamespaceKind::Wordprocessing {
570            self.push_plain_frame(FrameKind::Other, false);
571            return Ok(());
572        }
573        let table_path = (name == b"tbl").then(|| self.element_path.clone());
574        let row_path = (name == b"tr").then(|| self.element_path.clone());
575        let cell_path = (name == b"tc").then(|| self.element_path.clone());
576        let text_box_path = (name == b"txbxContent").then(|| self.element_path.clone());
577        let context = Self::inline_context(reader, element, name)?;
578        let context_pushed = context.is_some();
579        if let Some(context) = context {
580            self.active_contexts.push(context);
581        }
582
583        let kind = if name == b"p" {
584            let path = self.element_path.clone();
585            self.begin_paragraph(&path)?
586        } else if name == b"r" && self.current_block_id().is_some() {
587            FrameKind::Run
588        } else if matches!(name, b"t" | b"delText") {
589            self.pending_text = Some(PendingText {
590                block_id: self.current_block_id(),
591                path: self.element_path.clone(),
592                value: String::new(),
593            });
594            FrameKind::Text
595        } else {
596            if name == b"tab" && self.inside_run() {
597                self.append_segment("\t", SegmentSource::Tab, self.element_path.clone())?;
598            } else if matches!(name, b"br" | b"cr") && self.inside_run() {
599                self.append_segment("\n", SegmentSource::Break, self.element_path.clone())?;
600            }
601            FrameKind::Other
602        };
603
604        self.frames.push(Frame {
605            kind,
606            next_child: 0,
607            context_pushed,
608            table_path,
609            row_path,
610            cell_path,
611            text_box_path,
612            content_suppressed: false,
613        });
614        Ok(())
615    }
616
617    fn append_text(&mut self, value: &str) {
618        let Some(pending) = self.pending_text.as_mut() else {
619            return;
620        };
621        pending.value.push_str(value);
622    }
623
624    fn append_segment(
625        &mut self,
626        value: &str,
627        source: SegmentSource,
628        path: Vec<usize>,
629    ) -> Result<(), ScanError> {
630        let Some(block_id) = self.current_block_id() else {
631            return Ok(());
632        };
633        self.append_segment_to(block_id, value, source, path)
634    }
635
636    fn append_segment_to(
637        &mut self,
638        block_id: usize,
639        value: &str,
640        source: SegmentSource,
641        path: Vec<usize>,
642    ) -> Result<(), ScanError> {
643        if value.is_empty() {
644            return Ok(());
645        }
646        if self.segment_count >= self.limits.segments {
647            return Err(ScanError::TooManySegments);
648        }
649        self.segment_count = self
650            .segment_count
651            .checked_add(1)
652            .ok_or(ScanError::TooManySegments)?;
653        let contexts = self.contexts()?;
654        let builder = self
655            .builders
656            .get_mut(&block_id)
657            .ok_or(ScanError::InvalidXml)?;
658        let units = value.encode_utf16().count();
659        let end_utf16 = builder
660            .utf16_len
661            .checked_add(units)
662            .ok_or(ScanError::TextLengthOverflow)?;
663        if contexts
664            .iter()
665            .any(|item| matches!(item, InlineContext::Hyperlink { .. }))
666        {
667            self.coverage.hyperlink_segments = self
668                .coverage
669                .hyperlink_segments
670                .checked_add(1)
671                .ok_or(ScanError::TooManySegments)?;
672        }
673        if contexts
674            .iter()
675            .any(|item| matches!(item, InlineContext::Revision(_)))
676        {
677            self.coverage.revision_segments = self
678                .coverage
679                .revision_segments
680                .checked_add(1)
681                .ok_or(ScanError::TooManySegments)?;
682        }
683        builder.text.push_str(value);
684        builder.segments.push(TextSegment {
685            start_utf16: builder.utf16_len,
686            end_utf16,
687            source,
688            contexts,
689            xml_path: path,
690        });
691        builder.utf16_len = end_utf16;
692        Ok(())
693    }
694
695    fn end(&mut self) -> Result<(), ScanError> {
696        let frame = self.frames.pop().ok_or(ScanError::InvalidXml)?;
697        match frame.kind {
698            FrameKind::Paragraph(block_id) => {
699                let builder = self
700                    .builders
701                    .remove(&block_id)
702                    .ok_or(ScanError::InvalidXml)?;
703                if self
704                    .completed
705                    .insert(
706                        block_id,
707                        TextBlock {
708                            text: builder.text,
709                            location: builder.location,
710                            segments: builder.segments,
711                        },
712                    )
713                    .is_some()
714                {
715                    return Err(ScanError::InvalidXml);
716                }
717                self.flush_completed()?;
718            }
719            FrameKind::Text => {
720                let pending = self.pending_text.take().ok_or(ScanError::InvalidXml)?;
721                if !pending.value.is_empty() {
722                    let block_id = pending.block_id.ok_or(ScanError::TextOutsideParagraph)?;
723                    self.append_segment_to(
724                        block_id,
725                        &pending.value,
726                        SegmentSource::Text,
727                        pending.path,
728                    )?;
729                }
730            }
731            FrameKind::AlternateContent { .. }
732            | FrameKind::AlternateContentBranch
733            | FrameKind::Other
734            | FrameKind::Run => {}
735        }
736        if frame.context_pushed {
737            self.active_contexts.pop().ok_or(ScanError::InvalidXml)?;
738        }
739        self.element_path.pop().ok_or(ScanError::InvalidXml)?;
740        Ok(())
741    }
742
743    fn flush_completed(&mut self) -> Result<(), ScanError> {
744        while let Some(block) = self.completed.remove(&self.next_emit_id) {
745            (self.sink)(block)?;
746            self.next_emit_id = self
747                .next_emit_id
748                .checked_add(1)
749                .ok_or(ScanError::TooManyBlocks)?;
750        }
751        Ok(())
752    }
753
754    fn finish(mut self) -> Result<PartCoverage, ScanError> {
755        self.flush_completed()?;
756        if !self.root_seen
757            || self.frames.len() != 1
758            || !self.element_path.is_empty()
759            || !self.builders.is_empty()
760            || !self.completed.is_empty()
761            || self.pending_text.is_some()
762            || !self.active_contexts.is_empty()
763            || self.next_emit_id != self.next_block_id
764        {
765            return Err(ScanError::InvalidXml);
766        }
767        Ok(self.coverage)
768    }
769}
770
771/// Scan one `WordprocessingML` part and deliver completed blocks in document
772/// order without retaining a second whole-part block collection.
773///
774/// A sink may observe blocks before the scanner reaches a later malformed
775/// element. Callers that require atomic behavior must stage sink output until
776/// this function returns successfully.
777///
778/// The input boundary is deliberately UTF-8. DOCX producers conventionally
779/// store `WordprocessingML` parts as UTF-8; callers with another XML encoding
780/// must transcode before scanning.
781///
782/// # Errors
783///
784/// Returns [`ScanError`] when the XML is malformed, violates a configured
785/// resource bound, contains text in an invalid location, or the sink rejects a
786/// block.
787pub fn scan_wordprocessing_part_with(
788    xml: &[u8],
789    limits: ScanLimits,
790    sink: impl FnMut(TextBlock) -> Result<(), ScanError>,
791) -> Result<PartCoverage, ScanError> {
792    std::str::from_utf8(xml).map_err(|_| ScanError::UnsupportedEncoding)?;
793    let mut reader = NsReader::from_reader(xml);
794    reader.config_mut().check_end_names = true;
795    let mut scanner = Scanner::new(limits, sink);
796    loop {
797        let event = reader.read_event().map_err(|_| ScanError::InvalidXml)?;
798        if matches!(event, Event::Eof) {
799            break;
800        }
801        scanner.record_event()?;
802        match event {
803            Event::Start(element) => scanner.start(&reader, &element)?,
804            Event::Empty(element) => {
805                scanner.start(&reader, &element)?;
806                scanner.end()?;
807            }
808            Event::Text(text) => {
809                let decoded = text.xml10_content().map_err(|_| ScanError::InvalidXml)?;
810                scanner.append_text(&decoded);
811            }
812            Event::CData(text) => {
813                let decoded = text.xml10_content().map_err(|_| ScanError::InvalidXml)?;
814                scanner.append_text(&decoded);
815            }
816            Event::GeneralRef(reference) => {
817                let value = if let Some(character) = reference
818                    .resolve_char_ref()
819                    .map_err(|_| ScanError::InvalidXml)?
820                {
821                    character.to_string()
822                } else {
823                    let name = reference.decode().map_err(|_| ScanError::InvalidXml)?;
824                    resolve_predefined_entity(&name)
825                        .ok_or(ScanError::InvalidXml)?
826                        .to_owned()
827                };
828                scanner.append_text(&value);
829            }
830            Event::End(_) => scanner.end()?,
831            Event::DocType(_) => return Err(ScanError::DocumentTypeDeclaration),
832            Event::Eof => return Err(ScanError::InvalidXml),
833            _ => {}
834        }
835    }
836    scanner.finish()
837}
838
839/// Scan one `WordprocessingML` part into an owned semantic projection.
840///
841/// # Errors
842///
843/// Returns [`ScanError`] when the XML is malformed, violates a configured
844/// resource bound, or contains `WordprocessingML` text outside a paragraph.
845pub fn scan_wordprocessing_part(xml: &[u8], limits: ScanLimits) -> Result<PartScan, ScanError> {
846    let mut blocks = Vec::new();
847    let coverage = scan_wordprocessing_part_with(xml, limits, |block| {
848        blocks.push(block);
849        Ok(())
850    })?;
851    Ok(PartScan { blocks, coverage })
852}
853
854#[cfg(test)]
855mod tests {
856    use std::fmt::Write as _;
857
858    use proptest::strategy::Strategy;
859    use proptest::test_runner::TestCaseError;
860    use proptest::{collection, prop_assert, prop_assert_eq, proptest};
861
862    use super::{
863        BlockLocation, InlineContext, RevisionKind, ScanLimits, SegmentSource,
864        scan_wordprocessing_part,
865    };
866
867    const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
868    const W_STRICT: &str = "http://purl.oclc.org/ooxml/wordprocessingml/main";
869    const R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
870    const MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
871
872    #[test]
873    fn scans_locations_contexts_controls_and_utf16() -> Result<(), super::ScanError> {
874        let xml = format!(
875            "<x:document xmlns:x=\"{W}\" xmlns:rel=\"{R}\"><x:body><x:p><x:r><x:t>😀 </x:t></x:r><x:hyperlink rel:id=\"r1\" x:anchor=\"a\"><x:r><x:t>A</x:t></x:r></x:hyperlink><x:ins><x:r><x:t>B</x:t></x:r></x:ins><x:r><x:tab/><x:br/></x:r></x:p><x:tbl><x:tr><x:tc><x:p><x:r><x:t>C</x:t></x:r></x:p></x:tc></x:tr></x:tbl></x:body></x:document>"
876        );
877        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())?;
878        assert_eq!(scan.blocks.len(), 2);
879        let first = scan.blocks.first().ok_or(super::ScanError::InvalidXml)?;
880        let second = scan.blocks.get(1).ok_or(super::ScanError::InvalidXml)?;
881        assert_eq!(first.text, "😀 AB\t\n");
882        assert_eq!(
883            first
884                .segments
885                .first()
886                .ok_or(super::ScanError::InvalidXml)?
887                .end_utf16,
888            3
889        );
890        assert_eq!(
891            first
892                .segments
893                .get(3)
894                .ok_or(super::ScanError::InvalidXml)?
895                .source,
896            SegmentSource::Tab
897        );
898        assert!(matches!(
899          first
900            .segments
901            .get(1)
902            .and_then(|segment| segment.contexts.first()),
903          Some(InlineContext::Hyperlink {
904            relationship_id: Some(value),
905            anchor: Some(anchor),
906          }) if value == "r1" && anchor == "a"
907        ));
908        assert!(matches!(
909            first
910                .segments
911                .get(2)
912                .and_then(|segment| segment.contexts.first()),
913            Some(InlineContext::Revision(RevisionKind::Insertion))
914        ));
915        assert!(matches!(
916            second.location,
917            BlockLocation::TableCellParagraph { .. }
918        ));
919        Ok(())
920    }
921
922    #[test]
923    fn projects_exactly_one_markup_compatibility_branch() -> Result<(), super::ScanError> {
924        let unsupported_choice = format!(
925            "<w:document xmlns:w=\"{W}\" xmlns:mc=\"{MC}\" xmlns:x=\"urn:unsupported\"><w:body><mc:AlternateContent><mc:Choice Requires=\"x\"><w:p><w:r><w:t>choice</w:t></w:r></w:p></mc:Choice><mc:Fallback><w:p><w:r><w:t>fallback</w:t></w:r></w:p></mc:Fallback></mc:AlternateContent></w:body></w:document>"
926        );
927        let fallback_scan =
928            scan_wordprocessing_part(unsupported_choice.as_bytes(), ScanLimits::default())?;
929        assert_eq!(
930            fallback_scan
931                .blocks
932                .iter()
933                .map(|block| block.text.as_str())
934                .collect::<Vec<_>>(),
935            ["fallback"]
936        );
937        assert_eq!(fallback_scan.coverage.unsupported_alternate_content, 1);
938
939        let supported_choice = format!(
940            "<w:document xmlns:w=\"{W}\" xmlns:mc=\"{MC}\"><w:body><mc:AlternateContent><mc:Choice Requires=\"w\"><w:p><w:r><w:t>choice</w:t></w:r></w:p></mc:Choice><mc:Fallback><w:p><w:r><w:t>fallback</w:t></w:r></w:p></mc:Fallback></mc:AlternateContent></w:body></w:document>"
941        );
942        let choice_scan =
943            scan_wordprocessing_part(supported_choice.as_bytes(), ScanLimits::default())?;
944        assert_eq!(
945            choice_scan
946                .blocks
947                .iter()
948                .map(|block| block.text.as_str())
949                .collect::<Vec<_>>(),
950            ["choice"]
951        );
952        Ok(())
953    }
954
955    #[test]
956    fn emits_text_controls_only_from_run_content() -> Result<(), super::ScanError> {
957        let xml = format!(
958            "<w:document xmlns:w=\"{W}\"><w:body><w:p><w:pPr><w:tabs><w:tab w:val=\"left\"/></w:tabs></w:pPr><w:r><w:t>A</w:t><w:tab/><w:br/></w:r></w:p></w:body></w:document>"
959        );
960        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())?;
961        let block = scan.blocks.first().ok_or(super::ScanError::InvalidXml)?;
962        assert_eq!(block.text, "A\t\n");
963        assert_eq!(
964            block
965                .segments
966                .iter()
967                .map(|segment| segment.source)
968                .collect::<Vec<_>>(),
969            [
970                SegmentSource::Text,
971                SegmentSource::Tab,
972                SegmentSource::Break
973            ]
974        );
975        Ok(())
976    }
977
978    #[test]
979    fn decodes_xml_references_exactly_once() -> Result<(), super::ScanError> {
980        let xml = format!(
981            "<w:document xmlns:w=\"{W}\"><w:body><w:p><w:r><w:t>&amp;amp; &lt; &#x1F600;</w:t></w:r></w:p></w:body></w:document>"
982        );
983        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())?;
984        assert_eq!(
985            scan.blocks
986                .first()
987                .ok_or(super::ScanError::InvalidXml)?
988                .text,
989            "&amp; < 😀"
990        );
991        Ok(())
992    }
993
994    #[test]
995    fn reports_the_utf8_input_boundary_explicitly() {
996        assert_eq!(
997            scan_wordprocessing_part(&[0xff], ScanLimits::default()),
998            Err(super::ScanError::UnsupportedEncoding)
999        );
1000    }
1001
1002    #[test]
1003    fn rejects_nonempty_text_bearing_elements_outside_paragraphs() -> Result<(), super::ScanError> {
1004        let xml = format!(
1005            "<w:document xmlns:w=\"{W}\"><w:body><w:r><w:t>outside</w:t></w:r></w:body></w:document>"
1006        );
1007        assert_eq!(
1008            scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default()),
1009            Err(super::ScanError::TextOutsideParagraph)
1010        );
1011
1012        let empty =
1013            format!("<w:document xmlns:w=\"{W}\"><w:body><w:r><w:t/></w:r></w:body></w:document>");
1014        let empty_scan = scan_wordprocessing_part(empty.as_bytes(), ScanLimits::default())?;
1015        assert!(empty_scan.blocks.is_empty());
1016        Ok(())
1017    }
1018
1019    #[test]
1020    fn rejects_namespace_spoofing_and_accepts_strict_ooxml() -> Result<(), super::ScanError> {
1021        let spoof = b"<w:document xmlns:w=\"urn:not-word\"><w:body><w:p><w:r><w:t>secret</w:t></w:r></w:p></w:body></w:document>";
1022        let spoof_scan = scan_wordprocessing_part(spoof, ScanLimits::default())?;
1023        assert!(spoof_scan.blocks.is_empty());
1024
1025        let strict = b"<d:document xmlns:d=\"http://purl.oclc.org/ooxml/wordprocessingml/main\"><d:body><d:p><d:r><d:t>strict</d:t></d:r></d:p></d:body></d:document>";
1026        let strict_scan = scan_wordprocessing_part(strict, ScanLimits::default())?;
1027        assert_eq!(
1028            strict_scan
1029                .blocks
1030                .first()
1031                .ok_or(super::ScanError::InvalidXml)?
1032                .text,
1033            "strict"
1034        );
1035        Ok(())
1036    }
1037
1038    #[test]
1039    fn nested_namespace_rebinding_cannot_spoof_wordprocessing_elements()
1040    -> Result<(), super::ScanError> {
1041        let xml = format!(
1042            "<w:document xmlns:w=\"{W}\"><w:body><w:p><w:r><w:t>before</w:t></w:r></w:p><x xmlns:w=\"urn:not-word\"><w:p><w:r><w:t>hidden</w:t></w:r></w:p></x><w:p><w:r><w:t>after</w:t></w:r></w:p></w:body></w:document>"
1043        );
1044        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())?;
1045        let texts = scan
1046            .blocks
1047            .iter()
1048            .map(|block| block.text.as_str())
1049            .collect::<Vec<_>>();
1050        assert_eq!(texts, ["before", "after"]);
1051        Ok(())
1052    }
1053
1054    #[test]
1055    fn rejects_document_type_declarations() {
1056        let doctype = format!("<!DOCTYPE w:document><w:document xmlns:w=\"{W}\"/>");
1057        assert_eq!(
1058            scan_wordprocessing_part(doctype.as_bytes(), ScanLimits::default()),
1059            Err(super::ScanError::DocumentTypeDeclaration)
1060        );
1061    }
1062
1063    #[test]
1064    fn rejects_multiple_roots_and_resource_limit_overruns() {
1065        let multiple_roots = format!("<w:p xmlns:w=\"{W}\"/><w:p xmlns:w=\"{W}\"/>");
1066        assert_eq!(
1067            scan_wordprocessing_part(multiple_roots.as_bytes(), ScanLimits::default()),
1068            Err(super::ScanError::InvalidXml)
1069        );
1070        let two_blocks =
1071            format!("<w:document xmlns:w=\"{W}\"><w:body><w:p/><w:p/></w:body></w:document>");
1072        assert_eq!(
1073            scan_wordprocessing_part(
1074                two_blocks.as_bytes(),
1075                ScanLimits {
1076                    blocks: 1,
1077                    ..ScanLimits::default()
1078                }
1079            ),
1080            Err(super::ScanError::TooManyBlocks)
1081        );
1082
1083        let empty_document = format!("<w:document xmlns:w=\"{W}\"/>");
1084        let one_event = scan_wordprocessing_part(
1085            empty_document.as_bytes(),
1086            ScanLimits {
1087                events: 1,
1088                ..ScanLimits::default()
1089            },
1090        );
1091        assert!(matches!(
1092            one_event,
1093            Ok(super::PartScan {
1094                coverage: super::PartCoverage {
1095                    work: super::ScanWork { events: 1, .. },
1096                    ..
1097                },
1098                ..
1099            })
1100        ));
1101
1102        let one_block = format!(
1103            "<w:document xmlns:w=\"{W}\"><w:body><w:p><w:r><w:t>x</w:t></w:r></w:p></w:body></w:document>"
1104        );
1105        assert_eq!(
1106            scan_wordprocessing_part(
1107                one_block.as_bytes(),
1108                ScanLimits {
1109                    events: 1,
1110                    ..ScanLimits::default()
1111                }
1112            ),
1113            Err(super::ScanError::TooManyEvents)
1114        );
1115
1116        let contextual = format!(
1117            "<w:document xmlns:w=\"{W}\"><w:body><w:p><w:ins><w:r><w:t>x</w:t></w:r></w:ins></w:p></w:body></w:document>"
1118        );
1119        assert_eq!(
1120            scan_wordprocessing_part(
1121                contextual.as_bytes(),
1122                ScanLimits {
1123                    inline_context_copies: 0,
1124                    ..ScanLimits::default()
1125                }
1126            ),
1127            Err(super::ScanError::TooManyInlineContextCopies)
1128        );
1129    }
1130
1131    fn sibling_run_fixture(run_count: usize) -> String {
1132        let mut runs = String::new();
1133        for _ in 0..run_count {
1134            runs.push_str("<w:r><w:t>x</w:t></w:r>");
1135        }
1136        format!("<w:document xmlns:w=\"{W}\"><w:body><w:p>{runs}</w:p></w:body></w:document>")
1137    }
1138
1139    fn sibling_paragraph_fixture(paragraph_count: usize) -> String {
1140        let mut paragraphs = String::new();
1141        for _ in 0..paragraph_count {
1142            paragraphs.push_str("<w:p><w:r><w:t>x</w:t></w:r></w:p>");
1143        }
1144        format!("<w:document xmlns:w=\"{W}\"><w:body>{paragraphs}</w:body></w:document>")
1145    }
1146
1147    #[test]
1148    fn scan_work_is_additive_across_independent_growth_axes() -> Result<(), super::ScanError> {
1149        let empty_runs =
1150            scan_wordprocessing_part(sibling_run_fixture(0).as_bytes(), ScanLimits::default())?;
1151        let small_runs =
1152            scan_wordprocessing_part(sibling_run_fixture(512).as_bytes(), ScanLimits::default())?;
1153        let large_runs =
1154            scan_wordprocessing_part(sibling_run_fixture(1_024).as_bytes(), ScanLimits::default())?;
1155        assert_eq!(
1156            large_runs.coverage.work.events - empty_runs.coverage.work.events,
1157            2 * (small_runs.coverage.work.events - empty_runs.coverage.work.events),
1158        );
1159        assert_eq!(small_runs.coverage.work.inline_context_copies, 0);
1160        assert_eq!(large_runs.coverage.work.inline_context_copies, 0);
1161
1162        let empty_paragraphs = scan_wordprocessing_part(
1163            sibling_paragraph_fixture(0).as_bytes(),
1164            ScanLimits::default(),
1165        )?;
1166        let small_paragraphs = scan_wordprocessing_part(
1167            sibling_paragraph_fixture(256).as_bytes(),
1168            ScanLimits::default(),
1169        )?;
1170        let large_paragraphs = scan_wordprocessing_part(
1171            sibling_paragraph_fixture(512).as_bytes(),
1172            ScanLimits::default(),
1173        )?;
1174        assert_eq!(
1175            large_paragraphs.coverage.work.events - empty_paragraphs.coverage.work.events,
1176            2 * (small_paragraphs.coverage.work.events - empty_paragraphs.coverage.work.events),
1177        );
1178        Ok(())
1179    }
1180
1181    proptest! {
1182      #[test]
1183      fn arbitrary_prefixes_and_run_splits_preserve_text_and_utf16_offsets(
1184        prefix in "[a-z]{1,8}".prop_filter(
1185          "XML namespace prefixes must not use reserved names",
1186          |prefix| !matches!(prefix.as_str(), "xml" | "xmlns"),
1187        ),
1188        fragments in collection::vec("[A-Za-z0-9 ]{0,16}", 1..32),
1189      ) {
1190        let mut runs = String::new();
1191        for fragment in &fragments {
1192          write!(
1193            &mut runs,
1194            "<{prefix}:r><{prefix}:t>{fragment}</{prefix}:t></{prefix}:r>"
1195          )
1196          .map_err(|error| TestCaseError::fail(error.to_string()))?;
1197        }
1198        let xml = format!(
1199          "<{prefix}:document xmlns:{prefix}=\"{W}\"><{prefix}:body><{prefix}:p>{runs}</{prefix}:p></{prefix}:body></{prefix}:document>"
1200        );
1201        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())
1202          .map_err(|error| TestCaseError::fail(error.to_string()))?;
1203        let block = scan
1204          .blocks
1205          .first()
1206          .ok_or_else(|| TestCaseError::fail("missing projected paragraph"))?;
1207        prop_assert_eq!(&block.text, &fragments.concat());
1208        let mut expected_start = 0_usize;
1209        for segment in &block.segments {
1210          prop_assert_eq!(segment.start_utf16, expected_start);
1211          prop_assert!(segment.end_utf16 >= segment.start_utf16);
1212          expected_start = segment.end_utf16;
1213        }
1214        prop_assert_eq!(expected_start, block.text.encode_utf16().count());
1215      }
1216
1217      #[test]
1218      fn nonempty_wordprocessing_text_requires_a_paragraph(
1219        prefix in "[a-z]{1,8}".prop_filter(
1220          "XML namespace prefixes must not use reserved names",
1221          |prefix| !matches!(prefix.as_str(), "xml" | "xmlns"),
1222        ),
1223        namespace in proptest::sample::select(&[W, W_STRICT][..]),
1224        text_element in proptest::sample::select(&["t", "delText"][..]),
1225        value in "[A-Za-z0-9 ]{1,32}",
1226        run_wrapped in proptest::bool::ANY,
1227      ) {
1228        let text = format!(
1229          "<{prefix}:{text_element}>{value}</{prefix}:{text_element}>"
1230        );
1231        let content = if run_wrapped {
1232          format!("<{prefix}:r>{text}</{prefix}:r>")
1233        } else {
1234          text
1235        };
1236        let xml = format!(
1237          "<{prefix}:document xmlns:{prefix}=\"{namespace}\"><{prefix}:body>{content}</{prefix}:body></{prefix}:document>"
1238        );
1239        prop_assert_eq!(
1240          scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default()),
1241          Err(super::ScanError::TextOutsideParagraph)
1242        );
1243      }
1244
1245      #[test]
1246      fn document_type_declarations_have_a_stable_typed_error(
1247        document_type in "[A-Za-z][A-Za-z0-9]{0,15}",
1248      ) {
1249        let xml = format!(
1250          "<!DOCTYPE {document_type}><w:document xmlns:w=\"{W}\"/>"
1251        );
1252        prop_assert_eq!(
1253          scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default()),
1254          Err(super::ScanError::DocumentTypeDeclaration)
1255        );
1256      }
1257    }
1258}