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