use structured_asn1::memory::{MemoryError, OwnedVec, Work};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Part {
Whole,
Query,
Fragment,
QueryAndFragment,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Negative {
Outside,
Malformed,
Encoding,
}
#[derive(Debug)]
pub enum Target<'uri> {
SameDocument(Part),
Root(Part),
Member {
name: MemberName<'uri>,
part: Part,
},
Negative(Negative),
}
#[derive(Debug)]
pub enum MemberName<'uri> {
Borrowed(&'uri [u8]),
Owned(OwnedVec<u8>),
}
impl AsRef<[u8]> for MemberName<'_> {
fn as_ref(&self) -> &[u8] {
match self {
Self::Borrowed(bytes) => bytes,
Self::Owned(bytes) => bytes,
}
}
}
pub fn resolve<'uri, W: Work>(
source_path: &[u8],
uri: &'uri str,
work: &W,
) -> Result<Target<'uri>, MemoryError> {
let cost = source_path
.len()
.checked_add(uri.len())
.and_then(|n| n.checked_mul(16))
.and_then(|n| n.checked_add(1))
.ok_or(MemoryError::WorkExceeded)?;
work.charge_work(u64::try_from(cost).map_err(|_| MemoryError::WorkExceeded)?)?;
let bytes = uri.as_bytes();
if !lexical(bytes) {
return Ok(Target::Negative(Negative::Malformed));
}
let fragment = bytes.iter().position(|byte| *byte == b'#');
let before_fragment = &bytes[..fragment.unwrap_or(bytes.len())];
let query = before_fragment.iter().position(|byte| *byte == b'?');
let path = &before_fragment[..query.unwrap_or(before_fragment.len())];
let part = match (query.is_some(), fragment.is_some()) {
(false, false) => Part::Whole,
(true, false) => Part::Query,
(false, true) => Part::Fragment,
(true, true) => Part::QueryAndFragment,
};
if path.is_empty() {
return Ok(Target::SameDocument(part));
}
if path.starts_with(b"//") {
return Ok(Target::Negative(Negative::Outside));
}
let first = &path[..match path.iter().position(|byte| *byte == b'/') {
Some(at) => at,
None => path.len(),
}];
if first.contains(&b':') {
return Ok(Target::Negative(Negative::Outside));
}
let mut base = if source_path.starts_with(b"META-INF/") || path[0] == b'/' {
&[][..]
} else {
match source_path.iter().rposition(|byte| *byte == b'/') {
Some(at) => &source_path[..=at],
None => &[],
}
};
let mut path = if path[0] == b'/' { &path[1..] } else { path };
loop {
if path.starts_with(b"./") {
path = &path[2..];
continue;
}
if path == b"." {
path = &[];
break;
}
if path == b".." || path.starts_with(b"../") {
if base.is_empty() {
return Ok(Target::Negative(Negative::Outside));
}
let without_separator = &base[..base.len() - 1];
base = match without_separator.iter().rposition(|byte| *byte == b'/') {
Some(at) => &base[..=at],
None => &[],
};
path = if path.len() == 2 { &[] } else { &path[3..] };
continue;
}
break;
}
if base.is_empty() && !path.contains(&b'%') {
if path.is_empty() {
return Ok(Target::Root(part));
}
return Ok(Target::Member {
name: MemberName::Borrowed(path),
part,
});
}
decoded_name(base, path, part, work)
}
fn decoded_name<'uri, W: Work>(
base: &[u8],
path: &'uri [u8],
part: Part,
work: &W,
) -> Result<Target<'uri>, MemoryError> {
let maximum = base
.len()
.checked_add(path.len())
.ok_or(MemoryError::LimitExceeded)?;
let mut name = work.vector();
name.try_reserve_exact(maximum)?;
name.try_extend_from_slice(base)?;
let mut rest = path;
while let Some(at) = rest.iter().position(|byte| *byte == b'%') {
name.try_extend_from_slice(&rest[..at])?;
if rest.len() - at < 3 {
return Ok(Target::Negative(Negative::Malformed));
}
let (Some(high), Some(low)) = (hex(rest[at + 1]), hex(rest[at + 2])) else {
return Ok(Target::Negative(Negative::Malformed));
};
name.try_push(high * 16 + low)?;
rest = &rest[at + 3..];
}
name.try_extend_from_slice(rest)?;
if core::str::from_utf8(&name).is_err() {
return Ok(Target::Negative(Negative::Encoding));
}
if name.is_empty() {
return Ok(Target::Root(part));
}
Ok(Target::Member {
name: MemberName::Owned(name.into_owned()),
part,
})
}
fn hex(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
fn lexical(bytes: &[u8]) -> bool {
let mut at = 0;
while at < bytes.len() {
let byte = bytes[at];
if byte < 0x20 || byte == 0x7f || byte == b'\\' {
return false;
}
if byte == b'%' {
if bytes.len() - at < 3 {
return false;
}
if hex(bytes[at + 1]).is_none() || hex(bytes[at + 2]).is_none() {
return false;
}
at += 3;
} else {
at += 1;
}
}
true
}
#[cfg(test)]
mod tests;