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
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
22/// OPC Relationship Transform algorithm URI (both supported editions).
23pub 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/// An OPC selector. Repeated selectors form a union, never duplicate output.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum RelationshipSelector {
33    /// Select relationships by their Id attribute.
34    SourceId(String),
35    /// Select every relationship of the given Type.
36    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            // Charge the complete new allocation before reserve; old capacity
121            // remains live during realloc and counts toward the cumulative limit.
122            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
137/// Validate one algorithm chain, not an OPC package signature.
138/// ECMA-376 Part 2 (2021) §10.5.8.2 also constrains package Manifest
139/// placement and per-part uniqueness; those require a package-level validator,
140/// not restrictions on this reusable XMLDSig/XMLEnc transform mechanism.
141/// https://ecma-international.org/publications-and-standards/standards/ecma-376/
142pub(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            // ECMA-376 Part 2 (2021) §§10.5.8.2, 10.6; 2012 §13.2.4.24:
147            // https://ecma-international.org/publications-and-standards/standards/ecma-376/
148            // The normalized XML must immediately be followed by C14N.
149            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    // 2021 §§10.5.5, 10.5.8.2 and 2012 §13.2.4.4 restrict OPC
172    // canonicalization to inclusive C14N 1.0 with/without comments. In 2012,
173    // consumers "shall fail the validation" for other methods (M6.34).
174    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
175    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        // ECMA-376 Part 2: 2021 C.2 / 2012 D.3 extend xsd:string.
228        // Character content is permitted but does not participate in selection.
229        // https://ecma-international.org/publications-and-standards/standards/ecma-376/
230        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/// No intermediate DOM or copied attribute strings: retained records borrow
326/// the input arena; only the bounded traversal stack and sorted records allocate.
327#[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 = &current;
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            // CT_Relationship extends xsd:string (2012 D.4 / 2021 C.5).
383            // 2012 §13.2.4.24 step 3(2) removes its contents; 2021 §10.6
384            // step 3(a) removes text. CT_Relationships remains element-only.
385            // https://ecma-international.org/publications-and-standards/standards/ecma-376/
386            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                // XSD 1.0 §§3.3.8, 4.3.6: ID's whiteSpace facet is collapse.
464                // Schema validation does not replace lexical attribute content
465                // in the transform's input XML infoset.
466                // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#ID
467                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    // String comparison visits are conservatively charged before sorting.
522    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            // ECMA-376 Part 2 (2021) §10.6 step 2 changes selector
572            // comparison only; sorting still compares case-sensitive Ids.
573            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                    // 2012 §13.2.4.24 step 3 removes all edge characters,
607                    // whereas 2021 §10.6 step 3 removes text/comments only:
608                    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
609                    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    // Output retains the original typed failure, checked before returning from
675    // normalize_in. The io::Write boundary needs no boxed diagnostic allocation.
676    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    // Namespaces in XML 1.0 §3 reserves xml's implicit binding, even when it
749    // is not declared: https://www.w3.org/TR/2009/REC-xml-names-20091208/#ns-decl
750    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        // Part 3:2012 §10.1.1 resets this namespace's inherited rules, after
815        // applying local ProcessContent. Part 3:2015 §9.2 instead matches any
816        // ancestor's name pair; do not carry the older reset into that edition.
817        // https://ecma-international.org/publications-and-standards/standards/ecma-376/
818        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    // Part 3:2012 §§10.1, 10.1.3, 10.2.1 checks MCE attributes and
834    // MustUnderstand before ignoring an encountered element, including all
835    // Choice/Fallback attributes. Part 3:2015 §9.4 checks MustUnderstand only
836    // after ignore/selection; descendants of unselected branches are not visited.
837    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
838    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    // ECMA-376 Part 3 (2015) §§7, 9: namespace lists bind at their
851    // declaration element, not at a descendant that might rebind a prefix.
852    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
853    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                // Part 3:2012 §10 and Part 3:2015 §7 both exclude the
868                // preservation attributes of the original 2006 vocabulary.
869                // https://ecma-international.org/publications-and-standards/standards/ecma-376/
870                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                    // Part 3:2012 §10.1.2 requires Ignorable on the same
876                    // element; Part 3:2015 §7.3 permits ancestor declarations.
877                    // Prefix aliases are compared by their expanded namespace.
878                    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
879                    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        // Part 3 (2015) §9.2 marks ProcessContent matches as unwrapped,
961        // not ignored. §9.4(3a) permits only ignored extension children here.
962        // https://ecma-international.org/publications-and-standards/standards/ecma-376/
963        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    // ECMA-376 Part 2:2021 §§3.2.8, 6.5.3.4 (2012 §§3, 9.3.2)
1006    // requires an absolute IRI, including on unselected relationships.
1007    // RFC 3987 §2.2 requires an ASCII scheme, allows Unicode in components,
1008    // and excludes fragments from the absolute-IRI production.
1009    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
1010    // https://www.rfc-editor.org/rfc/rfc3987#section-2.2
1011    // Check the collapsed anyURI value without changing its lexical infoset.
1012    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    // Part 2:2012 §9.3.2.2 permits a URI or relative reference, including
1044    // format-defined fragments (O1.6); 2021 §6.5.3.4 instead specifies a
1045    // relative reference or absolute-IRI. RFC 3987 §2.2 excludes fragments
1046    // from absolute-IRI, but permits them in relative references.
1047    // https://ecma-international.org/publications-and-standards/standards/ecma-376/
1048    // https://www.rfc-editor.org/rfc/rfc3987#section-2.2
1049    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    // ECMA-376 Part 2 (2021) §6.5.3.4 / 2012 §9.3.2.2 requires a
1067    // relative reference. RFC 3986 §4.2 includes network-path references;
1068    // §5.2.2 can resolve one to the SAME pack authority. Without the base
1069    // pack IRI (2021 §§6.5.2.2-3), this transform cannot check membership.
1070    // Do not equate relative-reference syntax with package target resolution.
1071    // https://www.rfc-editor.org/rfc/rfc3986#section-4.2
1072    // xsd:anyURI collapses edge whitespace before lexical validation.
1073    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
1081// RFC 3987 §2.2: validate components on borrowed slices, preserving the lexical
1082// infoset. Relative references differ only by forbidding ':' in the first
1083// non-slash path segment. No resolution, decoding, or scheme-specific rewriting.
1084// https://www.rfc-editor.org/rfc/rfc3987#section-2.2
1085fn 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        // Every OPC stack, record, PI list, sort and output buffer shares the
1223        // current execution. Refusal must preserve its typed cause and release
1224        // already admitted storage, without borrowing the producer's ceiling.
1225        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            // Relationship records contain their actual storage strategy. Measure
1246            // the Execution-backed layout, not a differently sized Memory layout.
1247            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        // A scheme alone is insufficient: every component must satisfy RFC 3987.
1317        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        // Relative references include network paths, but not malformed escapes/authorities.
1350        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        // Neither selected MCE vocabulary includes the original preservation attributes.
1388        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        // Type defines a role by absolute IRI in both editions. The scheme
1411        // uses ASCII grammar, but its remainder can contain Unicode.
1412        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        // Both selector schemas extend xsd:string; only their attribute
1482        // participates in selection, and nested elements remain invalid.
1483        for (name, attribute, value) in [
1484            ("RelationshipReference", "SourceId", "x"),
1485            ("RelationshipsGroupReference", "SourceType", "urn:t"),
1486        ] {
1487            for content in ["", "metadata &amp; <![CDATA[value]]>", "<child/>"] {
1488                let parameters = format!(
1489                    "<Transform><{name} xmlns=\"{PARAM}\" {attribute}=\"{value}\">{content}</{name}></Transform>"
1490                );
1491                let parameters = Document::parse(&parameters).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        // Part 3 §9.4(3a) allows ignored extension children, not unwrapped
1526        // ones. ProcessContent may be inherited or local and use a wildcard.
1527        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        // CT_Relationship is xsd:string simple content in both editions.
1565        // Stripping it must not permit container text or child elements.
1566        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 &amp; <![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        // A later nonmatch must never undo membership in the selector union.
1609        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        // Both editions reject other canonicalization methods after OPC.
1639        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        // XSD ID has a collapse whitespace facet; validation is not a request
1666        // to rewrite the attribute in the transform's XML input infoset.
1667        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        // 2012 step 3 removes container-edge data and all Relationship content,
1702        // but does not remove PIs between two retained relationships.
1703        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}