use std::cmp::{Ordering, PartialEq};
use std::ffi::OsStr;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Apath(String);
impl Apath {
pub fn is_valid(a: &str) -> bool {
if !a.starts_with('/') {
return false;
} else if a.len() == 1 {
return true;
}
for part in a[1..].split('/') {
if part.is_empty() || part == "." || part == ".." || part.contains('\0') {
return false;
}
}
true
}
#[must_use]
pub fn append(&self, child_name: &str) -> Apath {
let mut c = self.0.clone();
if c != "/" {
c.push('/');
}
c.push_str(child_name);
Apath(c)
}
#[must_use]
pub fn is_prefix_of(&self, a: &Apath) -> bool {
let len = self.0.len();
match len.cmp(&a.0.len()) {
Ordering::Greater => false,
Ordering::Equal => self.0 == a.0,
Ordering::Less => {
a.0.starts_with(&self.0)
&& (self.0.ends_with('/') || a.0.chars().nth(self.0.len()) == Some('/'))
}
}
}
#[must_use]
pub fn below<R: Into<PathBuf>>(&self, tree_root: R) -> PathBuf {
let mut buf: PathBuf = tree_root.into();
buf.push(&self[1..]);
buf
}
#[must_use]
pub fn root() -> Apath {
"/".into()
}
}
impl FromStr for Apath {
type Err = ApathParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !Apath::is_valid(s) {
Err(ApathParseError {})
} else {
Ok(Apath(s.to_owned()))
}
}
}
#[derive(Debug)]
pub struct DecodeFilenameError<'name> {
name: &'name OsStr,
}
impl<'name> fmt::Display for DecodeFilenameError<'name> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Couldn't decode filename {:?}", self.name)
}
}
#[derive(Debug)]
pub struct ApathParseError {}
impl std::error::Error for ApathParseError {}
impl fmt::Display for ApathParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Invalid apath: must have an initial slash and no ..")
}
}
impl From<Apath> for String {
fn from(a: Apath) -> String {
a.0
}
}
impl<'a> From<&'a Apath> for &'a str {
fn from(a: &'a Apath) -> &'a str {
&a.0
}
}
impl<'a> From<&'a str> for Apath {
fn from(s: &'a str) -> Apath {
assert!(Apath::is_valid(s), "invalid apath: {s:?}");
Apath(s.to_string())
}
}
impl From<String> for Apath {
fn from(s: String) -> Apath {
assert!(Apath::is_valid(&s), "invalid apath: {s:?}");
Apath(s)
}
}
impl Display for Apath {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
write!(fmt, "{}", self.0)
}
}
impl PartialEq<str> for Apath {
fn eq(&self, other: &str) -> bool {
self.0 == *other
}
}
impl PartialEq<&str> for Apath {
fn eq(&self, other: &&str) -> bool {
self.0 == **other
}
}
impl PartialEq<Apath> for &str {
fn eq(&self, other: &Apath) -> bool {
other == *self
}
}
impl Deref for Apath {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for Apath {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<Path> for Apath {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl Ord for Apath {
fn cmp(&self, b: &Apath) -> Ordering {
let Apath(a) = self;
let Apath(b) = b;
let mut ait = a.split('/');
let mut bit = b.split('/');
let mut oa = ait.next().expect("paths must not be empty");
let mut ob = bit.next().expect("paths must not be empty");
loop {
match (ait.next(), bit.next()) {
(None, None) => return oa.cmp(ob),
(None, Some(_bc)) => return Ordering::Less,
(Some(_ac), None) => return Ordering::Greater,
(Some(ac), Some(bc)) => match oa.cmp(ob) {
Ordering::Equal => {
oa = ac;
ob = bc;
continue;
}
other => return other,
},
}
}
}
}
impl PartialOrd for Apath {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Default)]
pub struct CheckOrder {
last_apath: Option<Apath>,
}
impl CheckOrder {
#[allow(clippy::new_without_default)]
pub fn new() -> CheckOrder {
CheckOrder { last_apath: None }
}
pub fn check(&mut self, a: &Apath) {
if let Some(ref last_apath) = self.last_apath {
assert!(
last_apath < a,
"apaths out of order: {last_apath:?} should be before {a:?}"
);
}
self.last_apath = Some(a.clone());
}
}
#[cfg(debug_assertions)]
#[derive(Debug, Default)]
pub struct DebugCheckOrder(CheckOrder);
#[cfg(debug_assertions)]
impl DebugCheckOrder {
#[allow(clippy::new_without_default)]
pub fn new() -> DebugCheckOrder {
DebugCheckOrder(CheckOrder::new())
}
pub fn check(&mut self, apath: &Apath) {
self.0.check(apath)
}
}
#[cfg(not(debug_assertions))]
#[derive(Debug, Default)]
pub struct DebugCheckOrder();
#[cfg(not(debug_assertions))]
impl DebugCheckOrder {
#[allow(clippy::new_without_default)]
pub fn new() -> DebugCheckOrder {
DebugCheckOrder()
}
pub fn check(&mut self, _apath: &Apath) {}
}
#[cfg(test)]
mod test {
use super::Apath;
#[test]
fn parse() {
let apath: Apath = "/something".parse().unwrap();
assert_eq!(apath.to_string(), "/something");
}
#[test]
fn is_prefix_of() {
use std::ops::Not;
assert!(Apath::from("/").is_prefix_of(&Apath::from("/stuff")));
assert!(Apath::from("/").is_prefix_of(&Apath::from("/")));
assert!(Apath::from("/stuff").is_prefix_of(&Apath::from("/stuff/file")));
assert!(Apath::from("/stuff/file")
.is_prefix_of(&Apath::from("/stuff"))
.not());
assert!(Apath::from("/this")
.is_prefix_of(&Apath::from("/that"))
.not());
assert!(Apath::from("/this")
.is_prefix_of(&Apath::from("/that/other"))
.not());
}
#[test]
pub fn invalid() {
let invalid_cases = [
"",
"//",
"//a",
"/a//b",
"/a/",
"/a//",
"./a/b",
"/./a/b",
"/a/b/.",
"/a/./b",
"/a/b/../c",
"../a",
"/hello\0",
];
for v in invalid_cases.iter() {
assert!(!Apath::is_valid(v), "{v:?} incorrectly marked valid");
}
}
#[test]
pub fn valid_and_ordered() {
let ordered = [
"/",
"/...a",
"/.a",
"/a",
"/b",
"/kleine Katze Fuß",
"/~~",
"/ñ",
"/a/...",
"/a/..obscure",
"/a/.config",
"/a/1",
"/a/100",
"/a/2",
"/a/añejo",
"/a/b/c",
"/b/((",
"/b/,",
"/b/A",
"/b/AAAA",
"/b/a",
"/b/b",
"/b/c",
"/b/a/c",
"/b/b/c",
"/b/b/b/z",
"/b/b/b/{zz}",
];
for (i, a) in ordered.iter().enumerate() {
assert!(Apath::is_valid(a), "{a:?} incorrectly marked invalid");
let ap = Apath::from(*a);
assert_eq!(format!("{ap}"), *a);
for (j, b) in ordered.iter().enumerate() {
let expected_order = i.cmp(&j);
let bp = Apath::from(*b);
let r = ap.cmp(&bp);
assert_eq!(
r, expected_order,
"cmp({ap:?}, {bp:?}): returned {r:?} expected {expected_order:?}"
);
}
}
}
}