use dmsg_types::*;
pub const MAX_URI_BYTES: usize = 8192;
const MAX_NAMESPACE_BYTES: usize = MAX_URI_BYTES - 64;
fn parse_uri(value: &str) -> Result<url::Url> {
ensure_valid(
!value.is_empty() && value.len() <= MAX_URI_BYTES,
"absolute URI",
)?;
for (index, byte) in value.bytes().enumerate() {
if !byte.is_ascii_graphic() {
return Err(invalid("absolute URI"));
}
if byte == b'%' {
ensure_valid(
value
.as_bytes()
.get(index + 1..index + 3)
.is_some_and(|escape| escape.iter().all(u8::is_ascii_hexdigit)),
"URI percent escape",
)?;
}
}
let uri = url::Url::parse(value).map_err(|_| invalid("absolute URI"))?;
ensure_valid(
uri.as_str() == value && uri.username().is_empty() && uri.password().is_none(),
"canonical URI",
)?;
Ok(uri)
}
pub fn validate_uri(value: &str) -> Result<()> {
parse_uri(value).map(|_| ())
}
pub fn validate_namespace(value: &str) -> Result<()> {
ensure_valid(
value.len() <= MAX_NAMESPACE_BYTES && (value.ends_with('/') || value.ends_with(':')),
"identity namespace",
)?;
let uri = parse_uri(value)?;
ensure_valid(
uri.query().is_none() && uri.fragment().is_none(),
"identity namespace",
)
}
pub fn account_issuer(namespace: &str, id: &AccountId) -> String {
format!("{namespace}{id}")
}
pub fn validate_origin(origin: &str, environment: &Environment) -> Result<()> {
ensure_valid(origin.len() <= 256, "origin")?;
if let Some(id) = origin.strip_prefix("chrome-extension://") {
return ensure_valid(
id.len() == 32 && id.bytes().all(|b| (b'a'..=b'p').contains(&b)),
"origin",
);
}
let url = url::Url::parse(origin).map_err(|_| invalid("origin"))?;
let loopback = *environment == Environment::Local
&& url.scheme() == "http"
&& matches!(url.host_str(), Some("localhost" | "127.0.0.1" | "[::1]"));
ensure_valid(
(url.scheme() == "https" || loopback) && url.origin().ascii_serialization() == origin,
"origin",
)
}
#[cfg(test)]
mod tests;