Skip to main content

xml_sec/xmldsig/extension/
scalar.rs

1//! Exact original XML scalar facts; format acceptance remains with the consumer.
2
3use super::{ScalarError, charge_bytes};
4use crate::document::{DocumentView, NodeIdentity};
5use crate::xmldsig::LexicalSource;
6use structured_asn1::memory::{OwnedVec, Work};
7
8/// A direct non-namespace attribute, already normalized by the XML parser.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct DirectAttribute<'a> {
11    /// Original owner, including document and generation.
12    pub owner: NodeIdentity,
13    /// Expanded namespace name; default namespaces do not apply to attributes.
14    pub namespace: Option<&'a str>,
15    /// Original local name.
16    pub local_name: &'a str,
17    /// XML-normalized value, not schema-normalized or source-byte spelling.
18    pub value: &'a str,
19}
20
21/// An in-scope namespace binding, not a direct attribute/declaration claim.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct NamespaceBinding<'a> {
24    /// Original owner.
25    pub owner: NodeIdentity,
26    /// None denotes the default namespace.
27    pub prefix: Option<&'a str>,
28    /// In-scope namespace URI.
29    pub uri: &'a str,
30}
31
32/// Processing refusal and caller refusal keep their independent typed causes.
33#[derive(Debug)]
34pub enum InspectionError<E> {
35    /// Original document or current execution failure.
36    Processing(ScalarError),
37    /// Original callback error, with no stringification or boxing.
38    Visitor(E),
39}
40
41fn element<'a>(
42    view: DocumentView<'a>,
43    owner: NodeIdentity,
44    work: &impl Work,
45) -> Result<crate::xml::dom::Node<'a, 'a>, ScalarError> {
46    charge_bytes(work, 1)?;
47    let node = view.resolve_node(owner)?;
48    if !node.is_element() {
49        return Err(ScalarError::NotElement);
50    }
51    Ok(node)
52}
53
54/// Visit every direct attribute once in source order. Namespace declarations
55/// are excluded (Namespaces in XML 1.0 §6.2), including inherited bindings.
56/// The callback cannot return a borrow into its temporary inspection scope.
57pub fn visit_direct_attributes<W: Work, E>(
58    view: DocumentView<'_>,
59    owner: NodeIdentity,
60    work: &W,
61    mut visitor: impl for<'a> FnMut(DirectAttribute<'a>) -> Result<(), E>,
62) -> Result<(), InspectionError<E>> {
63    let node = element(view, owner, work).map_err(InspectionError::Processing)?;
64    for attribute in node.attributes() {
65        charge_bytes(work, 1).map_err(InspectionError::Processing)?;
66        charge_bytes(work, attribute.name().len()).map_err(InspectionError::Processing)?;
67        charge_bytes(work, attribute.namespace().map_or(0, str::len))
68            .map_err(InspectionError::Processing)?;
69        charge_bytes(work, attribute.value().len()).map_err(InspectionError::Processing)?;
70        visitor(DirectAttribute {
71            owner,
72            namespace: attribute.namespace(),
73            local_name: attribute.name(),
74            value: attribute.value(),
75        })
76        .map_err(InspectionError::Visitor)?;
77    }
78    Ok(())
79}
80
81/// Visit the semantic in-scope namespace axis separately from attributes.
82/// This does not reconstruct which declarations were written on this element.
83pub fn visit_in_scope_namespaces<W: Work, E>(
84    view: DocumentView<'_>,
85    owner: NodeIdentity,
86    work: &W,
87    mut visitor: impl for<'a> FnMut(NamespaceBinding<'a>) -> Result<(), E>,
88) -> Result<(), InspectionError<E>> {
89    let node = element(view, owner, work).map_err(InspectionError::Processing)?;
90    let mut xml_seen = false;
91    for namespace in node.namespaces() {
92        charge_bytes(work, 1).map_err(InspectionError::Processing)?;
93        charge_bytes(work, namespace.name().map_or(0, str::len))
94            .map_err(InspectionError::Processing)?;
95        charge_bytes(work, namespace.uri().len()).map_err(InspectionError::Processing)?;
96        // Namespaces in XML 1.0 §§3, 6.2: xml is always bound, whereas
97        // xmlns="" removes the default binding rather than naming a namespace.
98        // https://www.w3.org/TR/2009/REC-xml-names-20091208/#defaulting
99        if namespace.uri().is_empty() {
100            continue;
101        }
102        if namespace.name() == Some("xml") {
103            xml_seen = true;
104        }
105        visitor(NamespaceBinding {
106            owner,
107            prefix: namespace.name(),
108            uri: namespace.uri(),
109        })
110        .map_err(InspectionError::Visitor)?;
111    }
112    if !xml_seen {
113        const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
114        charge_bytes(work, 1 + 3 + XML_NAMESPACE.len()).map_err(InspectionError::Processing)?;
115        visitor(NamespaceBinding {
116            owner,
117            prefix: Some("xml"),
118            uri: XML_NAMESPACE,
119        })
120        .map_err(InspectionError::Visitor)?;
121    }
122    Ok(())
123}
124
125/// Requested base datatype. Other schemas are explicitly unsupported, not invalid.
126#[derive(Clone, Copy, Debug)]
127pub enum ScalarType<'a> {
128    /// XML Schema 1.0 boolean.
129    Boolean,
130    /// XML Schema 1.0 integer validity, without finite-width conversion.
131    Integer,
132    /// XML Schema 1.0 QName, expanded in the original owner's namespace scope.
133    QName,
134    /// XML Schema 1.0 dateTime, not RFC 3339 or XSD 1.1.
135    DateTime,
136    /// XML Schema 1.0 ID lexical facts, without registering an ID.
137    Id,
138    /// XML Schema 1.0 anyURI, not a URL/scheme policy.
139    AnyUri,
140    /// An expanded schema name for which this kernel has no semantics.
141    Other {
142        /// Original schema namespace URI.
143        namespace: &'a str,
144        /// Original schema local name.
145        local_name: &'a str,
146    },
147}
148
149/// Exact lexical dateTime components. These are not a normalized UTC instant.
150/// In particular hour 24 denotes the following midnight, and absent timezone
151/// is not UTC. No comparison or conversion through a finite numeric year occurs.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct DateTimeLexical<'a> {
154    /// Year sign; XSD 1.0 has no year zero.
155    pub negative_year: bool,
156    /// Four or more original year digits, with no precision limit.
157    pub year_digits: &'a str,
158    /// Gregorian month.
159    pub month: u8,
160    /// Day within the original month.
161    pub day: u8,
162    /// Original hour, including the special end-of-day value 24.
163    pub hour: u8,
164    /// Minute within the hour.
165    pub minute: u8,
166    /// Whole seconds (0 through 60). This does not certify a UTC leap second.
167    pub second: u8,
168    /// Exact fractional digits; no rounding or removal of trailing zeros.
169    pub fraction_digits: Option<&'a str>,
170    /// None preserves an absent timezone; zero denotes explicit UTC.
171    pub timezone_minutes: Option<i16>,
172}
173
174/// Expanded QName value, not an element/attribute name or a resolved schema type.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct QNameValue<'a> {
177    /// Original in-scope namespace URI, or None when no default binding exists.
178    pub namespace: Option<&'a str>,
179    /// Validated original local component, without a prefix or whitespace.
180    pub local_name: &'a str,
181}
182
183/// A supported schema value, preserving exact lexical data when required.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum ScalarValue<'a> {
186    /// Explicit schema boolean.
187    Boolean(bool),
188    /// Valid arbitrary-precision integer spelling; not a numeric value or a
189    /// claim about derived types such as int, nonNegativeInteger or totalDigits.
190    Integer,
191    /// Expanded original QName, without type-definition lookup or schema loading.
192    QName(QNameValue<'a>),
193    /// Exact dateTime lexical components, not canonicalized value fields.
194    DateTime(DateTimeLexical<'a>),
195    /// Collapsed NCName; no ID registration, uniqueness or authentication claim.
196    Id(&'a str),
197    /// Collapsed anyURI spelling; no escaping, absolutization or fetching.
198    AnyUri(&'a str),
199}
200
201/// Source and schema outcomes are facts, not processing failures or defaults.
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum ScalarFact<'a> {
204    /// The selected original attribute is absent.
205    Absent,
206    /// An element child prevents reading simple content.
207    ComplexContent,
208    /// A present lexical value is outside the requested base datatype.
209    Malformed,
210    /// A syntactically valid QName uses this prefix without an in-scope binding.
211    /// This is a value-interpretation fact, not a terminal processing failure.
212    UnboundPrefix(&'a str),
213    /// The requested datatype has no implemented semantics.
214    Unsupported,
215    /// A supported base-datatype value.
216    Value(ScalarValue<'a>),
217}
218
219/// Original XML-normalized lexical text and its separate schema outcome.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct ScalarInspection<'a> {
222    /// Original document/generation-bound element.
223    pub owner: NodeIdentity,
224    /// Original XML text, before schema whitespace collapse; absent/complex has none.
225    pub lexical: Option<&'a str>,
226    /// Exact base schema outcome.
227    pub fact: ScalarFact<'a>,
228}
229
230enum Text<'a, W: Work> {
231    Borrowed(&'a str),
232    Joined(OwnedVec<u8, W>),
233}
234impl<W: Work> Text<'_, W> {
235    fn view(&self) -> &str {
236        match self {
237            Self::Borrowed(text) => text,
238            Self::Joined(bytes) => {
239                std::str::from_utf8(bytes).expect("only UTF-8 fragments appended")
240            }
241        }
242    }
243}
244
245/// Inspect an original attribute or simple-content source within a paid scope.
246/// Temporary joined/collapsed storage is owned by current Work and released
247/// when the callback returns; R cannot carry a borrowed temporary scalar.
248pub fn with_scalar<W: Work, R, E>(
249    view: DocumentView<'_>,
250    owner: NodeIdentity,
251    source: LexicalSource<'_>,
252    datatype: ScalarType<'_>,
253    work: &W,
254    consume: impl for<'a> FnOnce(ScalarInspection<'a>) -> Result<R, E>,
255) -> Result<R, InspectionError<E>> {
256    let node = element(view, owner, work).map_err(InspectionError::Processing)?;
257    let read = || -> Result<_, ScalarError> {
258        match source {
259            LexicalSource::Attribute {
260                namespace,
261                local_name,
262            } => {
263                for attribute in node.attributes() {
264                    charge_bytes(work, 1)?;
265                    charge_bytes(work, attribute.name().len())?;
266                    charge_bytes(work, attribute.namespace().map_or(0, str::len))?;
267                    if attribute.name() == local_name && attribute.namespace() == namespace {
268                        admit_scalar_passes(work, attribute.value().len())?;
269                        return Ok(Ok(Some(Text::Borrowed(attribute.value()))));
270                    }
271                }
272                Ok(Ok(None))
273            }
274            LexicalSource::SimpleContent => {
275                // Validate structure before creating any joined prefix. A malformed
276                // simple-content source must not allocate only to discard the text.
277                let mut text_bytes = 0usize;
278                let mut child_visits = 0usize;
279                for child in node.children() {
280                    charge_bytes(work, 1)?;
281                    child_visits = child_visits.checked_add(1).ok_or(ScalarError::Resource(
282                        structured_asn1::memory::MemoryError::WorkExceeded,
283                    ))?;
284                    if child.is_element() {
285                        return Ok(Err(()));
286                    }
287                    if child.is_text() {
288                        let text = child.text().expect("text node has a text value");
289                        text_bytes =
290                            text_bytes
291                                .checked_add(text.len())
292                                .ok_or(ScalarError::Resource(
293                                    structured_asn1::memory::MemoryError::WorkExceeded,
294                                ))?;
295                    }
296                }
297                // Copies have their own OwnedVec work charge, but admit every
298                // subsequent linear pass before constructing a doomed join.
299                admit_scalar_passes(work, text_bytes)?;
300                // The second traversal also visits comments and PIs, even when
301                // no text remains. Admit those visits before the first join.
302                charge_bytes(work, child_visits)?;
303                let mut value = Text::Borrowed("");
304                for child in node.children() {
305                    if child.is_element() {
306                        return Ok(Err(()));
307                    }
308                    if !child.is_text() {
309                        continue;
310                    }
311                    let text = child.text().expect("text node has a text value");
312                    match &mut value {
313                        Text::Borrowed(first) if first.is_empty() => *first = text,
314                        Text::Borrowed(first) => {
315                            let mut bytes = work.vector();
316                            bytes
317                                .try_reserve_exact(text_bytes)
318                                .map_err(ScalarError::Resource)?;
319                            bytes
320                                .try_extend_from_slice(first.as_bytes())
321                                .map_err(ScalarError::Resource)?;
322                            bytes
323                                .try_extend_from_slice(text.as_bytes())
324                                .map_err(ScalarError::Resource)?;
325                            value = Text::Joined(bytes);
326                        }
327                        Text::Joined(bytes) => bytes
328                            .try_extend_from_slice(text.as_bytes())
329                            .map_err(ScalarError::Resource)?,
330                    }
331                }
332                Ok(Ok(Some(value)))
333            }
334        }
335    };
336    let value = read().map_err(InspectionError::Processing)?;
337    let value = match value {
338        Err(()) => {
339            return consume(ScalarInspection {
340                owner,
341                lexical: None,
342                fact: ScalarFact::ComplexContent,
343            })
344            .map_err(InspectionError::Visitor);
345        }
346        Ok(None) => {
347            return consume(ScalarInspection {
348                owner,
349                lexical: None,
350                fact: ScalarFact::Absent,
351            })
352            .map_err(InspectionError::Visitor);
353        }
354        Ok(Some(value)) => value,
355    };
356    let lexical = value.view();
357    let collapsed = match datatype {
358        ScalarType::Other { .. } | ScalarType::Integer | ScalarType::QName => {
359            Text::Borrowed(lexical)
360        }
361        _ => collapse(lexical, work).map_err(InspectionError::Processing)?,
362    };
363    let text = collapsed.view();
364    let fact = match datatype {
365        ScalarType::Other { .. } => ScalarFact::Unsupported,
366        ScalarType::Boolean => match text {
367            "true" | "1" => ScalarFact::Value(ScalarValue::Boolean(true)),
368            "false" | "0" => ScalarFact::Value(ScalarValue::Boolean(false)),
369            _ => ScalarFact::Malformed,
370        },
371        ScalarType::Integer => {
372            if valid_integer(text) {
373                ScalarFact::Value(ScalarValue::Integer)
374            } else {
375                ScalarFact::Malformed
376            }
377        }
378        ScalarType::QName => {
379            inspect_qname(node, text, work).map_err(InspectionError::Processing)?
380        }
381        ScalarType::DateTime => parse_date_time(text)
382            .map(|value| ScalarFact::Value(ScalarValue::DateTime(value)))
383            .unwrap_or(ScalarFact::Malformed),
384        ScalarType::Id => {
385            // XSD 1.0 §3.3.8; this is lexical validity, not uniqueness.
386            // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#ID
387            if crate::xml::is_xml_ncname(text) {
388                ScalarFact::Value(ScalarValue::Id(text))
389            } else {
390                ScalarFact::Malformed
391            }
392        }
393        ScalarType::AnyUri => {
394            if valid_any_uri(text) {
395                ScalarFact::Value(ScalarValue::AnyUri(text))
396            } else {
397                ScalarFact::Malformed
398            }
399        }
400    };
401    consume(ScalarInspection {
402        owner,
403        lexical: Some(lexical),
404        fact,
405    })
406    .map_err(InspectionError::Visitor)
407}
408
409fn whitespace(byte: u8) -> bool {
410    matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
411}
412
413fn inspect_qname<'a>(
414    node: crate::xml::dom::Node<'a, 'a>,
415    text: &'a str,
416    work: &impl Work,
417) -> Result<ScalarFact<'a>, ScalarError> {
418    // XSD 1.0 Part 2 §§3.2.18,4.3.6: a QName contains no internal whitespace,
419    // so trimming XML whitespace is collapse-equivalent without a new buffer.
420    // Namespaces in XML 1.0 §3 supplies the shared NCName grammar.
421    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#QName
422    // https://www.w3.org/TR/2009/REC-xml-names-20091208/#ns-qualnames
423    let text = text.trim_matches([' ', '\t', '\r', '\n']);
424    let (prefix, local_name) = match text.split_once(':') {
425        Some((prefix, local_name)) => {
426            if !crate::xml::is_xml_ncname(prefix) {
427                return Ok(ScalarFact::Malformed);
428            }
429            (Some(prefix), local_name)
430        }
431        None => (None, text),
432    };
433    if !crate::xml::is_xml_ncname(local_name) {
434        return Ok(ScalarFact::Malformed);
435    }
436    // XSD 1.0 Part 1 §3.15.3: QName *values*, including xsi:type (§2.6.1),
437    // use the containing element's bindings, including the default namespace.
438    // This differs from the namespace rule for unprefixed attribute *names*.
439    // A binding does not resolve the value to any schema declaration (§3.15.4).
440    // https://www.w3.org/TR/2004/REC-xmlschema-1-20041028/#src-qname
441    let mut namespace = None;
442    if prefix == Some("xml") {
443        // Namespaces in XML 1.0 §3: xml is bound even without a declaration.
444        // https://www.w3.org/TR/2009/REC-xml-names-20091208/#ns-decl
445        const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
446        charge_bytes(work, 1 + 3 + XML_NAMESPACE.len())?;
447        namespace = Some(XML_NAMESPACE);
448    } else {
449        for binding in node.namespaces() {
450            charge_bytes(work, 1)?;
451            if let Some(name) = binding.name() {
452                charge_bytes(work, name.len())?;
453            }
454            if binding.name() != prefix {
455                continue;
456            }
457            charge_bytes(work, binding.uri().len())?;
458            // An empty default declaration removes the binding (§6.2);
459            // it is not a namespace whose name is the empty string.
460            // https://www.w3.org/TR/2009/REC-xml-names-20091208/#defaulting
461            if !binding.uri().is_empty() {
462                namespace = Some(binding.uri());
463            }
464            break;
465        }
466    }
467    if let Some(prefix) = prefix
468        && namespace.is_none()
469    {
470        return Ok(ScalarFact::UnboundPrefix(prefix));
471    }
472    Ok(ScalarFact::Value(ScalarValue::QName(QNameValue {
473        namespace,
474        local_name,
475    })))
476}
477
478fn valid_integer(text: &str) -> bool {
479    // XSD 1.0 §§3.3.13.1,4.3.6: optional sign and one or more ASCII digits
480    // after collapse. Internal whitespace always invalidates this grammar, so
481    // trimming XML whitespace is equivalent without allocating a collapsed copy.
482    // Canonical-form rules (§3.3.13.2) do not restrict lexical validity.
483    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#integer
484    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#rf-whiteSpace
485    let text = text.trim_matches([' ', '\t', '\r', '\n']);
486    let digits = match text.as_bytes().first() {
487        Some(b'+' | b'-') => &text.as_bytes()[1..],
488        _ => text.as_bytes(),
489    };
490    if digits.is_empty() {
491        return false;
492    }
493    for digit in digits {
494        if !digit.is_ascii_digit() {
495            return false;
496        }
497    }
498    true
499}
500
501fn collapse<'a, W: Work>(input: &'a str, work: &W) -> Result<Text<'a, W>, ScalarError> {
502    // XSD 1.0 §4.3.6: only the four XML whitespace characters collapse.
503    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#rf-whiteSpace
504    let text = input.trim_matches([' ', '\t', '\r', '\n']);
505    let mut previous_space = false;
506    let mut needs_copy = false;
507    for byte in text.bytes() {
508        if whitespace(byte) && (byte != b' ' || previous_space) {
509            needs_copy = true;
510            break;
511        }
512        previous_space = byte == b' ';
513    }
514    if !needs_copy {
515        return Ok(Text::Borrowed(text));
516    }
517    let mut bytes = work.vector();
518    bytes
519        .try_reserve_exact(text.len())
520        .map_err(ScalarError::Resource)?;
521    let mut space = false;
522    for byte in text.bytes() {
523        if whitespace(byte) {
524            space = true;
525            continue;
526        }
527        if space {
528            bytes.try_push(b' ').map_err(ScalarError::Resource)?;
529            space = false;
530        }
531        bytes.try_push(byte).map_err(ScalarError::Resource)?;
532    }
533    Ok(Text::Joined(bytes))
534}
535
536fn two(input: &str) -> Option<u8> {
537    let bytes = input.as_bytes();
538    if bytes.len() != 2 || !bytes[0].is_ascii_digit() || !bytes[1].is_ascii_digit() {
539        return None;
540    }
541    Some((bytes[0] - b'0') * 10 + bytes[1] - b'0')
542}
543
544fn parse_date_time(input: &str) -> Option<DateTimeLexical<'_>> {
545    // XSD 1.0 §§3.2.7.1,3.2.7.3 and Appendix E: retain original lexical
546    // components, including end-of-day; never substitute XSD 1.1 year zero.
547    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#dateTime-lexical-representation
548    let (negative_year, rest) = input
549        .strip_prefix('-')
550        .map_or((false, input), |s| (true, s));
551    let (year, tail) = rest.split_once('-')?;
552    if year.len() < 4 || (year.len() > 4 && year.starts_with('0')) {
553        return None;
554    }
555    let mut modulo = 0u16;
556    let mut nonzero = false;
557    for digit in year.bytes() {
558        if !digit.is_ascii_digit() {
559            return None;
560        }
561        nonzero |= digit != b'0';
562        modulo = (modulo * 10 + u16::from(digit - b'0')) % 400;
563    }
564    if !nonzero {
565        return None;
566    }
567    let (month, tail) = tail.split_once('-')?;
568    let (day, tail) = tail.split_once('T')?;
569    let month = two(month)?;
570    let day = two(day)?;
571    let (hour, tail) = tail.split_once(':')?;
572    let (minute, tail) = tail.split_once(':')?;
573    let hour = two(hour)?;
574    let minute = two(minute)?;
575    let second = two(tail.get(..2)?)?;
576    let mut tail = tail.get(2..)?;
577    let fraction_digits = if let Some(fraction) = tail.strip_prefix('.') {
578        let end = fraction
579            .bytes()
580            .position(|c| !c.is_ascii_digit())
581            .unwrap_or(fraction.len());
582        if end == 0 {
583            return None;
584        }
585        tail = &fraction[end..];
586        Some(&fraction[..end])
587    } else {
588        None
589    };
590    let timezone_minutes = if tail.is_empty() {
591        None
592    } else if tail == "Z" {
593        Some(0)
594    } else {
595        let negative = match tail.as_bytes()[0] {
596            b'-' => true,
597            b'+' => false,
598            _ => return None,
599        };
600        let (h, m) = tail.get(1..)?.split_once(':')?;
601        let h = two(h)?;
602        let m = two(m)?;
603        if h > 14 || m > 59 || (h == 14 && m != 0) {
604            return None;
605        }
606        let offset = i16::from(h) * 60 + i16::from(m);
607        Some(if negative { -offset } else { offset })
608    };
609    let maximum_day = match month {
610        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
611        4 | 6 | 9 | 11 => 30,
612        2 if modulo == 0 || (!modulo.is_multiple_of(100) && modulo.is_multiple_of(4)) => 29,
613        2 => 28,
614        _ => return None,
615    };
616    // XSD 1.0 Appendix D.1 permits 60 (unlike XSD 1.1 and libxml2).
617    // Retain it as a lexical fact: UTC-adjustment validation and arithmetic
618    // require a consumer's temporal evidence, not a POSIX normalization here.
619    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#isoformats
620    if day == 0 || day > maximum_day || hour > 24 || minute > 59 || second > 60 {
621        return None;
622    }
623    if hour == 24
624        && (minute != 0
625            || second != 0
626            || fraction_digits.is_some_and(|f| f.bytes().any(|d| d != b'0')))
627    {
628        return None;
629    }
630    Some(DateTimeLexical {
631        negative_year,
632        year_digits: year,
633        month,
634        day,
635        hour,
636        minute,
637        second,
638        fraction_digits,
639        timezone_minutes,
640    })
641}
642
643fn escaped(byte: u8) -> bool {
644    byte <= 0x20
645        || byte >= 0x7f
646        || matches!(
647            byte,
648            b'<' | b'>' | b'"' | b'{' | b'}' | b'|' | b'\\' | b'^' | b'`'
649        )
650}
651fn unreserved(byte: u8) -> bool {
652    escaped(byte)
653        || byte.is_ascii_alphanumeric()
654        || matches!(
655            byte,
656            b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
657        )
658}
659fn uri_chars(input: &str, extra: &[u8]) -> bool {
660    let mut bytes = input.bytes();
661    while let Some(byte) = bytes.next() {
662        if byte == b'%' {
663            if !bytes.next().is_some_and(|b| b.is_ascii_hexdigit())
664                || !bytes.next().is_some_and(|b| b.is_ascii_hexdigit())
665            {
666                return false;
667            }
668        } else if !unreserved(byte) && !extra.contains(&byte) {
669            return false;
670        }
671    }
672    true
673}
674fn admit_scalar_passes(work: &impl Work, bytes: usize) -> Result<(), ScalarError> {
675    // UTF-8 validation, whitespace handling and base datatype parsing. OwnedVec
676    // separately admits copying; no owned intermediate precedes this bound.
677    let bound = bytes.checked_mul(12).ok_or(ScalarError::Resource(
678        structured_asn1::memory::MemoryError::WorkExceeded,
679    ))?;
680    charge_bytes(work, bound)
681}
682
683fn valid_any_uri(input: &str) -> bool {
684    // XSD 1.0 §3.2.17.1 incorporates RFC 2396 + RFC 2732, not URL scheme
685    // validation or implicit XML Base. XLink 1.0 §5.4 escapes spaces/non-ASCII
686    // and excluded characters; they are grammar-equivalent to an escaped octet
687    // here, so validation needs no escaped copy.
688    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#anyURI
689    // https://www.rfc-editor.org/rfc/rfc2396#appendix-A
690    let (body, fragment) = input
691        .split_once('#')
692        .map_or((input, None), |(a, b)| (a, Some(b)));
693    if fragment.is_some_and(|f| !uri_chars(f, b";/?:@&=+$,[]")) {
694        return false;
695    }
696    let (path, query) = body
697        .split_once('?')
698        .map_or((body, None), |(a, b)| (a, Some(b)));
699    if query.is_some_and(|q| !uri_chars(q, b";/?:@&=+$,[]")) {
700        return false;
701    }
702    let mut path = path;
703    if let Some(colon) = body.find(':') {
704        let prefix = &body[..colon];
705        if !prefix.contains(['/', '?']) {
706            let mut scheme = prefix.bytes();
707            if !scheme.next().is_some_and(|c| c.is_ascii_alphabetic())
708                || !scheme.all(|c| c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.'))
709            {
710                return false;
711            }
712            let rest = &body[colon + 1..];
713            if rest.is_empty() {
714                return false;
715            }
716            if !rest.starts_with('/') {
717                // RFC 2396 §3 / Appendix A: opaque_part begins with
718                // uric_no_slash, not uric. RFC 2732 §3 expands reserved/uric
719                // with brackets but does not add them to uric_no_slash.
720                // Escaped brackets and brackets after the first token remain legal.
721                // https://www.rfc-editor.org/rfc/rfc2396#section-3
722                // https://www.rfc-editor.org/rfc/rfc2732#section-3
723                if matches!(rest.as_bytes()[0], b'[' | b']') {
724                    return false;
725                }
726                return uri_chars(rest, b";/?:@&=+$,[]");
727            }
728            path = &path[colon + 1..];
729        }
730    }
731    if let Some(rest) = path.strip_prefix("//") {
732        let (authority, suffix) = rest.split_once('/').map_or((rest, ""), |(a, b)| (a, b));
733        if authority.contains(['[', ']']) {
734            let server = authority
735                .rsplit_once('@')
736                .map_or(authority, |(user, host)| {
737                    if uri_chars(user, b";:&=+$,") {
738                        host
739                    } else {
740                        ""
741                    }
742                });
743            let Some(server) = server.strip_prefix('[') else {
744                return false;
745            };
746            let Some((address, port)) = server.split_once(']') else {
747                return false;
748            };
749            if !valid_ipv6_reference(address) {
750                return false;
751            }
752            if !port.is_empty()
753                && !port
754                    .strip_prefix(':')
755                    .is_some_and(|p| p.bytes().all(|b| b.is_ascii_digit()))
756            {
757                return false;
758            }
759        } else if !uri_chars(authority, b"$,:;@&=+") {
760            return false;
761        }
762        path = suffix;
763    }
764    uri_chars(path, b"/;:@&=+$,")
765}
766
767fn valid_ipv6_reference(address: &str) -> bool {
768    // XSD 1.0 §3.2.17.1 incorporates RFC 2732 §3 and RFC 2373 Appendix B:
769    // embedded IPv4 octets are 1*3DIGIT, including leading zeroes. RFC 2373
770    // §2.2(3) still requires 8-bit decimal values, not arbitrary three digits.
771    // https://www.rfc-editor.org/rfc/rfc2732#section-3
772    // https://www.rfc-editor.org/rfc/rfc2373#appendix-B
773    if let Some((left, right)) = address.split_once("::") {
774        let Some(left) = ipv6_units(left, false) else {
775            return false;
776        };
777        let Some(right) = ipv6_units(right, true) else {
778            return false;
779        };
780        left + right < 8
781    } else {
782        ipv6_units(address, true) == Some(8)
783    }
784}
785
786fn ipv6_units(part: &str, allow_ipv4_tail: bool) -> Option<usize> {
787    if part.is_empty() {
788        return Some(0);
789    }
790    let mut units = 0;
791    let mut groups = part.split(':').peekable();
792    while let Some(group) = groups.next() {
793        if group.contains('.') {
794            if !allow_ipv4_tail || groups.peek().is_some() {
795                return None;
796            }
797            let mut count = 0;
798            for octet in group.split('.') {
799                if octet.is_empty() || octet.len() > 3 {
800                    return None;
801                }
802                let mut value = 0u16;
803                for byte in octet.bytes() {
804                    if !byte.is_ascii_digit() {
805                        return None;
806                    }
807                    value = value * 10 + u16::from(byte - b'0');
808                }
809                if value > 255 {
810                    return None;
811                }
812                count += 1;
813            }
814            if count != 4 {
815                return None;
816            }
817            units += 2;
818        } else {
819            if group.is_empty() || group.len() > 4 || !group.bytes().all(|b| b.is_ascii_hexdigit())
820            {
821                return None;
822            }
823            units += 1;
824        }
825        if units > 8 {
826            return None;
827        }
828    }
829    Some(units)
830}