pub mod encoder;
mod estring;
mod imp;
pub(crate) mod table;
mod utf8;
pub use estring::EString;
pub use table::Table;
pub(crate) use imp::{decode_octet, encode_byte, OCTET_TABLE_LO};
pub(crate) use utf8::{next_code_point, Utf8Chunks};
use crate::internal::PathEncoder;
use alloc::{
borrow::{Cow, ToOwned},
string::{FromUtf8Error, String},
vec::Vec,
};
use core::{cmp::Ordering, hash, iter::FusedIterator, marker::PhantomData, str};
use ref_cast::{ref_cast_custom, RefCastCustom};
pub trait Encoder: 'static {
const TABLE: &'static Table;
}
#[derive(RefCastCustom)]
#[repr(transparent)]
pub struct EStr<E: Encoder> {
encoder: PhantomData<E>,
inner: str,
}
struct Assert<L: Encoder, R: Encoder> {
_marker: PhantomData<(L, R)>,
}
impl<L: Encoder, R: Encoder> Assert<L, R> {
const L_IS_SUB_ENCODER_OF_R: () = assert!(L::TABLE.is_subset(R::TABLE), "not a sub-encoder");
}
impl<E: Encoder> EStr<E> {
const ASSERT_ALLOWS_PCT_ENCODED: () = assert!(
E::TABLE.allows_pct_encoded(),
"table does not allow percent-encoded octets"
);
#[ref_cast_custom]
pub(crate) const fn new_validated(s: &str) -> &Self;
pub const EMPTY: &'static Self = Self::new_validated("");
pub(crate) fn cast<F: Encoder>(&self) -> &EStr<F> {
EStr::new_validated(&self.inner)
}
#[must_use]
pub const fn new_or_panic(s: &str) -> &Self {
match Self::new(s) {
Some(s) => s,
None => panic!("improperly encoded string"),
}
}
#[must_use]
pub const fn new(s: &str) -> Option<&Self> {
if E::TABLE.validate(s.as_bytes()) {
Some(EStr::new_validated(s))
} else {
None
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.inner
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[cfg(fluent_uri_unstable)]
#[must_use]
pub fn upcast<SuperE: Encoder>(&self) -> &EStr<SuperE> {
let () = Assert::<E, SuperE>::L_IS_SUB_ENCODER_OF_R;
EStr::new_validated(self.as_str())
}
#[must_use]
pub fn decode(&self) -> Decode<'_> {
let () = Self::ASSERT_ALLOWS_PCT_ENCODED;
match imp::decode(self.inner.as_bytes()) {
Some(vec) => Decode::Owned(vec),
None => Decode::Borrowed(self.as_str()),
}
}
pub fn split(&self, delim: char) -> Split<'_, E> {
assert!(
delim.is_ascii() && table::RESERVED.allows(delim),
"splitting with non-reserved character"
);
Split {
inner: self.inner.split(delim),
encoder: PhantomData,
}
}
#[must_use]
pub fn split_once(&self, delim: char) -> Option<(&Self, &Self)> {
assert!(
delim.is_ascii() && table::RESERVED.allows(delim),
"splitting with non-reserved character"
);
self.inner
.split_once(delim)
.map(|(a, b)| (Self::new_validated(a), Self::new_validated(b)))
}
#[must_use]
pub fn rsplit_once(&self, delim: char) -> Option<(&Self, &Self)> {
assert!(
delim.is_ascii() && table::RESERVED.allows(delim),
"splitting with non-reserved character"
);
self.inner
.rsplit_once(delim)
.map(|(a, b)| (Self::new_validated(a), Self::new_validated(b)))
}
}
impl<E: Encoder> AsRef<Self> for EStr<E> {
fn as_ref(&self) -> &Self {
self
}
}
impl<E: Encoder> AsRef<str> for EStr<E> {
fn as_ref(&self) -> &str {
&self.inner
}
}
impl<E: Encoder> PartialEq for EStr<E> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<E: Encoder> PartialEq<str> for EStr<E> {
fn eq(&self, other: &str) -> bool {
&self.inner == other
}
}
impl<E: Encoder> PartialEq<EStr<E>> for str {
fn eq(&self, other: &EStr<E>) -> bool {
self == &other.inner
}
}
impl<E: Encoder> Eq for EStr<E> {}
impl<E: Encoder> hash::Hash for EStr<E> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl<E: Encoder> PartialOrd for EStr<E> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<E: Encoder> Ord for EStr<E> {
fn cmp(&self, other: &Self) -> Ordering {
self.inner.cmp(&other.inner)
}
}
impl<E: Encoder> Default for &EStr<E> {
fn default() -> Self {
EStr::EMPTY
}
}
impl<E: Encoder> ToOwned for EStr<E> {
type Owned = EString<E>;
fn to_owned(&self) -> EString<E> {
EString::new_validated(self.inner.to_owned())
}
fn clone_into(&self, target: &mut EString<E>) {
self.inner.clone_into(&mut target.buf);
}
}
impl<E: PathEncoder> EStr<E> {
#[inline]
#[must_use]
pub fn is_absolute(&self) -> bool {
self.inner.starts_with('/')
}
#[inline]
#[must_use]
pub fn is_rootless(&self) -> bool {
!self.inner.starts_with('/')
}
#[inline]
#[must_use]
pub fn segments_if_absolute(&self) -> Option<Split<'_, E>> {
self.inner
.strip_prefix('/')
.map(|s| EStr::new_validated(s).split('/'))
}
}
#[derive(Clone, Debug)]
pub enum Decode<'a> {
Borrowed(&'a str),
Owned(Vec<u8>),
}
impl<'a> Decode<'a> {
#[inline]
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Borrowed(s) => s.as_bytes(),
Self::Owned(vec) => vec,
}
}
#[inline]
#[must_use]
pub fn into_bytes(self) -> Cow<'a, [u8]> {
match self {
Self::Borrowed(s) => Cow::Borrowed(s.as_bytes()),
Self::Owned(vec) => Cow::Owned(vec),
}
}
#[inline]
pub fn into_string(self) -> Result<Cow<'a, str>, FromUtf8Error> {
match self {
Self::Borrowed(s) => Ok(Cow::Borrowed(s)),
Self::Owned(vec) => String::from_utf8(vec).map(Cow::Owned),
}
}
#[must_use]
pub fn into_string_lossy(self) -> Cow<'a, str> {
match self.into_string() {
Ok(string) => string,
Err(e) => Cow::Owned(String::from_utf8_lossy(e.as_bytes()).into_owned()),
}
}
}
#[derive(Clone, Debug)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct Split<'a, E: Encoder> {
inner: str::Split<'a, char>,
encoder: PhantomData<E>,
}
impl<'a, E: Encoder> Iterator for Split<'a, E> {
type Item = &'a EStr<E>;
fn next(&mut self) -> Option<&'a EStr<E>> {
self.inner.next().map(EStr::new_validated)
}
}
impl<'a, E: Encoder> DoubleEndedIterator for Split<'a, E> {
fn next_back(&mut self) -> Option<&'a EStr<E>> {
self.inner.next_back().map(EStr::new_validated)
}
}
impl<E: Encoder> FusedIterator for Split<'_, E> {}