Skip to main content

xml_sec/xmldsig/transforms/
relationship.rs

1//! ECMA-376 Part 2 relationship normalization, with one MCE semantic path.
2
3use 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
16/// OPC Relationship Transform algorithm URI (both supported editions).
17pub 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/// An OPC selector. Repeated selectors form a union, never duplicate output.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum RelationshipSelector {
27    /// Select relationships by their Id attribute.
28    SourceId(String),
29    /// Select every relationship of the given Type.
30    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            // Charge the complete new allocation before reserve; old capacity
109            // remains live during realloc and counts toward the cumulative limit.
110            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
128/// Validate one algorithm chain, not an OPC package signature.
129/// ECMA-376 Part 2 (2021) §10.5.8.2 also constrains package Manifest
130/// placement and per-part uniqueness; those require a package-level validator,
131/// not restrictions on this reusable XMLDSig/XMLEnc transform mechanism.
132/// https://ecma-international.org/publications-and-standards/standards/ecma-376/
133pub(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            // ECMA-376 Part 2 (2021) §§10.5.8.2, 10.6; 2012 §13.2.4.24:
138            // https://ecma-international.org/publications-and-standards/standards/ecma-376/
139            // The normalized XML must immediately be followed by C14N.
140            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    // 2021 §§10.5.5, 10.5.8.2 and 2012 §13.2.4.4 restrict OPC
160    // canonicalization to inclusive C14N 1.0 with/without comments. In 2012,
161    // consumers "shall fail the validation" for other methods (M6.34).
162    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
163    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        // ECMA-376 Part 2: 2021 C.2 / 2012 D.3 extend xsd:string.
194        // Character content is permitted but does not participate in selection.
195        // https://ecma-international.org/publications-and-standards/standards/ecma-376/
196        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
244/// No intermediate DOM or copied attribute strings: retained records borrow
245/// the input arena; only the bounded traversal stack and sorted records allocate.
246pub(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            // CT_Relationship extends xsd:string (2012 D.4 / 2021 C.5).
279            // 2012 §13.2.4.24 step 3(2) removes its contents; 2021 §10.6
280            // step 3(a) removes text. CT_Relationships remains element-only.
281            // https://ecma-international.org/publications-and-standards/standards/ecma-376/
282            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                // XSD 1.0 §§3.3.8, 4.3.6: ID's whiteSpace facet is collapse.
360                // Schema validation does not replace lexical attribute content
361                // in the transform's input XML infoset.
362                // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#ID
363                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    // String comparison visits are conservatively charged before sorting.
416    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            // ECMA-376 Part 2 (2021) §10.6 step 2 changes selector
457            // comparison only; sorting still compares case-sensitive Ids.
458            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                // 2012 §13.2.4.24 step 3 removes all edge characters,
490                // whereas 2021 §10.6 step 3 removes text/comments only:
491                // https://ecma-international.org/publications-and-standards/standards/ecma-376/
492                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        // Part 3:2012 §10.1.1 resets this namespace's inherited rules, after
675        // applying local ProcessContent. Part 3:2015 §9.2 instead matches any
676        // ancestor's name pair; do not carry the older reset into that edition.
677        // https://ecma-international.org/publications-and-standards/standards/ecma-376/
678        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    // Part 3:2012 §§10.1, 10.1.3, 10.2.1 checks MCE attributes and
694    // MustUnderstand before ignoring an encountered element, including all
695    // Choice/Fallback attributes. Part 3:2015 §9.4 checks MustUnderstand only
696    // after ignore/selection; descendants of unselected branches are not visited.
697    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
698    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    // ECMA-376 Part 3 (2015) §§7, 9: namespace lists bind at their
711    // declaration element, not at a descendant that might rebind a prefix.
712    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
713    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                // Part 3:2012 §10 and Part 3:2015 §7 both exclude the
728                // preservation attributes of the original 2006 vocabulary.
729                // https://ecma-international.org/publications-and-standards/standards/ecma-376/
730                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                    // Part 3:2012 §10.1.2 requires Ignorable on the same
736                    // element; Part 3:2015 §7.3 permits ancestor declarations.
737                    // Prefix aliases are compared by their expanded namespace.
738                    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
739                    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        // Part 3 (2015) §9.2 marks ProcessContent matches as unwrapped,
819        // not ignored. §9.4(3a) permits only ignored extension children here.
820        // https://ecma-international.org/publications-and-standards/standards/ecma-376/
821        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    // ECMA-376 Part 2:2021 §§3.2.8, 6.5.3.4 (2012 §§3, 9.3.2)
864    // requires an absolute IRI, including on unselected relationships.
865    // RFC 3987 §2.2 requires an ASCII scheme, allows Unicode in components,
866    // and excludes fragments from the absolute-IRI production.
867    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
868    // https://www.rfc-editor.org/rfc/rfc3987#section-2.2
869    // Check the collapsed anyURI value without changing its lexical infoset.
870    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    // Part 2:2012 §9.3.2.2 permits a URI or relative reference, including
902    // format-defined fragments (O1.6); 2021 §6.5.3.4 instead specifies a
903    // relative reference or absolute-IRI. RFC 3987 §2.2 excludes fragments
904    // from absolute-IRI, but permits them in relative references.
905    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
906    // https://www.rfc-editor.org/rfc/rfc3987#section-2.2
907    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    // ECMA-376 Part 2 (2021) §6.5.3.4 / 2012 §9.3.2.2 requires a
925    // relative reference. RFC 3986 §4.2 includes network-path references;
926    // §5.2.2 can resolve one to the SAME pack authority. Without the base
927    // pack IRI (2021 §§6.5.2.2-3), this transform cannot check membership.
928    // Do not equate relative-reference syntax with package target resolution.
929    // https://www.rfc-editor.org/rfc/rfc3986#section-4.2
930    // xsd:anyURI collapses edge whitespace before lexical validation.
931    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
939// RFC 3987 §2.2: validate components on borrowed slices, preserving the lexical
940// infoset. Relative references differ only by forbidding ':' in the first
941// non-slash path segment. No resolution, decoding, or scheme-specific rewriting.
942// https://www.rfc-editor.org/rfc/rfc3987#section-2.2
943fn 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        // A scheme alone is insufficient: every component must satisfy RFC 3987.
1081        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        // Relative references include network paths, but not malformed escapes/authorities.
1114        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        // Neither selected MCE vocabulary includes the original preservation attributes.
1152        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        // Type defines a role by absolute IRI in both editions. The scheme
1175        // uses ASCII grammar, but its remainder can contain Unicode.
1176        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        // Both selector schemas extend xsd:string; only their attribute
1246        // participates in selection, and nested elements remain invalid.
1247        for (name, attribute, value) in [
1248            ("RelationshipReference", "SourceId", "x"),
1249            ("RelationshipsGroupReference", "SourceType", "urn:t"),
1250        ] {
1251            for content in ["", "metadata &amp; <![CDATA[value]]>", "<child/>"] {
1252                let parameters = format!(
1253                    "<Transform><{name} xmlns=\"{PARAM}\" {attribute}=\"{value}\">{content}</{name}></Transform>"
1254                );
1255                let parameters = Document::parse(&parameters).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        // Part 3 §9.4(3a) allows ignored extension children, not unwrapped
1290        // ones. ProcessContent may be inherited or local and use a wildcard.
1291        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        // CT_Relationship is xsd:string simple content in both editions.
1329        // Stripping it must not permit container text or child elements.
1330        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 &amp; <![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        // A later nonmatch must never undo membership in the selector union.
1373        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        // Both editions reject other canonicalization methods after OPC.
1403        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        // XSD ID has a collapse whitespace facet; validation is not a request
1430        // to rewrite the attribute in the transform's XML input infoset.
1431        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        // 2012 step 3 removes container-edge data and all Relationship content,
1466        // but does not remove PIs between two retained relationships.
1467        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}