#![no_std]
extern crate alloc;
use alloc::{
borrow::Cow,
format,
string::{String, ToString},
};
use core::{ffi::CStr, fmt};
const DEFAULT_SCHEME: &str = "file";
pub const PATH_MAX: usize = 4096;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct RedoxScheme<'a>(Cow<'a, str>);
impl<'a> RedoxScheme<'a> {
pub fn new<S: Into<Cow<'a, str>>>(scheme: S) -> Option<Self> {
let scheme = scheme.into();
if !scheme.is_ascii()
|| scheme
.as_bytes()
.iter()
.any(|b| matches!(b, b'\0' | b'/' | b':'))
{
return None;
}
Some(Self(scheme))
}
pub fn canonicalize_as_scheme<'b>(&self, path: RedoxStr<'b>) -> RedoxPath<'b> {
canonicalize_using_scheme_checked(self, path)
}
pub fn into_owned<'b>(self) -> RedoxScheme<'b> {
RedoxScheme(self.0.into_owned().into())
}
}
impl<'a> AsRef<str> for RedoxScheme<'a> {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl<'a> fmt::Display for RedoxScheme<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct RedoxReference<'a>(Cow<'a, str>);
impl<'a> RedoxReference<'a> {
pub fn new<S: Into<Cow<'a, str>>>(reference: S) -> Option<Self> {
let reference = reference.into();
#[cfg(not(feature = "simd"))]
if reference.as_bytes().iter().any(|&b| b == b'\0') {
return None;
}
#[cfg(feature = "simd")]
if memchr::memchr(b'\0', reference.as_bytes()).is_some() {
return None;
}
Some(Self(reference))
}
pub fn new_from_c<S: Into<&'a CStr>>(reference: S) -> Option<Self> {
let reference = reference.into();
#[cfg(feature = "simd")]
let s = simdutf8::basic::from_utf8(reference.to_bytes()).ok();
#[cfg(not(feature = "simd"))]
let s = reference.to_str().ok();
Some(Self(Cow::Borrowed(s?)))
}
pub fn new_from_buf(path: &'a [u8], len: usize) -> Option<Self> {
let buf = path.get(..len)?;
#[cfg(not(feature = "simd"))]
let s = str::from_utf8(buf).ok()?;
#[cfg(feature = "simd")]
let s = simdutf8::basic::from_utf8(buf).ok()?;
Self::new(s)
}
pub unsafe fn new_unchecked<S: Into<Cow<'a, str>>>(reference: S) -> Self {
Self(reference.into())
}
pub fn join<'b, S: Into<Cow<'b, str>>>(&self, path: S) -> Option<RedoxReference<'b>> {
let path = path.into();
Some(self.join_checked(RedoxReference::new(path)?))
}
pub fn join_checked<'b>(&self, path: RedoxReference<'b>) -> RedoxReference<'b> {
if !path.is_relative() {
path
} else if path.0.is_empty() {
RedoxReference(self.0.to_string().into())
} else {
let mut reference = self.0.clone().into_owned();
if !reference.is_empty() && !reference.ends_with('/') {
reference.push('/');
}
reference.push_str(&path.0);
RedoxReference(reference.into())
}
}
pub fn canonical(self) -> Self {
let mut canonicalized = true;
let mut is_absolute = false;
let mut parts = [""; PATH_MAX];
let mut parts_len = 0;
for (i, part) in self.0.split('/').enumerate() {
if part.is_empty() {
if i == 0 {
is_absolute = true;
} else {
canonicalized = false;
}
} else if part == "." {
canonicalized = false;
} else if part == ".." {
if parts_len > 0 {
parts_len -= 1;
}
canonicalized = false;
} else {
if parts_len >= PATH_MAX {
break;
}
parts[parts_len] = part;
parts_len += 1;
}
}
if canonicalized {
return self;
}
let mut string = String::with_capacity(self.0.len());
if is_absolute {
string.push('/');
}
for (i, part) in parts[..parts_len].iter().enumerate() {
if i > 0 {
string.push('/');
}
string.push_str(part);
}
Self(Cow::Owned(string))
}
pub fn canonical_max_upward(self, max_upward: usize) -> Option<(Self, usize)> {
let mut canonicalized = true;
let mut parts = [""; PATH_MAX];
let mut parts_len = 0;
let mut upward_depth = 0;
for (i, part) in self.0.split('/').enumerate() {
if part.is_empty() {
if i == 0 {
return None;
}
canonicalized = false;
} else if part == "." {
canonicalized = false;
} else if part == ".." {
if parts_len == 0 {
upward_depth += 1;
if upward_depth > max_upward {
return None;
}
} else {
parts_len -= 1;
}
canonicalized = false;
} else {
if parts_len + upward_depth >= PATH_MAX {
break;
}
parts[parts_len] = part;
parts_len += 1;
}
}
if canonicalized {
return Some((self, max_upward + parts_len));
}
let mut string = String::with_capacity(self.0.len());
for _ in 0..upward_depth {
if !string.is_empty() {
string.push('/');
}
string.push_str("..");
}
for part in parts[..parts_len].iter() {
if !string.is_empty() {
string.push('/');
}
string.push_str(part);
}
Some((Self(string.into()), max_upward + parts_len - upward_depth))
}
pub fn to_relative(self) -> Self {
if self.is_relative() {
return self;
}
match self.0 {
Cow::Borrowed(s) => Self(s[1..].into()),
Cow::Owned(s) => Self(s[1..].to_string().into()),
}
}
pub fn is_canon(&self) -> bool {
(self.0.is_empty() || self.0 == "/")
|| (!self.is_relative()
&& self
.0
.split('/')
.skip(1)
.all(|seg| seg != ".." && seg != "." && seg != ""))
}
pub fn dirname<'b>(&self) -> RedoxReference<'b> {
self.dirname_split().0.into_owned()
}
pub fn dirname_split(&'a self) -> (RedoxReference<'a>, Option<RedoxReference<'a>>) {
let mut slice = &*self.0;
let mut was_upward = false;
loop {
let Some((dir, file)) = slice.rsplit_once('/') else {
let file = if matches!(slice, "." | "..") {
None
} else {
Some(RedoxReference(slice.into()))
};
return (RedoxReference("".into()), file);
};
slice = dir;
was_upward = match file {
"" | "." => false,
".." => true,
_ if was_upward => false,
file => {
return (
RedoxReference(slice.into()),
Some(RedoxReference(file.into())),
);
}
};
if slice.is_empty() {
return (RedoxReference("/".into()), None);
}
}
}
pub fn is_relative(&self) -> bool {
!self.0.starts_with('/')
}
pub fn into_owned<'b>(self) -> RedoxReference<'b> {
RedoxReference(self.0.into_owned().into())
}
}
impl<'a> AsRef<str> for RedoxReference<'a> {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl<'a> fmt::Display for RedoxReference<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'a> From<RedoxReference<'a>> for Cow<'a, str> {
fn from(value: RedoxReference<'a>) -> Self {
value.0
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum RedoxPath<'a> {
Standard(RedoxReference<'a>),
Legacy(RedoxScheme<'a>, RedoxReference<'a>),
}
impl<'a> RedoxPath<'a> {
pub fn from_absolute<S: Into<Cow<'a, str>>>(path: S) -> Option<Self> {
RedoxStr::new(path).and_then(|s| s.abs())
}
pub fn from_absolute_c<S: Into<&'a CStr>>(path: S) -> Option<Self> {
RedoxStr::new_from_c(path).and_then(|s| s.abs())
}
pub fn from_absolute_buf(path: &'a [u8], len: usize) -> Option<Self> {
RedoxStr::new_from_buf(path, len).and_then(|s| s.abs())
}
pub fn from_reference(path: RedoxReference<'a>) -> Option<Self> {
RedoxStr::from(path).abs()
}
pub fn join<'b, S: Into<Cow<'b, str>>>(&self, path: S) -> Option<RedoxPath<'b>> {
let path = path.into();
Some(self.join_checked(RedoxReference::new(path)?))
}
pub fn join_checked<'b>(&self, path: RedoxReference<'b>) -> RedoxPath<'b> {
if !path.is_relative() {
RedoxPath::Standard(path)
} else {
match self {
Self::Standard(reference) => RedoxPath::Standard(reference.join_checked(path)),
Self::Legacy(scheme, reference) => RedoxPath::Legacy(
RedoxScheme(scheme.0.to_string().into()),
reference.join_checked(path),
),
}
}
}
pub fn canonical(self) -> Self {
match self {
Self::Standard(reference) => Self::Standard(reference.canonical()),
Self::Legacy(scheme, reference) => {
Self::Legacy(scheme, reference)
}
}
}
pub fn is_canon(&self) -> bool {
match self {
Self::Standard(reference) => reference.is_canon(),
Self::Legacy(_scheme, _reference) => true,
}
}
pub fn as_parts(&'a self) -> Option<(RedoxScheme<'a>, RedoxReference<'a>)> {
let (scheme, path) = self.get_scheme_prefixed(true)?;
Some((scheme, RedoxReference(path.into())))
}
pub fn get_scheme(&'a self) -> Option<RedoxScheme<'a>> {
Some(self.get_scheme_prefixed(false)?.0)
}
fn get_scheme_prefixed(&'a self, fallback: bool) -> Option<(RedoxScheme<'a>, &'a str)> {
const SCHEME_PREFIX_LENGTH: usize = "/scheme/".len();
match self {
Self::Standard(reference) => {
let mut parts = reference.0.split('/');
if parts.next() == Some("") && parts.next() == Some("scheme") {
match parts.next() {
Some(scheme_name) => {
let scheme_length = SCHEME_PREFIX_LENGTH + scheme_name.len() + 1;
let remainder = reference.0.get(scheme_length..).unwrap_or("");
return Some((RedoxScheme::new(scheme_name)?, remainder));
}
None => {
return Some((RedoxScheme(Cow::from("")), ""));
}
}
}
if !fallback {
return None;
}
let remainder = reference.0.get(1..).unwrap_or("");
return Some((RedoxScheme(Cow::from(DEFAULT_SCHEME)), remainder));
}
Self::Legacy(scheme, reference) => {
Some((scheme.clone(), reference.as_ref()))
}
}
}
pub fn matches_scheme(&self, other: &str) -> bool {
if let Some((scheme, _)) = self.get_scheme_prefixed(true) {
scheme.0 == other
} else {
false
}
}
pub fn is_scheme_category(&self, category: &str) -> bool {
if let Some((scheme, _)) = self.get_scheme_prefixed(true) {
let mut parts = scheme.0.splitn(2, '.');
if let Some(cat) = parts.next() {
cat == category && parts.next().is_some()
} else {
false
}
} else {
false
}
}
pub fn is_default_scheme(&self) -> bool {
self.matches_scheme(DEFAULT_SCHEME)
}
pub fn is_legacy(&self) -> bool {
matches!(self, RedoxPath::Legacy(_, _))
}
pub fn to_standard(self) -> RedoxReference<'a> {
match self {
RedoxPath::Standard(reference) => reference,
RedoxPath::Legacy(scheme, reference) => {
RedoxReference(format!("/scheme/{}/{}", scheme.0, reference.0).into())
}
}
}
pub fn to_standard_canon(self) -> RedoxPath<'a> {
match self {
RedoxPath::Standard(reference) => RedoxPath::Standard(reference.canonical()),
RedoxPath::Legacy(scheme, reference) => {
canonicalize_using_scheme_checked(&scheme, RedoxStr::Relative(reference))
}
}
}
pub fn dirname<'b>(&self) -> RedoxPath<'b> {
match self {
RedoxPath::Standard(redox_reference) => RedoxPath::Standard(redox_reference.dirname()),
RedoxPath::Legacy(redox_scheme, redox_reference) => RedoxPath::Legacy(
RedoxScheme(redox_scheme.to_string().into()),
redox_reference.dirname(),
),
}
}
pub fn dirname_split(&'a self) -> (RedoxPath<'a>, Option<RedoxReference<'a>>) {
match self {
RedoxPath::Standard(redox_reference) => {
let (dir, name) = redox_reference.dirname_split();
(RedoxPath::Standard(dir), name)
}
RedoxPath::Legacy(redox_scheme, redox_reference) => {
let (dir, name) = redox_reference.dirname_split();
(RedoxPath::Legacy(redox_scheme.clone(), dir), name)
}
}
}
pub fn to_reference(self) -> RedoxReference<'a> {
match self {
RedoxPath::Standard(redox_reference) => redox_reference,
RedoxPath::Legacy(_, redox_reference) => redox_reference,
}
}
pub fn as_reference(&self) -> &RedoxReference<'a> {
match self {
RedoxPath::Standard(redox_reference) => redox_reference,
RedoxPath::Legacy(_, redox_reference) => redox_reference,
}
}
pub fn canonicalize_as_cwd<'b>(&self, path: RedoxStr<'b>) -> RedoxPath<'b> {
canonicalize_using_cwd_checked(Some(self), path, false).unwrap()
}
pub fn canonicalize_as_cwd_standard<'b>(&self, path: RedoxStr<'b>) -> RedoxPath<'b> {
canonicalize_to_standard_checked(Some(self), path).unwrap()
}
pub fn canonicalize_as_cwd_with_max_upward<'b>(
&self,
relpath: RedoxReference<'b>,
max_upward: usize,
) -> Option<(RedoxPath<'b>, usize)> {
let (relpath, max_upward) = relpath.canonical_max_upward(max_upward)?;
let newpath = self.join_checked(relpath);
Some((newpath, max_upward))
}
pub fn into_owned<'b>(self) -> RedoxPath<'b> {
match self {
RedoxPath::Standard(redox_reference) => {
RedoxPath::Standard(redox_reference.into_owned())
}
RedoxPath::Legacy(redox_scheme, redox_reference) => {
RedoxPath::Legacy(redox_scheme.into_owned(), redox_reference.into_owned())
}
}
}
}
impl<'a> fmt::Display for RedoxPath<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RedoxPath::Standard(reference) => {
write!(f, "{}", reference.0)
}
RedoxPath::Legacy(scheme, reference) => {
write!(f, "{}:{}", scheme.0, reference.0)
}
}
}
}
impl<'a> From<RedoxPath<'a>> for Cow<'a, str> {
fn from(value: RedoxPath<'a>) -> Self {
match value {
RedoxPath::Standard(redox_reference) => redox_reference.0,
a => Cow::Owned(a.to_string()),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum RedoxStr<'a> {
Absolute(RedoxPath<'a>),
Relative(RedoxReference<'a>),
}
impl<'a> RedoxStr<'a> {
pub fn new<S: Into<Cow<'a, str>>>(path: S) -> Option<Self> {
let path = path.into();
let r = if path.starts_with('/') {
RedoxPath::Standard(RedoxReference::new(path)?)
} else {
if let Some((scheme, reference)) = Self::split_scheme(&path) {
RedoxPath::Legacy(scheme, RedoxReference::new(reference)?)
} else {
return Some(RedoxStr::Relative(RedoxReference::new(path)?));
}
};
Some(RedoxStr::Absolute(r))
}
pub fn new_from_c<S: Into<&'a CStr>>(path: S) -> Option<Self> {
let path = path.into();
if matches!(path.to_bytes_with_nul().first(), Some(b'/')) {
Some(RedoxStr::Absolute(RedoxPath::Standard(
RedoxReference::new_from_c(path)?,
)))
} else {
#[cfg(feature = "simd")]
let s = simdutf8::basic::from_utf8(path.to_bytes()).ok()?;
#[cfg(not(feature = "simd"))]
let s = path.to_str().ok()?;
unsafe { Some(RedoxStr::new_unchecked(s)) }
}
}
pub fn new_from_buf(path: &'a [u8], len: usize) -> Option<RedoxStr<'a>> {
let buf = path.get(..len)?;
#[cfg(not(feature = "simd"))]
let s = str::from_utf8(buf).ok()?;
#[cfg(feature = "simd")]
let s = simdutf8::basic::from_utf8(buf).ok()?;
Self::new(s)
}
pub unsafe fn new_unchecked<S: Into<Cow<'a, str>>>(path: S) -> Self {
let path = path.into();
let r = if path.starts_with('/') {
RedoxPath::Standard(RedoxReference(path))
} else {
if let Some((scheme, reference)) = Self::split_scheme(&path) {
RedoxPath::Legacy(scheme, RedoxReference(reference))
} else {
return RedoxStr::Relative(RedoxReference(path));
}
};
RedoxStr::Absolute(r)
}
fn split_scheme(path: &Cow<'a, str>) -> Option<(RedoxScheme<'a>, Cow<'a, str>)> {
#[cfg(not(feature = "simd"))]
let i = {
let mut i = 0;
for &byte in path.as_bytes().iter() {
if byte == b'/' {
return None;
}
if byte == b':' {
break;
}
i += 1;
}
i
};
#[cfg(feature = "simd")]
let i = {
let path_b = path.as_bytes();
let chr = memchr::memchr2(b'/', b':', path_b);
match chr {
Some(c) if path_b.get(c).filter(|&&b| b == b':').is_some() => c,
_ => return None,
}
};
let (scheme, path): (Cow<'a, str>, Cow<'a, str>) = match path {
Cow::Borrowed(s) => (s.get(..i)?.into(), s.get(i + 1..)?.into()),
Cow::Owned(s) => (
s.get(..i)?.to_string().into(),
s.get(i + 1..)?.to_string().into(),
),
};
Some((RedoxScheme::new(scheme)?, path))
}
pub fn as_abs(&'a self) -> Option<&'a RedoxPath<'a>> {
match self {
RedoxStr::Absolute(redox_path) => Some(redox_path),
_ => None,
}
}
pub fn as_rel(&'a self) -> Option<&'a RedoxReference<'a>> {
match self {
RedoxStr::Relative(redox_reference) => Some(redox_reference),
_ => None,
}
}
pub fn abs(self) -> Option<RedoxPath<'a>> {
match self {
RedoxStr::Absolute(redox_path) => Some(redox_path),
_ => None,
}
}
pub fn rel(self) -> Option<RedoxReference<'a>> {
match self {
RedoxStr::Relative(redox_reference) => Some(redox_reference),
_ => None,
}
}
pub fn is_canon(&self) -> bool {
match self {
RedoxStr::Absolute(redox_path) => redox_path.is_canon(),
RedoxStr::Relative(redox_reference) => redox_reference.is_canon(),
}
}
pub fn canonical(self) -> Self {
match self {
RedoxStr::Absolute(redox_path) => RedoxStr::from(redox_path.canonical()),
RedoxStr::Relative(redox_reference) => RedoxStr::from(redox_reference.canonical()),
}
}
pub fn to_standard(self) -> RedoxReference<'a> {
match self {
RedoxStr::Absolute(redox_path) => redox_path.to_standard(),
RedoxStr::Relative(redox_reference) => redox_reference,
}
}
pub fn dirname<'b>(&self) -> RedoxStr<'b> {
match self {
RedoxStr::Absolute(redox_path) => RedoxStr::from(redox_path.dirname()),
RedoxStr::Relative(redox_reference) => RedoxStr::from(redox_reference.dirname()),
}
}
pub fn dirname_split(&'a self) -> (RedoxStr<'a>, Option<RedoxReference<'a>>) {
match self {
RedoxStr::Absolute(redox_path) => {
let (dir, name) = redox_path.dirname_split();
(RedoxStr::from(dir), name)
}
RedoxStr::Relative(redox_reference) => {
let (dir, name) = redox_reference.dirname_split();
(RedoxStr::from(dir), name)
}
}
}
pub fn is_empty(&self) -> bool {
match self {
RedoxStr::Absolute(_) => false, RedoxStr::Relative(redox_reference) => redox_reference.as_ref().is_empty(),
}
}
pub fn join<'b, S: Into<Cow<'b, str>>>(&self, path: S) -> Option<RedoxStr<'b>> {
match self {
RedoxStr::Absolute(redox_path) => Some(RedoxStr::from(redox_path.join(path)?)),
RedoxStr::Relative(redox_reference) => {
Some(RedoxStr::from(redox_reference.join(path)?))
}
}
}
pub fn join_checked<'b>(&self, path: RedoxReference<'b>) -> RedoxStr<'b> {
match self {
RedoxStr::Absolute(redox_path) => RedoxStr::from(redox_path.join_checked(path)),
RedoxStr::Relative(redox_reference) => {
RedoxStr::from(redox_reference.join_checked(path))
}
}
}
pub fn is_relative(&self) -> bool {
matches!(self, RedoxStr::Relative(_))
}
pub fn into_owned<'b>(self) -> RedoxStr<'b> {
match self {
RedoxStr::Absolute(redox_path) => RedoxStr::Absolute(redox_path.into_owned()),
RedoxStr::Relative(redox_reference) => RedoxStr::Relative(redox_reference.into_owned()),
}
}
}
impl<'a> From<RedoxPath<'a>> for RedoxStr<'a> {
fn from(value: RedoxPath<'a>) -> Self {
Self::Absolute(value)
}
}
impl<'a> From<RedoxReference<'a>> for RedoxStr<'a> {
fn from(value: RedoxReference<'a>) -> Self {
if !value.is_relative() {
RedoxStr::Absolute(RedoxPath::Standard(value))
} else {
RedoxStr::Relative(value)
}
}
}
impl<'a> From<RedoxStr<'a>> for Cow<'a, str> {
fn from(value: RedoxStr<'a>) -> Self {
match value {
RedoxStr::Absolute(redox_path) => redox_path.into(),
RedoxStr::Relative(redox_reference) => redox_reference.into(),
}
}
}
pub fn canonicalize_using_cwd<'a>(cwd_opt: Option<&'a str>, path: &'a str) -> Option<Cow<'a, str>> {
let cwd_opt = cwd_opt.and_then(|s| RedoxPath::from_absolute(Cow::Borrowed(s)));
let absolute = canonicalize_using_cwd_checked(cwd_opt.as_ref(), RedoxStr::new(path)?, false)?;
Some(absolute.into())
}
fn canonicalize_using_cwd_checked<'a, 'b>(
cwd_opt: Option<&RedoxPath<'a>>,
path: RedoxStr<'b>,
in_scheme_root: bool,
) -> Option<RedoxPath<'b>> {
let absolute = match (path, in_scheme_root) {
(RedoxStr::Absolute(absolute), true) if absolute.get_scheme().is_some() => absolute,
(RedoxStr::Absolute(absolute), true) => {
cwd_opt?.join_checked(absolute.to_standard().to_relative())
}
(RedoxStr::Absolute(absolute), false) => absolute,
(RedoxStr::Relative(relative), true) => cwd_opt?.join_checked(relative.canonical()),
(RedoxStr::Relative(relative), false) => cwd_opt?.join_checked(relative),
};
Some(absolute.canonical())
}
pub fn canonicalize_to_standard<'a>(
cwd_opt: Option<&'a str>,
path: &'a str,
) -> Option<Cow<'a, str>> {
let absolute = match RedoxPath::from_absolute(path) {
Some(absolute) => absolute,
None => {
let absolute = RedoxPath::from_absolute(cwd_opt?)?;
absolute.join(path)?
}
};
Some(absolute.to_standard_canon().into())
}
fn canonicalize_to_standard_checked<'a, 'b>(
cwd_opt: Option<&RedoxPath<'a>>,
path: RedoxStr<'b>,
) -> Option<RedoxPath<'b>> {
let path = match path {
RedoxStr::Absolute(absolute) => absolute,
RedoxStr::Relative(relative) => cwd_opt?.join_checked(relative),
}
.to_standard_canon();
Some(path)
}
pub fn canonicalize_using_scheme<'a>(scheme: &'a str, path: &'a str) -> Option<Cow<'a, str>> {
let path = RedoxStr::new(path)?;
Some(canonicalize_using_scheme_checked(&RedoxScheme::new(scheme)?, path).into())
}
fn canonicalize_using_scheme_checked<'a, 'b>(
scheme: &RedoxScheme<'_>,
path: RedoxStr<'b>,
) -> RedoxPath<'b> {
let path = match path {
RedoxStr::Absolute(s) => {
if !s.is_legacy() && s.matches_scheme(&scheme.0) {
return s;
}
RedoxStr::Absolute(s)
}
path => path,
};
canonicalize_using_cwd_checked(Some(&scheme_path_checked(&scheme)), path, true).unwrap()
}
pub fn canonicalize_using_cwd_with_max_upward<'a>(
cwd: &str,
relpath: &'a str,
max_upward: usize,
) -> Option<(Cow<'a, str>, usize)> {
let cwd = RedoxStr::new(Cow::Borrowed(cwd))?;
let (absolute, max_upward) = canonicalize_using_cwd_with_max_upward_checked(
&cwd,
RedoxStr::new(relpath)?.rel()?,
max_upward,
)?;
Some((absolute.into(), max_upward))
}
fn canonicalize_using_cwd_with_max_upward_checked<'a, 'b>(
cwd: &RedoxStr<'a>,
relpath: RedoxReference<'b>,
max_upward: usize,
) -> Option<(RedoxStr<'b>, usize)> {
let (relpath, max_upward) = relpath.canonical_max_upward(max_upward)?;
let relnew = cwd.join_checked(relpath);
Some((relnew, max_upward))
}
pub fn scheme_path(name: &str) -> Option<RedoxPath<'_>> {
let scheme = RedoxScheme::new(name)?;
Some(scheme_path_checked(&scheme))
}
fn scheme_path_checked<'a, 'b>(scheme: &'a RedoxScheme<'a>) -> RedoxPath<'b> {
if scheme.0.is_empty() {
return RedoxPath::Standard(RedoxReference("/scheme".into()));
}
RedoxPath::Standard(RedoxReference(format!("/scheme/{}", scheme.0).into()))
}
pub fn make_scheme_name<'a, 'b>(category: &'a str, detail: &'a str) -> Option<RedoxScheme<'b>> {
let name = format!("{}.{}", category, detail);
RedoxScheme::new(name)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
#[test]
fn test_absolute() {
let cwd_opt = None;
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/"),
Some(Cow::Borrowed("/"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/file"),
Some(Cow::Borrowed("/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/folder/file"),
Some(Cow::Borrowed("/folder/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/folder/file/"),
Some(Cow::Borrowed("/folder/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/folder/file//"),
Some(Cow::Borrowed("/folder/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/folder/../file"),
Some(Cow::Borrowed("/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/folder/../.."),
Some(Cow::Borrowed("/"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/folder/../../../.."),
Some(Cow::Borrowed("/"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/.."),
Some(Cow::Borrowed("/"))
);
}
#[test]
fn test_new_relative() {
let cwd_opt = Some("/scheme/foo");
assert_eq!(
canonicalize_using_cwd(cwd_opt, "file"),
Some(Cow::Borrowed("/scheme/foo/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "folder/file"),
Some(Cow::Borrowed("/scheme/foo/folder/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "folder/../file"),
Some(Cow::Borrowed("/scheme/foo/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "folder/../.."),
Some(Cow::Borrowed("/scheme"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "folder/../../../.."),
Some(Cow::Borrowed("/"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, ".."),
Some(Cow::Borrowed("/scheme"))
);
}
#[test]
fn test_new_scheme() {
let cwd_opt = None;
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/scheme/bar/"),
Some(Cow::Borrowed("/scheme/bar"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/scheme/bar/file"),
Some(Cow::Borrowed("/scheme/bar/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/file"),
Some(Cow::Borrowed("/scheme/bar/folder/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../file"),
Some(Cow::Borrowed("/scheme/bar/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../.."),
Some(Cow::Borrowed("/scheme"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../../../.."),
Some(Cow::Borrowed("/"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "/scheme/bar/.."),
Some(Cow::Borrowed("/scheme"))
);
assert_eq!(
canonicalize_using_scheme("bar", ""),
Some(Cow::Borrowed("/scheme/bar"))
);
assert_eq!(
canonicalize_using_scheme("bar", "foo"),
Some(Cow::Borrowed("/scheme/bar/foo"))
);
assert_eq!(
canonicalize_using_scheme("foo", "/bar"),
Some(Cow::Borrowed("/scheme/foo/bar"))
);
assert_eq!(
canonicalize_using_scheme("bar", ".."),
Some(Cow::Borrowed("/scheme/bar"))
);
assert_eq!(
canonicalize_using_scheme("file", "/scheme/foo/bar"),
Some(Cow::Borrowed("/scheme/foo/bar"))
);
}
#[test]
fn test_old_relative() {
let cwd_opt = Some("foo:");
assert_eq!(
canonicalize_using_cwd(cwd_opt, "file"),
Some(Cow::Borrowed("foo:file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "folder/file"),
Some(Cow::Borrowed("foo:folder/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "folder/../file"),
Some(Cow::Borrowed("foo:folder/../file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "folder/../.."),
Some(Cow::Borrowed("foo:folder/../.."))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "folder/../../../.."),
Some(Cow::Borrowed("foo:folder/../../../.."))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, ".."),
Some(Cow::Borrowed("foo:.."))
);
}
#[test]
fn test_old_scheme() {
let cwd_opt = None;
assert_eq!(
canonicalize_using_cwd(cwd_opt, "bar:"),
Some(Cow::Borrowed("bar:"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "bar:file"),
Some(Cow::Borrowed("bar:file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "bar:folder/file"),
Some(Cow::Borrowed("bar:folder/file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "bar:folder/../file"),
Some(Cow::Borrowed("bar:folder/../file"))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "bar:folder/../.."),
Some(Cow::Borrowed("bar:folder/../.."))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "bar:folder/../../../.."),
Some(Cow::Borrowed("bar:folder/../../../.."))
);
assert_eq!(
canonicalize_using_cwd(cwd_opt, "bar:.."),
Some(Cow::Borrowed("bar:.."))
);
}
#[test]
fn test_max_upward() {
assert_eq!(
canonicalize_using_cwd_with_max_upward("/", "foo/bar", 0),
Some((Cow::Borrowed("/foo/bar"), 2))
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("/foo/", "bar/../baz/qux", 0),
Some((Cow::Borrowed("/foo/baz/qux"), 2))
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("foo/../bar", "baz/./../qux", 0),
Some((Cow::Borrowed("foo/../bar/qux"), 1))
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("/foo", "../bar", 1),
Some((Cow::Borrowed("/foo/../bar"), 1))
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("foo/..", "../bar/..", 2),
Some((Cow::Borrowed("foo/../.."), 1))
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("foo:", "bar/baz", 0),
Some((Cow::Borrowed("foo:bar/baz"), 2))
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("foo:", "bar/baz//../../", 0),
Some((Cow::Borrowed("foo:"), 0))
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("foo:/bar/..", "baz/./qux/..", 0),
Some((Cow::Borrowed("foo:/bar/../baz"), 1))
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("/foo", "../bar", 0),
None
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("/foo", "/bar", 0),
None
);
assert_eq!(
canonicalize_using_cwd_with_max_upward("/foo/..", "../bar/..", 0),
None
);
}
#[test]
fn test_cow_borrowing() {
let cwd_opt = None;
for (path, should_owned) in [
("/foo/bar/baz", false),
("/scheme/foo/bar/baz", false),
("foo/bar/baz", false), ("/scheme/foo/../baz", true),
("foo:bar", true),
] {
assert_eq!(
matches!(canonicalize_using_cwd(cwd_opt, path), Some(Cow::Owned(_))),
should_owned
);
}
}
#[test]
fn test_dirname() {
for (a, b) in [
("/foo/bar/baz", "/foo/bar"),
("/baz/foo/bar/", "/baz/foo"),
("/", "/"),
("/scheme/foo", "/scheme"),
("foo:bar/baz/", "foo:bar"),
("foo:bar", "foo:"),
("foo:", "foo:"),
] {
assert_eq!(
RedoxPath::from_absolute(a).unwrap().dirname(),
RedoxPath::from_absolute(b).unwrap()
);
}
for (a, b) in [
("foo/bar/.", "foo"),
("./foo/bar/..", "."),
("./.", ""),
(".", ""),
("", ""),
] {
assert_eq!(
RedoxStr::new(a).unwrap().dirname(),
RedoxStr::new(b).unwrap()
);
}
}
#[test]
fn test_orbital_scheme() {
for flag_str in &["", "abflrtu"] {
for x in &[-1, 0, 1] {
for y in &[-1, 0, 1] {
for w in &[0, 1] {
for h in &[0, 1] {
for title in &[
"",
"title",
"title/with/slashes",
"title:with:colons",
"title/../with/../dots/..",
] {
let path = format!(
"orbital:{}/{}/{}/{}/{}/{}",
flag_str, x, y, w, h, title
);
assert_eq!(
canonicalize_using_cwd(None, &path),
Some((&path).into())
);
}
}
}
}
}
}
}
#[test]
fn test_parts() {
for (path, scheme, reference) in [
("/foo/bar/baz", "file", "foo/bar/baz"),
("/scheme/foo/bar/baz", "foo", "bar/baz"),
("/", "file", ""),
("/bar", "file", "bar"),
("/...", "file", "..."),
("//double/slash", "file", "/double/slash"),
("/ending/in/slash/", "file", "ending/in/slash/"),
("/contains/dot/.", "file", "contains/dot/."),
("/contains/dotdot/..", "file", "contains/dotdot/.."),
] {
let redox_path = RedoxPath::from_absolute(path).unwrap();
let parts = redox_path.as_parts();
assert_eq!(
(path, parts),
(
path,
Some((
RedoxScheme::new(scheme).unwrap(),
RedoxReference::new(reference).unwrap()
))
)
);
if path.starts_with("/scheme") {
let to_string = format!("/scheme/{scheme}");
let joined_path = RedoxPath::from_absolute(&to_string)
.unwrap()
.join(reference)
.unwrap();
assert_eq!(path, &format!("{joined_path}"));
} else {
assert_eq!(path, &format!("/{reference}"));
}
}
assert_eq!(RedoxPath::from_absolute("not/absolute"), None);
for path in ["/scheme/use:colon/", "/scheme/βeta/"] {
let redox_path = RedoxPath::from_absolute(path).unwrap();
let parts = redox_path.as_parts();
assert_eq!((path, parts), (path, None));
}
}
#[test]
fn test_old_scheme_parts() {
for (path, scheme, reference) in [
("foo:bar/baz", "foo", "bar/baz"),
("emptyref:", "emptyref", ""),
(":emptyscheme", "", "emptyscheme"),
] {
let redox_path = RedoxPath::from_absolute(path).unwrap();
let parts = redox_path.as_parts();
assert_eq!(
(path, parts),
(
path,
Some((
RedoxScheme::new(scheme).unwrap(),
RedoxReference::new(reference).unwrap()
))
)
);
}
assert_eq!(RedoxPath::from_absolute("scheme/withslash:path"), None);
assert_eq!(RedoxPath::from_absolute(""), None)
}
#[test]
fn test_matches() {
assert!(RedoxPath::from_absolute("/scheme/foo")
.unwrap()
.matches_scheme("foo"));
assert!(RedoxPath::from_absolute("/scheme/foo/bar")
.unwrap()
.matches_scheme("foo"));
assert!(!RedoxPath::from_absolute("/scheme/foo")
.unwrap()
.matches_scheme("bar"));
assert!(RedoxPath::from_absolute("foo:")
.unwrap()
.matches_scheme("foo"));
assert!(RedoxPath::from_absolute(
canonicalize_using_cwd(Some("/scheme/foo"), "bar").unwrap()
)
.unwrap()
.matches_scheme("foo"));
assert!(
RedoxPath::from_absolute(canonicalize_using_cwd(Some("/foo"), "bar").unwrap())
.unwrap()
.matches_scheme("file")
);
assert!(RedoxPath::from_absolute(
canonicalize_using_cwd(Some("/scheme"), "foo/bar").unwrap()
)
.unwrap()
.matches_scheme("foo"));
assert!(RedoxPath::from_absolute("foo:/bar")
.unwrap()
.matches_scheme("foo"));
assert!(!RedoxPath::from_absolute("foo:/bar")
.unwrap()
.matches_scheme("bar"));
assert!(RedoxPath::from_absolute("/scheme/file")
.unwrap()
.is_default_scheme());
assert!(!RedoxPath::from_absolute("/scheme/foo")
.unwrap()
.is_default_scheme());
assert!(RedoxPath::from_absolute("file:bar")
.unwrap()
.is_default_scheme());
assert!(RedoxPath::from_absolute("file:")
.unwrap()
.is_default_scheme());
assert!(!RedoxPath::from_absolute("foo:bar")
.unwrap()
.is_default_scheme());
assert!(RedoxPath::from_absolute("foo:bar").unwrap().is_legacy());
assert!(!RedoxPath::from_absolute("/foo/bar").unwrap().is_legacy());
}
#[test]
fn test_to_standard() {
assert_eq!(
RedoxPath::from_absolute("foo:bar")
.unwrap()
.to_standard()
.as_ref(),
"/scheme/foo/bar"
);
assert_eq!(
RedoxPath::from_absolute("file:bar")
.unwrap()
.to_standard()
.as_ref(),
"/scheme/file/bar"
);
assert_eq!(
RedoxPath::from_absolute("/scheme/foo/bar")
.unwrap()
.to_standard()
.as_ref(),
"/scheme/foo/bar"
);
assert_eq!(
RedoxPath::from_absolute("/foo/bar")
.unwrap()
.to_standard()
.as_ref(),
"/foo/bar"
);
assert_eq!(
&RedoxPath::from_absolute("foo:bar/../bar2")
.unwrap()
.to_standard_canon()
.to_string(),
"/scheme/foo/bar2"
);
assert_eq!(
&RedoxPath::from_absolute("file:bar/./../bar2")
.unwrap()
.to_standard_canon()
.to_string(),
"/scheme/file/bar2"
);
assert_eq!(
&RedoxPath::from_absolute("/scheme/file/bar/./../../foo/bar")
.unwrap()
.to_standard_canon()
.to_string(),
"/scheme/foo/bar"
);
assert_eq!(
&RedoxPath::from_absolute("/foo/bar")
.unwrap()
.to_standard_canon()
.to_string(),
"/foo/bar"
);
assert_eq!(
&canonicalize_to_standard(None, "/scheme/foo/bar").unwrap(),
"/scheme/foo/bar"
);
assert_eq!(
&canonicalize_to_standard(None, "foo:bar").unwrap(),
"/scheme/foo/bar"
);
assert_eq!(
&canonicalize_to_standard(None, "foo:bar/../..").unwrap(),
"/scheme/foo"
);
assert_eq!(
&canonicalize_to_standard(None, "/scheme/foo/bar/..").unwrap(),
"/scheme/foo"
);
assert_eq!(
&canonicalize_to_standard(None, "foo:bar/bar2/..").unwrap(),
"/scheme/foo/bar"
);
}
#[test]
fn test_scheme_path() {
assert_eq!(
scheme_path("foo"),
Some(RedoxPath::from_absolute("/scheme/foo").unwrap())
);
assert_eq!(
scheme_path(""),
Some(RedoxPath::from_absolute("/scheme").unwrap())
);
assert_eq!(scheme_path("/foo"), None);
assert_eq!(scheme_path("foo/bar"), None);
assert_eq!(scheme_path("foo:"), None);
}
#[test]
fn test_category() {
assert_eq!(
make_scheme_name("foo", "bar"),
Some(RedoxScheme::new("foo.bar").unwrap())
);
assert_eq!(
RedoxPath::from_absolute(
scheme_path(make_scheme_name("foo", "bar").unwrap().as_ref()).unwrap()
)
.unwrap(),
RedoxPath::Standard(RedoxReference::new("/scheme/foo.bar").unwrap())
);
assert_eq!(make_scheme_name("foo", "/bar"), None);
assert_eq!(make_scheme_name("foo", ":bar"), None);
assert!(RedoxPath::from_absolute(
scheme_path(make_scheme_name("foo", "bar").unwrap().as_ref()).unwrap()
)
.unwrap()
.is_scheme_category("foo"));
assert!(RedoxPath::from_absolute("/scheme/foo.bar/bar2")
.unwrap()
.is_scheme_category("foo"));
assert!(!RedoxPath::from_absolute("/scheme/foo/bar")
.unwrap()
.is_scheme_category("foo"));
assert!(!RedoxPath::from_absolute("/foo.bar/bar2")
.unwrap()
.is_scheme_category("foo"));
}
}