use super::{Error, Result};
pub(crate) fn is_ncname(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
is_ncname_start(first) && chars.all(is_ncname_char)
}
pub(crate) const fn is_xml_whitespace(character: char) -> bool {
matches!(character, ' ' | '\t' | '\r' | '\n')
}
pub(crate) fn trim_xml_whitespace(value: &str) -> &str {
value.trim_matches(is_xml_whitespace)
}
pub(crate) struct ValidatedXPointerFragment<'a> {
source: &'a str,
decoded_len: Option<usize>,
}
impl<'a> ValidatedXPointerFragment<'a> {
pub(crate) fn new(source: &'a str) -> Result<Self> {
if !source.as_bytes().contains(&b'%') {
return Ok(Self {
source,
decoded_len: None,
});
}
let bytes = source.as_bytes();
let mut cursor = 0usize;
let mut decoded_len = 0usize;
let mut scalar = [0_u8; 4];
while cursor < bytes.len() {
scalar[0] = next_percent_decoded_byte(bytes, &mut cursor)?;
let width = match scalar[0] {
0x00..=0x7f => 1,
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf4 => 4,
_ => return Err(invalid_xpointer_utf8()),
};
for byte in &mut scalar[1..width] {
*byte = next_percent_decoded_byte(bytes, &mut cursor)?;
}
if std::str::from_utf8(&scalar[..width]).is_err() {
return Err(invalid_xpointer_utf8());
}
decoded_len = decoded_len
.checked_add(width)
.ok_or_else(|| Error::Unsupported("URI fragment decoded length overflow".into()))?;
}
Ok(Self {
source,
decoded_len: Some(decoded_len),
})
}
pub(crate) const fn decoded_len(&self) -> Option<usize> {
self.decoded_len
}
pub(crate) const fn source(&self) -> &'a str {
self.source
}
pub(crate) fn equals(&self, candidate: &str) -> bool {
if self.decoded_len.is_none() {
return self.source == candidate;
}
let mut candidate = candidate.as_bytes().iter().copied();
let mut cursor = 0usize;
let bytes = self.source.as_bytes();
while cursor < bytes.len() {
let byte = decoded_byte_after_validation(bytes, &mut cursor);
if candidate.next() != Some(byte) {
return false;
}
}
candidate.next().is_none()
}
pub(crate) fn write_decoded_bytes(&self, output: &mut Vec<u8>) {
let mut cursor = 0usize;
let bytes = self.source.as_bytes();
while cursor < bytes.len() {
output.push(decoded_byte_after_validation(bytes, &mut cursor));
}
}
}
fn next_percent_decoded_byte(bytes: &[u8], cursor: &mut usize) -> Result<u8> {
let byte = bytes
.get(*cursor)
.copied()
.ok_or_else(|| Error::Unsupported("URI fragment has a truncated percent escape".into()))?;
if byte != b'%' {
*cursor += 1;
return Ok(byte);
}
let encoded = bytes
.get(*cursor + 1..*cursor + 3)
.ok_or_else(|| Error::Unsupported("URI fragment has a truncated percent escape".into()))?;
if !encoded.iter().all(u8::is_ascii_hexdigit) {
return Err(Error::Unsupported(
"URI fragment has an invalid percent escape".into(),
));
}
*cursor += 3;
Ok((hex_value(encoded[0]) << 4) | hex_value(encoded[1]))
}
fn decoded_byte_after_validation(bytes: &[u8], cursor: &mut usize) -> u8 {
let byte = bytes[*cursor];
if byte != b'%' {
*cursor += 1;
return byte;
}
let decoded = (hex_value(bytes[*cursor + 1]) << 4) | hex_value(bytes[*cursor + 2]);
*cursor += 3;
decoded
}
fn hex_value(byte: u8) -> u8 {
match byte {
b'0'..=b'9' => byte - b'0',
b'a'..=b'f' => byte - b'a' + 10,
b'A'..=b'F' => byte - b'A' + 10,
_ => unreachable!("validated percent escape contains only hexadecimal digits"),
}
}
fn invalid_xpointer_utf8() -> Error {
Error::Unsupported("URI fragment percent escapes are not valid UTF-8".into())
}
pub(crate) fn xpath_string_literal(value: &str) -> Option<&str> {
let value = value.trim_matches(is_xml_whitespace);
let quote @ ('\'' | '"') = value.chars().next()? else {
return None;
};
let literal = value.strip_prefix(quote)?.strip_suffix(quote)?;
(!literal.contains(quote)).then_some(literal)
}
pub(crate) fn strip_xpath_attribute_axis(value: &str) -> Option<&str> {
value
.strip_prefix('@')
.map(|value| value.trim_start_matches(is_xml_whitespace))
}
pub(crate) const fn is_xml10_character(character: char) -> bool {
matches!(
character,
'\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
)
}
pub(crate) fn is_xml_name(value: &str) -> bool {
let mut characters = value.chars();
let Some(first) = characters.next() else {
return false;
};
(first == ':' || is_ncname_start(first))
&& characters.all(|character| character == ':' || is_ncname_char(character))
}
pub(crate) fn unicode_decimal_value(character: char) -> Option<u32> {
const ZEROES: &[char] = &[
'0', '٠', '۰', '०', '০', '੦', '૦', '୦', '௦', '౦', '೦', '൦', '๐', '໐', '༠', '၀', '០', '᠐',
'ᥐ', '᧐', '᮰', '᱀', '꘠', '꣐', '꩐', '0',
];
ZEROES.iter().find_map(|zero| {
let offset = u32::from(character).checked_sub(u32::from(*zero))?;
(offset <= 9).then_some(offset)
})
}
pub(crate) fn is_ncname_start(ch: char) -> bool {
matches!(
ch,
'A'..='Z'
| '_'
| 'a'..='z'
| '\u{C0}'..='\u{D6}'
| '\u{D8}'..='\u{F6}'
| '\u{F8}'..='\u{2FF}'
| '\u{370}'..='\u{37D}'
| '\u{37F}'..='\u{1FFF}'
| '\u{200C}'..='\u{200D}'
| '\u{2070}'..='\u{218F}'
| '\u{2C00}'..='\u{2FEF}'
| '\u{3001}'..='\u{D7FF}'
| '\u{F900}'..='\u{FDCF}'
| '\u{FDF0}'..='\u{FFFD}'
| '\u{10000}'..='\u{EFFFF}'
)
}
pub(crate) fn is_ncname_char(ch: char) -> bool {
is_ncname_start(ch)
|| matches!(
ch,
'-' | '.' | '0'..='9' | '\u{B7}' | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
)
}
#[cfg(all(test, not(feature = "embedded")))]
mod tests;