use crate::errors::{
AbsUtf8PathError, AbsUtf8PathErrorKind, CurrentDirError, MalformedPathKind,
NativePathErrorKind, RelUtf8PathError, RelUtf8PathErrorKind, ResolvePathError,
ResolvePathErrorKind, TryFromPathBufError,
};
use camino::{Utf8Component, Utf8Path, Utf8PathBuf, Utf8Prefix};
use std::{
ffi::OsString,
fmt, io,
path::{Path, PathBuf},
};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AbsUtf8PathBuf(Utf8PathBuf);
impl AbsUtf8PathBuf {
pub fn new<P: Into<Utf8PathBuf>>(path: P) -> Result<Self, AbsUtf8PathError> {
match classify_path(path.into()) {
PathClass::Absolute(absolute) => Ok(absolute),
PathClass::Malformed(path, kind) => Err(AbsUtf8PathError::new(path, kind.into())),
PathClass::Relative(relative) => Err(AbsUtf8PathError::new(
relative.into_path_buf(),
AbsUtf8PathErrorKind::NotAbsolute,
)),
PathClass::RootRelative(path) => Err(AbsUtf8PathError::new(
path,
AbsUtf8PathErrorKind::RootRelative,
)),
PathClass::DriveRelative(path) => Err(AbsUtf8PathError::new(
path,
AbsUtf8PathErrorKind::DriveRelative,
)),
}
}
pub fn resolve_against_current_dir<P: AsRef<Utf8Path>>(
path: P,
) -> Result<Self, ResolvePathError> {
let input = path.as_ref();
let kind = match malformed_kind(input) {
Some(kind) => kind.into(),
None => match Self::from_native(std::path::absolute(input)) {
Ok(resolved) => return Ok(resolved),
Err(kind) => ResolvePathErrorKind::Native(kind),
},
};
Err(ResolvePathError::new(input.to_owned(), kind))
}
pub fn current_dir() -> Result<Self, CurrentDirError> {
Self::from_native(std::env::current_dir()).map_err(CurrentDirError::new)
}
pub fn logical_current_dir() -> Result<Self, CurrentDirError> {
let physical = Self::current_dir()?;
Ok(choose_logical_current_dir(
std::env::var_os("PWD"),
physical,
))
}
fn from_native(path: io::Result<PathBuf>) -> Result<Self, NativePathErrorKind> {
let path = path.map_err(NativePathErrorKind::Io)?;
let path = Utf8PathBuf::try_from(path).map_err(NativePathErrorKind::NonUtf8)?;
Self::new(path).map_err(NativePathErrorKind::Invalid)
}
#[must_use]
pub fn as_path(&self) -> &Utf8Path {
&self.0
}
#[must_use]
pub fn into_path_buf(self) -> Utf8PathBuf {
self.0
}
#[must_use]
pub fn parent(&self) -> Option<Self> {
let parent = self.0.parent()?;
Some(Self::new(parent.to_owned()).expect("parent of an absolute path is absolute"))
}
#[must_use]
pub fn join(&self, suffix: &RelUtf8PathBuf) -> Self {
Self(self.0.join(&suffix.0))
}
#[must_use]
pub fn strip_prefix(&self, base: &AbsUtf8PathBuf) -> Option<RelUtf8PathBuf> {
strip_prefix_preserving_spelling(self.as_path(), base.as_path()).filter(|relative| {
base.join(relative) == *self
})
}
}
impl fmt::Display for AbsUtf8PathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl AsRef<Utf8Path> for AbsUtf8PathBuf {
fn as_ref(&self) -> &Utf8Path {
self.as_path()
}
}
impl AsRef<Path> for AbsUtf8PathBuf {
fn as_ref(&self) -> &Path {
self.as_path().as_std_path()
}
}
impl From<AbsUtf8PathBuf> for Utf8PathBuf {
fn from(path: AbsUtf8PathBuf) -> Self {
path.into_path_buf()
}
}
impl From<AbsUtf8PathBuf> for PathBuf {
fn from(path: AbsUtf8PathBuf) -> Self {
path.into_path_buf().into_std_path_buf()
}
}
impl TryFrom<Utf8PathBuf> for AbsUtf8PathBuf {
type Error = AbsUtf8PathError;
fn try_from(path: Utf8PathBuf) -> Result<Self, Self::Error> {
Self::new(path)
}
}
impl TryFrom<PathBuf> for AbsUtf8PathBuf {
type Error = TryFromPathBufError;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
let path = Utf8PathBuf::try_from(path).map_err(TryFromPathBufError::NonUtf8)?;
Self::new(path).map_err(TryFromPathBufError::Invalid)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RelUtf8PathBuf(Utf8PathBuf);
impl RelUtf8PathBuf {
pub fn new<P: Into<Utf8PathBuf>>(path: P) -> Result<Self, RelUtf8PathError> {
match classify_path(path.into()) {
PathClass::Relative(relative) => Ok(relative),
PathClass::Malformed(path, kind) => Err(RelUtf8PathError::new(path, kind.into())),
PathClass::Absolute(absolute) => Err(RelUtf8PathError::new(
absolute.into_path_buf(),
RelUtf8PathErrorKind::Absolute,
)),
PathClass::RootRelative(path) => Err(RelUtf8PathError::new(
path,
RelUtf8PathErrorKind::RootRelative,
)),
PathClass::DriveRelative(path) => Err(RelUtf8PathError::new(
path,
RelUtf8PathErrorKind::DriveRelative,
)),
}
}
#[must_use]
pub fn as_path(&self) -> &Utf8Path {
&self.0
}
#[must_use]
pub fn into_path_buf(self) -> Utf8PathBuf {
self.0
}
}
impl fmt::Display for RelUtf8PathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
pub(crate) fn choose_logical_current_dir(
pwd: Option<OsString>,
physical: AbsUtf8PathBuf,
) -> AbsUtf8PathBuf {
let Some(pwd) = pwd else {
return physical;
};
let Ok(pwd) = Utf8PathBuf::try_from(PathBuf::from(pwd)) else {
return physical;
};
let Ok(pwd) = AbsUtf8PathBuf::new(pwd) else {
return physical;
};
if names_same_directory(&pwd, &physical) {
pwd
} else {
physical
}
}
#[cfg(unix)]
fn names_same_directory(a: &AbsUtf8PathBuf, b: &AbsUtf8PathBuf) -> bool {
use std::os::unix::fs::MetadataExt;
match (std::fs::metadata(a), std::fs::metadata(b)) {
(Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
(Err(_), Ok(_)) | (Ok(_), Err(_)) | (Err(_), Err(_)) => false,
}
}
#[cfg(not(unix))]
fn names_same_directory(a: &AbsUtf8PathBuf, b: &AbsUtf8PathBuf) -> bool {
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
(Ok(a), Ok(b)) => a == b,
(Err(_), Ok(_)) | (Ok(_), Err(_)) | (Err(_), Err(_)) => false,
}
}
pub(crate) enum PathClass {
Malformed(Utf8PathBuf, MalformedPathKind),
Absolute(AbsUtf8PathBuf),
Relative(RelUtf8PathBuf),
RootRelative(Utf8PathBuf),
DriveRelative(Utf8PathBuf),
}
pub(crate) fn classify_path(path: Utf8PathBuf) -> PathClass {
if let Some(kind) = malformed_kind(&path) {
PathClass::Malformed(path, kind)
} else if path.is_absolute() {
PathClass::Absolute(AbsUtf8PathBuf(path))
} else if path.has_root() {
PathClass::RootRelative(path)
} else if path_prefix(&path).is_some() {
PathClass::DriveRelative(path)
} else {
PathClass::Relative(RelUtf8PathBuf(path))
}
}
fn malformed_kind(path: &Utf8Path) -> Option<MalformedPathKind> {
if path.as_str().is_empty() {
Some(MalformedPathKind::Empty)
} else if path.as_str().contains('\0') {
Some(MalformedPathKind::ContainsNul)
} else {
None
}
}
fn path_prefix(path: &Utf8Path) -> Option<Utf8Prefix<'_>> {
match path.components().next() {
Some(Utf8Component::Prefix(prefix)) => Some(prefix.kind()),
Some(
Utf8Component::RootDir
| Utf8Component::CurDir
| Utf8Component::ParentDir
| Utf8Component::Normal(_),
)
| None => None,
}
}
fn strip_prefix_preserving_spelling(path: &Utf8Path, base: &Utf8Path) -> Option<RelUtf8PathBuf> {
#[cfg(unix)]
if has_double_root(path.as_str()) != has_double_root(base.as_str()) {
return None;
}
let remainder = path.strip_prefix(base).ok()?;
if remainder.as_str().is_empty() {
return Some(RelUtf8PathBuf::new(".").expect("`.` is a well-formed relative path"));
}
let suffix = substr_start(path.as_str(), remainder.as_str())
.and_then(|start| path.as_str().get(start..));
let Some(suffix) = suffix else {
debug_assert!(false, "remainder {remainder:?} is a subslice of {path:?}");
return None;
};
match RelUtf8PathBuf::new(suffix) {
Ok(relative) => Some(relative),
Err(error) => match error.kind() {
RelUtf8PathErrorKind::Absolute
| RelUtf8PathErrorKind::DriveRelative
| RelUtf8PathErrorKind::RootRelative => None,
RelUtf8PathErrorKind::Empty | RelUtf8PathErrorKind::ContainsNul => {
debug_assert!(
false,
"suffix {suffix:?} is a well-formed relative path: {error}"
);
None
}
},
}
}
#[cfg(unix)]
fn has_double_root(path: &str) -> bool {
path.starts_with("//") && !path.starts_with("///")
}
fn substr_start(outer: &str, inner: &str) -> Option<usize> {
let start = inner.as_ptr().addr().checked_sub(outer.as_ptr().addr())?;
(start.checked_add(inner.len())? <= outer.len()).then_some(start)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::{absolute, assert_resolve_error};
use std::fmt;
#[test]
fn substr_start_accepts_only_subslices() {
let text = "/repo/src/lib.rs";
assert_eq!(substr_start(text, &text[6..]), Some(6));
assert_eq!(substr_start(text, text), Some(0));
assert_eq!(substr_start(text, &text[text.len()..]), Some(text.len()));
assert_eq!(substr_start(&text[..5], &text[3..8]), None);
assert_eq!(substr_start(&text[6..], text), None);
let copy = text.to_owned();
assert_eq!(substr_start(text, ©[6..]), None);
}
#[cfg(unix)]
const ABSOLUTE: &str = "/foo/bar";
#[cfg(windows)]
const ABSOLUTE: &str = r"C:\foo\bar";
#[cfg(unix)]
const ABSOLUTE_WITH_NUL: &str = "/a\0b";
#[cfg(windows)]
const ABSOLUTE_WITH_NUL: &str = "C:\\a\0b";
#[derive(Debug, PartialEq, Eq)]
enum PathKind {
Absolute,
Relative,
Neither(RelUtf8PathErrorKind),
}
#[track_caller]
fn classify(input: &str) -> PathKind {
match (AbsUtf8PathBuf::new(input), RelUtf8PathBuf::new(input)) {
(Ok(_), Err(relative_error)) => {
assert_eq!(relative_error.kind(), RelUtf8PathErrorKind::Absolute);
PathKind::Absolute
}
(Err(_), Ok(_)) => PathKind::Relative,
(Err(_), Err(relative_error)) => PathKind::Neither(relative_error.kind()),
(Ok(_), Ok(_)) => panic!("{input:?} was accepted as both absolute and relative"),
}
}
#[test]
fn classify_portable() {
assert_eq!(classify(""), PathKind::Neither(RelUtf8PathErrorKind::Empty));
assert_eq!(
classify("a\0b"),
PathKind::Neither(RelUtf8PathErrorKind::ContainsNul)
);
assert_eq!(classify("."), PathKind::Relative);
assert_eq!(classify(".."), PathKind::Relative);
assert_eq!(classify("../config.toml"), PathKind::Relative);
assert_eq!(classify("./a/../b/"), PathKind::Relative);
assert_eq!(classify("a//b"), PathKind::Relative);
assert_eq!(
classify(ABSOLUTE_WITH_NUL),
PathKind::Neither(RelUtf8PathErrorKind::ContainsNul)
);
}
#[cfg(windows)]
#[test]
fn classify_windows() {
assert_eq!(classify(r"C:\"), PathKind::Absolute);
assert_eq!(classify(r"C:\a\..\b\"), PathKind::Absolute);
assert_eq!(classify(r"\\server\share\a"), PathKind::Absolute);
assert_eq!(classify(r"\\?\C:\a"), PathKind::Absolute);
for input in [r"C:foo", r"C:"] {
assert_eq!(
classify(input),
PathKind::Neither(RelUtf8PathErrorKind::DriveRelative),
"{input:?}"
);
}
for input in [r"\foo", "/foo"] {
assert_eq!(
classify(input),
PathKind::Neither(RelUtf8PathErrorKind::RootRelative),
"{input:?}"
);
}
}
#[test]
fn absolute_path_error_kinds() {
let mut cases = vec![
("", AbsUtf8PathErrorKind::Empty),
("\0", AbsUtf8PathErrorKind::ContainsNul),
(ABSOLUTE_WITH_NUL, AbsUtf8PathErrorKind::ContainsNul),
("foo/bar", AbsUtf8PathErrorKind::NotAbsolute),
];
if cfg!(windows) {
cases.push((r"\foo", AbsUtf8PathErrorKind::RootRelative));
cases.push(("/foo", AbsUtf8PathErrorKind::RootRelative));
cases.push(("C:foo", AbsUtf8PathErrorKind::DriveRelative));
cases.push(("C:", AbsUtf8PathErrorKind::DriveRelative));
}
for (input, expected) in cases {
let error = AbsUtf8PathBuf::new(input).expect_err("path is rejected");
assert_eq!(error.path().as_str(), input);
assert_eq!(error.kind(), expected, "{input:?}");
}
}
#[cfg(windows)]
#[test]
fn join_onto_verbatim_base_normalizes() {
use crate::test_helpers::relative;
for (base, suffix, expected) in [
(r"\\?\C:\repo", "a/b", r"\\?\C:\repo\a\b"),
(r"\\?\C:\repo", r"a\.\b\", r"\\?\C:\repo\a\b"),
(r"\\?\C:\repo", r"..\config.toml", r"\\?\C:\config.toml"),
(
r"\\?\C:\repo",
r"link\..\config.toml",
r"\\?\C:\repo\config.toml",
),
(r"\\?\C:\", r"..\..\config.toml", r"\\?\C:\config.toml"),
] {
let base = absolute(base);
let suffix = relative(suffix);
assert_eq!(
base.join(&suffix).as_path().as_str(),
expected,
"joining {suffix:?} onto {base:?}"
);
}
}
#[test]
fn parent_stays_absolute() {
#[cfg(unix)]
let cases = [
("/", None),
("/repo", Some("/")),
("/repo/config.toml", Some("/repo")),
("/repo/", Some("/")),
("/repo/..", Some("/repo")),
("//host/a", Some("//host")),
];
#[cfg(windows)]
let cases = [
(r"C:\", None),
(r"C:\repo", Some(r"C:\")),
(r"C:\repo\config.toml", Some(r"C:\repo")),
(r"\\server\share", None),
(r"\\server\share\a", Some(r"\\server\share\")),
(r"\\?\C:\", None),
(r"\\?\C:\a", Some(r"\\?\C:\")),
];
for (input, expected) in cases {
assert_eq!(
absolute(input)
.parent()
.as_ref()
.map(|parent| parent.as_path().as_str()),
expected,
"{input:?}"
);
}
}
#[test]
fn display_preserves_spelling() {
use crate::test_helpers::relative;
let spelled = format!("{ABSOLUTE}//x/./");
assert_eq!(absolute(spelled.as_str()).to_string(), spelled);
assert_eq!(relative("a//b/./").to_string(), "a//b/./");
assert_eq!(format!("{:>10}", relative("a//b/./")), " a//b/./");
}
#[test]
fn logical_current_dir_falls_back_to_physical() {
let temp = camino_tempfile::tempdir().expect("created temp dir");
let physical = absolute(temp.path());
let mut cases = vec![
None,
Some(OsString::from("relative")),
Some(temp.path().join("missing").into_os_string()),
];
if cfg!(windows) {
cases.push(Some(OsString::from("/cygdrive/c/Users/me")));
cases.push(Some(OsString::from("/c/Users/me")));
cases.push(Some(OsString::from("/home/me")));
}
for pwd in cases {
assert_eq!(
choose_logical_current_dir(pwd.clone(), physical.clone()),
physical,
"{pwd:?}"
);
}
assert_eq!(
choose_logical_current_dir(Some(temp.path().as_os_str().to_owned()), physical.clone()),
physical,
);
}
#[cfg(unix)]
#[test]
fn logical_current_dir_prefers_pwd_through_symlink() {
use std::{
ffi::OsStr,
os::unix::{ffi::OsStrExt, fs::symlink},
};
let temp = camino_tempfile::tempdir().expect("created temp dir");
let target = temp.path().join("target");
let link = temp.path().join("link");
std::fs::create_dir(&target).expect("created target");
symlink(&target, &link).expect("created link");
let physical = absolute(&target);
assert_eq!(
choose_logical_current_dir(Some(link.as_os_str().to_owned()), physical.clone()),
absolute(&link),
);
assert_eq!(
choose_logical_current_dir(Some(temp.path().as_os_str().to_owned()), physical.clone()),
physical,
);
assert_eq!(
choose_logical_current_dir(
Some(OsStr::from_bytes(b"/\xff").to_owned()),
physical.clone()
),
physical,
);
}
#[test]
fn resolve_against_current_dir_rejects_invalid_input() {
for (input, expected) in [
("", ResolvePathErrorKind::Empty),
("a\0b", ResolvePathErrorKind::ContainsNul),
("\0", ResolvePathErrorKind::ContainsNul),
] {
assert_resolve_error(
AbsUtf8PathBuf::resolve_against_current_dir(input),
input,
expected,
);
}
}
#[test]
fn absolute_path_conversions_match_new() {
for input in ["", ".", "foo/../bar", "a\0b", ABSOLUTE] {
assert_eq!(
AbsUtf8PathBuf::try_from(Utf8PathBuf::from(input)),
AbsUtf8PathBuf::new(input),
"{input:?}"
);
assert_eq!(
AbsUtf8PathBuf::try_from(PathBuf::from(input)),
AbsUtf8PathBuf::new(input).map_err(TryFromPathBufError::Invalid),
"{input:?}"
);
}
}
#[track_caller]
fn assert_non_utf8<T: fmt::Debug>(result: Result<T, TryFromPathBufError>, expected: &Path) {
match result {
Err(TryFromPathBufError::NonUtf8(error)) => {
assert_eq!(error.into_path_buf(), expected);
}
other => panic!("expected NonUtf8 for {expected:?}, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn try_from_path_buf_rejects_non_utf8() {
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
let absolute = PathBuf::from(OsStr::from_bytes(b"/foo/\xff"));
assert_non_utf8(AbsUtf8PathBuf::try_from(absolute.clone()), &absolute);
}
#[cfg(windows)]
#[test]
fn try_from_path_buf_rejects_non_utf8() {
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
const LONE_SURROGATE: u16 = 0xD800;
let absolute = PathBuf::from(OsString::from_wide(&[
u16::from(b'C'),
u16::from(b':'),
u16::from(b'\\'),
LONE_SURROGATE,
]));
assert_non_utf8(AbsUtf8PathBuf::try_from(absolute.clone()), &absolute);
}
#[track_caller]
fn assert_strip_prefix(base: &str, path: &str, expected: Option<&str>) {
let base = absolute(base);
let path = absolute(path);
let stripped = path.strip_prefix(&base);
assert_eq!(
stripped
.as_ref()
.map(|relative| relative.as_path().as_str()),
expected,
"stripping {base:?} from {path:?}"
);
if let Some(stripped) = stripped {
assert_eq!(
base.join(&stripped).as_path(),
path.as_path(),
"joining {stripped:?} onto {base:?}"
);
}
}
#[test]
fn strip_prefix_portable() {
#[cfg(unix)]
let (base, separator) = ("/repo", "/");
#[cfg(windows)]
let (base, separator) = (r"C:\repo", r"\");
let parent = Utf8Path::new(base).parent().expect("base has a parent");
assert_strip_prefix(base, base, Some("."));
assert_strip_prefix(base, &format!("{base}{separator}"), Some("."));
assert_strip_prefix(base, &format!("{base}{separator}{separator}"), Some("."));
assert_strip_prefix(&format!("{base}{separator}"), base, Some("."));
assert_strip_prefix(
&format!("{base}{separator}{separator}"),
&format!("{base}{separator}"),
Some("."),
);
assert_strip_prefix(&format!("{base}{separator}"), &format!("{base}itory"), None);
assert_strip_prefix(
base,
&format!("{base}{separator}{separator}config.toml"),
Some("config.toml"),
);
for suffix in [
"config.toml",
"a/../b/",
"a//b",
"file/",
"file/.",
"file//",
"file/./",
] {
assert_strip_prefix(base, &format!("{base}{separator}{suffix}"), Some(suffix));
}
assert_strip_prefix(base, &format!("{base}itory{separator}file"), None);
assert_strip_prefix(base, parent.join("other.toml").as_str(), None);
assert_strip_prefix(base, parent.as_str(), None);
}
#[cfg(unix)]
#[test]
fn strip_prefix_unix() {
for (base, path, expected) in [
("/repo/", "/repo/config.toml", Some("config.toml")),
("/repo/", "/repo", Some(".")),
("/repo/.", "/repo/config.toml", Some("config.toml")),
("/repo//", "/repo/config.toml", Some("config.toml")),
("/a/./", "/a/b", Some("b")),
("/a//b", "/a/b/c", Some("c")),
("/repo", "/repo/./config.toml", Some("config.toml")),
("/repo", "/repo/./.hidden", Some(".hidden")),
("/repo", "/repo/./a/", Some("a/")),
("/repo", "/repo/./", Some(".")),
("/repo/.", "/repo/file/", Some("file/")),
("/repo//", "/repo/a/.", Some("a/.")),
("/a/..", "/a/../b", Some("b")),
("/", "/./a/", Some("a/")),
("//host", "//host/./a/", Some("a/")),
("/", "/config.toml", Some("config.toml")),
("/", "///a", Some("a")),
("///", "/a", Some("a")),
("/", "//host/config.toml", None),
("//host", "/host/config.toml", None),
("//host", "//host/config.toml", Some("config.toml")),
("//", "///a", None),
("///", "//a", None),
("/a/.", "/a/.config", Some(".config")),
("/a/.", "/a/../file", Some("../file")),
("/a/.", "/a/..hidden", Some("..hidden")),
("/a/b", "/a/bc", None),
] {
assert_strip_prefix(base, path, expected);
}
}
#[cfg(windows)]
#[test]
fn strip_prefix_windows() {
for (base, path, expected) in [
(r"C:\repo", r"C:\repo\C:stream", None),
(r"C:\repo", r"C:\repo\C:\x", None),
(r"C:\repo", r"C:\repo\.\C:stream", None),
(r"C:\repo", r"C:\repo\.\file", Some("file")),
(r"C:\", r"C:\file\", Some(r"file\")),
("C:/", r"C:\file", Some("file")),
(r"\\server\share\", r"\\server\share", Some(".")),
(r"\\server\share", r"\\SERVER\share\x", None),
(r"C:\repo", r"D:\repo\config.toml", None),
(r"C:\repo", r"\\server\share\config.toml", None),
(r"C:\repo", r"c:\repo\config.toml", Some("config.toml")),
(r"C:/repo", r"C:\repo\config.toml", Some("config.toml")),
(r"C:\repo\\", r"C:\repo\config.toml", Some("config.toml")),
(
r"\\server\share",
"//server/share/config.toml",
Some("config.toml"),
),
(r"C:\Repo", r"C:\repo\config.toml", None),
(r"\\?\C:\repo", r"\\?\C:\repo\file.", Some("file.")),
(
r"\\?\C:\repo",
r"\\?\C:\repo\src\lib.rs",
Some(r"src\lib.rs"),
),
(r"\\?\C:\repo", r"\\?\C:\repo\x\", Some(r"x\")),
(r"\\?\C:\repo", r"\\?\C:\repo\a/b", None),
(r"\\?\C:\repo", r"\\?\C:\repo\.\x", None),
(r"\\?\C:\repo", r"\\?\C:\repo\x\..", None),
(r"\\?\C:\repo", r"\\?\C:\repo\/x", None),
(r"\\?\C:\repo", r"\\?\C:\repo\", Some(".")),
(r"\\?\C:\repo", r"\\?\C:\repo\.", None),
(r"\\?\C:\repo", r"\\?\C:\repo\..\x", None),
(r"\\?\C:\", r"\\?\C:\..\x", None),
(r"\\?\C:", r"\\?\C:\x", None),
(r"\\?\C:", r"\\?\C:\", None),
(r"C:\repo", r"\\?\C:\repo\x", None),
(r"\\?\C:\repo", r"C:\repo\x", None),
(r"\\.\C:\repo", r"\\.\C:\repo\..\x", Some(r"..\x")),
(
r"\\?\UNC\server\share\repo",
r"\\?\UNC\server\share\repo\file.",
Some("file."),
),
(r"\\.\C:\repo", r"\\.\C:\repo\file.", Some("file.")),
(r"\\.\C:\repo", r"\\.\C:\repo\.\x", Some("x")),
] {
assert_strip_prefix(base, path, expected);
}
}
}