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::test_runner::TestCaseError;
859    use proptest::{collection, prop_assert, prop_assert_eq, proptest};
860
861    use super::{
862        BlockLocation, InlineContext, RevisionKind, ScanLimits, SegmentSource,
863        scan_wordprocessing_part,
864    };
865
866    const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
867    const W_STRICT: &str = "http://purl.oclc.org/ooxml/wordprocessingml/main";
868    const R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
869    const MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
870
871    #[test]
872    fn scans_locations_contexts_controls_and_utf16() -> Result<(), super::ScanError> {
873        let xml = format!(
874            "<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>"
875        );
876        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())?;
877        assert_eq!(scan.blocks.len(), 2);
878        let first = scan.blocks.first().ok_or(super::ScanError::InvalidXml)?;
879        let second = scan.blocks.get(1).ok_or(super::ScanError::InvalidXml)?;
880        assert_eq!(first.text, "😀 AB\t\n");
881        assert_eq!(
882            first
883                .segments
884                .first()
885                .ok_or(super::ScanError::InvalidXml)?
886                .end_utf16,
887            3
888        );
889        assert_eq!(
890            first
891                .segments
892                .get(3)
893                .ok_or(super::ScanError::InvalidXml)?
894                .source,
895            SegmentSource::Tab
896        );
897        assert!(matches!(
898          first
899            .segments
900            .get(1)
901            .and_then(|segment| segment.contexts.first()),
902          Some(InlineContext::Hyperlink {
903            relationship_id: Some(value),
904            anchor: Some(anchor),
905          }) if value == "r1" && anchor == "a"
906        ));
907        assert!(matches!(
908            first
909                .segments
910                .get(2)
911                .and_then(|segment| segment.contexts.first()),
912            Some(InlineContext::Revision(RevisionKind::Insertion))
913        ));
914        assert!(matches!(
915            second.location,
916            BlockLocation::TableCellParagraph { .. }
917        ));
918        Ok(())
919    }
920
921    #[test]
922    fn projects_exactly_one_markup_compatibility_branch() -> Result<(), super::ScanError> {
923        let unsupported_choice = format!(
924            "<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>"
925        );
926        let fallback_scan =
927            scan_wordprocessing_part(unsupported_choice.as_bytes(), ScanLimits::default())?;
928        assert_eq!(
929            fallback_scan
930                .blocks
931                .iter()
932                .map(|block| block.text.as_str())
933                .collect::<Vec<_>>(),
934            ["fallback"]
935        );
936        assert_eq!(fallback_scan.coverage.unsupported_alternate_content, 1);
937
938        let supported_choice = format!(
939            "<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>"
940        );
941        let choice_scan =
942            scan_wordprocessing_part(supported_choice.as_bytes(), ScanLimits::default())?;
943        assert_eq!(
944            choice_scan
945                .blocks
946                .iter()
947                .map(|block| block.text.as_str())
948                .collect::<Vec<_>>(),
949            ["choice"]
950        );
951        Ok(())
952    }
953
954    #[test]
955    fn emits_text_controls_only_from_run_content() -> Result<(), super::ScanError> {
956        let xml = format!(
957            "<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>"
958        );
959        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())?;
960        let block = scan.blocks.first().ok_or(super::ScanError::InvalidXml)?;
961        assert_eq!(block.text, "A\t\n");
962        assert_eq!(
963            block
964                .segments
965                .iter()
966                .map(|segment| segment.source)
967                .collect::<Vec<_>>(),
968            [
969                SegmentSource::Text,
970                SegmentSource::Tab,
971                SegmentSource::Break
972            ]
973        );
974        Ok(())
975    }
976
977    #[test]
978    fn decodes_xml_references_exactly_once() -> Result<(), super::ScanError> {
979        let xml = format!(
980            "<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>"
981        );
982        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())?;
983        assert_eq!(
984            scan.blocks
985                .first()
986                .ok_or(super::ScanError::InvalidXml)?
987                .text,
988            "&amp; < 😀"
989        );
990        Ok(())
991    }
992
993    #[test]
994    fn reports_the_utf8_input_boundary_explicitly() {
995        assert_eq!(
996            scan_wordprocessing_part(&[0xff], ScanLimits::default()),
997            Err(super::ScanError::UnsupportedEncoding)
998        );
999    }
1000
1001    #[test]
1002    fn rejects_nonempty_text_bearing_elements_outside_paragraphs() -> Result<(), super::ScanError> {
1003        let xml = format!(
1004            "<w:document xmlns:w=\"{W}\"><w:body><w:r><w:t>outside</w:t></w:r></w:body></w:document>"
1005        );
1006        assert_eq!(
1007            scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default()),
1008            Err(super::ScanError::TextOutsideParagraph)
1009        );
1010
1011        let empty =
1012            format!("<w:document xmlns:w=\"{W}\"><w:body><w:r><w:t/></w:r></w:body></w:document>");
1013        let empty_scan = scan_wordprocessing_part(empty.as_bytes(), ScanLimits::default())?;
1014        assert!(empty_scan.blocks.is_empty());
1015        Ok(())
1016    }
1017
1018    #[test]
1019    fn rejects_namespace_spoofing_and_accepts_strict_ooxml() -> Result<(), super::ScanError> {
1020        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>";
1021        let spoof_scan = scan_wordprocessing_part(spoof, ScanLimits::default())?;
1022        assert!(spoof_scan.blocks.is_empty());
1023
1024        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>";
1025        let strict_scan = scan_wordprocessing_part(strict, ScanLimits::default())?;
1026        assert_eq!(
1027            strict_scan
1028                .blocks
1029                .first()
1030                .ok_or(super::ScanError::InvalidXml)?
1031                .text,
1032            "strict"
1033        );
1034        Ok(())
1035    }
1036
1037    #[test]
1038    fn nested_namespace_rebinding_cannot_spoof_wordprocessing_elements()
1039    -> Result<(), super::ScanError> {
1040        let xml = format!(
1041            "<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>"
1042        );
1043        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())?;
1044        let texts = scan
1045            .blocks
1046            .iter()
1047            .map(|block| block.text.as_str())
1048            .collect::<Vec<_>>();
1049        assert_eq!(texts, ["before", "after"]);
1050        Ok(())
1051    }
1052
1053    #[test]
1054    fn rejects_document_type_declarations() {
1055        let doctype = format!("<!DOCTYPE w:document><w:document xmlns:w=\"{W}\"/>");
1056        assert_eq!(
1057            scan_wordprocessing_part(doctype.as_bytes(), ScanLimits::default()),
1058            Err(super::ScanError::DocumentTypeDeclaration)
1059        );
1060    }
1061
1062    #[test]
1063    fn rejects_multiple_roots_and_resource_limit_overruns() {
1064        let multiple_roots = format!("<w:p xmlns:w=\"{W}\"/><w:p xmlns:w=\"{W}\"/>");
1065        assert_eq!(
1066            scan_wordprocessing_part(multiple_roots.as_bytes(), ScanLimits::default()),
1067            Err(super::ScanError::InvalidXml)
1068        );
1069        let two_blocks =
1070            format!("<w:document xmlns:w=\"{W}\"><w:body><w:p/><w:p/></w:body></w:document>");
1071        assert_eq!(
1072            scan_wordprocessing_part(
1073                two_blocks.as_bytes(),
1074                ScanLimits {
1075                    blocks: 1,
1076                    ..ScanLimits::default()
1077                }
1078            ),
1079            Err(super::ScanError::TooManyBlocks)
1080        );
1081
1082        let empty_document = format!("<w:document xmlns:w=\"{W}\"/>");
1083        let one_event = scan_wordprocessing_part(
1084            empty_document.as_bytes(),
1085            ScanLimits {
1086                events: 1,
1087                ..ScanLimits::default()
1088            },
1089        );
1090        assert!(matches!(
1091            one_event,
1092            Ok(super::PartScan {
1093                coverage: super::PartCoverage {
1094                    work: super::ScanWork { events: 1, .. },
1095                    ..
1096                },
1097                ..
1098            })
1099        ));
1100
1101        let one_block = format!(
1102            "<w:document xmlns:w=\"{W}\"><w:body><w:p><w:r><w:t>x</w:t></w:r></w:p></w:body></w:document>"
1103        );
1104        assert_eq!(
1105            scan_wordprocessing_part(
1106                one_block.as_bytes(),
1107                ScanLimits {
1108                    events: 1,
1109                    ..ScanLimits::default()
1110                }
1111            ),
1112            Err(super::ScanError::TooManyEvents)
1113        );
1114
1115        let contextual = format!(
1116            "<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>"
1117        );
1118        assert_eq!(
1119            scan_wordprocessing_part(
1120                contextual.as_bytes(),
1121                ScanLimits {
1122                    inline_context_copies: 0,
1123                    ..ScanLimits::default()
1124                }
1125            ),
1126            Err(super::ScanError::TooManyInlineContextCopies)
1127        );
1128    }
1129
1130    fn sibling_run_fixture(run_count: usize) -> String {
1131        let mut runs = String::new();
1132        for _ in 0..run_count {
1133            runs.push_str("<w:r><w:t>x</w:t></w:r>");
1134        }
1135        format!("<w:document xmlns:w=\"{W}\"><w:body><w:p>{runs}</w:p></w:body></w:document>")
1136    }
1137
1138    fn sibling_paragraph_fixture(paragraph_count: usize) -> String {
1139        let mut paragraphs = String::new();
1140        for _ in 0..paragraph_count {
1141            paragraphs.push_str("<w:p><w:r><w:t>x</w:t></w:r></w:p>");
1142        }
1143        format!("<w:document xmlns:w=\"{W}\"><w:body>{paragraphs}</w:body></w:document>")
1144    }
1145
1146    #[test]
1147    fn scan_work_is_additive_across_independent_growth_axes() -> Result<(), super::ScanError> {
1148        let empty_runs =
1149            scan_wordprocessing_part(sibling_run_fixture(0).as_bytes(), ScanLimits::default())?;
1150        let small_runs =
1151            scan_wordprocessing_part(sibling_run_fixture(512).as_bytes(), ScanLimits::default())?;
1152        let large_runs =
1153            scan_wordprocessing_part(sibling_run_fixture(1_024).as_bytes(), ScanLimits::default())?;
1154        assert_eq!(
1155            large_runs.coverage.work.events - empty_runs.coverage.work.events,
1156            2 * (small_runs.coverage.work.events - empty_runs.coverage.work.events),
1157        );
1158        assert_eq!(small_runs.coverage.work.inline_context_copies, 0);
1159        assert_eq!(large_runs.coverage.work.inline_context_copies, 0);
1160
1161        let empty_paragraphs = scan_wordprocessing_part(
1162            sibling_paragraph_fixture(0).as_bytes(),
1163            ScanLimits::default(),
1164        )?;
1165        let small_paragraphs = scan_wordprocessing_part(
1166            sibling_paragraph_fixture(256).as_bytes(),
1167            ScanLimits::default(),
1168        )?;
1169        let large_paragraphs = scan_wordprocessing_part(
1170            sibling_paragraph_fixture(512).as_bytes(),
1171            ScanLimits::default(),
1172        )?;
1173        assert_eq!(
1174            large_paragraphs.coverage.work.events - empty_paragraphs.coverage.work.events,
1175            2 * (small_paragraphs.coverage.work.events - empty_paragraphs.coverage.work.events),
1176        );
1177        Ok(())
1178    }
1179
1180    proptest! {
1181      #[test]
1182      fn arbitrary_prefixes_and_run_splits_preserve_text_and_utf16_offsets(
1183        prefix in "[a-z]{1,8}",
1184        fragments in collection::vec("[A-Za-z0-9 ]{0,16}", 1..32),
1185      ) {
1186        let mut runs = String::new();
1187        for fragment in &fragments {
1188          write!(
1189            &mut runs,
1190            "<{prefix}:r><{prefix}:t>{fragment}</{prefix}:t></{prefix}:r>"
1191          )
1192          .map_err(|error| TestCaseError::fail(error.to_string()))?;
1193        }
1194        let xml = format!(
1195          "<{prefix}:document xmlns:{prefix}=\"{W}\"><{prefix}:body><{prefix}:p>{runs}</{prefix}:p></{prefix}:body></{prefix}:document>"
1196        );
1197        let scan = scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default())
1198          .map_err(|error| TestCaseError::fail(error.to_string()))?;
1199        let block = scan
1200          .blocks
1201          .first()
1202          .ok_or_else(|| TestCaseError::fail("missing projected paragraph"))?;
1203        prop_assert_eq!(&block.text, &fragments.concat());
1204        let mut expected_start = 0_usize;
1205        for segment in &block.segments {
1206          prop_assert_eq!(segment.start_utf16, expected_start);
1207          prop_assert!(segment.end_utf16 >= segment.start_utf16);
1208          expected_start = segment.end_utf16;
1209        }
1210        prop_assert_eq!(expected_start, block.text.encode_utf16().count());
1211      }
1212
1213      #[test]
1214      fn nonempty_wordprocessing_text_requires_a_paragraph(
1215        prefix in "[a-z]{1,8}",
1216        namespace in proptest::sample::select(&[W, W_STRICT][..]),
1217        text_element in proptest::sample::select(&["t", "delText"][..]),
1218        value in "[A-Za-z0-9 ]{1,32}",
1219        run_wrapped in proptest::bool::ANY,
1220      ) {
1221        let text = format!(
1222          "<{prefix}:{text_element}>{value}</{prefix}:{text_element}>"
1223        );
1224        let content = if run_wrapped {
1225          format!("<{prefix}:r>{text}</{prefix}:r>")
1226        } else {
1227          text
1228        };
1229        let xml = format!(
1230          "<{prefix}:document xmlns:{prefix}=\"{namespace}\"><{prefix}:body>{content}</{prefix}:body></{prefix}:document>"
1231        );
1232        prop_assert_eq!(
1233          scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default()),
1234          Err(super::ScanError::TextOutsideParagraph)
1235        );
1236      }
1237
1238      #[test]
1239      fn document_type_declarations_have_a_stable_typed_error(
1240        document_type in "[A-Za-z][A-Za-z0-9]{0,15}",
1241      ) {
1242        let xml = format!(
1243          "<!DOCTYPE {document_type}><w:document xmlns:w=\"{W}\"/>"
1244        );
1245        prop_assert_eq!(
1246          scan_wordprocessing_part(xml.as_bytes(), ScanLimits::default()),
1247          Err(super::ScanError::DocumentTypeDeclaration)
1248        );
1249      }
1250    }
1251}