1use super::{ScalarError, charge_bytes};
4use crate::document::{DocumentView, NodeIdentity};
5use crate::xmldsig::LexicalSource;
6use structured_asn1::memory::{OwnedVec, Work};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct DirectAttribute<'a> {
11 pub owner: NodeIdentity,
13 pub namespace: Option<&'a str>,
15 pub local_name: &'a str,
17 pub value: &'a str,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct NamespaceBinding<'a> {
24 pub owner: NodeIdentity,
26 pub prefix: Option<&'a str>,
28 pub uri: &'a str,
30}
31
32#[derive(Debug)]
34pub enum InspectionError<E> {
35 Processing(ScalarError),
37 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
54pub 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
81pub 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 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#[derive(Clone, Copy, Debug)]
127pub enum ScalarType<'a> {
128 Boolean,
130 Integer,
132 QName,
134 DateTime,
136 Id,
138 AnyUri,
140 Other {
142 namespace: &'a str,
144 local_name: &'a str,
146 },
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct DateTimeLexical<'a> {
154 pub negative_year: bool,
156 pub year_digits: &'a str,
158 pub month: u8,
160 pub day: u8,
162 pub hour: u8,
164 pub minute: u8,
166 pub second: u8,
168 pub fraction_digits: Option<&'a str>,
170 pub timezone_minutes: Option<i16>,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct QNameValue<'a> {
177 pub namespace: Option<&'a str>,
179 pub local_name: &'a str,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum ScalarValue<'a> {
186 Boolean(bool),
188 Integer,
191 QName(QNameValue<'a>),
193 DateTime(DateTimeLexical<'a>),
195 Id(&'a str),
197 AnyUri(&'a str),
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum ScalarFact<'a> {
204 Absent,
206 ComplexContent,
208 Malformed,
210 UnboundPrefix(&'a str),
213 Unsupported,
215 Value(ScalarValue<'a>),
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct ScalarInspection<'a> {
222 pub owner: NodeIdentity,
224 pub lexical: Option<&'a str>,
226 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
245pub 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 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 admit_scalar_passes(work, text_bytes)?;
300 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 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 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 let mut namespace = None;
442 if prefix == Some("xml") {
443 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 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 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 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 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 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 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 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 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 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}