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
774pub 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
842pub 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; < 😀</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 "& < 😀"
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}