use super::{ScalarError, charge_bytes};
use crate::document::{DocumentView, NodeIdentity};
use crate::xmldsig::LexicalSource;
use structured_asn1::memory::{OwnedVec, Work};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DirectAttribute<'a> {
pub owner: NodeIdentity,
pub namespace: Option<&'a str>,
pub local_name: &'a str,
pub value: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NamespaceBinding<'a> {
pub owner: NodeIdentity,
pub prefix: Option<&'a str>,
pub uri: &'a str,
}
#[derive(Debug)]
pub enum InspectionError<E> {
Processing(ScalarError),
Visitor(E),
}
fn element<'a>(
view: DocumentView<'a>,
owner: NodeIdentity,
work: &impl Work,
) -> Result<crate::xml::dom::Node<'a, 'a>, ScalarError> {
charge_bytes(work, 1)?;
let node = view.resolve_node(owner)?;
if !node.is_element() {
return Err(ScalarError::NotElement);
}
Ok(node)
}
pub fn visit_direct_attributes<W: Work, E>(
view: DocumentView<'_>,
owner: NodeIdentity,
work: &W,
mut visitor: impl for<'a> FnMut(DirectAttribute<'a>) -> Result<(), E>,
) -> Result<(), InspectionError<E>> {
let node = element(view, owner, work).map_err(InspectionError::Processing)?;
for attribute in node.attributes() {
charge_bytes(work, 1).map_err(InspectionError::Processing)?;
charge_bytes(work, attribute.name().len()).map_err(InspectionError::Processing)?;
charge_bytes(work, attribute.namespace().map_or(0, str::len))
.map_err(InspectionError::Processing)?;
charge_bytes(work, attribute.value().len()).map_err(InspectionError::Processing)?;
visitor(DirectAttribute {
owner,
namespace: attribute.namespace(),
local_name: attribute.name(),
value: attribute.value(),
})
.map_err(InspectionError::Visitor)?;
}
Ok(())
}
pub fn visit_in_scope_namespaces<W: Work, E>(
view: DocumentView<'_>,
owner: NodeIdentity,
work: &W,
mut visitor: impl for<'a> FnMut(NamespaceBinding<'a>) -> Result<(), E>,
) -> Result<(), InspectionError<E>> {
let node = element(view, owner, work).map_err(InspectionError::Processing)?;
let mut xml_seen = false;
for namespace in node.namespaces() {
charge_bytes(work, 1).map_err(InspectionError::Processing)?;
charge_bytes(work, namespace.name().map_or(0, str::len))
.map_err(InspectionError::Processing)?;
charge_bytes(work, namespace.uri().len()).map_err(InspectionError::Processing)?;
if namespace.uri().is_empty() {
continue;
}
if namespace.name() == Some("xml") {
xml_seen = true;
}
visitor(NamespaceBinding {
owner,
prefix: namespace.name(),
uri: namespace.uri(),
})
.map_err(InspectionError::Visitor)?;
}
if !xml_seen {
const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
charge_bytes(work, 1 + 3 + XML_NAMESPACE.len()).map_err(InspectionError::Processing)?;
visitor(NamespaceBinding {
owner,
prefix: Some("xml"),
uri: XML_NAMESPACE,
})
.map_err(InspectionError::Visitor)?;
}
Ok(())
}
#[derive(Clone, Copy, Debug)]
pub enum ScalarType<'a> {
Boolean,
Integer,
QName,
DateTime,
Id,
AnyUri,
Other {
namespace: &'a str,
local_name: &'a str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateTimeLexical<'a> {
pub negative_year: bool,
pub year_digits: &'a str,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub fraction_digits: Option<&'a str>,
pub timezone_minutes: Option<i16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QNameValue<'a> {
pub namespace: Option<&'a str>,
pub local_name: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarValue<'a> {
Boolean(bool),
Integer,
QName(QNameValue<'a>),
DateTime(DateTimeLexical<'a>),
Id(&'a str),
AnyUri(&'a str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarFact<'a> {
Absent,
ComplexContent,
Malformed,
UnboundPrefix(&'a str),
Unsupported,
Value(ScalarValue<'a>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScalarInspection<'a> {
pub owner: NodeIdentity,
pub lexical: Option<&'a str>,
pub fact: ScalarFact<'a>,
}
enum Text<'a, W: Work> {
Borrowed(&'a str),
Joined(OwnedVec<u8, W>),
}
impl<W: Work> Text<'_, W> {
fn view(&self) -> &str {
match self {
Self::Borrowed(text) => text,
Self::Joined(bytes) => {
std::str::from_utf8(bytes).expect("only UTF-8 fragments appended")
}
}
}
}
pub fn with_scalar<W: Work, R, E>(
view: DocumentView<'_>,
owner: NodeIdentity,
source: LexicalSource<'_>,
datatype: ScalarType<'_>,
work: &W,
consume: impl for<'a> FnOnce(ScalarInspection<'a>) -> Result<R, E>,
) -> Result<R, InspectionError<E>> {
let node = element(view, owner, work).map_err(InspectionError::Processing)?;
let read = || -> Result<_, ScalarError> {
match source {
LexicalSource::Attribute {
namespace,
local_name,
} => {
for attribute in node.attributes() {
charge_bytes(work, 1)?;
charge_bytes(work, attribute.name().len())?;
charge_bytes(work, attribute.namespace().map_or(0, str::len))?;
if attribute.name() == local_name && attribute.namespace() == namespace {
admit_scalar_passes(work, attribute.value().len())?;
return Ok(Ok(Some(Text::Borrowed(attribute.value()))));
}
}
Ok(Ok(None))
}
LexicalSource::SimpleContent => {
let mut text_bytes = 0usize;
let mut child_visits = 0usize;
for child in node.children() {
charge_bytes(work, 1)?;
child_visits = child_visits.checked_add(1).ok_or(ScalarError::Resource(
structured_asn1::memory::MemoryError::WorkExceeded,
))?;
if child.is_element() {
return Ok(Err(()));
}
if child.is_text() {
let text = child.text().expect("text node has a text value");
text_bytes =
text_bytes
.checked_add(text.len())
.ok_or(ScalarError::Resource(
structured_asn1::memory::MemoryError::WorkExceeded,
))?;
}
}
admit_scalar_passes(work, text_bytes)?;
charge_bytes(work, child_visits)?;
let mut value = Text::Borrowed("");
for child in node.children() {
if child.is_element() {
return Ok(Err(()));
}
if !child.is_text() {
continue;
}
let text = child.text().expect("text node has a text value");
match &mut value {
Text::Borrowed(first) if first.is_empty() => *first = text,
Text::Borrowed(first) => {
let mut bytes = work.vector();
bytes
.try_reserve_exact(text_bytes)
.map_err(ScalarError::Resource)?;
bytes
.try_extend_from_slice(first.as_bytes())
.map_err(ScalarError::Resource)?;
bytes
.try_extend_from_slice(text.as_bytes())
.map_err(ScalarError::Resource)?;
value = Text::Joined(bytes);
}
Text::Joined(bytes) => bytes
.try_extend_from_slice(text.as_bytes())
.map_err(ScalarError::Resource)?,
}
}
Ok(Ok(Some(value)))
}
}
};
let value = read().map_err(InspectionError::Processing)?;
let value = match value {
Err(()) => {
return consume(ScalarInspection {
owner,
lexical: None,
fact: ScalarFact::ComplexContent,
})
.map_err(InspectionError::Visitor);
}
Ok(None) => {
return consume(ScalarInspection {
owner,
lexical: None,
fact: ScalarFact::Absent,
})
.map_err(InspectionError::Visitor);
}
Ok(Some(value)) => value,
};
let lexical = value.view();
let collapsed = match datatype {
ScalarType::Other { .. } | ScalarType::Integer | ScalarType::QName => {
Text::Borrowed(lexical)
}
_ => collapse(lexical, work).map_err(InspectionError::Processing)?,
};
let text = collapsed.view();
let fact = match datatype {
ScalarType::Other { .. } => ScalarFact::Unsupported,
ScalarType::Boolean => match text {
"true" | "1" => ScalarFact::Value(ScalarValue::Boolean(true)),
"false" | "0" => ScalarFact::Value(ScalarValue::Boolean(false)),
_ => ScalarFact::Malformed,
},
ScalarType::Integer => {
if valid_integer(text) {
ScalarFact::Value(ScalarValue::Integer)
} else {
ScalarFact::Malformed
}
}
ScalarType::QName => {
inspect_qname(node, text, work).map_err(InspectionError::Processing)?
}
ScalarType::DateTime => parse_date_time(text)
.map(|value| ScalarFact::Value(ScalarValue::DateTime(value)))
.unwrap_or(ScalarFact::Malformed),
ScalarType::Id => {
if crate::xml::is_xml_ncname(text) {
ScalarFact::Value(ScalarValue::Id(text))
} else {
ScalarFact::Malformed
}
}
ScalarType::AnyUri => {
if valid_any_uri(text) {
ScalarFact::Value(ScalarValue::AnyUri(text))
} else {
ScalarFact::Malformed
}
}
};
consume(ScalarInspection {
owner,
lexical: Some(lexical),
fact,
})
.map_err(InspectionError::Visitor)
}
fn whitespace(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
}
fn inspect_qname<'a>(
node: crate::xml::dom::Node<'a, 'a>,
text: &'a str,
work: &impl Work,
) -> Result<ScalarFact<'a>, ScalarError> {
let text = text.trim_matches([' ', '\t', '\r', '\n']);
let (prefix, local_name) = match text.split_once(':') {
Some((prefix, local_name)) => {
if !crate::xml::is_xml_ncname(prefix) {
return Ok(ScalarFact::Malformed);
}
(Some(prefix), local_name)
}
None => (None, text),
};
if !crate::xml::is_xml_ncname(local_name) {
return Ok(ScalarFact::Malformed);
}
let mut namespace = None;
if prefix == Some("xml") {
const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
charge_bytes(work, 1 + 3 + XML_NAMESPACE.len())?;
namespace = Some(XML_NAMESPACE);
} else {
for binding in node.namespaces() {
charge_bytes(work, 1)?;
if let Some(name) = binding.name() {
charge_bytes(work, name.len())?;
}
if binding.name() != prefix {
continue;
}
charge_bytes(work, binding.uri().len())?;
if !binding.uri().is_empty() {
namespace = Some(binding.uri());
}
break;
}
}
if let Some(prefix) = prefix
&& namespace.is_none()
{
return Ok(ScalarFact::UnboundPrefix(prefix));
}
Ok(ScalarFact::Value(ScalarValue::QName(QNameValue {
namespace,
local_name,
})))
}
fn valid_integer(text: &str) -> bool {
let text = text.trim_matches([' ', '\t', '\r', '\n']);
let digits = match text.as_bytes().first() {
Some(b'+' | b'-') => &text.as_bytes()[1..],
_ => text.as_bytes(),
};
if digits.is_empty() {
return false;
}
for digit in digits {
if !digit.is_ascii_digit() {
return false;
}
}
true
}
fn collapse<'a, W: Work>(input: &'a str, work: &W) -> Result<Text<'a, W>, ScalarError> {
let text = input.trim_matches([' ', '\t', '\r', '\n']);
let mut previous_space = false;
let mut needs_copy = false;
for byte in text.bytes() {
if whitespace(byte) && (byte != b' ' || previous_space) {
needs_copy = true;
break;
}
previous_space = byte == b' ';
}
if !needs_copy {
return Ok(Text::Borrowed(text));
}
let mut bytes = work.vector();
bytes
.try_reserve_exact(text.len())
.map_err(ScalarError::Resource)?;
let mut space = false;
for byte in text.bytes() {
if whitespace(byte) {
space = true;
continue;
}
if space {
bytes.try_push(b' ').map_err(ScalarError::Resource)?;
space = false;
}
bytes.try_push(byte).map_err(ScalarError::Resource)?;
}
Ok(Text::Joined(bytes))
}
fn two(input: &str) -> Option<u8> {
let bytes = input.as_bytes();
if bytes.len() != 2 || !bytes[0].is_ascii_digit() || !bytes[1].is_ascii_digit() {
return None;
}
Some((bytes[0] - b'0') * 10 + bytes[1] - b'0')
}
fn parse_date_time(input: &str) -> Option<DateTimeLexical<'_>> {
let (negative_year, rest) = input
.strip_prefix('-')
.map_or((false, input), |s| (true, s));
let (year, tail) = rest.split_once('-')?;
if year.len() < 4 || (year.len() > 4 && year.starts_with('0')) {
return None;
}
let mut modulo = 0u16;
let mut nonzero = false;
for digit in year.bytes() {
if !digit.is_ascii_digit() {
return None;
}
nonzero |= digit != b'0';
modulo = (modulo * 10 + u16::from(digit - b'0')) % 400;
}
if !nonzero {
return None;
}
let (month, tail) = tail.split_once('-')?;
let (day, tail) = tail.split_once('T')?;
let month = two(month)?;
let day = two(day)?;
let (hour, tail) = tail.split_once(':')?;
let (minute, tail) = tail.split_once(':')?;
let hour = two(hour)?;
let minute = two(minute)?;
let second = two(tail.get(..2)?)?;
let mut tail = tail.get(2..)?;
let fraction_digits = if let Some(fraction) = tail.strip_prefix('.') {
let end = fraction
.bytes()
.position(|c| !c.is_ascii_digit())
.unwrap_or(fraction.len());
if end == 0 {
return None;
}
tail = &fraction[end..];
Some(&fraction[..end])
} else {
None
};
let timezone_minutes = if tail.is_empty() {
None
} else if tail == "Z" {
Some(0)
} else {
let negative = match tail.as_bytes()[0] {
b'-' => true,
b'+' => false,
_ => return None,
};
let (h, m) = tail.get(1..)?.split_once(':')?;
let h = two(h)?;
let m = two(m)?;
if h > 14 || m > 59 || (h == 14 && m != 0) {
return None;
}
let offset = i16::from(h) * 60 + i16::from(m);
Some(if negative { -offset } else { offset })
};
let maximum_day = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if modulo == 0 || (!modulo.is_multiple_of(100) && modulo.is_multiple_of(4)) => 29,
2 => 28,
_ => return None,
};
if day == 0 || day > maximum_day || hour > 24 || minute > 59 || second > 60 {
return None;
}
if hour == 24
&& (minute != 0
|| second != 0
|| fraction_digits.is_some_and(|f| f.bytes().any(|d| d != b'0')))
{
return None;
}
Some(DateTimeLexical {
negative_year,
year_digits: year,
month,
day,
hour,
minute,
second,
fraction_digits,
timezone_minutes,
})
}
fn escaped(byte: u8) -> bool {
byte <= 0x20
|| byte >= 0x7f
|| matches!(
byte,
b'<' | b'>' | b'"' | b'{' | b'}' | b'|' | b'\\' | b'^' | b'`'
)
}
fn unreserved(byte: u8) -> bool {
escaped(byte)
|| byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
)
}
fn uri_chars(input: &str, extra: &[u8]) -> bool {
let mut bytes = input.bytes();
while let Some(byte) = bytes.next() {
if byte == b'%' {
if !bytes.next().is_some_and(|b| b.is_ascii_hexdigit())
|| !bytes.next().is_some_and(|b| b.is_ascii_hexdigit())
{
return false;
}
} else if !unreserved(byte) && !extra.contains(&byte) {
return false;
}
}
true
}
fn admit_scalar_passes(work: &impl Work, bytes: usize) -> Result<(), ScalarError> {
let bound = bytes.checked_mul(12).ok_or(ScalarError::Resource(
structured_asn1::memory::MemoryError::WorkExceeded,
))?;
charge_bytes(work, bound)
}
fn valid_any_uri(input: &str) -> bool {
let (body, fragment) = input
.split_once('#')
.map_or((input, None), |(a, b)| (a, Some(b)));
if fragment.is_some_and(|f| !uri_chars(f, b";/?:@&=+$,[]")) {
return false;
}
let (path, query) = body
.split_once('?')
.map_or((body, None), |(a, b)| (a, Some(b)));
if query.is_some_and(|q| !uri_chars(q, b";/?:@&=+$,[]")) {
return false;
}
let mut path = path;
if let Some(colon) = body.find(':') {
let prefix = &body[..colon];
if !prefix.contains(['/', '?']) {
let mut scheme = prefix.bytes();
if !scheme.next().is_some_and(|c| c.is_ascii_alphabetic())
|| !scheme.all(|c| c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.'))
{
return false;
}
let rest = &body[colon + 1..];
if rest.is_empty() {
return false;
}
if !rest.starts_with('/') {
if matches!(rest.as_bytes()[0], b'[' | b']') {
return false;
}
return uri_chars(rest, b";/?:@&=+$,[]");
}
path = &path[colon + 1..];
}
}
if let Some(rest) = path.strip_prefix("//") {
let (authority, suffix) = rest.split_once('/').map_or((rest, ""), |(a, b)| (a, b));
if authority.contains(['[', ']']) {
let server = authority
.rsplit_once('@')
.map_or(authority, |(user, host)| {
if uri_chars(user, b";:&=+$,") {
host
} else {
""
}
});
let Some(server) = server.strip_prefix('[') else {
return false;
};
let Some((address, port)) = server.split_once(']') else {
return false;
};
if !valid_ipv6_reference(address) {
return false;
}
if !port.is_empty()
&& !port
.strip_prefix(':')
.is_some_and(|p| p.bytes().all(|b| b.is_ascii_digit()))
{
return false;
}
} else if !uri_chars(authority, b"$,:;@&=+") {
return false;
}
path = suffix;
}
uri_chars(path, b"/;:@&=+$,")
}
fn valid_ipv6_reference(address: &str) -> bool {
if let Some((left, right)) = address.split_once("::") {
let Some(left) = ipv6_units(left, false) else {
return false;
};
let Some(right) = ipv6_units(right, true) else {
return false;
};
left + right < 8
} else {
ipv6_units(address, true) == Some(8)
}
}
fn ipv6_units(part: &str, allow_ipv4_tail: bool) -> Option<usize> {
if part.is_empty() {
return Some(0);
}
let mut units = 0;
let mut groups = part.split(':').peekable();
while let Some(group) = groups.next() {
if group.contains('.') {
if !allow_ipv4_tail || groups.peek().is_some() {
return None;
}
let mut count = 0;
for octet in group.split('.') {
if octet.is_empty() || octet.len() > 3 {
return None;
}
let mut value = 0u16;
for byte in octet.bytes() {
if !byte.is_ascii_digit() {
return None;
}
value = value * 10 + u16::from(byte - b'0');
}
if value > 255 {
return None;
}
count += 1;
}
if count != 4 {
return None;
}
units += 2;
} else {
if group.is_empty() || group.len() > 4 || !group.bytes().all(|b| b.is_ascii_hexdigit())
{
return None;
}
units += 1;
}
if units > 8 {
return None;
}
}
Some(units)
}