1use std::{
4 cell::Cell,
5 io::{self, Write},
6 mem::size_of,
7};
8
9#[cfg(test)]
10use super::Transform;
11use super::parameters::{Instruction, Selector, View};
12use super::{TransformError, TransformExecutionBudget, transform_resource_limit};
13#[cfg(test)]
14use crate::c14n::scratch::Native;
15use crate::c14n::scratch::{List, Scratch};
16use crate::{
17 c14n::C14nMode,
18 policy::OpcRelationshipEdition,
19 xml::dom::{Document, Node},
20};
21
22pub const RELATIONSHIP_TRANSFORM_URI: &str =
24 "http://schemas.openxmlformats.org/package/2006/RelationshipTransform";
25const REL: &str = "http://schemas.openxmlformats.org/package/2006/relationships";
26const PARAM: &str = "http://schemas.openxmlformats.org/package/2006/digital-signature";
27const MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
28const XML: &str = "http://www.w3.org/XML/1998/namespace";
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum RelationshipSelector {
33 SourceId(String),
35 SourceType(String),
37}
38
39impl RelationshipSelector {
40 pub(crate) fn parameter(&self) -> (&'static str, &'static str, &str) {
41 match self {
42 Self::SourceId(value) => ("RelationshipReference", "SourceId", value),
43 Self::SourceType(value) => ("RelationshipsGroupReference", "SourceType", value),
44 }
45 }
46}
47
48pub(super) fn validate_owned_selectors(
49 selectors: &[RelationshipSelector],
50 budget: &WorkspaceBudget,
51) -> Result<(), TransformError> {
52 budget.charge(
53 selectors
54 .len()
55 .checked_mul(size_of::<RelationshipSelector>())
56 .ok_or_else(|| invalid("parameter size overflow"))?,
57 )?;
58 for selector in selectors {
59 let (_, _, value) = selector.parameter();
60 budget.charge(value.len())?;
61 }
62 Ok(())
63}
64
65pub(super) struct WorkspaceBudget {
66 remaining: Cell<usize>,
67 maximum: usize,
68 resource: &'static str,
69}
70
71impl Default for WorkspaceBudget {
72 fn default() -> Self {
73 Self::new(crate::hard_limits::OPC_WORKSPACE_BYTE_CEILING)
74 }
75}
76
77impl WorkspaceBudget {
78 pub(super) fn new(maximum: usize) -> Self {
79 Self {
80 remaining: Cell::new(maximum),
81 maximum,
82 resource: crate::policy::resource_name::OPC_WORKSPACE_BYTES,
83 }
84 }
85
86 pub(super) fn parameters(maximum: usize) -> Self {
87 Self {
88 resource: crate::policy::resource_name::OPC_PARAMETER_BYTES,
89 ..Self::new(maximum)
90 }
91 }
92
93 fn charge(&self, bytes: usize) -> Result<(), TransformError> {
94 let remaining = self.remaining.get();
95 if bytes > remaining {
96 self.remaining.set(0);
97 return Err(transform_resource_limit(
98 self.resource,
99 self.maximum,
100 (self.maximum - remaining).saturating_add(bytes),
101 ));
102 }
103 self.remaining.set(remaining - bytes);
104 Ok(())
105 }
106
107 fn push<T>(&self, vector: &mut impl List<T>, value: T) -> Result<(), TransformError> {
108 self.reserve_slot(vector)?;
109 vector.push(value)?;
110 Ok(())
111 }
112
113 fn reserve_slot<T>(&self, vector: &mut impl List<T>) -> Result<(), TransformError> {
114 if vector.len() == vector.capacity() {
115 let capacity = vector
116 .capacity()
117 .max(4)
118 .checked_mul(2)
119 .ok_or_else(|| invalid("workspace overflow"))?;
120 self.charge(
123 capacity
124 .checked_mul(size_of::<T>())
125 .ok_or_else(|| invalid("workspace overflow"))?,
126 )?;
127 vector.reserve_exact(capacity - vector.len())?;
128 }
129 Ok(())
130 }
131}
132
133fn invalid(message: &'static str) -> TransformError {
134 TransformError::Relationship(message)
135}
136
137pub(crate) fn validate_chain<I: Instruction>(chain: &[I]) -> Result<(), TransformError> {
143 let mut seen = false;
144 for (index, transform) in chain.iter().enumerate() {
145 if let View::Relationship(selectors) = transform.view() {
146 if seen
150 || selectors.is_empty()
151 || !matches!(
152 chain.get(index + 1).map(Instruction::view),
153 Some(View::C14n(..))
154 )
155 {
156 return Err(invalid(
157 "each chain permits one relationship transform with selectors, immediately followed by canonicalization",
158 ));
159 }
160 seen = true;
161 }
162 }
163 Ok(())
164}
165
166pub(crate) fn validate_edition<I: Instruction>(
167 chain: &[I],
168 edition: OpcRelationshipEdition,
169) -> Result<(), TransformError> {
170 validate_chain(chain)?;
171 for pair in chain.windows(2) {
176 if matches!(pair[0].view(), View::Relationship(_))
177 && let View::C14n(algorithm, _) = pair[1].view()
178 && algorithm.mode() != C14nMode::Inclusive1_0
179 {
180 return Err(invalid(match edition {
181 OpcRelationshipEdition::Ecma2012 => {
182 "OPC Ecma2012 requires C14N 1.0 after relationship normalization"
183 }
184 OpcRelationshipEdition::Ecma2021 => {
185 "OPC Ecma2021 requires C14N 1.0 after relationship normalization"
186 }
187 }));
188 }
189 }
190 Ok(())
191}
192
193#[cfg(test)]
194fn parse_selectors(
195 node: Node<'_, '_>,
196 budget: &WorkspaceBudget,
197) -> Result<Vec<RelationshipSelector>, TransformError> {
198 parse_selectors_in(node, budget, &Native)
199}
200
201pub(super) fn parse_selectors_in<'a, S: super::ParseStorage<'a>>(
202 node: Node<'a, 'a>,
203 budget: &WorkspaceBudget,
204 scratch: &S,
205) -> Result<S::Values<S::Selector>, TransformError> {
206 let mut selectors = scratch.values();
207 scratch.charge(node.children().len())?;
208 for child in node.children() {
209 if child.is_text()
210 && let Some(text) = child.text()
211 {
212 scratch.charge(text.len())?;
213 }
214 if child.is_comment()
215 || child.is_pi()
216 || (child.is_text() && child.text().is_some_and(super::is_xml_whitespace_only))
217 {
218 continue;
219 }
220 let tag = child.tag_name();
221 scratch.charge(tag.name().len() + tag.namespace().map_or(0, str::len))?;
222 let attribute = match (tag.namespace(), tag.name()) {
223 (Some(PARAM), "RelationshipReference") => "SourceId",
224 (Some(PARAM), "RelationshipsGroupReference") => "SourceType",
225 _ => return Err(invalid("unexpected relationship selector")),
226 };
227 scratch.charge(child.children().len())?;
231 if child.attributes().len() != 1 || child.children().any(|n| n.is_element()) {
232 return Err(invalid(
233 "relationship selector requires exactly its named attribute and no child elements",
234 ));
235 }
236 for attribute in child.attributes() {
237 scratch.charge(attribute.name().len() + attribute.namespace().map_or(0, str::len))?;
238 }
239 let value = child
240 .attribute(attribute)
241 .ok_or_else(|| invalid("missing relationship selector attribute"))?;
242 budget.charge(value.len())?;
243 budget.reserve_slot(&mut selectors)?;
244 selectors.push(scratch.selector(attribute == "SourceId", value))?;
245 }
246 if selectors.is_empty() {
247 return Err(invalid("relationship transform requires selectors"));
248 }
249 Ok(selectors)
250}
251
252#[derive(Clone, Copy, PartialEq, Eq)]
253enum Parent {
254 Document,
255 Relationships,
256 Relationship(usize),
257}
258
259struct Frame<'a, D: crate::xml::dom::storage::Storage> {
260 next: Option<Node<'a, 'a, D>>,
261 parent: Parent,
262}
263
264struct Relationship<'a, S: Scratch, D: crate::xml::dom::storage::Storage> {
265 id: &'a str,
266 kind: &'a str,
267 target: &'a str,
268 mode: &'a str,
269 instructions: S::Values<Node<'a, 'a, D>>,
270 selected: bool,
271}
272
273enum Event<'a, D: crate::xml::dom::storage::Storage> {
274 Instruction(Node<'a, 'a, D>),
275 Relationship,
276}
277
278struct CurrentFilter<'a, S: Scratch> {
279 original: &'a super::NodeFilterWorkBudget,
280 scratch: &'a S,
281}
282impl<S: Scratch> CurrentFilter<'_, S> {
283 fn charge(&self, units: usize) -> Result<(), TransformError> {
284 self.original.charge(units)?;
285 self.scratch.charge(units)?;
286 Ok(())
287 }
288}
289struct CurrentBudget<'a, S: Scratch> {
290 original: &'a TransformExecutionBudget,
291 node_filter: CurrentFilter<'a, S>,
292}
293impl<S: Scratch> CurrentBudget<'_, S> {
294 fn attribute<'a, D: crate::xml::dom::storage::Storage>(
295 &self,
296 node: Node<'a, 'a, D>,
297 namespace: Option<&str>,
298 local: &str,
299 ) -> Result<Option<&'a str>, TransformError> {
300 for attribute in node.attributes() {
301 let work = attribute
302 .name()
303 .len()
304 .checked_add(attribute.namespace().map_or(0, str::len))
305 .and_then(|size| size.checked_add(local.len()))
306 .and_then(|size| size.checked_add(namespace.map_or(0, str::len)))
307 .and_then(|size| size.checked_add(1))
308 .ok_or(structured_asn1::memory::MemoryError::WorkExceeded)
309 .map_err(crate::c14n::C14nError::from)?;
310 self.node_filter.scratch.charge(work)?;
311 if attribute.name() == local && attribute.namespace() == namespace {
312 return Ok(Some(attribute.value()));
313 }
314 }
315 Ok(None)
316 }
317}
318impl<S: Scratch> std::ops::Deref for CurrentBudget<'_, S> {
319 type Target = TransformExecutionBudget;
320 fn deref(&self) -> &Self::Target {
321 self.original
322 }
323}
324
325#[cfg(test)]
328pub(super) fn normalize<'a>(
329 document: &'a Document<'a>,
330 selectors: &[RelationshipSelector],
331 edition: OpcRelationshipEdition,
332 budget: &TransformExecutionBudget,
333) -> Result<Vec<u8>, TransformError> {
334 normalize_in(document, selectors, edition, budget, &Native)
335}
336
337pub(super) fn normalize_in<'a, S: Scratch, D: crate::xml::dom::storage::Storage>(
338 document: &'a Document<'a, D>,
339 selectors: &[impl Selector],
340 edition: OpcRelationshipEdition,
341 budget: &TransformExecutionBudget,
342 scratch: &S,
343) -> Result<S::Bytes, TransformError> {
344 let current = CurrentBudget {
345 original: budget,
346 node_filter: CurrentFilter {
347 original: &budget.node_filter,
348 scratch,
349 },
350 };
351 let budget = ¤t;
352 let mut frames = scratch.values();
353 let mut records = scratch.values::<Relationship<'a, S, D>>();
354 let mut root_seen = false;
355 let mut padded_ids = false;
356 let mut before = scratch.values();
357 let mut after = scratch.values();
358 let mut events = scratch.values();
359 budget.opc_workspace.push(
360 &mut frames,
361 Frame {
362 next: document.root().first_child(),
363 parent: Parent::Document,
364 },
365 )?;
366 while let Some(frame) = frames.last_mut() {
367 let Some(node) = frame.next else {
368 frames.pop();
369 continue;
370 };
371 frame.next = node.next_sibling();
372 let parent = frame.parent;
373 budget.node_filter.charge(1)?;
374 if node.is_comment() {
375 continue;
376 }
377 if node.is_text() {
378 budget
379 .node_filter
380 .scratch
381 .charge(node.text().map_or(0, str::len))?;
382 if matches!(parent, Parent::Relationship(_)) {
387 continue;
388 }
389 if !node.text().is_some_and(super::is_xml_whitespace_only) {
390 return Err(invalid("Relationships part has character data"));
391 }
392 continue;
393 }
394 if node.is_pi() {
395 match parent {
396 Parent::Document if !root_seen => budget.opc_workspace.push(&mut before, node)?,
397 Parent::Document => budget.opc_workspace.push(&mut after, node)?,
398 Parent::Relationships => budget
399 .opc_workspace
400 .push(&mut events, Event::Instruction(node))?,
401 Parent::Relationship(_) if edition == OpcRelationshipEdition::Ecma2012 => {}
402 Parent::Relationship(index) => budget
403 .opc_workspace
404 .push(&mut records[index].instructions, node)?,
405 }
406 continue;
407 }
408 let tag = node.tag_name();
409 validate_mce_entry(node, edition, budget)?;
410 if tag.namespace() != Some(REL)
411 && tag.namespace() != Some(MC)
412 && ignorable(node, tag.namespace(), budget)?
413 {
414 if process_content(node, edition, budget)? {
415 check_unwrapped(node)?;
416 if edition == OpcRelationshipEdition::Ecma2021 {
417 must_understand(node, budget)?;
418 }
419 budget.opc_workspace.push(
420 &mut frames,
421 Frame {
422 next: node.first_child(),
423 parent,
424 },
425 )?;
426 }
427 continue;
428 }
429 if edition == OpcRelationshipEdition::Ecma2021 {
430 must_understand(node, budget)?;
431 }
432 if tag.namespace() == Some(MC) {
433 if tag.name() != "AlternateContent" {
434 return Err(invalid("MCE Choice/Fallback outside AlternateContent"));
435 }
436 check_unwrapped(node)?;
437 if let Some(branch) = alternate_content(node, edition, budget)? {
438 if edition == OpcRelationshipEdition::Ecma2021 {
439 must_understand(branch, budget)?;
440 }
441 budget.opc_workspace.push(
442 &mut frames,
443 Frame {
444 next: branch.first_child(),
445 parent,
446 },
447 )?;
448 }
449 continue;
450 }
451 let next_parent = match (parent, tag.namespace(), tag.name()) {
452 (Parent::Document, Some(REL), "Relationships") if !root_seen => {
453 root_seen = true;
454 validate_attributes(node, &[], budget)?;
455 Parent::Relationships
456 }
457 (Parent::Relationships, Some(REL), "Relationship") => {
458 validate_attributes(node, &["Id", "Type", "Target", "TargetMode"], budget)?;
459 let id = budget
460 .attribute(node, None, "Id")?
461 .ok_or_else(|| invalid("missing relationship Id"))?;
462 budget.node_filter.charge(id.len())?;
463 if !ncname(id.trim_matches(xml_whitespace)) {
468 return Err(invalid("relationship Id must be an xsd:ID"));
469 }
470 padded_ids |= id != id.trim_matches(xml_whitespace);
471 let kind = budget
472 .attribute(node, None, "Type")?
473 .ok_or_else(|| invalid("missing relationship Type"))?;
474 budget.node_filter.charge(kind.len())?;
475 validate_relationship_type_iri(kind)?;
476 let target = budget
477 .attribute(node, None, "Target")?
478 .ok_or_else(|| invalid("missing relationship Target"))?;
479 let mode = budget
480 .attribute(node, None, "TargetMode")?
481 .unwrap_or("Internal");
482 if mode != "Internal" && mode != "External" {
483 return Err(invalid("invalid relationship TargetMode"));
484 }
485 budget.node_filter.charge(target.len())?;
486 if mode == "Internal" {
487 validate_internal_target_reference_syntax(target)?;
488 } else {
489 validate_external_target_reference_syntax(target, edition)?;
490 }
491 let index = records.len();
492 budget.opc_workspace.push(
493 &mut records,
494 Relationship {
495 id,
496 kind,
497 target,
498 mode,
499 instructions: scratch.values(),
500 selected: false,
501 },
502 )?;
503 budget
504 .opc_workspace
505 .push(&mut events, Event::Relationship)?;
506 Parent::Relationship(index)
507 }
508 _ => return Err(invalid("invalid Relationships part element")),
509 };
510 budget.opc_workspace.push(
511 &mut frames,
512 Frame {
513 next: node.first_child(),
514 parent: next_parent,
515 },
516 )?;
517 }
518 if !root_seen {
519 return Err(invalid("missing Relationships root"));
520 }
521 let max_id = records
523 .iter()
524 .map(|record| record.id.len())
525 .max()
526 .unwrap_or(0);
527 let work = records
528 .len()
529 .checked_mul(4 * (records.len().max(1).ilog2() as usize + 1) * max_id.max(1))
530 .ok_or_else(|| invalid("sort work overflow"))?;
531 budget.node_filter.charge(work)?;
532 if padded_ids {
533 records.sort(|left, right| {
534 scratch.charge(left.id.len().max(right.id.len()))?;
535 Ok(left
536 .id
537 .trim_matches(xml_whitespace)
538 .cmp(right.id.trim_matches(xml_whitespace)))
539 })?;
540 } else {
541 records.sort(|left, right| {
542 scratch.charge(left.id.len().max(right.id.len()))?;
543 Ok(left.id.cmp(right.id))
544 })?;
545 }
546 if records.windows(2).any(|pair| {
547 pair[0].id.trim_matches(xml_whitespace) == pair[1].id.trim_matches(xml_whitespace)
548 }) {
549 return Err(invalid("duplicate relationship Id"));
550 }
551 if padded_ids {
552 budget.node_filter.charge(work)?;
553 records.sort(|left, right| {
554 scratch.charge(left.id.len().max(right.id.len()))?;
555 Ok(left.id.cmp(right.id))
556 })?;
557 }
558 let mut first_selected = None;
559 let mut last_selected = 0;
560 for (index, record) in records.iter_mut().enumerate() {
561 for selector in selectors {
562 let (_, attribute, expected) = selector.parameter();
563 let actual = if attribute == "SourceId" {
564 record.id
565 } else {
566 record.kind
567 };
568 budget
569 .node_filter
570 .charge(expected.len().max(actual.len()))?;
571 let matches = match edition {
574 OpcRelationshipEdition::Ecma2012 => actual == expected,
575 OpcRelationshipEdition::Ecma2021 => actual.eq_ignore_ascii_case(expected),
576 };
577 if matches {
578 record.selected = true;
579 break;
580 }
581 }
582 if record.selected {
583 first_selected.get_or_insert(index);
584 last_selected = index;
585 }
586 }
587 let mut output = Output {
588 bytes: scratch.bytes(),
589 budget,
590 failure: None,
591 };
592 let result: Result<(), TransformError> = (|| {
593 for node in before.iter() {
594 write_instruction(*node, &mut output)?;
595 }
596 output
597 .write_all(b"<Relationships xmlns=\"")
598 .map_err(output_error)?;
599 output.write_all(REL.as_bytes()).map_err(output_error)?;
600 output.write_all(b"\">").map_err(output_error)?;
601 let mut records = records.iter();
602 let mut slots_seen = 0;
603 for event in events.iter() {
604 let record = match event {
605 Event::Instruction(node) => {
606 if edition == OpcRelationshipEdition::Ecma2021
610 || first_selected
611 .is_some_and(|first| slots_seen > first && slots_seen <= last_selected)
612 {
613 write_instruction(*node, &mut output)?;
614 }
615 continue;
616 }
617 Event::Relationship => records
618 .next()
619 .ok_or_else(|| invalid("relationship slot mismatch"))?,
620 };
621 slots_seen += 1;
622 if !record.selected {
623 continue;
624 }
625 output.write_all(b"<Relationship").map_err(output_error)?;
626 for (name, value) in [
627 ("Id", record.id),
628 ("Target", record.target),
629 ("TargetMode", record.mode),
630 ("Type", record.kind),
631 ] {
632 write!(output, " {name}=\"").map_err(output_error)?;
633 crate::c14n::escape_attr(value, &mut output).map_err(output_error)?;
634 output.write_all(b"\"").map_err(output_error)?;
635 }
636 output.write_all(b">").map_err(output_error)?;
637 if edition == OpcRelationshipEdition::Ecma2021 {
638 for instruction in record.instructions.iter() {
639 write_instruction(*instruction, &mut output)?;
640 }
641 }
642 output.write_all(b"</Relationship>").map_err(output_error)?;
643 }
644 output
645 .write_all(b"</Relationships>")
646 .map_err(output_error)?;
647 for node in after.iter() {
648 write_instruction(*node, &mut output)?;
649 }
650 Ok(())
651 })();
652 if let Some(error) = output.failure {
653 return Err(error);
654 }
655 result?;
656 Ok(output.bytes)
657}
658
659fn write_instruction<B: crate::c14n::scratch::Bytes, D: crate::xml::dom::storage::Storage>(
660 node: Node<'_, '_, D>,
661 output: &mut Output<'_, B>,
662) -> Result<(), TransformError> {
663 let instruction = node
664 .pi()
665 .ok_or_else(|| invalid("expected processing instruction"))?;
666 write!(output, "<?{}", instruction.target).map_err(output_error)?;
667 if let Some(value) = instruction.value {
668 write!(output, " {value}").map_err(output_error)?;
669 }
670 output.write_all(b"?>").map_err(output_error)
671}
672
673fn output_error(_: io::Error) -> TransformError {
674 invalid("relationship output failed")
677}
678
679struct Output<'a, B: crate::c14n::scratch::Bytes> {
680 bytes: B,
681 budget: &'a TransformExecutionBudget,
682 failure: Option<TransformError>,
683}
684
685impl<B: crate::c14n::scratch::Bytes> Write for Output<'_, B> {
686 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
687 if self.failure.is_some() {
688 return Err(io::ErrorKind::Other.into());
689 }
690 let result = (|| {
691 self.budget.charge_c14n_output_policy(bytes.len())?;
692 if bytes.len() > self.bytes.capacity() - self.bytes.len() {
693 let capacity = self
694 .bytes
695 .len()
696 .checked_add(bytes.len())
697 .and_then(|n| n.checked_next_power_of_two())
698 .ok_or_else(|| invalid("output overflow"))?;
699 self.budget.opc_workspace.charge(capacity)?;
700 self.bytes.reserve(capacity - self.bytes.len())?;
701 }
702 self.bytes.append(bytes)?;
703 Ok::<_, TransformError>(bytes.len())
704 })();
705 result.map_err(|error| {
706 self.failure = Some(error);
707 io::ErrorKind::Other.into()
708 })
709 }
710 fn flush(&mut self) -> io::Result<()> {
711 Ok(())
712 }
713}
714
715fn tokens(value: &str) -> impl Iterator<Item = &str> {
716 value
717 .split([' ', '\t', '\r', '\n'])
718 .filter(|token| !token.is_empty())
719}
720
721fn namespace<'a, S: Scratch, D: crate::xml::dom::storage::Storage>(
722 node: Node<'a, 'a, D>,
723 prefix: &str,
724 budget: &CurrentBudget<'_, S>,
725) -> Result<&'a str, TransformError> {
726 budget
727 .node_filter
728 .charge(prefix.len().saturating_add(node.namespaces().len()))?;
729 if !ncname(prefix) {
730 return Err(invalid("invalid MCE prefix"));
731 }
732 let mut binding = None;
733 for namespace in node.namespaces() {
734 budget.node_filter.scratch.charge(
735 namespace
736 .name()
737 .map_or(0, str::len)
738 .checked_add(prefix.len())
739 .and_then(|size| size.checked_add(1))
740 .ok_or(structured_asn1::memory::MemoryError::WorkExceeded)
741 .map_err(crate::c14n::C14nError::from)?,
742 )?;
743 if namespace.name() == Some(prefix) {
744 binding = Some(namespace.uri());
745 break;
746 }
747 }
748 if prefix == "xml" {
751 binding = Some(XML);
752 }
753 match binding {
754 Some(uri) if !uri.is_empty() && uri != MC => Ok(uri),
755 _ => Err(invalid("MCE prefix must bind a non-MCE namespace")),
756 }
757}
758
759fn ignorable<S: Scratch, D: crate::xml::dom::storage::Storage>(
760 node: Node<'_, '_, D>,
761 uri: Option<&str>,
762 budget: &CurrentBudget<'_, S>,
763) -> Result<bool, TransformError> {
764 let Some(uri) = uri else {
765 return Ok(false);
766 };
767 for ancestor in node.ancestors() {
768 budget.node_filter.charge(1)?;
769 if declares_ignorable(ancestor, uri, budget)? {
770 return Ok(true);
771 }
772 }
773 Ok(false)
774}
775
776fn declares_ignorable<S: Scratch, D: crate::xml::dom::storage::Storage>(
777 node: Node<'_, '_, D>,
778 uri: &str,
779 budget: &CurrentBudget<'_, S>,
780) -> Result<bool, TransformError> {
781 if let Some(value) = budget.attribute(node, Some(MC), "Ignorable")? {
782 budget.node_filter.charge(value.len())?;
783 for prefix in tokens(value) {
784 budget.node_filter.charge(uri.len())?;
785 if namespace(node, prefix, budget)? == uri {
786 return Ok(true);
787 }
788 }
789 }
790 Ok(false)
791}
792
793fn process_content<S: Scratch, D: crate::xml::dom::storage::Storage>(
794 node: Node<'_, '_, D>,
795 edition: OpcRelationshipEdition,
796 budget: &CurrentBudget<'_, S>,
797) -> Result<bool, TransformError> {
798 let tag = node.tag_name();
799 for ancestor in node.ancestors() {
800 budget.node_filter.charge(1)?;
801 if let Some(value) = budget.attribute(ancestor, Some(MC), "ProcessContent")? {
802 budget.node_filter.charge(value.len())?;
803 for token in tokens(value) {
804 let (prefix, local) = token
805 .split_once(':')
806 .ok_or_else(|| invalid("MCE name requires a prefix"))?;
807 if tag.namespace() == Some(namespace(ancestor, prefix, budget)?)
808 && (local == "*" || local == tag.name())
809 {
810 return Ok(true);
811 }
812 }
813 }
814 if edition == OpcRelationshipEdition::Ecma2012
819 && let Some(uri) = tag.namespace()
820 && declares_ignorable(ancestor, uri, budget)?
821 {
822 return Ok(false);
823 }
824 }
825 Ok(false)
826}
827
828fn validate_mce_entry<S: Scratch, D: crate::xml::dom::storage::Storage>(
829 node: Node<'_, '_, D>,
830 edition: OpcRelationshipEdition,
831 budget: &CurrentBudget<'_, S>,
832) -> Result<(), TransformError> {
833 validate_mc_attributes(node, edition, budget)?;
839 if edition == OpcRelationshipEdition::Ecma2012 {
840 must_understand(node, budget)?;
841 }
842 Ok(())
843}
844
845fn validate_mc_attributes<S: Scratch, D: crate::xml::dom::storage::Storage>(
846 node: Node<'_, '_, D>,
847 edition: OpcRelationshipEdition,
848 budget: &CurrentBudget<'_, S>,
849) -> Result<(), TransformError> {
850 for attribute in node.attributes() {
854 budget
855 .node_filter
856 .charge(attribute.value().len().saturating_add(1))?;
857 if attribute.namespace() != Some(MC) {
858 continue;
859 }
860 match attribute.name() {
861 "Ignorable" | "MustUnderstand" => {
862 for prefix in tokens(attribute.value()) {
863 namespace(node, prefix, budget)?;
864 }
865 }
866 "ProcessContent" => {
867 for token in tokens(attribute.value()) {
871 let (prefix, local) = token
872 .split_once(':')
873 .ok_or_else(|| invalid("MCE name requires a prefix"))?;
874 let uri = namespace(node, prefix, budget)?;
875 let declared = match edition {
880 OpcRelationshipEdition::Ecma2012 => declares_ignorable(node, uri, budget)?,
881 OpcRelationshipEdition::Ecma2021 => ignorable(node, Some(uri), budget)?,
882 };
883 if (local != "*" && !ncname(local)) || !declared {
884 return Err(invalid(
885 "MCE qualified name must belong to an ignorable namespace",
886 ));
887 }
888 }
889 }
890 _ => return Err(invalid("unknown MCE attribute")),
891 }
892 }
893 Ok(())
894}
895
896fn must_understand<S: Scratch, D: crate::xml::dom::storage::Storage>(
897 node: Node<'_, '_, D>,
898 budget: &CurrentBudget<'_, S>,
899) -> Result<(), TransformError> {
900 if let Some(value) = budget.attribute(node, Some(MC), "MustUnderstand")? {
901 budget.node_filter.charge(value.len())?;
902 for prefix in tokens(value) {
903 if namespace(node, prefix, budget)? != REL {
904 return Err(invalid("unsupported MCE MustUnderstand namespace"));
905 }
906 }
907 }
908 Ok(())
909}
910
911fn check_unwrapped<D: crate::xml::dom::storage::Storage>(
912 node: Node<'_, '_, D>,
913) -> Result<(), TransformError> {
914 if node.attributes().any(|a| {
915 a.namespace() == Some(XML)
916 && (node.tag_name().namespace() == Some(MC)
917 || matches!(a.name(), "base" | "lang" | "space"))
918 }) {
919 return Err(invalid("unwrapped MCE element has forbidden xml attribute"));
920 }
921 Ok(())
922}
923
924fn validate_attributes<S: Scratch, D: crate::xml::dom::storage::Storage>(
925 node: Node<'_, '_, D>,
926 allowed: &[&str],
927 budget: &CurrentBudget<'_, S>,
928) -> Result<(), TransformError> {
929 for attribute in node.attributes() {
930 budget.node_filter.charge(1)?;
931 match attribute.namespace() {
932 Some(MC) => {}
933 Some(uri) if uri != REL && ignorable(node, Some(uri), budget)? => {}
934 None if allowed.contains(&attribute.name()) => {}
935 _ => return Err(invalid("unexpected Relationships part attribute")),
936 }
937 }
938 Ok(())
939}
940
941fn alternate_content<'a, S: Scratch, D: crate::xml::dom::storage::Storage>(
942 node: Node<'a, 'a, D>,
943 edition: OpcRelationshipEdition,
944 budget: &CurrentBudget<'_, S>,
945) -> Result<Option<Node<'a, 'a, D>>, TransformError> {
946 validate_attributes(node, &[], budget)?;
947 let mut chosen = None;
948 let mut fallback = None;
949 let mut choices = 0;
950 for child in node.children() {
951 budget.node_filter.charge(1)?;
952 if child.is_comment()
953 || child.is_pi()
954 || (child.is_text() && child.text().is_some_and(super::is_xml_whitespace_only))
955 {
956 continue;
957 }
958 let tag = child.tag_name();
959 validate_mce_entry(child, edition, budget)?;
960 if tag.namespace() != Some(MC)
964 && ignorable(child, tag.namespace(), budget)?
965 && !process_content(child, edition, budget)?
966 {
967 continue;
968 }
969 check_unwrapped(child)?;
970 match (tag.namespace(), tag.name()) {
971 (Some(MC), "Choice") if fallback.is_none() => {
972 choices += 1;
973 validate_attributes(child, &["Requires"], budget)?;
974 let requires = budget
975 .attribute(child, None, "Requires")?
976 .ok_or_else(|| invalid("MCE Choice requires Requires"))?;
977 budget.node_filter.charge(requires.len())?;
978 let mut all_known = true;
979 let mut any = false;
980 for prefix in tokens(requires) {
981 any = true;
982 all_known &= namespace(child, prefix, budget)? == REL;
983 }
984 if !any {
985 return Err(invalid("MCE Requires must not be empty"));
986 }
987 if chosen.is_none() && all_known {
988 chosen = Some(child);
989 }
990 }
991 (Some(MC), "Fallback") if fallback.is_none() && choices != 0 => {
992 validate_attributes(child, &[], budget)?;
993 fallback = Some(child);
994 }
995 _ => return Err(invalid("invalid MCE AlternateContent child order")),
996 }
997 }
998 if choices == 0 {
999 return Err(invalid("MCE AlternateContent requires a Choice"));
1000 }
1001 Ok(chosen.or(fallback))
1002}
1003
1004fn validate_relationship_type_iri(kind: &str) -> Result<(), TransformError> {
1005 let value = kind.trim_matches(xml_whitespace);
1013 if let Some(remainder) = iri_scheme_remainder(value)
1014 && !remainder.contains('#')
1015 && valid_iri_reference(remainder, false)
1016 {
1017 return Ok(());
1018 }
1019 Err(invalid("relationship Type must be an absolute IRI"))
1020}
1021
1022fn iri_scheme_remainder(value: &str) -> Option<&str> {
1023 let (scheme, remainder) = value.split_once(':')?;
1024 if !scheme
1025 .as_bytes()
1026 .first()
1027 .is_some_and(u8::is_ascii_alphabetic)
1028 {
1029 return None;
1030 }
1031 for byte in scheme.bytes() {
1032 if !byte.is_ascii_alphanumeric() && !matches!(byte, b'+' | b'-' | b'.') {
1033 return None;
1034 }
1035 }
1036 Some(remainder)
1037}
1038
1039fn validate_external_target_reference_syntax(
1040 target: &str,
1041 edition: OpcRelationshipEdition,
1042) -> Result<(), TransformError> {
1043 let value = target.trim_matches(xml_whitespace);
1050 if let Some(remainder) = iri_scheme_remainder(value) {
1051 if edition == OpcRelationshipEdition::Ecma2021 && remainder.contains('#') {
1052 return Err(invalid(
1053 "External relationship Target must be a relative reference or absolute IRI",
1054 ));
1055 }
1056 if valid_iri_reference(remainder, false) {
1057 return Ok(());
1058 }
1059 } else if valid_iri_reference(value, true) {
1060 return Ok(());
1061 }
1062 Err(invalid("invalid External relationship Target reference"))
1063}
1064
1065fn validate_internal_target_reference_syntax(target: &str) -> Result<(), TransformError> {
1066 if !valid_iri_reference(target.trim_matches(xml_whitespace), true) {
1074 return Err(invalid(
1075 "Internal relationship Target must be a relative reference",
1076 ));
1077 }
1078 Ok(())
1079}
1080
1081fn valid_iri_reference(value: &str, relative: bool) -> bool {
1086 let (value, fragment) = value
1087 .split_once('#')
1088 .map_or((value, None), |(v, f)| (v, Some(f)));
1089 if let Some(fragment) = fragment
1090 && !iri_component(fragment, |c| iri_pchar(c) || matches!(c, '/' | '?'))
1091 {
1092 return false;
1093 }
1094 let (path, query) = value
1095 .split_once('?')
1096 .map_or((value, None), |(v, q)| (v, Some(q)));
1097 if let Some(query) = query
1098 && !iri_component(query, |c| {
1099 iri_pchar(c)
1100 || matches!(c, '/' | '?')
1101 || matches!(c as u32, 0xe000..=0xf8ff | 0xf0000..=0xffffd | 0x100000..=0x10fffd)
1102 })
1103 {
1104 return false;
1105 }
1106 if let Some(authority_path) = path.strip_prefix("//") {
1107 let (authority, path) = authority_path
1108 .split_once('/')
1109 .map_or((authority_path, ""), |(a, p)| (a, p));
1110 return valid_iri_authority(authority) && iri_component(path, |c| iri_pchar(c) || c == '/');
1111 }
1112 if relative
1113 && !path.starts_with('/')
1114 && path
1115 .split('/')
1116 .next()
1117 .is_some_and(|segment| segment.contains(':'))
1118 {
1119 return false;
1120 }
1121 iri_component(path, |c| iri_pchar(c) || c == '/')
1122}
1123
1124fn valid_iri_authority(authority: &str) -> bool {
1125 let host_port = if let Some((userinfo, host_port)) = authority.split_once('@') {
1126 if !iri_component(userinfo, |c| {
1127 iri_unreserved(c) || iri_sub_delim(c) || c == ':'
1128 }) {
1129 return false;
1130 }
1131 host_port
1132 } else {
1133 authority
1134 };
1135 if let Some(literal) = host_port.strip_prefix('[') {
1136 let Some((address, suffix)) = literal.split_once(']') else {
1137 return false;
1138 };
1139 if !suffix.is_empty()
1140 && !suffix
1141 .strip_prefix(':')
1142 .is_some_and(|port| port.bytes().all(|b| b.is_ascii_digit()))
1143 {
1144 return false;
1145 }
1146 if address.parse::<std::net::Ipv6Addr>().is_ok() {
1147 return true;
1148 }
1149 if let Some(future) = address.strip_prefix(['v', 'V'])
1150 && let Some((version, address)) = future.split_once('.')
1151 {
1152 return !version.is_empty()
1153 && version.bytes().all(|b| b.is_ascii_hexdigit())
1154 && !address.is_empty()
1155 && address
1156 .chars()
1157 .all(|c| c.is_ascii() && (iri_unreserved(c) || iri_sub_delim(c) || c == ':'));
1158 }
1159 return false;
1160 }
1161 let host = if let Some((host, port)) = host_port.split_once(':') {
1162 if !port.bytes().all(|b| b.is_ascii_digit()) {
1163 return false;
1164 }
1165 host
1166 } else {
1167 host_port
1168 };
1169 iri_component(host, |c| iri_unreserved(c) || iri_sub_delim(c))
1170}
1171
1172fn iri_component(value: &str, allowed: impl Fn(char) -> bool) -> bool {
1173 let mut chars = value.chars();
1174 while let Some(c) = chars.next() {
1175 if c == '%' {
1176 if !chars.next().is_some_and(|c| c.is_ascii_hexdigit())
1177 || !chars.next().is_some_and(|c| c.is_ascii_hexdigit())
1178 {
1179 return false;
1180 }
1181 } else if !allowed(c) {
1182 return false;
1183 }
1184 }
1185 true
1186}
1187
1188fn iri_unreserved(c: char) -> bool {
1189 let code = c as u32;
1190 c.is_ascii_alphanumeric()
1191 || matches!(c, '-' | '.' | '_' | '~')
1192 || matches!(code, 0xa0..=0xd7ff | 0xf900..=0xfdcf | 0xfdf0..=0xffef)
1193 || ((0x10000..=0xdfffd).contains(&code) && code & 0xffff <= 0xfffd)
1194 || (0xe1000..=0xefffd).contains(&code)
1195}
1196
1197fn iri_sub_delim(c: char) -> bool {
1198 matches!(
1199 c,
1200 '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
1201 )
1202}
1203
1204fn iri_pchar(c: char) -> bool {
1205 iri_unreserved(c) || iri_sub_delim(c) || matches!(c, ':' | '@')
1206}
1207
1208fn ncname(value: &str) -> bool {
1209 !value.contains(':') && crate::xml_input::lexical::is_qname(value)
1210}
1211
1212fn xml_whitespace(value: char) -> bool {
1213 matches!(value, ' ' | '\t' | '\r' | '\n')
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218 use super::*;
1219
1220 #[test]
1221 fn scoped_normalization_preserves_bytes_and_current_execution_boundaries() {
1222 use structured_asn1::{
1226 Encoding, Limits, Parser,
1227 memory::{Memory, MemoryAccount, MemoryError},
1228 };
1229 let document = Document::parse(
1230 r#"<?before a?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="b" Type="urn:type" Target="b"/><?between x?><Relationship Id="a" Type="urn:type" Target="a"/></Relationships><?after b?>"#,
1231 ).expect("fixed Relationships XML");
1232 let selectors = [RelationshipSelector::SourceType("urn:type".into())];
1233 for edition in [
1234 OpcRelationshipEdition::Ecma2012,
1235 OpcRelationshipEdition::Ecma2021,
1236 ] {
1237 let expected = normalize(
1238 &document,
1239 &selectors,
1240 edition,
1241 &TransformExecutionBudget::default(),
1242 )
1243 .expect("native normalization");
1244 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
1245 let baseline = Parser::new(Encoding::Der, Limits::default(), memory.clone());
1248 let baseline_reader = baseline.read(&[], 0).expect("empty baseline input");
1249 let baseline_work = baseline_reader.execution();
1250 let before = memory.work_used();
1251 let output = normalize_in(
1252 &document,
1253 &selectors,
1254 edition,
1255 &TransformExecutionBudget::default(),
1256 &crate::c14n::scratch::Accounted(&baseline_work),
1257 )
1258 .expect("paid normalization");
1259 assert_eq!(&*output, expected);
1260 assert!(memory.used() >= output.len());
1261 let cost = memory.work_used() - before;
1262 drop(output);
1263 assert_eq!(memory.used(), 0);
1264 for limit in [cost, cost - 1, 0] {
1265 let parser = Parser::new(
1266 Encoding::Der,
1267 Limits {
1268 max_work: limit as u64,
1269 ..Default::default()
1270 },
1271 memory.clone(),
1272 );
1273 let reader = parser.read(&[], 0).expect("empty query input");
1274 let work = reader.execution();
1275 let before = memory.work_used();
1276 let result = normalize_in(
1277 &document,
1278 &selectors,
1279 edition,
1280 &TransformExecutionBudget::default(),
1281 &crate::c14n::scratch::Accounted(&work),
1282 );
1283 if limit == cost {
1284 assert_eq!(&*result.expect("exact work admits normalization"), expected);
1285 assert_eq!(memory.work_used() - before, cost);
1286 } else {
1287 assert!(matches!(
1288 result,
1289 Err(TransformError::C14n(
1290 crate::c14n::C14nError::SharedExecution(MemoryError::WorkExceeded)
1291 ))
1292 ));
1293 }
1294 assert_eq!(memory.used(), 0);
1295 }
1296 let small = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 24)));
1297 let result = normalize_in(
1298 &document,
1299 &selectors,
1300 edition,
1301 &TransformExecutionBudget::default(),
1302 &crate::c14n::scratch::Accounted(&small),
1303 );
1304 assert!(matches!(
1305 result,
1306 Err(TransformError::C14n(
1307 crate::c14n::C14nError::SharedExecution(MemoryError::LimitExceeded)
1308 ))
1309 ));
1310 assert_eq!(small.used(), 0);
1311 }
1312 }
1313
1314 #[test]
1315 fn complete_relationship_iri_syntax() {
1316 for value in [
1318 "urn:%ZZ",
1319 "urn:a b",
1320 "urn:x#fragment",
1321 "http://[broken]/x",
1322 "http://host:port/x",
1323 "urn:\u{e000}",
1324 "urn:\u{fdd0}",
1325 "urn:\u{1fffe}",
1326 "http://user@@host/x",
1327 "http://[::1%25zone]/x",
1328 "http://[v1.]/x",
1329 "http://[vG.a]/x",
1330 "http://[::1]suffix/x",
1331 ] {
1332 assert!(validate_relationship_type_iri(value).is_err(), "{value}");
1333 }
1334 for value in [
1335 "urn:%20",
1336 "https://例え.example/役割",
1337 "http://[::1]:80/x",
1338 "urn:x?\u{e000}",
1339 "http://user:pass@host:/x",
1340 "http://[v1.a:b]/x",
1341 "urn:\u{10000}\u{e1000}",
1342 ] {
1343 assert!(validate_relationship_type_iri(value).is_ok(), "{value}");
1344 }
1345 }
1346
1347 #[test]
1348 fn complete_internal_target_syntax() {
1349 for value in [
1351 "part%GG.xml",
1352 "a%",
1353 "a%2",
1354 "a b",
1355 "//[broken]/x",
1356 "//host:port/x",
1357 "a\\b",
1358 "//user@@host/path",
1359 "a#b#c",
1360 "a#\u{e000}",
1361 ] {
1362 assert!(
1363 validate_internal_target_reference_syntax(value).is_err(),
1364 "{value}"
1365 );
1366 }
1367 for value in [
1368 "part%20.xml",
1369 "//host/path",
1370 "//[::1]/x",
1371 "a?b#c",
1372 "../役割.xml",
1373 "//user:pass@host:80/path",
1374 "//[V1.a:b]/path",
1375 "/a:b",
1376 "a?\u{f0000}",
1377 ] {
1378 assert!(
1379 validate_internal_target_reference_syntax(value).is_ok(),
1380 "{value}"
1381 );
1382 }
1383 }
1384
1385 #[test]
1386 fn mce_preservation_attributes_follow_selected_edition() {
1387 for attribute in ["PreserveElements", "PreserveAttributes"] {
1389 let xml = format!(
1390 "<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>"
1391 );
1392 let document = Document::parse(&xml).expect("well-formed MCE document");
1393 for edition in [
1394 OpcRelationshipEdition::Ecma2012,
1395 OpcRelationshipEdition::Ecma2021,
1396 ] {
1397 let result = normalize(
1398 &document,
1399 &[RelationshipSelector::SourceId("x".into())],
1400 edition,
1401 &TransformExecutionBudget::default(),
1402 );
1403 assert!(result.is_err(), "{edition:?}: {attribute}");
1404 }
1405 }
1406 }
1407
1408 #[test]
1409 fn relationship_type_requires_an_absolute_iri_scheme_before_selection() {
1410 for kind in [
1413 "",
1414 "relative/type",
1415 "//example.com/type",
1416 ":type",
1417 "1urn:type",
1418 "x_y:type",
1419 "x/y:type",
1420 "?x:type",
1421 "#x:type",
1422 "urn:%ZZ",
1423 "http://[broken]/type",
1424 "urn:a b",
1425 ] {
1426 let xml = format!(
1427 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Type=\"{kind}\" Target=\"a\"/></Relationships>"
1428 );
1429 let document = Document::parse(&xml).expect("well-formed relationship XML");
1430 for edition in [
1431 OpcRelationshipEdition::Ecma2012,
1432 OpcRelationshipEdition::Ecma2021,
1433 ] {
1434 for id in ["x", "absent"] {
1435 assert!(
1436 normalize(
1437 &document,
1438 &[RelationshipSelector::SourceId(id.into())],
1439 edition,
1440 &TransformExecutionBudget::default()
1441 )
1442 .is_err(),
1443 "{edition:?}, {id}, {kind}"
1444 );
1445 }
1446 }
1447 }
1448 for kind in [
1449 "urn:t",
1450 "https://example.com/type",
1451 "https://例え.example/役割",
1452 "urn:роль",
1453 "x+y.z-1:type",
1454 " urn:t ",
1455 ] {
1456 let xml = format!(
1457 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Type=\"{kind}\" Target=\"a\"/></Relationships>"
1458 );
1459 let document = Document::parse(&xml).expect("well-formed relationship XML");
1460 for edition in [
1461 OpcRelationshipEdition::Ecma2012,
1462 OpcRelationshipEdition::Ecma2021,
1463 ] {
1464 let output = normalize(
1465 &document,
1466 &[RelationshipSelector::SourceId("x".into())],
1467 edition,
1468 &TransformExecutionBudget::default(),
1469 )
1470 .expect("absolute IRI is accepted");
1471 let expected = format!(
1472 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Target=\"a\" TargetMode=\"Internal\" Type=\"{kind}\"></Relationship></Relationships>"
1473 );
1474 assert_eq!(output, expected.as_bytes());
1475 }
1476 }
1477 }
1478
1479 #[test]
1480 fn selector_simple_content_does_not_change_selection() {
1481 for (name, attribute, value) in [
1484 ("RelationshipReference", "SourceId", "x"),
1485 ("RelationshipsGroupReference", "SourceType", "urn:t"),
1486 ] {
1487 for content in ["", "metadata & <![CDATA[value]]>", "<child/>"] {
1488 let parameters = format!(
1489 "<Transform><{name} xmlns=\"{PARAM}\" {attribute}=\"{value}\">{content}</{name}></Transform>"
1490 );
1491 let parameters = Document::parse(¶meters).expect("well-formed selector XML");
1492 let parsed =
1493 parse_selectors(parameters.root_element(), &WorkspaceBudget::default());
1494 if content == "<child/>" {
1495 assert!(parsed.is_err());
1496 continue;
1497 }
1498 let parsed = parsed.expect("selector simple content is valid");
1499 let xml = format!(
1500 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"/></Relationships>"
1501 );
1502 let document = Document::parse(&xml).expect("well-formed relationship XML");
1503 for edition in [
1504 OpcRelationshipEdition::Ecma2012,
1505 OpcRelationshipEdition::Ecma2021,
1506 ] {
1507 let output = normalize(
1508 &document,
1509 &parsed,
1510 edition,
1511 &TransformExecutionBudget::default(),
1512 )
1513 .expect("selector simple content is ignored");
1514 let expected = format!(
1515 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Target=\"a\" TargetMode=\"Internal\" Type=\"urn:t\"></Relationship></Relationships>"
1516 );
1517 assert_eq!(output, expected.as_bytes());
1518 }
1519 }
1520 }
1521 }
1522
1523 #[test]
1524 fn alternate_content_distinguishes_ignored_and_unwrapped_extensions() {
1525 for declaration in [
1528 "",
1529 "mc:ProcessContent=\"u:extension\"",
1530 "mc:ProcessContent=\"u:*\"",
1531 ] {
1532 for local in [false, true] {
1533 let (root_attributes, child_attributes) = if local {
1534 ("", declaration)
1535 } else {
1536 (declaration, "")
1537 };
1538 let xml = format!(
1539 "<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>"
1540 );
1541 let document = Document::parse(&xml).expect("well-formed MCE input");
1542 for edition in [
1543 OpcRelationshipEdition::Ecma2012,
1544 OpcRelationshipEdition::Ecma2021,
1545 ] {
1546 let result = normalize(
1547 &document,
1548 &[RelationshipSelector::SourceId("x".into())],
1549 edition,
1550 &TransformExecutionBudget::default(),
1551 );
1552 assert_eq!(
1553 result.is_ok(),
1554 declaration.is_empty(),
1555 "{edition:?}: {declaration}, local={local}"
1556 );
1557 }
1558 }
1559 }
1560 }
1561
1562 #[test]
1563 fn relationship_simple_content_is_removed_but_invalid_structure_is_rejected() {
1564 for edition in [
1567 OpcRelationshipEdition::Ecma2012,
1568 OpcRelationshipEdition::Ecma2021,
1569 ] {
1570 for (content, accepted) in [
1571 (
1572 "<Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\">metadata & <![CDATA[value]]></Relationship>",
1573 true,
1574 ),
1575 (
1576 "metadata<Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"/>",
1577 false,
1578 ),
1579 (
1580 "<Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"><child/></Relationship>",
1581 false,
1582 ),
1583 ] {
1584 let xml = format!("<Relationships xmlns=\"{REL}\">{content}</Relationships>");
1585 let document = Document::parse(&xml).expect("well-formed XML");
1586 let result = normalize(
1587 &document,
1588 &[RelationshipSelector::SourceId("x".into())],
1589 edition,
1590 &TransformExecutionBudget::default(),
1591 );
1592 assert_eq!(result.is_ok(), accepted, "{edition:?}: {content}");
1593 if accepted {
1594 let expected = format!(
1595 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Target=\"a\" TargetMode=\"Internal\" Type=\"urn:t\"></Relationship></Relationships>"
1596 );
1597 assert_eq!(
1598 result.expect("simple content is valid"),
1599 expected.as_bytes()
1600 );
1601 }
1602 }
1603 }
1604 }
1605
1606 #[test]
1607 fn selection_union_survives_nonmatching_selectors_on_either_side() {
1608 let xml = format!(
1610 "<Relationships xmlns=\"{REL}\"><Relationship Id=\"x\" Type=\"urn:t\" Target=\"a\"/></Relationships>"
1611 );
1612 let document = Document::parse(&xml).expect("valid relationship document");
1613 for edition in [
1614 OpcRelationshipEdition::Ecma2012,
1615 OpcRelationshipEdition::Ecma2021,
1616 ] {
1617 let output = normalize(
1618 &document,
1619 &[
1620 RelationshipSelector::SourceId("absent".into()),
1621 RelationshipSelector::SourceType("urn:t".into()),
1622 RelationshipSelector::SourceId("other".into()),
1623 ],
1624 edition,
1625 &TransformExecutionBudget::default(),
1626 )
1627 .expect("selector union normalization succeeds");
1628 assert!(
1629 std::str::from_utf8(&output)
1630 .expect("normalized relationships are UTF-8")
1631 .contains("Id=\"x\"")
1632 );
1633 }
1634 }
1635
1636 #[test]
1637 fn both_editions_require_inclusive_c14n_1_0() {
1638 for edition in [
1640 OpcRelationshipEdition::Ecma2012,
1641 OpcRelationshipEdition::Ecma2021,
1642 ] {
1643 for mode in [
1644 C14nMode::Inclusive1_0,
1645 C14nMode::Inclusive1_1,
1646 C14nMode::Exclusive1_0,
1647 ] {
1648 for comments in [false, true] {
1649 let chain = [
1650 Transform::Relationship(vec![RelationshipSelector::SourceId("x".into())]),
1651 Transform::C14n(crate::c14n::C14nAlgorithm::new(mode, comments)),
1652 ];
1653 assert!(validate_chain(&chain).is_ok());
1654 assert_eq!(
1655 validate_edition(&chain, edition).is_ok(),
1656 mode == C14nMode::Inclusive1_0
1657 );
1658 }
1659 }
1660 }
1661 }
1662
1663 #[test]
1664 fn schema_id_validation_collapses_whitespace_without_rewriting_xml() {
1665 let xml = format!(
1668 "<Relationships xmlns=\"{REL}\"><Relationship Id=\" x \" Type=\"urn:t\" Target=\"a\"/></Relationships>"
1669 );
1670 let document = Document::parse(&xml).expect("valid XML");
1671 let output = normalize(
1672 &document,
1673 &[RelationshipSelector::SourceId(" x ".into())],
1674 OpcRelationshipEdition::Ecma2021,
1675 &TransformExecutionBudget::default(),
1676 )
1677 .expect("schema-valid ID");
1678 assert!(
1679 std::str::from_utf8(&output)
1680 .expect("UTF-8")
1681 .contains("Id=\" x \"")
1682 );
1683 let duplicate = xml.replace(
1684 "</Relationships>",
1685 "<Relationship Id=\"x\" Type=\"urn:t\" Target=\"b\"/></Relationships>",
1686 );
1687 let document = Document::parse(&duplicate).expect("valid XML");
1688 assert!(
1689 normalize(
1690 &document,
1691 &[RelationshipSelector::SourceId("none".into())],
1692 OpcRelationshipEdition::Ecma2021,
1693 &TransformExecutionBudget::default()
1694 )
1695 .is_err()
1696 );
1697 }
1698
1699 #[test]
1700 fn legacy_preparation_removes_edge_and_relationship_contents() {
1701 let xml = format!(
1704 "<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>"
1705 );
1706 let document = Document::parse(&xml).expect("valid Relationships XML");
1707 let output = normalize(
1708 &document,
1709 &[RelationshipSelector::SourceType("urn:t".into())],
1710 OpcRelationshipEdition::Ecma2012,
1711 &TransformExecutionBudget::default(),
1712 )
1713 .expect("valid legacy normalization");
1714 let expected = format!(
1715 "<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>"
1716 );
1717 assert_eq!(output, expected.as_bytes());
1718 }
1719}