use core::fmt::{self, Display as _, Write as _};
use core::marker::PhantomData;
use crate::format::Censored;
use crate::normalize::{self, DisplayPctCaseNormalize};
use crate::parser::str::{find_split, prior_byte2};
use crate::parser::validate as parser;
use crate::spec::Spec;
use crate::types::{RiAbsoluteStr, RiReferenceStr, RiRelativeStr, RiStr};
#[cfg(feature = "alloc")]
use crate::types::{RiAbsoluteString, RiReferenceString, RiRelativeString, RiString};
use crate::validate;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Normalization {
None,
Rfc3986,
Whatwg,
}
#[derive(Debug, Clone)]
pub struct PortBuilder<'a>(PortBuilderRepr<'a>);
impl Default for PortBuilder<'_> {
#[inline]
fn default() -> Self {
Self(PortBuilderRepr::Empty)
}
}
impl<'a> From<u8> for PortBuilder<'a> {
#[inline]
fn from(v: u8) -> Self {
Self(PortBuilderRepr::Integer(v.into()))
}
}
impl<'a> From<u16> for PortBuilder<'a> {
#[inline]
fn from(v: u16) -> Self {
Self(PortBuilderRepr::Integer(v))
}
}
impl<'a> From<&'a str> for PortBuilder<'a> {
#[inline]
fn from(v: &'a str) -> Self {
Self(PortBuilderRepr::String(v))
}
}
#[cfg(feature = "alloc")]
impl<'a> From<&'a alloc::string::String> for PortBuilder<'a> {
#[inline]
fn from(v: &'a alloc::string::String) -> Self {
Self(PortBuilderRepr::String(v.as_str()))
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
enum PortBuilderRepr<'a> {
Empty,
Integer(u16),
String(&'a str),
}
#[derive(Clone)]
pub struct UserinfoBuilder<'a>(UserinfoRepr<'a>);
impl Default for UserinfoBuilder<'_> {
#[inline]
fn default() -> Self {
Self(UserinfoRepr::None)
}
}
impl fmt::Debug for UserinfoBuilder<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct("UserinfoBuilder");
if let Some((user, password)) = self.to_user_password() {
debug.field("user", &user);
if matches!(password, None | Some("")) {
debug.field("password", &password);
} else {
debug.field("password", &Some(Censored));
}
}
debug.finish()
}
}
impl<'a> UserinfoBuilder<'a> {
#[must_use]
fn to_user_password(&self) -> Option<(Option<&'a str>, Option<&'a str>)> {
match &self.0 {
UserinfoRepr::None => None,
UserinfoRepr::Direct(s) => match find_split(s, b':') {
None => Some((Some(s), None)),
Some((user, password)) => Some((Some(user), Some(password))),
},
UserinfoRepr::UserPass(user, password) => Some((*user, *password)),
}
}
}
impl<'a> From<&'a str> for UserinfoBuilder<'a> {
#[inline]
fn from(direct: &'a str) -> Self {
Self(UserinfoRepr::Direct(direct))
}
}
impl<'a> From<(&'a str, &'a str)> for UserinfoBuilder<'a> {
#[inline]
fn from((user, password): (&'a str, &'a str)) -> Self {
Self(UserinfoRepr::UserPass(Some(user), Some(password)))
}
}
impl<'a> From<(&'a str, Option<&'a str>)> for UserinfoBuilder<'a> {
#[inline]
fn from((user, password): (&'a str, Option<&'a str>)) -> Self {
Self(UserinfoRepr::UserPass(Some(user), password))
}
}
impl<'a> From<(Option<&'a str>, &'a str)> for UserinfoBuilder<'a> {
#[inline]
fn from((user, password): (Option<&'a str>, &'a str)) -> Self {
Self(UserinfoRepr::UserPass(user, Some(password)))
}
}
impl<'a> From<(Option<&'a str>, Option<&'a str>)> for UserinfoBuilder<'a> {
#[inline]
fn from((user, password): (Option<&'a str>, Option<&'a str>)) -> Self {
Self(UserinfoRepr::UserPass(user, password))
}
}
#[cfg(feature = "alloc")]
impl<'a> From<&'a alloc::string::String> for UserinfoBuilder<'a> {
#[inline]
fn from(v: &'a alloc::string::String) -> Self {
Self::from(v.as_str())
}
}
#[derive(Clone, Copy)]
enum UserinfoRepr<'a> {
None,
Direct(&'a str),
UserPass(Option<&'a str>, Option<&'a str>),
}
#[derive(Default, Debug, Clone)]
struct AuthorityBuilder<'a> {
host: HostRepr<'a>,
port: PortBuilder<'a>,
userinfo: UserinfoBuilder<'a>,
}
impl AuthorityBuilder<'_> {
fn fmt_write_to<S: Spec>(
&self,
f: &mut fmt::Formatter<'_>,
normalize: Normalization,
) -> fmt::Result {
match &self.userinfo.0 {
UserinfoRepr::None => {}
UserinfoRepr::Direct(userinfo) => {
if normalize == Normalization::None {
userinfo.fmt(f)?;
} else {
DisplayPctCaseNormalize::<S>::new(userinfo).fmt(f)?;
}
f.write_char('@')?;
}
UserinfoRepr::UserPass(user, password) => {
if let Some(user) = user {
if normalize == Normalization::None {
f.write_str(user)?;
} else {
DisplayPctCaseNormalize::<S>::new(user).fmt(f)?;
}
}
if let Some(password) = password {
f.write_char(':')?;
if normalize == Normalization::None {
password.fmt(f)?;
} else {
DisplayPctCaseNormalize::<S>::new(password).fmt(f)?;
}
}
f.write_char('@')?;
}
}
match self.host {
HostRepr::String(host) => {
if normalize == Normalization::None {
f.write_str(host)?;
} else {
normalize::normalize_host_port::<S>(f, host)?;
}
}
#[cfg(feature = "std")]
HostRepr::IpAddr(ipaddr) => match ipaddr {
std::net::IpAddr::V4(v) => v.fmt(f)?,
std::net::IpAddr::V6(v) => write!(f, "[{v}]")?,
},
}
match self.port.0 {
PortBuilderRepr::Empty => {}
PortBuilderRepr::Integer(v) => write!(f, ":{v}")?,
PortBuilderRepr::String(v) => {
if !(v.is_empty() && (normalize != Normalization::None)) {
write!(f, ":{v}")?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
enum HostRepr<'a> {
String(&'a str),
#[cfg(feature = "std")]
IpAddr(std::net::IpAddr),
}
impl Default for HostRepr<'_> {
#[inline]
fn default() -> Self {
Self::String("")
}
}
#[derive(Debug, Clone)]
pub struct Builder<'a> {
scheme: Option<&'a str>,
authority: Option<AuthorityBuilder<'a>>,
path: &'a str,
query: Option<&'a str>,
fragment: Option<&'a str>,
normalize: Normalization,
}
impl Default for Builder<'_> {
#[inline]
fn default() -> Self {
Self {
scheme: None,
authority: None,
path: "",
query: None,
fragment: None,
normalize: Normalization::None,
}
}
}
impl<'a> Builder<'a> {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn fmt_write_to<S: Spec>(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(scheme) = self.scheme {
if self.normalize == Normalization::None {
f.write_str(scheme)?;
} else {
normalize::normalize_scheme(f, scheme)?;
}
f.write_char(':')?;
}
if let Some(authority) = &self.authority {
f.write_str("//")?;
authority.fmt_write_to::<S>(f, self.normalize)?;
}
if self.normalize == Normalization::None {
f.write_str(self.path)?;
} else {
let op = normalize::NormalizationOp {
case_pct_normalization: true,
};
normalize::PathToNormalize::from_single_path(self.path).fmt_write_normalize::<S, _>(
f,
op,
self.authority.is_some(),
)?;
}
if let Some(query) = self.query {
f.write_char('?')?;
if self.normalize == Normalization::None {
f.write_str(query)?;
} else {
normalize::normalize_query::<S>(f, query)?;
}
}
if let Some(fragment) = self.fragment {
f.write_char('#')?;
if self.normalize == Normalization::None {
f.write_str(fragment)?;
} else {
normalize::normalize_fragment::<S>(f, fragment)?;
}
}
Ok(())
}
#[inline]
pub fn build<T>(self) -> Result<DisplayBuild<'a, T>, Error>
where
T: ?Sized + Buildable<'a>,
{
<T as private::Sealed<'a>>::validate_builder(self)
}
}
impl<'a> Builder<'a> {
#[inline]
pub fn scheme(&mut self, v: &'a str) {
self.scheme = Some(v);
}
#[inline]
pub fn unset_scheme(&mut self) {
self.scheme = None;
}
#[inline]
pub fn path(&mut self, v: &'a str) {
self.path = v;
}
#[inline]
fn authority_builder(&mut self) -> &mut AuthorityBuilder<'a> {
self.authority.get_or_insert_with(AuthorityBuilder::default)
}
#[inline]
pub fn unset_authority(&mut self) {
self.authority = None;
}
#[inline]
pub fn userinfo<T: Into<UserinfoBuilder<'a>>>(&mut self, v: T) {
self.authority_builder().userinfo = v.into();
}
#[inline]
pub fn unset_userinfo(&mut self) {
self.authority_builder().userinfo = UserinfoBuilder::default();
}
#[inline]
pub fn host(&mut self, v: &'a str) {
self.authority_builder().host = HostRepr::String(v);
}
#[cfg(feature = "std")]
#[inline]
pub fn ip_address<T: Into<std::net::IpAddr>>(&mut self, addr: T) {
self.authority_builder().host = HostRepr::IpAddr(addr.into());
}
#[inline]
pub fn port<T: Into<PortBuilder<'a>>>(&mut self, v: T) {
self.authority_builder().port = v.into();
}
#[inline]
pub fn unset_port(&mut self) {
self.authority_builder().port = PortBuilder::default();
}
#[inline]
pub fn query(&mut self, v: &'a str) {
self.query = Some(v);
}
#[inline]
pub fn unset_query(&mut self) {
self.query = None;
}
#[inline]
pub fn fragment(&mut self, v: &'a str) {
self.fragment = Some(v);
}
#[inline]
pub fn unset_fragment(&mut self) {
self.fragment = None;
}
#[inline]
pub fn unset_normalize(&mut self) {
self.normalize = Normalization::None;
}
#[inline]
pub fn normalize_rfc3986(&mut self) {
self.normalize = Normalization::Rfc3986;
}
#[inline]
pub fn normalize_whatwg(&mut self) {
self.normalize = Normalization::Whatwg;
}
}
#[derive(Debug)]
pub struct DisplayBuild<'a, T: ?Sized> {
builder: Builder<'a>,
_ty_str: PhantomData<fn() -> T>,
}
impl<T: ?Sized> Clone for DisplayBuild<'_, T> {
#[inline]
fn clone(&self) -> Self {
Self {
builder: self.builder.clone(),
_ty_str: PhantomData,
}
}
}
impl<S: Spec> fmt::Display for DisplayBuild<'_, RiReferenceStr<S>> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.builder.fmt_write_to::<S>(f)
}
}
impl<S: Spec> fmt::Display for DisplayBuild<'_, RiStr<S>> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.builder.fmt_write_to::<S>(f)
}
}
impl<S: Spec> fmt::Display for DisplayBuild<'_, RiAbsoluteStr<S>> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.builder.fmt_write_to::<S>(f)
}
}
impl<S: Spec> fmt::Display for DisplayBuild<'_, RiRelativeStr<S>> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.builder.fmt_write_to::<S>(f)
}
}
#[cfg(feature = "alloc")]
impl<S: Spec> From<DisplayBuild<'_, RiReferenceStr<S>>> for RiReferenceString<S> {
#[inline]
fn from(builder: DisplayBuild<'_, RiReferenceStr<S>>) -> Self {
(&builder).into()
}
}
#[cfg(feature = "alloc")]
impl<S: Spec> From<&DisplayBuild<'_, RiReferenceStr<S>>> for RiReferenceString<S> {
fn from(builder: &DisplayBuild<'_, RiReferenceStr<S>>) -> Self {
let s = builder.to_string();
Self::try_from(s)
.expect("[validity] the string to be built should already have been validated")
}
}
#[cfg(feature = "alloc")]
impl<S: Spec> From<DisplayBuild<'_, RiStr<S>>> for RiString<S> {
#[inline]
fn from(builder: DisplayBuild<'_, RiStr<S>>) -> Self {
(&builder).into()
}
}
#[cfg(feature = "alloc")]
impl<S: Spec> From<&DisplayBuild<'_, RiStr<S>>> for RiString<S> {
fn from(builder: &DisplayBuild<'_, RiStr<S>>) -> Self {
let s = builder.to_string();
Self::try_from(s)
.expect("[validity] the string to be built should already have been validated")
}
}
#[cfg(feature = "alloc")]
impl<S: Spec> From<DisplayBuild<'_, RiAbsoluteStr<S>>> for RiAbsoluteString<S> {
#[inline]
fn from(builder: DisplayBuild<'_, RiAbsoluteStr<S>>) -> Self {
(&builder).into()
}
}
#[cfg(feature = "alloc")]
impl<S: Spec> From<&DisplayBuild<'_, RiAbsoluteStr<S>>> for RiAbsoluteString<S> {
fn from(builder: &DisplayBuild<'_, RiAbsoluteStr<S>>) -> Self {
let s = builder.to_string();
Self::try_from(s)
.expect("[validity] the string to be built should already have been validated")
}
}
#[cfg(feature = "alloc")]
impl<S: Spec> From<DisplayBuild<'_, RiRelativeStr<S>>> for RiRelativeString<S> {
#[inline]
fn from(builder: DisplayBuild<'_, RiRelativeStr<S>>) -> Self {
(&builder).into()
}
}
#[cfg(feature = "alloc")]
impl<S: Spec> From<&DisplayBuild<'_, RiRelativeStr<S>>> for RiRelativeString<S> {
fn from(builder: &DisplayBuild<'_, RiRelativeStr<S>>) -> Self {
let s = builder.to_string();
Self::try_from(s)
.expect("[validity] the string to be built should already have been validated")
}
}
#[derive(Debug, Clone)]
pub enum Error {
Validate(validate::Error),
Normalize(normalize::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = match self {
Self::Validate(_) => "build result won't be a valid IRI",
Self::Normalize(_) => "build result won't be normalizable with the specified algorithm",
};
f.write_str(msg)
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Validate(e) => Some(e),
Self::Normalize(e) => Some(e),
}
}
}
impl From<validate::Error> for Error {
#[inline]
fn from(e: validate::Error) -> Self {
Self::Validate(e)
}
}
impl From<normalize::Error> for Error {
#[inline]
fn from(e: normalize::Error) -> Self {
Self::Normalize(e)
}
}
pub trait Buildable<'a>: private::Sealed<'a> {}
impl<'a, S: Spec> private::Sealed<'a> for RiReferenceStr<S> {
fn validate_builder(builder: Builder<'a>) -> Result<DisplayBuild<'a, Self>, Error> {
validate_builder_for_iri_reference::<S>(&builder)?;
Ok(DisplayBuild {
builder,
_ty_str: PhantomData,
})
}
}
impl<'a, S: Spec> Buildable<'a> for RiReferenceStr<S> {}
impl<'a, S: Spec> private::Sealed<'a> for RiStr<S> {
fn validate_builder(builder: Builder<'a>) -> Result<DisplayBuild<'a, Self>, Error> {
if builder.scheme.is_none() {
return Err(validate::Error::new().into());
}
validate_builder_for_iri_reference::<S>(&builder)?;
Ok(DisplayBuild {
builder,
_ty_str: PhantomData,
})
}
}
impl<'a, S: Spec> Buildable<'a> for RiStr<S> {}
impl<'a, S: Spec> private::Sealed<'a> for RiAbsoluteStr<S> {
fn validate_builder(builder: Builder<'a>) -> Result<DisplayBuild<'a, Self>, Error> {
if builder.scheme.is_none() {
return Err(validate::Error::new().into());
}
if builder.fragment.is_some() {
return Err(validate::Error::new().into());
}
validate_builder_for_iri_reference::<S>(&builder)?;
Ok(DisplayBuild {
builder,
_ty_str: PhantomData,
})
}
}
impl<'a, S: Spec> Buildable<'a> for RiAbsoluteStr<S> {}
impl<'a, S: Spec> private::Sealed<'a> for RiRelativeStr<S> {
fn validate_builder(builder: Builder<'a>) -> Result<DisplayBuild<'a, Self>, Error> {
if builder.scheme.is_some() {
return Err(validate::Error::new().into());
}
validate_builder_for_iri_reference::<S>(&builder)?;
Ok(DisplayBuild {
builder,
_ty_str: PhantomData,
})
}
}
impl<'a, S: Spec> Buildable<'a> for RiRelativeStr<S> {}
fn validate_builder_for_iri_reference<S: Spec>(builder: &Builder<'_>) -> Result<(), Error> {
if let Some(scheme) = builder.scheme {
parser::validate_scheme(scheme)?;
}
if let Some(authority) = &builder.authority {
match &authority.userinfo.0 {
UserinfoRepr::None => {}
UserinfoRepr::Direct(userinfo) => {
parser::validate_userinfo::<S>(userinfo)?;
}
UserinfoRepr::UserPass(user, password) => {
if let Some(user) = user {
parser::validate_userinfo::<S>(user)?;
}
if let Some(password) = password {
parser::validate_userinfo::<S>(password)?;
}
}
}
match authority.host {
HostRepr::String(s) => parser::validate_host::<S>(s)?,
#[cfg(feature = "std")]
HostRepr::IpAddr(_) => {}
}
if let PortBuilderRepr::String(s) = authority.port.0 {
if !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(validate::Error::new().into());
}
}
}
let is_path_acceptable = if builder.authority.is_some() {
builder.path.is_empty() || builder.path.starts_with('/')
} else if builder.scheme.is_some() {
!builder.path.starts_with("//")
} else {
!builder.path.starts_with("//")
&& (prior_byte2(builder.path.as_bytes(), b'/', b':') != Some(b':'))
};
if !is_path_acceptable {
return Err(validate::Error::new().into());
}
if (builder.normalize == Normalization::Rfc3986) && builder.authority.is_none() {
let path = normalize::PathToNormalize::from_single_path(builder.path);
path.ensure_rfc3986_normalizable_with_authority_absent()?;
}
if let Some(query) = builder.query {
parser::validate_query::<S>(query)?;
}
if let Some(fragment) = builder.fragment {
parser::validate_fragment::<S>(fragment)?;
}
Ok(())
}
mod private {
use super::{Builder, DisplayBuild, Error};
pub trait Sealed<'a> {
fn validate_builder(builder: Builder<'a>) -> Result<DisplayBuild<'a, Self>, Error>;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "alloc")]
use alloc::string::ToString;
use crate::types::{IriReferenceStr, IriStr};
#[test]
fn set_port() {
let mut builder = Builder::new();
builder.port(80_u8);
builder.port(80_u16);
builder.port("80");
}
#[cfg(feature = "std")]
#[test]
fn set_ipaddr() {
let mut builder = Builder::new();
builder.ip_address(std::net::Ipv4Addr::new(192, 0, 2, 0));
builder.ip_address(std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0));
}
#[test]
fn set_userinfo() {
let mut builder = Builder::new();
builder.userinfo("arbitrary-valid-string");
builder.userinfo("user:password");
builder.userinfo((None, None));
builder.userinfo(("user", None));
builder.userinfo((None, "password"));
builder.userinfo(("user", "password"));
}
#[test]
fn all() {
let mut builder = Builder::new();
builder.scheme("http");
builder.userinfo(("user", "password"));
builder.host("example.com");
builder.port(80_u16);
builder.path("/path/to/somewhere");
builder.query("query");
builder.fragment("fragment");
#[cfg_attr(not(feature = "alloc"), allow(unused_variables))]
let built = builder.build::<IriStr>().expect("valid");
#[cfg(feature = "alloc")]
assert_eq!(
built.to_string(),
"http://user:password@example.com:80/path/to/somewhere?query#fragment"
);
}
#[test]
fn large_port() {
let mut builder = Builder::new();
builder.port("99999999999999999999999999999999");
builder.port("99999999999999999999999999999999");
#[cfg_attr(not(feature = "alloc"), allow(unused_variables))]
let built = builder.build::<IriReferenceStr>().expect("valid");
#[cfg(feature = "alloc")]
assert_eq!(built.to_string(), "//:99999999999999999999999999999999");
}
#[test]
fn authority_and_relative_path() {
let mut builder = Builder::new();
builder.host("example.com");
builder.path("relative/path");
assert!(builder.build::<IriReferenceStr>().is_err());
}
#[test]
fn no_authority_and_double_slash_prefix() {
let mut builder = Builder::new();
builder.path("//double-slash");
assert!(builder.build::<IriReferenceStr>().is_err());
}
#[test]
fn no_authority_and_relative_first_segment_colon() {
let mut builder = Builder::new();
builder.path("foo:bar");
assert!(builder.build::<IriReferenceStr>().is_err());
}
#[test]
fn normalize_rfc3986_double_slash_prefix() {
let mut builder = Builder::new();
builder.scheme("scheme");
builder.path("/..//bar");
builder.normalize_rfc3986();
assert!(builder.build::<IriStr>().is_err());
}
#[test]
fn normalize_whawtg_double_slash_prefix() {
let mut builder = Builder::new();
builder.scheme("scheme");
builder.path("/..//bar");
builder.normalize_whatwg();
#[cfg_attr(not(feature = "alloc"), allow(unused_variables))]
let built = builder.build::<IriStr>().expect("normalizable");
#[cfg(feature = "alloc")]
assert_eq!(built.to_string(), "scheme:/.//bar");
}
}