Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions tests/absolutize_contracts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#![cfg(any(target_family = "unix", target_family = "windows"))]

#[cfg(target_family = "windows")]
use std::path::PathBuf;
use std::{borrow::Cow, env, path::Path};

use sugar_path::SugarPath;

#[derive(Clone, Copy)]
enum ExpectedCow {
Borrowed,
Owned,
}

fn assert_result(
result: Cow<'_, Path>,
input: &Path,
expected: &Path,
expected_cow: ExpectedCow,
context: &str,
) {
assert_eq!(result.as_os_str(), expected.as_os_str(), "{context}");
match (expected_cow, result) {
(ExpectedCow::Borrowed, Cow::Borrowed(result)) => {
assert!(std::ptr::eq(result, input), "{context}: result did not borrow the receiver");
}
(ExpectedCow::Owned, Cow::Owned(_)) => {}
(ExpectedCow::Borrowed, Cow::Owned(_)) => panic!("{context}: expected borrowed result"),
(ExpectedCow::Owned, Cow::Borrowed(_)) => panic!("{context}: expected owned result"),
}
}

fn assert_ambient_case(input: &Path, expected: &Path, expected_cow: ExpectedCow, name: &str) {
assert_result(input.absolutize(), input, expected, expected_cow, &format!("{name} strict"));
assert_result(
input.try_absolutize().expect("fixture should resolve against cwd"),
input,
expected,
expected_cow,
&format!("{name} try"),
);
}

fn assert_string_receiver(input: String, expected: &Path) {
let strict = input.absolutize();
assert_eq!(strict.as_os_str(), expected.as_os_str());
assert!(matches!(strict, Cow::Owned(_)));

let fallible = input.try_absolutize().expect("String fixture should resolve against cwd");
assert_eq!(fallible.as_os_str(), expected.as_os_str());
assert!(matches!(fallible, Cow::Owned(_)));
}

#[cfg(target_family = "unix")]
#[test]
fn unix_ambient_absolutize_and_try_contract_matrix() {
let cwd = env::current_dir().expect("read current directory");

let clean = Path::new("/some/β/file");
assert_ambient_case(clean, clean, ExpectedCow::Borrowed, "clean absolute");
assert_ambient_case(
Path::new("/some/./β/../file/"),
Path::new("/some/file"),
ExpectedCow::Owned,
"dirty absolute",
);
assert_ambient_case(Path::new(""), &cwd, ExpectedCow::Owned, "empty");
assert_ambient_case(Path::new("."), &cwd, ExpectedCow::Owned, "dot");
assert_ambient_case(
Path::new("./pkg//β/../file/"),
&cwd.join("pkg/file"),
ExpectedCow::Owned,
"ordinary relative",
);

assert_string_receiver("./owned/../file".to_owned(), &cwd.join("file"));
}

#[cfg(target_family = "windows")]
fn current_ordinary_disk_drive(path: &Path) -> Option<u8> {
use std::path::{Component, Prefix};

match path.components().next() {
Some(Component::Prefix(prefix)) => match prefix.kind() {
Prefix::Disk(drive) => Some(drive),
_ => None,
},
_ => None,
}
}

#[cfg(target_family = "windows")]
fn preserve_drive_spelling(path: &Path, drive: u8) -> PathBuf {
use std::{
ffi::OsString,
os::windows::ffi::{OsStrExt, OsStringExt},
};

let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
if wide.get(1) == Some(&(b':' as u16)) {
wide[0] = drive as u16;
} else if wide.get(5) == Some(&(b':' as u16))
&& wide.get(..4) == Some(&[b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16])
{
wide[4] = drive as u16;
} else {
panic!("expected disk path, found {path:?}");
}
PathBuf::from(OsString::from_wide(&wide))
}

#[cfg(target_family = "windows")]
#[test]
fn windows_ambient_absolutize_and_try_contract_matrix() {
let cwd = env::current_dir().expect("read current directory");

let clean = Path::new(r"c:\some\β\file");
assert_ambient_case(clean, clean, ExpectedCow::Borrowed, "clean absolute");
assert_ambient_case(
Path::new(r"c:/some/.\β\../file\"),
Path::new(r"c:\some\file"),
ExpectedCow::Owned,
"dirty absolute",
);
assert_ambient_case(Path::new(""), &cwd, ExpectedCow::Owned, "empty");
assert_ambient_case(Path::new("."), &cwd, ExpectedCow::Owned, "dot");
assert_ambient_case(
Path::new(r".\pkg\\β\..\file\"),
&cwd.join(r"pkg\file"),
ExpectedCow::Owned,
"ordinary relative",
);

let root_relative = Path::new(r"\pkg\.\β\..\file\");
let mut expected = cwd.clone();
expected.push(r"\pkg\file");
assert_ambient_case(root_relative, &expected, ExpectedCow::Owned, "root relative");

if let Some(drive) = current_ordinary_disk_drive(&cwd) {
let drive = drive.to_ascii_lowercase();
let drive_relative = format!("{}:folder\\.\\file\\", drive as char);
let mut expected = preserve_drive_spelling(&cwd, drive);
expected.push(r"folder\file");
assert_ambient_case(
Path::new(&drive_relative),
&expected,
ExpectedCow::Owned,
"drive relative",
);
}

assert_string_receiver(r".\owned\..\file".to_owned(), &cwd.join("file"));
}
137 changes: 137 additions & 0 deletions tests/absolutize_invalid_encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#![cfg(any(target_family = "unix", target_family = "windows"))]

use std::{borrow::Cow, path::Path};

use sugar_path::SugarPath;

#[cfg(target_family = "unix")]
mod unix {
use std::{
env,
ffi::OsString,
os::unix::ffi::{OsStrExt, OsStringExt},
path::PathBuf,
};

use super::*;

fn path(bytes: &[u8]) -> PathBuf {
PathBuf::from(OsString::from_vec(bytes.to_vec()))
}

fn assert_bytes(actual: &Path, expected: &Path, context: &str) {
assert_eq!(actual.as_os_str().as_bytes(), expected.as_os_str().as_bytes(), "{context}",);
}

fn assert_ambient(input: &Path, expected: &Path, require_owned: bool, name: &str) {
let strict = input.absolutize();
assert_bytes(&strict, expected, &format!("{name} strict"));
if require_owned {
assert!(matches!(strict, Cow::Owned(_)), "{name} strict should own");
}

let fallible = input.try_absolutize().expect("fixture should resolve against cwd");
assert_bytes(&fallible, expected, &format!("{name} try"));
if require_owned {
assert!(matches!(fallible, Cow::Owned(_)), "{name} try should own");
}
}

#[test]
fn ambient_absolutization_preserves_invalid_unix_bytes() {
let clean = path(b"/sugar-path/invalid-\x80/file");
assert_ambient(&clean, &clean, false, "clean absolute");

let dirty = path(b"/sugar-path/invalid-\x80/./file/");
let normalized = path(b"/sugar-path/invalid-\x80/file");
assert_ambient(&dirty, &normalized, true, "dirty absolute");

let relative = path(b"pkg/invalid-\x80/./file/");
let expected =
env::current_dir().expect("read current directory").join(path(b"pkg/invalid-\x80/file"));
assert_ambient(&relative, &expected, true, "relative");
}
}

#[cfg(target_family = "windows")]
mod windows {
use std::{
env,
ffi::OsString,
os::windows::ffi::{OsStrExt, OsStringExt},
path::PathBuf,
};

use super::*;

const INVALID_UNIT: u16 = 0xd800;

fn invalid_path(before: &str, after: &str) -> PathBuf {
let mut wide: Vec<u16> = before.encode_utf16().collect();
wide.push(INVALID_UNIT);
wide.extend(after.encode_utf16());
PathBuf::from(OsString::from_wide(&wide))
}

fn assert_wide(actual: &Path, expected: &Path, context: &str) {
assert_eq!(
actual.as_os_str().encode_wide().collect::<Vec<_>>(),
expected.as_os_str().encode_wide().collect::<Vec<_>>(),
"{context}",
);
}

fn current_ordinary_disk_drive(path: &Path) -> Option<u8> {
use std::path::{Component, Prefix};

match path.components().next() {
Some(Component::Prefix(prefix)) => match prefix.kind() {
Prefix::Disk(drive) => Some(drive),
_ => None,
},
_ => None,
}
}

fn assert_ambient(input: &Path, expected: &Path, require_owned: bool, name: &str) {
let strict = input.absolutize();
assert_wide(&strict, expected, &format!("{name} strict"));
if require_owned {
assert!(matches!(strict, Cow::Owned(_)), "{name} strict should own");
}

let fallible = input.try_absolutize().expect("fixture should resolve against cwd");
assert_wide(&fallible, expected, &format!("{name} try"));
if require_owned {
assert!(matches!(fallible, Cow::Owned(_)), "{name} try should own");
}
}

#[test]
fn ambient_absolutization_preserves_invalid_windows_units() {
let cwd = env::current_dir().expect("read current directory");

let clean = invalid_path(r"C:\sugar-path\invalid-", r"\file");
assert_ambient(&clean, &clean, false, "clean absolute");

let dirty = invalid_path(r"C:\sugar-path\invalid-", r"\.\file\");
let normalized = invalid_path(r"C:\sugar-path\invalid-", r"\file");
assert_ambient(&dirty, &normalized, true, "dirty absolute");

let relative = invalid_path(r"pkg\invalid-", r"\.\file\");
let expected = cwd.join(invalid_path(r"pkg\invalid-", r"\file"));
assert_ambient(&relative, &expected, true, "relative");

let root_relative = invalid_path(r"\pkg\invalid-", r"\.\file\");
let mut expected = cwd.clone();
expected.push(invalid_path(r"\pkg\invalid-", r"\file"));
assert_ambient(&root_relative, &expected, true, "root relative");

if let Some(drive) = current_ordinary_disk_drive(&cwd) {
let prefix = format!("{}:pkg\\invalid-", drive as char);
let drive_relative = invalid_path(&prefix, r"\.\file\");
let expected = cwd.join(invalid_path(r"pkg\invalid-", r"\file"));
assert_ambient(&drive_relative, &expected, false, "drive relative");
}
}
}
45 changes: 45 additions & 0 deletions tests/absolutize_windows_roots.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#![cfg(target_family = "windows")]

use std::{
borrow::Cow,
path::{Path, PathBuf},
};

use sugar_path::SugarPath;

fn assert_explicit_case(cwd: &str, input: &str, expected: &str, name: &str) {
assert!(Path::new(cwd).is_absolute(), "{name}: fixture cwd is not absolute");

let borrowed = Path::new(input).absolutize_with(Path::new(cwd));
assert_eq!(borrowed.as_os_str(), Path::new(expected).as_os_str(), "{name} borrowed cwd");
assert!(matches!(borrowed, Cow::Owned(_)), "{name} borrowed cwd should own");

let owned = Path::new(input).absolutize_with(PathBuf::from(cwd));
assert_eq!(owned.as_os_str(), Path::new(expected).as_os_str(), "{name} owned cwd");
assert!(matches!(owned, Cow::Owned(_)), "{name} owned cwd should own");
}

#[test]
fn windows_root_relative_inputs_use_the_exact_explicit_prefix() {
let cases = [
("disk", r"C:\workspace", r"\pkg\.\temp\..\β\file\", r"C:\pkg\β\file"),
("disk root", r"C:\workspace", r"\", r"C:\"),
("UNC", r"\\Server\Share\workspace", r"\pkg\..\file\", r"\\Server\Share\file"),
("UNC root", r"\\Server\Share\workspace", r"\", r"\\Server\Share\"),
("verbatim disk", r"\\?\c:\workspace", r"\pkg\..\file\", r"\\?\c:\file"),
("verbatim disk root", r"\\?\c:\workspace", r"\", r"\\?\c:\"),
(
"verbatim UNC",
r"\\?\UNC\Server\Share\workspace",
r"\pkg\..\file\",
r"\\?\UNC\Server\Share\file",
),
// A verbatim UNC prefix is already rooted and absolute without an explicit
// RootDir component, so resolution strips the optional trailing separator.
("verbatim UNC root", r"\\?\UNC\Server\Share\workspace", r"\", r"\\?\UNC\Server\Share"),
];

for (name, cwd, input, expected) in cases {
assert_explicit_case(cwd, input, expected, name);
}
}
8 changes: 6 additions & 2 deletions tests/absolutize_without_cwd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ fn absolute_paths_do_not_require_current_directory() {
let clean_output = clean.absolutize();
assert!(matches!(clean_output, Cow::Borrowed(_)));
assert_eq!(clean_output.as_os_str(), clean.as_os_str());
assert!(clean.try_absolutize().is_ok());
let clean_try = clean.try_absolutize().expect("clean absolute path should succeed");
assert!(matches!(clean_try, Cow::Borrowed(_)));
assert_eq!(clean_try.as_os_str(), clean.as_os_str());

let dirty = Path::new("/sugar-path/../file.js/");
let dirty_output = dirty.absolutize();
assert!(matches!(dirty_output, Cow::Owned(_)));
assert_eq!(dirty_output.as_os_str(), Path::new("/file.js").as_os_str());
assert!(dirty.try_absolutize().is_ok());
let dirty_try = dirty.try_absolutize().expect("dirty absolute path should succeed");
assert!(matches!(dirty_try, Cow::Owned(_)));
assert_eq!(dirty_try.as_os_str(), Path::new("/file.js").as_os_str());

assert!(Path::new("relative.js").try_absolutize().is_err());
assert!(std::panic::catch_unwind(|| Path::new("relative.js").absolutize()).is_err());
Expand Down
Loading