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