1use std::{
4 cell::Cell,
5 io::{self, Write},
6 mem::size_of,
7};
8
9use super::{Transform, TransformError, TransformExecutionBudget, transform_resource_limit};
10use crate::{
11 c14n::C14nMode,
12 policy::OpcRelationshipEdition,
13 xml::dom::{Document, Node},
14};
15
16pub const RELATIONSHIP_TRANSFORM_URI: &str =
18 "http://schemas.openxmlformats.org/package/2006/RelationshipTransform";
19const REL: &str = "http://schemas.openxmlformats.org/package/2006/relationships";
20const PARAM: &str = "http://schemas.openxmlformats.org/package/2006/digital-signature";
21const MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
22const XML: &str = "http://www.w3.org/XML/1998/namespace";
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum RelationshipSelector {
27 SourceId(String),
29 SourceType(String),
31}
32
33impl RelationshipSelector {
34 pub(crate) fn parameter(&self) -> (&'static str, &'static str, &str) {
35 match self {
36 Self::SourceId(value) => ("RelationshipReference", "SourceId", value),
37 Self::SourceType(value) => ("RelationshipsGroupReference", "SourceType", value),
38 }
39 }
40}
41
42pub(super) fn validate_owned_selectors(
43 selectors: &[RelationshipSelector],
44 budget: &WorkspaceBudget,
45) -> Result<(), TransformError> {
46 budget.charge(
47 selectors
48 .len()
49 .checked_mul(size_of::<RelationshipSelector>())
50 .ok_or_else(|| invalid("parameter size overflow"))?,
51 )?;
52 for selector in selectors {
53 let (_, _, value) = selector.parameter();
54 budget.charge(value.len())?;
55 }
56 Ok(())
57}
58
59pub(super) struct WorkspaceBudget {
60 remaining: Cell<usize>,
61 maximum: usize,
62 resource: &'static str,
63}
64
65impl Default for WorkspaceBudget {
66 fn default() -> Self {
67 Self::new(crate::hard_limits::OPC_WORKSPACE_BYTE_CEILING)
68 }
69}
70
71impl WorkspaceBudget {
72 pub(super) fn new(maximum: usize) -> Self {
73 Self {
74 remaining: Cell::new(maximum),
75 maximum,
76 resource: crate::policy::resource_name::OPC_WORKSPACE_BYTES,
77 }
78 }
79
80 pub(super) fn parameters(maximum: usize) -> Self {
81 Self {
82 resource: crate::policy::resource_name::OPC_PARAMETER_BYTES,
83 ..Self::new(maximum)
84 }
85 }
86
87 fn charge(&self, bytes: usize) -> Result<(), TransformError> {
88 let remaining = self.remaining.get();
89 if bytes > remaining {
90 self.remaining.set(0);
91 return Err(transform_resource_limit(
92 self.resource,
93 self.maximum,
94 (self.maximum - remaining).saturating_add(bytes),
95 ));
96 }
97 self.remaining.set(remaining - bytes);
98 Ok(())
99 }
100
101 fn push<T>(&self, vector: &mut Vec<T>, value: T) -> Result<(), TransformError> {
102 if vector.len() == vector.capacity() {
103 let capacity = vector
104 .capacity()
105 .max(4)
106 .checked_mul(2)
107 .ok_or_else(|| invalid("workspace overflow"))?;
108 self.charge(
111 capacity
112 .checked_mul(size_of::<T>())
113 .ok_or_else(|| invalid("workspace overflow"))?,
114 )?;
115 vector
116 .try_reserve_exact(capacity - vector.len())
117 .map_err(|_| invalid("workspace allocation failed"))?;
118 }
119 vector.push(value);
120 Ok(())
121 }
122}
123
124fn invalid(message: &str) -> TransformError {
125 TransformError::Relationship(message.to_owned())
126}
127
128pub(crate) fn validate_chain(chain: &[Transform]) -> Result<(), TransformError> {
134 let mut seen = false;
135 for (index, transform) in chain.iter().enumerate() {
136 if let Transform::Relationship(selectors) = transform {
137 if seen
141 || selectors.is_empty()
142 || !matches!(chain.get(index + 1), Some(Transform::C14n(_)))
143 {
144 return Err(invalid(
145 "each chain permits one relationship transform with selectors, immediately followed by canonicalization",
146 ));
147 }
148 seen = true;
149 }
150 }
151 Ok(())
152}
153
154pub(crate) fn validate_edition(
155 chain: &[Transform],
156 edition: OpcRelationshipEdition,
157) -> Result<(), TransformError> {
158 validate_chain(chain)?;
159 for pair in chain.windows(2) {
164 if let [Transform::Relationship(_), Transform::C14n(algorithm)] = pair
165 && algorithm.mode() != C14nMode::Inclusive1_0
166 {
167 return Err(invalid(&format!(
168 "OPC {edition:?} requires C14N 1.0 after relationship normalization"
169 )));
170 }
171 }
172 Ok(())
173}
174
175pub(super) fn parse_selectors(
176 node: Node<'_, '_>,
177 budget: &WorkspaceBudget,
178) -> Result<Vec<RelationshipSelector>, TransformError> {
179 let mut selectors = Vec::new();
180 for child in node.children() {
181 if child.is_comment()
182 || child.is_pi()
183 || (child.is_text() && child.text().is_some_and(super::is_xml_whitespace_only))
184 {
185 continue;
186 }
187 let tag = child.tag_name();
188 let attribute = match (tag.namespace(), tag.name()) {
189 (Some(PARAM), "RelationshipReference") => "SourceId",
190 (Some(PARAM), "RelationshipsGroupReference") => "SourceType",
191 _ => return Err(invalid("unexpected relationship selector")),
192 };
193 if child.attributes().len() != 1 || child.children().any(|n| n.is_element()) {
197 return Err(invalid(
198 "relationship selector requires exactly its named attribute and no child elements",
199 ));
200 }
201 let value = child
202 .attribute(attribute)
203 .ok_or_else(|| invalid("missing relationship selector attribute"))?;
204 budget.charge(value.len())?;
205 let selector = if attribute == "SourceId" {
206 RelationshipSelector::SourceId(value.to_owned())
207 } else {
208 RelationshipSelector::SourceType(value.to_owned())
209 };
210 budget.push(&mut selectors, selector)?;
211 }
212 if selectors.is_empty() {
213 return Err(invalid("relationship transform requires selectors"));
214 }
215 Ok(selectors)
216}
217
218#[derive(Clone, Copy, PartialEq, Eq)]
219enum Parent {
220 Document,
221 Relationships,
222 Relationship(usize),
223}
224
225struct Frame<'a> {
226 next: Option<Node<'a, 'a>>,
227 parent: Parent,
228}
229
230struct Relationship<'a> {
231 id: &'a str,
232 kind: &'a str,
233 target: &'a str,
234 mode: &'a str,
235 instructions: Vec<Node<'a, 'a>>,
236 selected: bool,
237}
238
239enum Event<'a> {
240 Instruction(Node<'a, 'a>),
241 Relationship,
242}
243
244pub(super) fn normalize<'a>(
247 document: &'a Document<'a>,
248 selectors: &[RelationshipSelector],
249 edition: OpcRelationshipEdition,
250 budget: &TransformExecutionBudget,
251) -> Result<Vec<u8>, TransformError> {
252 let mut frames = Vec::new();
253 let mut records: Vec<Relationship<'a>> = Vec::new();
254 let mut root_seen = false;
255 let mut padded_ids = false;
256 let mut before = Vec::new();
257 let mut after = Vec::new();
258 let mut events = Vec::new();
259 budget.opc_workspace.push(
260 &mut frames,
261 Frame {
262 next: document.root().first_child(),
263 parent: Parent::Document,
264 },
265 )?;
266 while let Some(frame) = frames.last_mut() {
267 let Some(node) = frame.next else {
268 frames.pop();
269 continue;
270 };
271 frame.next = node.next_sibling();
272 let parent = frame.parent;
273 budget.node_filter.charge(1)?;
274 if node.is_comment() {
275 continue;
276 }
277 if node.is_text() {
278 if matches!(parent, Parent::Relationship(_)) {
283 continue;
284 }
285 if !node.text().is_some_and(super::is_xml_whitespace_only) {
286 return Err(invalid("Relationships part has character data"));
287 }
288 continue;
289 }
290 if node.is_pi() {
291 match parent {
292 Parent::Document if !root_seen => budget.opc_workspace.push(&mut before, node)?,
293 Parent::Document => budget.opc_workspace.push(&mut after, node)?,
294 Parent::Relationships => budget
295 .opc_workspace
296 .push(&mut events, Event::Instruction(node))?,
297 Parent::Relationship(_) if edition == OpcRelationshipEdition::Ecma2012 => {}
298 Parent::Relationship(index) => budget
299 .opc_workspace
300 .push(&mut records[index].instructions, node)?,
301 }
302 continue;
303 }
304 let tag = node.tag_name();
305 validate_mce_entry(node, edition, budget)?;
306 if tag.namespace() != Some(REL)
307 && tag.namespace() != Some(MC)
308 && ignorable(node, tag.namespace(), budget)?
309 {
310 if process_content(node, edition, budget)? {
311 check_unwrapped(node)?;
312 if edition == OpcRelationshipEdition::Ecma2021 {
313 must_understand(node, budget)?;
314 }
315 budget.opc_workspace.push(
316 &mut frames,
317 Frame {
318 next: node.first_child(),
319 parent,
320 },
321 )?;
322 }
323 continue;
324 }
325 if edition == OpcRelationshipEdition::Ecma2021 {
326 must_understand(node, budget)?;
327 }
328 if tag.namespace() == Some(MC) {
329 if tag.name() != "AlternateContent" {
330 return Err(invalid("MCE Choice/Fallback outside AlternateContent"));
331 }
332 check_unwrapped(node)?;
333 if let Some(branch) = alternate_content(node, edition, budget)? {
334 if edition == OpcRelationshipEdition::Ecma2021 {
335 must_understand(branch, budget)?;
336 }
337 budget.opc_workspace.push(
338 &mut frames,
339 Frame {
340 next: branch.first_child(),
341 parent,
342 },
343 )?;
344 }
345 continue;
346 }
347 let next_parent = match (parent, tag.namespace(), tag.name()) {
348 (Parent::Document, Some(REL), "Relationships") if !root_seen => {
349 root_seen = true;
350 validate_attributes(node, &[], budget)?;
351 Parent::Relationships
352 }
353 (Parent::Relationships, Some(REL), "Relationship") => {
354 validate_attributes(node, &["Id", "Type", "Target", "TargetMode"], budget)?;
355 let id = node
356 .attribute("Id")
357 .ok_or_else(|| invalid("missing relationship Id"))?;
358 budget.node_filter.charge(id.len())?;
359 if !ncname(id.trim_matches(xml_whitespace)) {
364 return Err(invalid("relationship Id must be an xsd:ID"));
365 }
366 padded_ids |= id != id.trim_matches(xml_whitespace);
367 let kind = node
368 .attribute("Type")
369 .ok_or_else(|| invalid("missing relationship Type"))?;
370 budget.node_filter.charge(kind.len())?;
371 validate_relationship_type_iri(kind)?;
372 let target = node
373 .attribute("Target")
374 .ok_or_else(|| invalid("missing relationship Target"))?;
375 let mode = node.attribute("TargetMode").unwrap_or("Internal");
376 if mode != "Internal" && mode != "External" {
377 return Err(invalid("invalid relationship TargetMode"));
378 }
379 budget.node_filter.charge(target.len())?;
380 if mode == "Internal" {
381 validate_internal_target_reference_syntax(target)?;
382 } else {
383 validate_external_target_reference_syntax(target, edition)?;
384 }
385 let index = records.len();
386 budget.opc_workspace.push(
387 &mut records,
388 Relationship {
389 id,
390 kind,
391 target,
392 mode,
393 instructions: Vec::new(),
394 selected: false,
395 },
396 )?;
397 budget
398 .opc_workspace
399 .push(&mut events, Event::Relationship)?;
400 Parent::Relationship(index)
401 }
402 _ => return Err(invalid("invalid Relationships part element")),
403 };
404 budget.opc_workspace.push(
405 &mut frames,
406 Frame {
407 next: node.first_child(),
408 parent: next_parent,
409 },
410 )?;
411 }
412 if !root_seen {
413 return Err(invalid("missing Relationships root"));
414 }
415 let max_id = records
417 .iter()
418 .map(|record| record.id.len())
419 .max()
420 .unwrap_or(0);
421 let work = records
422 .len()
423 .checked_mul(4 * (records.len().max(1).ilog2() as usize + 1) * max_id.max(1))
424 .ok_or_else(|| invalid("sort work overflow"))?;
425 budget.node_filter.charge(work)?;
426 if padded_ids {
427 records.sort_unstable_by(|left, right| {
428 left.id
429 .trim_matches(xml_whitespace)
430 .cmp(right.id.trim_matches(xml_whitespace))
431 });
432 } else {
433 records.sort_unstable_by(|left, right| left.id.cmp(right.id));
434 }
435 if records.windows(2).any(|pair| {
436 pair[0].id.trim_matches(xml_whitespace) == pair[1].id.trim_matches(xml_whitespace)
437 }) {
438 return Err(invalid("duplicate relationship Id"));
439 }
440 if padded_ids {
441 budget.node_filter.charge(work)?;
442 records.sort_unstable_by(|left, right| left.id.cmp(right.id));
443 }
444 let mut first_selected = None;
445 let mut last_selected = 0;
446 for (index, record) in records.iter_mut().enumerate() {
447 for selector in selectors {
448 let (_, _, expected) = selector.parameter();
449 let actual = match selector {
450 RelationshipSelector::SourceId(_) => record.id,
451 RelationshipSelector::SourceType(_) => record.kind,
452 };
453 budget
454 .node_filter
455 .charge(expected.len().max(actual.len()))?;
456 let matches = match edition {
459 OpcRelationshipEdition::Ecma2012 => actual == expected,
460 OpcRelationshipEdition::Ecma2021 => actual.eq_ignore_ascii_case(expected),
461 };
462 if matches {
463 record.selected = true;
464 break;
465 }
466 }
467 if record.selected {
468 first_selected.get_or_insert(index);
469 last_selected = index;
470 }
471 }
472 let mut output = Output {
473 bytes: Vec::new(),
474 budget,
475 };
476 for node in before {
477 write_instruction(node, &mut output)?;
478 }
479 output
480 .write_all(b"<Relationships xmlns=\"")
481 .map_err(output_error)?;
482 output.write_all(REL.as_bytes()).map_err(output_error)?;
483 output.write_all(b"\">").map_err(output_error)?;
484 let mut records = records.into_iter();
485 let mut slots_seen = 0;
486 for event in events {
487 let record = match event {
488 Event::Instruction(node) => {
489 if edition == OpcRelationshipEdition::Ecma2021
493 || first_selected
494 .is_some_and(|first| slots_seen > first && slots_seen <= last_selected)
495 {
496 write_instruction(node, &mut output)?;
497 }
498 continue;
499 }
500 Event::Relationship => records
501 .next()
502 .ok_or_else(|| invalid("relationship slot mismatch"))?,
503 };
504 slots_seen += 1;
505 if !record.selected {
506 continue;
507 }
508 output.write_all(b"<Relationship").map_err(output_error)?;
509 for (name, value) in [
510 ("Id", record.id),
511 ("Target", record.target),
512 ("TargetMode", record.mode),
513 ("Type", record.kind),
514 ] {
515 write!(output, " {name}=\"").map_err(output_error)?;
516 crate::c14n::escape_attr(value, &mut output).map_err(output_error)?;
517 output.write_all(b"\"").map_err(output_error)?;
518 }
519 output.write_all(b">").map_err(output_error)?;
520 if edition == OpcRelationshipEdition::Ecma2021 {
521 for instruction in record.instructions {
522 write_instruction(instruction, &mut output)?;
523 }
524 }
525 output.write_all(b"</Relationship>").map_err(output_error)?;
526 }
527 output
528 .write_all(b"</Relationships>")
529 .map_err(output_error)?;
530 for node in after {
531 write_instruction(node, &mut output)?;
532 }
533 Ok(output.bytes)
534}
535
536fn write_instruction(node: Node<'_, '_>, output: &mut Output<'_>) -> Result<(), TransformError> {
537 let instruction = node
538 .pi()
539 .ok_or_else(|| invalid("expected processing instruction"))?;
540 write!(output, "<?{}", instruction.target).map_err(output_error)?;
541 if let Some(value) = instruction.value {
542 write!(output, " {value}").map_err(output_error)?;
543 }
544 output.write_all(b"?>").map_err(output_error)
545}
546
547fn output_error(error: io::Error) -> TransformError {
548 if let Some(violation) = error
549 .get_ref()
550 .and_then(|inner| inner.downcast_ref::<crate::policy::PolicyViolation>())
551 {
552 return TransformError::Policy(violation.clone());
553 }
554 if let Some(TransformError::Policy(violation)) = error
555 .get_ref()
556 .and_then(|inner| inner.downcast_ref::<TransformError>())
557 {
558 return TransformError::Policy(violation.clone());
559 }
560 invalid("relationship output allocation failed")
561}
562
563struct Output<'a> {
564 bytes: Vec<u8>,
565 budget: &'a TransformExecutionBudget,
566}
567
568impl Write for Output<'_> {
569 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
570 self.budget
571 .charge_c14n_output_policy(bytes.len())
572 .map_err(io::Error::other)?;
573 if bytes.len() > self.bytes.capacity() - self.bytes.len() {
574 let capacity = self
575 .bytes
576 .len()
577 .checked_add(bytes.len())
578 .and_then(|n| n.checked_next_power_of_two())
579 .ok_or_else(|| io::Error::other("output overflow"))?;
580 self.budget
581 .opc_workspace
582 .charge(capacity)
583 .map_err(io::Error::other)?;
584 self.bytes
585 .try_reserve_exact(capacity - self.bytes.len())
586 .map_err(io::Error::other)?;
587 }
588 self.bytes.extend_from_slice(bytes);
589 Ok(bytes.len())
590 }
591 fn flush(&mut self) -> io::Result<()> {
592 Ok(())
593 }
594}
595
596fn tokens(value: &str) -> impl Iterator<Item = &str> {
597 value
598 .split([' ', '\t', '\r', '\n'])
599 .filter(|token| !token.is_empty())
600}
601
602fn namespace<'a>(
603 node: Node<'a, 'a>,
604 prefix: &str,
605 budget: &TransformExecutionBudget,
606) -> Result<&'a str, TransformError> {
607 budget
608 .node_filter
609 .charge(prefix.len().saturating_add(node.namespaces().len()))?;
610 if !ncname(prefix) {
611 return Err(invalid("invalid MCE prefix"));
612 }
613 match node.lookup_namespace_uri(Some(prefix)) {
614 Some(uri) if !uri.is_empty() && uri != MC => Ok(uri),
615 _ => Err(invalid("MCE prefix must bind a non-MCE namespace")),
616 }
617}
618
619fn ignorable(
620 node: Node<'_, '_>,
621 uri: Option<&str>,
622 budget: &TransformExecutionBudget,
623) -> Result<bool, TransformError> {
624 let Some(uri) = uri else {
625 return Ok(false);
626 };
627 for ancestor in node.ancestors() {
628 budget.node_filter.charge(1)?;
629 if declares_ignorable(ancestor, uri, budget)? {
630 return Ok(true);
631 }
632 }
633 Ok(false)
634}
635
636fn declares_ignorable(
637 node: Node<'_, '_>,
638 uri: &str,
639 budget: &TransformExecutionBudget,
640) -> Result<bool, TransformError> {
641 if let Some(value) = node.attribute((MC, "Ignorable")) {
642 budget.node_filter.charge(value.len())?;
643 for prefix in tokens(value) {
644 budget.node_filter.charge(uri.len())?;
645 if namespace(node, prefix, budget)? == uri {
646 return Ok(true);
647 }
648 }
649 }
650 Ok(false)
651}
652
653fn process_content(
654 node: Node<'_, '_>,
655 edition: OpcRelationshipEdition,
656 budget: &TransformExecutionBudget,
657) -> Result<bool, TransformError> {
658 let tag = node.tag_name();
659 for ancestor in node.ancestors() {
660 budget.node_filter.charge(1)?;
661 if let Some(value) = ancestor.attribute((MC, "ProcessContent")) {
662 budget.node_filter.charge(value.len())?;
663 for token in tokens(value) {
664 let (prefix, local) = token
665 .split_once(':')
666 .ok_or_else(|| invalid("MCE name requires a prefix"))?;
667 if tag.namespace() == Some(namespace(ancestor, prefix, budget)?)
668 && (local == "*" || local == tag.name())
669 {
670 return Ok(true);
671 }
672 }
673 }
674 if edition == OpcRelationshipEdition::Ecma2012
679 && let Some(uri) = tag.namespace()
680 && declares_ignorable(ancestor, uri, budget)?
681 {
682 return Ok(false);
683 }
684 }
685 Ok(false)
686}
687
688fn validate_mce_entry(
689 node: Node<'_, '_>,
690 edition: OpcRelationshipEdition,
691 budget: &TransformExecutionBudget,
692) -> Result<(), TransformError> {
693 validate_mc_attributes(node, edition, budget)?;
699 if edition == OpcRelationshipEdition::Ecma2012 {
700 must_understand(node, budget)?;
701 }
702 Ok(())
703}
704
705fn validate_mc_attributes(
706 node: Node<'_, '_>,
707 edition: OpcRelationshipEdition,
708 budget: &TransformExecutionBudget,
709) -> Result<(), TransformError> {
710 for attribute in node.attributes() {
714 budget
715 .node_filter
716 .charge(attribute.value().len().saturating_add(1))?;
717 if attribute.namespace() != Some(MC) {
718 continue;
719 }
720 match attribute.name() {
721 "Ignorable" | "MustUnderstand" => {
722 for prefix in tokens(attribute.value()) {
723 namespace(node, prefix, budget)?;
724 }
725 }
726 "ProcessContent" => {
727 for token in tokens(attribute.value()) {
731 let (prefix, local) = token
732 .split_once(':')
733 .ok_or_else(|| invalid("MCE name requires a prefix"))?;
734 let uri = namespace(node, prefix, budget)?;
735 let declared = match edition {
740 OpcRelationshipEdition::Ecma2012 => declares_ignorable(node, uri, budget)?,
741 OpcRelationshipEdition::Ecma2021 => ignorable(node, Some(uri), budget)?,
742 };
743 if (local != "*" && !ncname(local)) || !declared {
744 return Err(invalid(
745 "MCE qualified name must belong to an ignorable namespace",
746 ));
747 }
748 }
749 }
750 _ => return Err(invalid("unknown MCE attribute")),
751 }
752 }
753 Ok(())
754}
755
756fn must_understand(
757 node: Node<'_, '_>,
758 budget: &TransformExecutionBudget,
759) -> Result<(), TransformError> {
760 if let Some(value) = node.attribute((MC, "MustUnderstand")) {
761 budget.node_filter.charge(value.len())?;
762 for prefix in tokens(value) {
763 if namespace(node, prefix, budget)? != REL {
764 return Err(invalid("unsupported MCE MustUnderstand namespace"));
765 }
766 }
767 }
768 Ok(())
769}
770
771fn check_unwrapped(node: Node<'_, '_>) -> Result<(), TransformError> {
772 if node.attributes().any(|a| {
773 a.namespace() == Some(XML)
774 && (node.tag_name().namespace() == Some(MC)
775 || matches!(a.name(), "base" | "lang" | "space"))
776 }) {
777 return Err(invalid("unwrapped MCE element has forbidden xml attribute"));
778 }
779 Ok(())
780}
781
782fn validate_attributes(
783 node: Node<'_, '_>,
784 allowed: &[&str],
785 budget: &TransformExecutionBudget,
786) -> Result<(), TransformError> {
787 for attribute in node.attributes() {
788 budget.node_filter.charge(1)?;
789 match attribute.namespace() {
790 Some(MC) => {}
791 Some(uri) if uri != REL && ignorable(node, Some(uri), budget)? => {}
792 None if allowed.contains(&attribute.name()) => {}
793 _ => return Err(invalid("unexpected Relationships part attribute")),
794 }
795 }
796 Ok(())
797}
798
799fn alternate_content<'a>(
800 node: Node<'a, 'a>,
801 edition: OpcRelationshipEdition,
802 budget: &TransformExecutionBudget,
803) -> Result<Option<Node<'a, 'a>>, TransformError> {
804 validate_attributes(node, &[], budget)?;
805 let mut chosen = None;
806 let mut fallback = None;
807 let mut choices = 0;
808 for child in node.children() {
809 budget.node_filter.charge(1)?;
810 if child.is_comment()
811 || child.is_pi()
812 || (child.is_text() && child.text().is_some_and(super::is_xml_whitespace_only))
813 {
814 continue;
815 }
816 let tag = child.tag_name();
817 validate_mce_entry(child, edition, budget)?;
818 if tag.namespace() != Some(MC)
822 && ignorable(child, tag.namespace(), budget)?
823 && !process_content(child, edition, budget)?
824 {
825 continue;
826 }
827 check_unwrapped(child)?;
828 match (tag.namespace(), tag.name()) {
829 (Some(MC), "Choice") if fallback.is_none() => {
830 choices += 1;
831 validate_attributes(child, &["Requires"], budget)?;
832 let requires = child
833 .attribute("Requires")
834 .ok_or_else(|| invalid("MCE Choice requires Requires"))?;
835 budget.node_filter.charge(requires.len())?;
836 let mut all_known = true;
837 let mut any = false;
838 for prefix in tokens(requires) {
839 any = true;
840 all_known &= namespace(child, prefix, budget)? == REL;
841 }
842 if !any {
843 return Err(invalid("MCE Requires must not be empty"));
844 }
845 if chosen.is_none() && all_known {
846 chosen = Some(child);
847 }
848 }
849 (Some(MC), "Fallback") if fallback.is_none() && choices != 0 => {
850 validate_attributes(child, &[], budget)?;
851 fallback = Some(child);
852 }
853 _ => return Err(invalid("invalid MCE AlternateContent child order")),
854 }
855 }
856 if choices == 0 {
857 return Err(invalid("MCE AlternateContent requires a Choice"));
858 }
859 Ok(chosen.or(fallback))
860}
861
862fn validate_relationship_type_iri(kind: &str) -> Result<(), TransformError> {
863 let value = kind.trim_matches(xml_whitespace);
871 if let Some(remainder) = iri_scheme_remainder(value)
872 && !remainder.contains('#')
873 && valid_iri_reference(remainder, false)
874 {
875 return Ok(());
876 }
877 Err(invalid("relationship Type must be an absolute IRI"))
878}
879
880fn iri_scheme_remainder(value: &str) -> Option<&str> {
881 let (scheme, remainder) = value.split_once(':')?;
882 if !scheme
883 .as_bytes()
884 .first()
885 .is_some_and(u8::is_ascii_alphabetic)
886 {
887 return None;
888 }
889 for byte in scheme.bytes() {
890 if !byte.is_ascii_alphanumeric() && !matches!(byte, b'+' | b'-' | b'.') {
891 return None;
892 }
893 }
894 Some(remainder)
895}
896
897fn validate_external_target_reference_syntax(
898 target: &str,
899 edition: OpcRelationshipEdition,
900) -> Result<(), TransformError> {
901 let value = target.trim_matches(xml_whitespace);
908 if let Some(remainder) = iri_scheme_remainder(value) {
909 if edition == OpcRelationshipEdition::Ecma2021 && remainder.contains('#') {
910 return Err(invalid(
911 "External relationship Target must be a relative reference or absolute IRI",
912 ));
913 }
914 if valid_iri_reference(remainder, false) {
915 return Ok(());
916 }
917 } else if valid_iri_reference(value, true) {
918 return Ok(());
919 }
920 Err(invalid("invalid External relationship Target reference"))
921}
922
923fn validate_internal_target_reference_syntax(target: &str) -> Result<(), TransformError> {
924 if !valid_iri_reference(target.trim_matches(xml_whitespace), true) {
932 return Err(invalid(
933 "Internal relationship Target must be a relative reference",
934 ));
935 }
936 Ok(())
937}
938
939fn valid_iri_reference(value: &str, relative: bool) -> bool {
944 let (value, fragment) = value
945 .split_once('#')
946 .map_or((value, None), |(v, f)| (v, Some(f)));
947 if let Some(fragment) = fragment
948 && !iri_component(fragment, |c| iri_pchar(c) || matches!(c, '/' | '?'))
949 {
950 return false;
951 }
952 let (path, query) = value
953 .split_once('?')
954 .map_or((value, None), |(v, q)| (v, Some(q)));
955 if let Some(query) = query
956 && !iri_component(query, |c| {
957 iri_pchar(c)
958 || matches!(c, '/' | '?')
959 || matches!(c as u32, 0xe000..=0xf8ff | 0xf0000..=0xffffd | 0x100000..=0x10fffd)
960 })
961 {
962 return false;
963 }
964 if let Some(authority_path) = path.strip_prefix("//") {
965 let (authority, path) = authority_path
966 .split_once('/')
967 .map_or((authority_path, ""), |(a, p)| (a, p));
968 return valid_iri_authority(authority) && iri_component(path, |c| iri_pchar(c) || c == '/');
969 }
970 if relative
971 && !path.starts_with('/')
972 && path
973 .split('/')
974 .next()
975 .is_some_and(|segment| segment.contains(':'))
976 {
977 return false;
978 }
979 iri_component(path, |c| iri_pchar(c) || c == '/')
980}
981
982fn valid_iri_authority(authority: &str) -> bool {
983 let host_port = if let Some((userinfo, host_port)) = authority.split_once('@') {
984 if !iri_component(userinfo, |c| {
985 iri_unreserved(c) || iri_sub_delim(c) || c == ':'
986 }) {
987 return false;
988 }
989 host_port
990 } else {
991 authority
992 };
993 if let Some(literal) = host_port.strip_prefix('[') {
994 let Some((address, suffix)) = literal.split_once(']') else {
995 return false;
996 };
997 if !suffix.is_empty()
998 && !suffix
999 .strip_prefix(':')
1000 .is_some_and(|port| port.bytes().all(|b| b.is_ascii_digit()))
1001 {
1002 return false;
1003 }
1004 if address.parse::<std::net::Ipv6Addr>().is_ok() {
1005 return true;
1006 }
1007 if let Some(future) = address.strip_prefix(['v', 'V'])
1008 && let Some((version, address)) = future.split_once('.')
1009 {
1010 return !version.is_empty()
1011 && version.bytes().all(|b| b.is_ascii_hexdigit())
1012 && !address.is_empty()
1013 && address
1014 .chars()
1015 .all(|c| c.is_ascii() && (iri_unreserved(c) || iri_sub_delim(c) || c == ':'));
1016 }
1017 return false;
1018 }
1019 let host = if let Some((host, port)) = host_port.split_once(':') {
1020 if !port.bytes().all(|b| b.is_ascii_digit()) {
1021 return false;
1022 }
1023 host
1024 } else {
1025 host_port
1026 };
1027 iri_component(host, |c| iri_unreserved(c) || iri_sub_delim(c))
1028}
1029
1030fn iri_component(value: &str, allowed: impl Fn(char) -> bool) -> bool {
1031 let mut chars = value.chars();
1032 while let Some(c) = chars.next() {
1033 if c == '%' {
1034 if !chars.next().is_some_and(|c| c.is_ascii_hexdigit())
1035 || !chars.next().is_some_and(|c| c.is_ascii_hexdigit())
1036 {
1037 return false;
1038 }
1039 } else if !allowed(c) {
1040 return false;
1041 }
1042 }
1043 true
1044}
1045
1046fn iri_unreserved(c: char) -> bool {
1047 let code = c as u32;
1048 c.is_ascii_alphanumeric()
1049 || matches!(c, '-' | '.' | '_' | '~')
1050 || matches!(code, 0xa0..=0xd7ff | 0xf900..=0xfdcf | 0xfdf0..=0xffef)
1051 || ((0x10000..=0xdfffd).contains(&code) && code & 0xffff <= 0xfffd)
1052 || (0xe1000..=0xefffd).contains(&code)
1053}
1054
1055fn iri_sub_delim(c: char) -> bool {
1056 matches!(
1057 c,
1058 '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
1059 )
1060}
1061
1062fn iri_pchar(c: char) -> bool {
1063 iri_unreserved(c) || iri_sub_delim(c) || matches!(c, ':' | '@')
1064}
1065
1066fn ncname(value: &str) -> bool {
1067 !value.contains(':') && crate::xml_input::lexical::is_qname(value)
1068}
1069
1070fn xml_whitespace(value: char) -> bool {
1071 matches!(value, ' ' | '\t' | '\r' | '\n')
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076 use super::*;
1077
1078 #[test]
1079 fn complete_relationship_iri_syntax() {
1080 for value in [
1082 "urn:%ZZ",
1083 "urn:a b",
1084 "urn:x#fragment",
1085 "http://[broken]/x",
1086 "http://host:port/x",
1087 "urn:\u{e000}",
1088 "urn:\u{fdd0}",
1089 "urn:\u{1fffe}",
1090 "http://user@@host/x",
1091 "http://[::1%25zone]/x",
1092 "http://[v1.]/x",
1093 "http://[vG.a]/x",
1094 "http://[::1]suffix/x",
1095 ] {
1096 assert!(validate_relationship_type_iri(value).is_err(), "{value}");
1097 }
1098 for value in [
1099 "urn:%20",
1100 "https://例え.example/役割",
1101 "http://[::1]:80/x",
1102 "urn:x?\u{e000}",
1103 "http://user:pass@host:/x",
1104 "http://[v1.a:b]/x",
1105 "urn:\u{10000}\u{e1000}",
1106 ] {
1107 assert!(validate_relationship_type_iri(value).is_ok(), "{value}");
1108 }
1109 }
1110
1111 #[test]
1112 fn complete_internal_target_syntax() {
1113 for value in [
1115 "part%GG.xml",
1116 "a%",
1117 "a%2",
1118 "a b",
1119 "//[broken]/x",
1120 "//host:port/x",
1121 "a\\b",
1122 "//user@@host/path",
1123 "a#b#c",
1124 "a#\u{e000}",
1125 ] {
1126 assert!(
1127 validate_internal_target_reference_syntax(value).is_err(),
1128 "{value}"
1129 );
1130 }
1131 for value in [
1132 "part%20.xml",
1133 "//host/path",
1134 "//[::1]/x",
1135 "a?b#c",
1136 "../役割.xml",
1137 "//user:pass@host:80/path",
1138 "//[V1.a:b]/path",
1139 "/a:b",
1140 "a?\u{f0000}",
1141 ] {
1142 assert!(
1143 validate_internal_target_reference_syntax(value).is_ok(),
1144 "{value}"
1145 );
1146 }
1147 }
1148
1149 #[test]
1150 fn mce_preservation_attributes_follow_selected_edition() {
1151 for attribute in ["PreserveElements", "PreserveAttributes"] {
1153 let xml = format!(
1154 "<Relationships xmlns=\"{REL}\" xmlns:mc=\"{MC}\" xmlns:e=\"urn:extension\" mc:Ignorable=\"e\" mc:{attribute}=\"e:*\"><Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"/></Relationships>"
1155 );
1156 let document = Document::parse(&xml).expect("well-formed MCE document");
1157 for edition in [
1158 OpcRelationshipEdition::Ecma2012,
1159 OpcRelationshipEdition::Ecma2021,
1160 ] {
1161 let result = normalize(
1162 &document,
1163 &[RelationshipSelector::SourceId("x".into())],
1164 edition,
1165 &TransformExecutionBudget::default(),
1166 );
1167 assert!(result.is_err(), "{edition:?}: {attribute}");
1168 }
1169 }
1170 }
1171
1172 #[test]
1173 fn relationship_type_requires_an_absolute_iri_scheme_before_selection() {
1174 for kind in [
1177 "",
1178 "relative/type",
1179 "//example.com/type",
1180 ":type",
1181 "1urn:type",
1182 "x_y:type",
1183 "x/y:type",
1184 "?x:type",
1185 "#x:type",
1186 "urn:%ZZ",
1187 "http://[broken]/type",
1188 "urn:a b",
1189 ] {
1190 let xml = format!(
1191 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Type=\"{kind}\" Target=\"a\"/></Relationships>"
1192 );
1193 let document = Document::parse(&xml).expect("well-formed relationship XML");
1194 for edition in [
1195 OpcRelationshipEdition::Ecma2012,
1196 OpcRelationshipEdition::Ecma2021,
1197 ] {
1198 for id in ["x", "absent"] {
1199 assert!(
1200 normalize(
1201 &document,
1202 &[RelationshipSelector::SourceId(id.into())],
1203 edition,
1204 &TransformExecutionBudget::default()
1205 )
1206 .is_err(),
1207 "{edition:?}, {id}, {kind}"
1208 );
1209 }
1210 }
1211 }
1212 for kind in [
1213 "urn:t",
1214 "https://example.com/type",
1215 "https://例え.example/役割",
1216 "urn:роль",
1217 "x+y.z-1:type",
1218 " urn:t ",
1219 ] {
1220 let xml = format!(
1221 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Type=\"{kind}\" Target=\"a\"/></Relationships>"
1222 );
1223 let document = Document::parse(&xml).expect("well-formed relationship XML");
1224 for edition in [
1225 OpcRelationshipEdition::Ecma2012,
1226 OpcRelationshipEdition::Ecma2021,
1227 ] {
1228 let output = normalize(
1229 &document,
1230 &[RelationshipSelector::SourceId("x".into())],
1231 edition,
1232 &TransformExecutionBudget::default(),
1233 )
1234 .expect("absolute IRI is accepted");
1235 let expected = format!(
1236 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Target=\"a\" TargetMode=\"Internal\" Type=\"{kind}\"></Relationship></Relationships>"
1237 );
1238 assert_eq!(output, expected.as_bytes());
1239 }
1240 }
1241 }
1242
1243 #[test]
1244 fn selector_simple_content_does_not_change_selection() {
1245 for (name, attribute, value) in [
1248 ("RelationshipReference", "SourceId", "x"),
1249 ("RelationshipsGroupReference", "SourceType", "urn:t"),
1250 ] {
1251 for content in ["", "metadata & <![CDATA[value]]>", "<child/>"] {
1252 let parameters = format!(
1253 "<Transform><{name} xmlns=\"{PARAM}\" {attribute}=\"{value}\">{content}</{name}></Transform>"
1254 );
1255 let parameters = Document::parse(¶meters).expect("well-formed selector XML");
1256 let parsed =
1257 parse_selectors(parameters.root_element(), &WorkspaceBudget::default());
1258 if content == "<child/>" {
1259 assert!(parsed.is_err());
1260 continue;
1261 }
1262 let parsed = parsed.expect("selector simple content is valid");
1263 let xml = format!(
1264 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"/></Relationships>"
1265 );
1266 let document = Document::parse(&xml).expect("well-formed relationship XML");
1267 for edition in [
1268 OpcRelationshipEdition::Ecma2012,
1269 OpcRelationshipEdition::Ecma2021,
1270 ] {
1271 let output = normalize(
1272 &document,
1273 &parsed,
1274 edition,
1275 &TransformExecutionBudget::default(),
1276 )
1277 .expect("selector simple content is ignored");
1278 let expected = format!(
1279 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Target=\"a\" TargetMode=\"Internal\" Type=\"urn:t\"></Relationship></Relationships>"
1280 );
1281 assert_eq!(output, expected.as_bytes());
1282 }
1283 }
1284 }
1285 }
1286
1287 #[test]
1288 fn alternate_content_distinguishes_ignored_and_unwrapped_extensions() {
1289 for declaration in [
1292 "",
1293 "mc:ProcessContent=\"u:extension\"",
1294 "mc:ProcessContent=\"u:*\"",
1295 ] {
1296 for local in [false, true] {
1297 let (root_attributes, child_attributes) = if local {
1298 ("", declaration)
1299 } else {
1300 (declaration, "")
1301 };
1302 let xml = format!(
1303 "<Relationships xmlns=\"{REL}\" xmlns:mc=\"{MC}\" xmlns:r=\"{REL}\" xmlns:u=\"urn:unknown\" mc:Ignorable=\"u\" {root_attributes}><mc:AlternateContent><u:extension {child_attributes}/><mc:Choice Requires=\"r\"><Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"/></mc:Choice></mc:AlternateContent></Relationships>"
1304 );
1305 let document = Document::parse(&xml).expect("well-formed MCE input");
1306 for edition in [
1307 OpcRelationshipEdition::Ecma2012,
1308 OpcRelationshipEdition::Ecma2021,
1309 ] {
1310 let result = normalize(
1311 &document,
1312 &[RelationshipSelector::SourceId("x".into())],
1313 edition,
1314 &TransformExecutionBudget::default(),
1315 );
1316 assert_eq!(
1317 result.is_ok(),
1318 declaration.is_empty(),
1319 "{edition:?}: {declaration}, local={local}"
1320 );
1321 }
1322 }
1323 }
1324 }
1325
1326 #[test]
1327 fn relationship_simple_content_is_removed_but_invalid_structure_is_rejected() {
1328 for edition in [
1331 OpcRelationshipEdition::Ecma2012,
1332 OpcRelationshipEdition::Ecma2021,
1333 ] {
1334 for (content, accepted) in [
1335 (
1336 "<Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\">metadata & <![CDATA[value]]></Relationship>",
1337 true,
1338 ),
1339 (
1340 "metadata<Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"/>",
1341 false,
1342 ),
1343 (
1344 "<Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"><child/></Relationship>",
1345 false,
1346 ),
1347 ] {
1348 let xml = format!("<Relationships xmlns=\"{REL}\">{content}</Relationships>");
1349 let document = Document::parse(&xml).expect("well-formed XML");
1350 let result = normalize(
1351 &document,
1352 &[RelationshipSelector::SourceId("x".into())],
1353 edition,
1354 &TransformExecutionBudget::default(),
1355 );
1356 assert_eq!(result.is_ok(), accepted, "{edition:?}: {content}");
1357 if accepted {
1358 let expected = format!(
1359 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Target=\"a\" TargetMode=\"Internal\" Type=\"urn:t\"></Relationship></Relationships>"
1360 );
1361 assert_eq!(
1362 result.expect("simple content is valid"),
1363 expected.as_bytes()
1364 );
1365 }
1366 }
1367 }
1368 }
1369
1370 #[test]
1371 fn selection_union_survives_nonmatching_selectors_on_either_side() {
1372 let xml = format!(
1374 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"/></Relationships>"
1375 );
1376 let document = Document::parse(&xml).expect("valid relationship document");
1377 for edition in [
1378 OpcRelationshipEdition::Ecma2012,
1379 OpcRelationshipEdition::Ecma2021,
1380 ] {
1381 let output = normalize(
1382 &document,
1383 &[
1384 RelationshipSelector::SourceId("absent".into()),
1385 RelationshipSelector::SourceType("urn:t".into()),
1386 RelationshipSelector::SourceId("other".into()),
1387 ],
1388 edition,
1389 &TransformExecutionBudget::default(),
1390 )
1391 .expect("selector union normalization succeeds");
1392 assert!(
1393 std::str::from_utf8(&output)
1394 .expect("normalized relationships are UTF-8")
1395 .contains("Id=\"x\"")
1396 );
1397 }
1398 }
1399
1400 #[test]
1401 fn both_editions_require_inclusive_c14n_1_0() {
1402 for edition in [
1404 OpcRelationshipEdition::Ecma2012,
1405 OpcRelationshipEdition::Ecma2021,
1406 ] {
1407 for mode in [
1408 C14nMode::Inclusive1_0,
1409 C14nMode::Inclusive1_1,
1410 C14nMode::Exclusive1_0,
1411 ] {
1412 for comments in [false, true] {
1413 let chain = [
1414 Transform::Relationship(vec![RelationshipSelector::SourceId("x".into())]),
1415 Transform::C14n(crate::c14n::C14nAlgorithm::new(mode, comments)),
1416 ];
1417 assert!(validate_chain(&chain).is_ok());
1418 assert_eq!(
1419 validate_edition(&chain, edition).is_ok(),
1420 mode == C14nMode::Inclusive1_0
1421 );
1422 }
1423 }
1424 }
1425 }
1426
1427 #[test]
1428 fn schema_id_validation_collapses_whitespace_without_rewriting_xml() {
1429 let xml = format!(
1432 "<Relationships xmlns=\"{REL}\"><Relationship Id=\" x \" Type=\"urn:t\" Target=\"a\"/></Relationships>"
1433 );
1434 let document = Document::parse(&xml).expect("valid XML");
1435 let output = normalize(
1436 &document,
1437 &[RelationshipSelector::SourceId(" x ".into())],
1438 OpcRelationshipEdition::Ecma2021,
1439 &TransformExecutionBudget::default(),
1440 )
1441 .expect("schema-valid ID");
1442 assert!(
1443 std::str::from_utf8(&output)
1444 .expect("UTF-8")
1445 .contains("Id=\" x \"")
1446 );
1447 let duplicate = xml.replace(
1448 "</Relationships>",
1449 "<Relationship Id=\"x\" Type=\"urn:t\" Target=\"b\"/></Relationships>",
1450 );
1451 let document = Document::parse(&duplicate).expect("valid XML");
1452 assert!(
1453 normalize(
1454 &document,
1455 &[RelationshipSelector::SourceId("none".into())],
1456 OpcRelationshipEdition::Ecma2021,
1457 &TransformExecutionBudget::default()
1458 )
1459 .is_err()
1460 );
1461 }
1462
1463 #[test]
1464 fn legacy_preparation_removes_edge_and_relationship_contents() {
1465 let xml = format!(
1468 "<Relationships xmlns=\"{REL}\"><?edge a?><Relationship Id=\"a\" Type=\"urn:t\" Target=\"a\"><?child a?></Relationship><?middle a?><Relationship Id=\"b\" Type=\"urn:t\" Target=\"b\"/><?edge b?></Relationships>"
1469 );
1470 let document = Document::parse(&xml).expect("valid Relationships XML");
1471 let output = normalize(
1472 &document,
1473 &[RelationshipSelector::SourceType("urn:t".into())],
1474 OpcRelationshipEdition::Ecma2012,
1475 &TransformExecutionBudget::default(),
1476 )
1477 .expect("valid legacy normalization");
1478 let expected = format!(
1479 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"a\" Target=\"a\" TargetMode=\"Internal\" Type=\"urn:t\"></Relationship><?middle a?><Relationship Id=\"b\" Target=\"b\" TargetMode=\"Internal\" Type=\"urn:t\"></Relationship></Relationships>"
1480 );
1481 assert_eq!(output, expected.as_bytes());
1482 }
1483}