use std::{fmt, path::Path};
use camino::{Utf8Component, Utf8Path};
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StoredPathBuf(String);
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StoredPath(str);
impl StoredPathBuf {
pub fn new(v: String) -> Self {
Self(v)
}
pub fn from_std_path(path: &Path) -> Option<Self> {
Utf8Path::from_path(path).map(Self::from_utf8_path)
}
pub fn from_utf8_path(path: &Utf8Path) -> Self {
let mut result = String::new();
let mut multipart = false;
for component in path.components() {
if multipart {
result.push('\\');
}
let part = match component {
Utf8Component::Prefix(prefix) => prefix.as_str(),
Utf8Component::RootDir => "\\",
other => {
multipart = true;
other.as_str()
}
};
result.push_str(part);
}
Self(result)
}
}
impl StoredPath {
pub fn new(v: &str) -> &Self {
unsafe { std::mem::transmute(v) }
}
pub fn as_str(&self) -> &str {
self
}
pub fn extension(&self) -> Option<&str> {
self.stem_and_extension().1
}
pub fn file_stem(&self) -> Option<&str> {
self.stem_and_extension().0
}
fn stem_and_extension(&self) -> (Option<&str>, Option<&str>) {
let Some(name) = self.file_name() else {
return (None, None);
};
if let Some((stem, extension)) = name.rsplit_once('.') {
if stem.is_empty() {
(Some(name), None)
} else {
(Some(stem), Some(extension))
}
} else {
(Some(name), None)
}
}
pub fn file_name(&self) -> Option<&str> {
let mut path = self.as_str();
while let Some(prefix) = path
.strip_suffix('\\')
.or_else(|| path.strip_suffix('/'))
.or_else(|| path.strip_suffix("/."))
.or_else(|| path.strip_suffix("\\."))
{
path = prefix;
}
let name1 = path.rsplit_once('\\').map(|(_, name)| name);
let name2 = path.rsplit_once('/').map(|(_, name)| name);
let name = match (name1, name2) {
(Some(name), None) | (None, Some(name)) => name,
(Some(name1), Some(name2)) => {
if name1.len() < name2.len() {
name1
} else {
name2
}
}
(None, None) => path,
};
if name.is_empty() || name == "." || name == ".." {
None
} else {
Some(name)
}
}
}
impl std::ops::Deref for StoredPathBuf {
type Target = StoredPath;
fn deref(&self) -> &Self::Target {
StoredPath::new(&self.0)
}
}
impl std::ops::Deref for StoredPath {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::borrow::Borrow<StoredPath> for StoredPathBuf {
fn borrow(&self) -> &StoredPath {
self
}
}
impl std::borrow::ToOwned for StoredPath {
type Owned = StoredPathBuf;
fn to_owned(&self) -> StoredPathBuf {
StoredPathBuf::new(self.0.to_owned())
}
}
impl std::convert::From<String> for StoredPathBuf {
fn from(v: String) -> Self {
Self::new(v)
}
}
impl std::convert::From<StoredPathBuf> for String {
fn from(v: StoredPathBuf) -> Self {
v.0
}
}
impl<'a> std::convert::From<&'a StoredPathBuf> for String {
fn from(v: &'a StoredPathBuf) -> Self {
v.0.clone()
}
}
impl<'a> std::convert::From<&'a str> for StoredPathBuf {
fn from(v: &'a str) -> Self {
StoredPath::new(v).to_owned()
}
}
impl std::fmt::Debug for StoredPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Debug::fmt(self.as_str(), f)
}
}
impl std::fmt::Display for StoredPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(self.as_str(), f)
}
}
impl std::fmt::Debug for StoredPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Debug::fmt(self.as_str(), f)
}
}
impl std::fmt::Display for StoredPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(self.as_str(), f)
}
}
#[cfg(test)]
mod test {
use super::StoredPathBuf;
use camino::Utf8Path;
#[test]
fn absolute_windows_path_conversion() {
const INPUT: &str = "C:\\Users\\test\\AppData\\Local\\Temp\\.tmpMh0Mxg\\Example.tar.gz";
let path = StoredPathBuf::from_utf8_path(Utf8Path::new(INPUT));
assert_eq!(path.as_str(), INPUT);
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn verbatim_absolute_windows_path_conversion() {
const INPUT: &str =
"\\\\?\\C:\\Users\\test\\AppData\\Local\\Temp\\.tmpMh0Mxg\\Example.tar.gz";
let path = StoredPathBuf::from_utf8_path(Utf8Path::new(INPUT));
assert_eq!(path.as_str(), INPUT);
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn relative_windows_path_conversion() {
const INPUT: &str = "resource\\Example.tar.gz";
let path = StoredPathBuf::from_utf8_path(Utf8Path::new(INPUT));
assert_eq!(path.as_str(), INPUT);
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn absolute_unix_path_conversion() {
const INPUT: &str = "/users/home/test/Example.tar.gz";
let path = StoredPathBuf::from_utf8_path(Utf8Path::new(INPUT));
assert_eq!(path.as_str(), "\\users\\home\\test\\Example.tar.gz");
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn relative_unix_path_conversion() {
const INPUT: &str = "resource/Example.tar.gz";
let path = StoredPathBuf::from_utf8_path(Utf8Path::new(INPUT));
assert_eq!(path.as_str(), "resource\\Example.tar.gz");
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn mixed_path_conversion1() {
const INPUT: &str = "resource/blah\\Example.tar.gz";
let path = StoredPathBuf::from_utf8_path(Utf8Path::new(INPUT));
assert_eq!(path.as_str(), "resource\\blah\\Example.tar.gz");
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn mixed_path_conversion2() {
const INPUT: &str = "resource\\blah/Example.tar.gz";
let path = StoredPathBuf::from_utf8_path(Utf8Path::new(INPUT));
assert_eq!(path.as_str(), "resource\\blah\\Example.tar.gz");
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn mixed_path_unconverted1() {
const INPUT: &str = "resource\\blah/Example.tar.gz";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "resource\\blah/Example.tar.gz");
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn mixed_path_unconverted2() {
const INPUT: &str = "resource/blah\\Example.tar.gz";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "resource/blah\\Example.tar.gz");
assert_eq!(path.file_name(), Some("Example.tar.gz"));
assert_eq!(path.file_stem(), Some("Example.tar"));
assert_eq!(path.extension(), Some("gz"));
}
#[test]
fn empty_path() {
const INPUT: &str = "";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "");
assert_eq!(path.file_name(), None);
assert_eq!(path.file_stem(), None);
assert_eq!(path.extension(), None);
}
#[test]
fn just_file() {
const INPUT: &str = "abc.txt";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn trail_slash_1() {
const INPUT: &str = "abc.txt/";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt/");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn trail_slash_2() {
const INPUT: &str = "abc.txt\\";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt\\");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn trail_slash_3() {
const INPUT: &str = "abc.txt\\\\\\";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt\\\\\\");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn trail_slash_4() {
const INPUT: &str = "abc.txt/////";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt/////");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn trail_slash_5() {
const INPUT: &str = "abc.txt/\\//\\//";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt/\\//\\//");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn trail_slash_6() {
const INPUT: &str = "abc.txt/.";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt/.");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn trail_slash_7() {
const INPUT: &str = "abc.txt\\.";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt\\.");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn trail_slash_8() {
const INPUT: &str = "abc.txt/./.\\\\//\\././";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "abc.txt/./.\\\\//\\././");
assert_eq!(path.file_name(), Some("abc.txt"));
assert_eq!(path.file_stem(), Some("abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn just_dot() {
const INPUT: &str = ".";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), ".");
assert_eq!(path.file_name(), None);
assert_eq!(path.file_stem(), None);
assert_eq!(path.extension(), None);
}
#[test]
fn just_dotfile() {
const INPUT: &str = ".abc";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), ".abc");
assert_eq!(path.file_name(), Some(".abc"));
assert_eq!(path.file_stem(), Some(".abc"));
assert_eq!(path.extension(), None);
}
#[test]
fn just_dotfile_txt() {
const INPUT: &str = ".abc.txt";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), ".abc.txt");
assert_eq!(path.file_name(), Some(".abc.txt"));
assert_eq!(path.file_stem(), Some(".abc"));
assert_eq!(path.extension(), Some("txt"));
}
#[test]
fn just_dotdot() {
const INPUT: &str = "..";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "..");
assert_eq!(path.file_name(), None);
assert_eq!(path.file_stem(), None);
assert_eq!(path.extension(), None);
}
#[test]
fn opaque_dotdot1() {
const INPUT: &str = "a/b/..";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "a/b/..");
assert_eq!(path.file_name(), None);
assert_eq!(path.file_stem(), None);
assert_eq!(path.extension(), None);
}
#[test]
fn opaque_dotdot2() {
const INPUT: &str = "a/b/../";
let path = StoredPathBuf::new(INPUT.to_owned());
assert_eq!(path.as_str(), "a/b/../");
assert_eq!(path.file_name(), None);
assert_eq!(path.file_stem(), None);
assert_eq!(path.extension(), None);
}
}