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