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