Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: canonicalize_path_maybe_not_exists and other improvements #2

Merged
merged 4 commits into from
Sep 30, 2024
Merged
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
129 changes: 94 additions & 35 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,48 @@
#![deny(clippy::print_stdout)]
#![deny(clippy::unused_async)]

use std::io::ErrorKind;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use url::Url;

/// Gets the parent of this module specifier.
pub fn specifier_parent(specifier: &Url) -> Url {
let mut specifier = specifier.clone();
// don't use specifier.segments() because it will strip the leading slash
let mut segments = specifier.path().split('/').collect::<Vec<_>>();
/// Gets the parent of this url.
pub fn url_parent(url: &Url) -> Url {
let mut url = url.clone();
// don't use url.segments() because it will strip the leading slash
let mut segments = url.path().split('/').collect::<Vec<_>>();
if segments.iter().all(|s| s.is_empty()) {
return specifier;
return url;
}
if let Some(last) = segments.last() {
if last.is_empty() {
segments.pop();
}
segments.pop();
let new_path = format!("{}/", segments.join("/"));
specifier.set_path(&new_path);
url.set_path(&new_path);
}
specifier
url
}

#[derive(Debug, Error)]
#[error("Could not convert specifier to file path.\n Specifier: {0}")]
pub struct SpecifierToFilePathError(pub Url);
#[error("Could not convert URL to file path.\n URL: {0}")]
pub struct UrlToFilePathError(pub Url);

/// Attempts to convert a url to a file path. By default, uses the Url
/// crate's `to_file_path()` method, but falls back to try and resolve unix-style
/// paths on Windows.
pub fn url_to_file_path(
specifier: &Url,
) -> Result<PathBuf, SpecifierToFilePathError> {
let result = if specifier.scheme() != "file" {
pub fn url_to_file_path(url: &Url) -> Result<PathBuf, UrlToFilePathError> {
let result = if url.scheme() != "file" {
Err(())
} else {
url_to_file_path_inner(specifier)
url_to_file_path_inner(url)
};
match result {
Ok(path) => Ok(path),
Err(()) => Err(SpecifierToFilePathError(specifier.clone())),
Err(()) => Err(UrlToFilePathError(url.clone())),
}
}

Expand Down Expand Up @@ -97,8 +96,8 @@ fn url_to_file_path_real(url: &Url) -> Result<PathBuf, ()> {
not(any(unix, windows, target_os = "redox", target_os = "wasi"))
))]
fn url_to_file_path_wasm(url: &Url) -> Result<PathBuf, ()> {
fn is_windows_path_segment(specifier: &str) -> bool {
let mut chars = specifier.chars();
fn is_windows_path_segment(url: &str) -> bool {
let mut chars = url.chars();

let first_char = chars.next();
if first_char.is_none() || !first_char.unwrap().is_ascii_alphabetic() {
Expand Down Expand Up @@ -174,20 +173,27 @@ pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
inner(path.as_ref())
}

#[derive(Debug, Error)]
#[error("Could not convert path to URL.\n Path: {0}")]
pub struct PathToUrlError(pub PathBuf);

#[allow(clippy::result_unit_err)]
pub fn url_from_file_path(path: &Path) -> Result<Url, ()> {
pub fn url_from_file_path(path: &Path) -> Result<Url, PathToUrlError> {
#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
return Url::from_file_path(path);
return Url::from_file_path(path)
.map_err(|()| PathToUrlError(path.to_path_buf()));
#[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
url_from_file_path_wasm(path)
url_from_file_path_wasm(path).map_err(|()| PathToUrlError(path.to_path_buf()))
}

#[allow(clippy::result_unit_err)]
pub fn url_from_directory_path(path: &Path) -> Result<Url, ()> {
pub fn url_from_directory_path(path: &Path) -> Result<Url, PathToUrlError> {
#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
return Url::from_directory_path(path);
return Url::from_directory_path(path)
.map_err(|()| PathToUrlError(path.to_path_buf()));
#[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
url_from_directory_path_wasm(path)
.map_err(|()| PathToUrlError(path.to_path_buf()))
}

#[cfg(any(
Expand Down Expand Up @@ -304,27 +310,63 @@ pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
}
}

/// Canonicalizes a path which might be non-existent by going up the
/// ancestors until it finds a directory that exists, canonicalizes
/// that path, then adds back the remaining path components.
///
/// Note: When using this, you should be aware that a symlink may
/// subsequently be created along this path by some other code.
pub fn canonicalize_path_maybe_not_exists(
path: &Path,
canonicalize: &impl Fn(&Path) -> std::io::Result<PathBuf>,
) -> std::io::Result<PathBuf> {
let path = normalize_path(path);
let mut path = path.as_path();
let mut names_stack = Vec::new();
loop {
match canonicalize(path) {
Ok(mut canonicalized_path) => {
for name in names_stack.into_iter().rev() {
canonicalized_path = canonicalized_path.join(name);
}
return Ok(canonicalized_path);
}
Err(err) if err.kind() == ErrorKind::NotFound => {
names_stack.push(match path.file_name() {
Some(name) => name.to_owned(),
None => return Err(err),
});
path = match path.parent() {
Some(parent) => parent,
None => return Err(err),
};
}
Err(err) => return Err(err),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_specifier_parent() {
fn test_url_parent() {
run_test("file:///", "file:///");
run_test("file:///test", "file:///");
run_test("file:///test/", "file:///");
run_test("file:///test/other", "file:///test/");
run_test("file:///test/other.txt", "file:///test/");
run_test("file:///test/other/", "file:///test/");

fn run_test(specifier: &str, expected: &str) {
let result = specifier_parent(&Url::parse(specifier).unwrap());
fn run_test(url: &str, expected: &str) {
let result = url_parent(&Url::parse(url).unwrap());
assert_eq!(result.to_string(), expected);
}
}

#[test]
fn test_specifier_to_file_path() {
fn test_url_to_file_path() {
run_success_test("file:///", "/");
run_success_test("file:///test", "/test");
run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt");
Expand All @@ -333,18 +375,18 @@ mod tests {
"/dir/test test/test.txt",
);

assert_no_panic_specifier_to_file_path("file:/");
assert_no_panic_specifier_to_file_path("file://");
assert_no_panic_specifier_to_file_path("file://asdf/");
assert_no_panic_specifier_to_file_path("file://asdf/66666/a.ts");
assert_no_panic_url_to_file_path("file:/");
assert_no_panic_url_to_file_path("file://");
assert_no_panic_url_to_file_path("file://asdf/");
assert_no_panic_url_to_file_path("file://asdf/66666/a.ts");

fn run_success_test(specifier: &str, expected_path: &str) {
let result = url_to_file_path(&Url::parse(specifier).unwrap()).unwrap();
fn run_success_test(url: &str, expected_path: &str) {
let result = url_to_file_path(&Url::parse(url).unwrap()).unwrap();
assert_eq!(result, PathBuf::from(expected_path));
}

fn assert_no_panic_specifier_to_file_path(specifier: &str) {
let _result = url_to_file_path(&Url::parse(specifier).unwrap());
fn assert_no_panic_url_to_file_path(url: &str) {
let _result = url_to_file_path(&Url::parse(url).unwrap());
}
}

Expand Down Expand Up @@ -465,4 +507,21 @@ mod tests {
);
}
}

#[cfg(windows)]
#[test]
fn test_normalize_path() {
use super::*;

run_test("C:\\test\\./file.txt", "C:\\test\\file.txt");
run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt");
run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt");

fn run_test(input: &str, expected: &str) {
assert_eq!(
normalize_path(PathBuf::from(input)),
PathBuf::from(expected)
);
}
}
}