use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use fluent_uri::Uri as FluentUri;
use qubit_redact::RedactionPolicy;
use qubit_redact::formats::uri::UriRedactionBoundary;
use super::invalid_uri;
use crate::error::FsResult;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Uri {
parsed: FluentUri<String>,
}
impl Uri {
#[inline]
pub fn parse(text: &str) -> FsResult<Self> {
Self::parse_with_policy(text, &RedactionPolicy::standard())
}
#[inline]
pub fn parse_with_policy(text: &str, policy: &RedactionPolicy) -> FsResult<Self> {
let parsed = parse_canonical(text)?;
let floor = RedactionPolicy::standard();
reject_secrets(&parsed, &floor)?;
if policy != &floor {
reject_secrets(&parsed, policy)?;
}
Ok(Self { parsed })
}
#[inline]
#[must_use]
pub fn scheme(&self) -> &str {
self.parsed.scheme().as_str()
}
#[inline]
#[must_use]
pub fn authority(&self) -> Option<&str> {
self.parsed.authority().map(|authority| authority.as_str())
}
#[inline]
#[must_use]
pub fn has_authority(&self) -> bool {
self.parsed.has_authority()
}
#[inline]
#[must_use]
pub fn path(&self) -> &str {
self.parsed.path().as_str()
}
#[inline]
#[must_use]
pub fn query(&self) -> Option<&str> {
self.parsed.query().map(|query| query.as_str())
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
self.parsed.as_str()
}
}
impl Display for Uri {
#[inline]
fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str(self.as_str())
}
}
pub(crate) fn parse_canonical(text: &str) -> FsResult<FluentUri<String>> {
let (scheme, rest) = text
.split_once(':')
.ok_or_else(|| invalid_uri("URI must include a scheme"))?;
if scheme.is_empty() {
return Err(invalid_uri("URI scheme must not be empty"));
}
let canonical = format!("{}:{rest}", scheme.to_ascii_lowercase());
FluentUri::parse(canonical).map_err(|_| invalid_uri("URI is malformed"))
}
pub(crate) fn reject_secrets(parsed: &FluentUri<String>, policy: &RedactionPolicy) -> FsResult<()> {
if parsed.fragment().is_some() {
return Err(invalid_uri("URI fragments are not supported"));
}
match UriRedactionBoundary::new(policy).inspect_uri(parsed.as_str()) {
Ok(inspection) if !inspection.contains_sensitive() => {}
Ok(_) => {
return Err(invalid_uri("sensitive URI components are not supported"));
}
Err(_) => {
return Err(invalid_uri("URI contains invalid or uninspectable components"));
}
}
Ok(())
}
pub(crate) fn query_pair_is_sensitive(key: &str) -> bool {
RedactionPolicy::standard().sensitivity_for(key).is_some()
}
#[cfg(test)]
mod tests {
use std::hint::black_box;
use super::Uri;
#[test]
fn uri_accessors_are_executed_at_runtime() {
let parse: fn(&str) -> crate::error::FsResult<Uri> = black_box(Uri::parse);
let scheme: for<'a> fn(&'a Uri) -> &'a str = black_box(Uri::scheme);
let authority: for<'a> fn(&'a Uri) -> Option<&'a str> = black_box(Uri::authority);
let has_authority: fn(&Uri) -> bool = black_box(Uri::has_authority);
let path: for<'a> fn(&'a Uri) -> &'a str = black_box(Uri::path);
let query: for<'a> fn(&'a Uri) -> Option<&'a str> = black_box(Uri::query);
let as_str: for<'a> fn(&'a Uri) -> &'a str = black_box(Uri::as_str);
let uri = parse("HTTPS://example.test/path?query=value").expect("URI should parse");
assert_eq!("https", scheme(&uri));
assert_eq!(Some("example.test"), authority(&uri));
assert!(has_authority(&uri));
assert_eq!("/path", path(&uri));
assert_eq!(Some("query=value"), query(&uri));
assert_eq!("https://example.test/path?query=value", as_str(&uri));
assert_eq!(as_str(&uri), format!("{uri}"));
}
}