use std::{
borrow::Cow,
path::{Component, Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR},
};
use crate::utils::module::{export_default, ModuleInfo};
use rquickjs::{
function::Opt,
module::{Declarations, Exports, ModuleDef},
prelude::{Func, Rest},
Ctx, Object, Result,
};
pub struct PathModule;
#[cfg(windows)]
const DELIMITER: char = ';';
#[cfg(not(windows))]
const DELIMITER: char = ':';
#[cfg(windows)]
pub const CURRENT_DIR_STR: &str = ".\\";
#[cfg(windows)]
const FORWARD_SLASH_STR: &str = "/";
#[cfg(not(windows))]
pub const CURRENT_DIR_STR: &str = "./";
#[cfg(windows)]
use memchr::{memchr, memchr2, memchr2_iter};
#[cfg(windows)]
pub fn replace_backslash(path: impl Into<String>) -> String {
let mut path = path.into();
let bytes = unsafe { path.as_bytes_mut() };
let mut start = 0;
while let Some(pos) = memchr(b'\\', &bytes[start..]) {
bytes[start + pos] = b'/';
start += pos + 1;
}
path
}
#[cfg(not(windows))]
pub fn replace_backslash(path: impl Into<String>) -> String {
path.into().replace('\\', "/")
}
#[cfg(windows)]
fn find_next_separator(s: &str) -> Option<usize> {
memchr2(b'\\', b'/', s.as_bytes())
}
#[cfg(not(windows))]
fn find_next_separator(s: &str) -> Option<usize> {
s.find(MAIN_SEPARATOR)
}
#[cfg(windows)]
fn find_last_sep(path: &str) -> Option<usize> {
memchr2_iter(b'\\', b'/', path.as_bytes()).next_back()
}
#[cfg(not(windows))]
fn find_last_sep(path: &str) -> Option<usize> {
path.rfind(MAIN_SEPARATOR)
}
pub fn dirname<'a, P: Into<Cow<'a, str>>>(path: P) -> String {
let path = path.into();
let len = path.len();
if len == 0 {
return ".".into();
}
let bytes = path.as_bytes();
#[cfg(windows)]
{
if len == 1 {
return if is_sep(bytes[0]) {
path.into_owned()
} else {
".".to_string()
};
}
let (root_end, offset) = if is_sep(bytes[0]) {
if is_sep(bytes[1]) {
parse_unc_root(bytes, len).unwrap_or((1, 1))
} else {
(1, 1)
}
} else if bytes.len() > 1 && is_drive_letter(bytes[0]) && bytes[1] == b':' {
let r = if len > 2 && is_sep(bytes[2]) { 3 } else { 2 };
(r, r)
} else {
(0, 0)
};
let end = find_dirname_end(bytes, offset);
match end {
Some(e) => &path[..e],
None if root_end > 0 => &path[..root_end],
None => ".",
}
.into()
}
#[cfg(not(windows))]
{
if len == 1 {
return if bytes[0] == b'/' {
path.into_owned()
} else {
".".into()
};
}
let has_root = bytes[0] == b'/';
let end = find_dirname_end(bytes, 1);
match end {
Some(e) if has_root && e == 1 => "//",
Some(e) => &path[..e],
None if has_root => "/",
None => ".",
}
.into()
}
}
#[cfg(windows)]
fn is_sep(c: u8) -> bool {
c == b'/' || c == b'\\'
}
#[cfg(windows)]
fn is_drive_letter(c: u8) -> bool {
c.is_ascii_alphabetic()
}
#[cfg(windows)]
fn parse_unc_root(bytes: &[u8], len: usize) -> Option<(usize, usize)> {
let mut j = 2;
while j < len && !is_sep(bytes[j]) {
j += 1;
}
if j >= len || j == 2 {
return None;
}
while j < len && is_sep(bytes[j]) {
j += 1;
}
if j >= len {
return None;
}
let share_start = j;
while j < len && !is_sep(bytes[j]) {
j += 1;
}
if j == share_start {
return None;
}
if j == len {
return None;
} Some((j + 1, j + 1))
}
fn find_dirname_end(bytes: &[u8], offset: usize) -> Option<usize> {
let mut matched_slash = true;
for i in (offset..bytes.len()).rev() {
#[cfg(windows)]
let is_separator = is_sep(bytes[i]);
#[cfg(not(windows))]
let is_separator = bytes[i] == b'/';
if is_separator {
if !matched_slash {
return Some(i);
}
} else {
matched_slash = false;
}
}
None
}
pub fn name_extname(path: &str) -> (&str, &str) {
let path = strip_last_sep(path);
let sep_pos = find_last_sep(path);
let path = match sep_pos {
Some(idx) => &path[idx + 1..],
None => path,
};
if path.starts_with('.') {
return (path, "");
}
match path.rfind('.') {
Some(idx) => path.split_at(idx),
None => (path, ""),
}
}
fn strip_last_sep(path: &str) -> &str {
if ends_with_sep(path) {
&path[..path.len() - 1]
} else {
path
}
}
pub fn basename(path: String, suffix: Opt<String>) -> String {
#[cfg(windows)]
{
if path.is_empty() || path == MAIN_SEPARATOR_STR || path == FORWARD_SLASH_STR {
return String::from("");
}
}
#[cfg(not(windows))]
{
if path.is_empty() || path == MAIN_SEPARATOR_STR {
return String::from("");
}
}
let (base, ext) = name_extname(&path);
let mut name = [base, ext].concat();
if let Some(suffix) = suffix.0 {
if let Some(location) = name.rfind(&suffix) {
name.truncate(location);
return name;
}
}
name
}
fn extname(path: String) -> String {
let (_, ext) = name_extname(&path);
ext.to_string()
}
fn format(obj: Object) -> String {
let dir: String = obj.get("dir").unwrap_or_default();
let root: String = obj.get("root").unwrap_or_default();
let base: String = obj.get("base").unwrap_or_default();
let name: String = obj.get("name").unwrap_or_default();
let ext: String = obj.get("ext").unwrap_or_default();
let mut path = String::new();
if !dir.is_empty() {
path.push_str(&dir);
if !ends_with_sep(&dir) {
path.push(MAIN_SEPARATOR);
}
} else if !root.is_empty() {
path.push_str(&root);
if !ends_with_sep(&root) {
path.push(MAIN_SEPARATOR);
}
}
if !base.is_empty() {
path.push_str(&base);
} else {
path.push_str(&name);
if !ext.is_empty() {
if !ext.starts_with('.') {
path.push('.');
}
path.push_str(&ext);
}
}
path
}
fn parse(ctx: Ctx, path_str: String) -> Result<Object> {
let obj = Object::new(ctx)?;
let path = Path::new(&path_str);
let parent = path
.parent()
.map(|p| p.to_str().unwrap())
.unwrap_or_default();
let filename = path
.file_name()
.map(|n| n.to_str().unwrap())
.unwrap_or_default();
let (name, extension) = name_extname(filename);
let root = path
.components()
.next()
.and_then(|c| match c {
Component::Prefix(prefix) => prefix.as_os_str().to_str(),
Component::RootDir => c.as_os_str().to_str(),
_ => Some(""),
})
.unwrap_or_default();
obj.set("root", root)?;
obj.set("dir", parent)?;
obj.set("base", [name, extension].concat())?;
obj.set("ext", extension)?;
obj.set("name", name)?;
Ok(obj)
}
fn join(parts: Rest<String>) -> String {
join_path(parts.0.iter())
}
pub fn join_path<S, I>(parts: I) -> String
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
join_path_with_separator(parts, false)
}
pub fn join_path_with_separator<S, I>(parts: I, force_posix_sep: bool) -> String
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let parts_vec: Vec<S> = parts.into_iter().collect();
let likely_max_size = parts_vec
.iter()
.map(|p| p.as_ref().len() + 1)
.sum::<usize>()
+ 10;
let result = String::with_capacity(likely_max_size);
join_resolve_path(parts_vec, false, result, PathBuf::new(), force_posix_sep)
}
pub fn resolve_path<S, I>(parts: I) -> Result<String>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
resolve_path_with_separator(parts, false)
}
pub fn resolve_path_with_separator<S, I>(parts: I, force_posix_sep: bool) -> Result<String>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let cwd = std::env::current_dir()?;
let mut result = cwd.clone().into_os_string().into_string().unwrap();
if !result.ends_with(MAIN_SEPARATOR) {
result.push(MAIN_SEPARATOR);
}
#[cfg(windows)]
{
if force_posix_sep {
result = result.replace(MAIN_SEPARATOR, FORWARD_SLASH_STR);
}
}
Ok(join_resolve_path(parts, true, result, cwd, force_posix_sep))
}
pub fn relative<F, T>(from: F, to: T) -> Result<String>
where
F: AsRef<str>,
T: AsRef<str>,
{
let from_ref = from.as_ref();
let to_ref = to.as_ref();
if from_ref == to_ref {
return Ok("".into());
}
let mut abs_from = None;
if !is_absolute(from_ref) {
abs_from = Some(
std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + from_ref,
);
}
let mut abs_to = None;
if !is_absolute(to_ref) {
abs_to = Some(
std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + to_ref,
);
}
let from_ref = abs_from.as_deref().unwrap_or(from_ref);
let to_ref = abs_to.as_deref().unwrap_or(to_ref);
let mut from_index = 0;
let mut to_index = 0;
while from_index < from_ref.len() && to_index < to_ref.len() {
let from_next = find_next_separator(&from_ref[from_index..])
.unwrap_or(from_ref.len() - from_index)
+ from_index;
let to_next =
find_next_separator(&to_ref[to_index..]).unwrap_or(to_ref.len() - to_index) + to_index;
if from_ref[from_index..from_next] != to_ref[to_index..to_next] {
break;
}
from_index = from_next + 1; to_index = to_next + 1; }
let mut relative = String::new();
while from_index < from_ref.len() {
let from_next = find_next_separator(&from_ref[from_index..])
.unwrap_or(from_ref.len() - from_index)
+ from_index;
if !relative.is_empty() {
relative.push(MAIN_SEPARATOR);
}
relative.push_str("..");
from_index = from_next + 1; }
while to_index < to_ref.len() {
let to_next =
find_next_separator(&to_ref[to_index..]).unwrap_or(to_ref.len() - to_index) + to_index;
if !relative.is_empty() {
relative.push(MAIN_SEPARATOR);
}
let component = &to_ref[to_index..to_next];
if component != "." {
relative.push_str(component);
}
to_index = to_next + 1; }
Ok(if relative.is_empty() {
".".into()
} else {
relative
})
}
fn join_resolve_path<S, I>(
parts: I,
resolve: bool,
mut result: String,
cwd: PathBuf,
force_posix_sep: bool,
) -> String
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let (sep, sep_str) = if force_posix_sep {
('/', "/")
} else {
(MAIN_SEPARATOR, MAIN_SEPARATOR_STR)
};
let mut resolve_cow: Cow<str>;
let mut empty = true;
let mut prefix_len = 0;
let mut index_stack = Vec::with_capacity(16);
if ends_with_sep(&result) && result.len() > 1 {
result.truncate(result.len() - 1);
}
for part in parts {
let mut part_ref: &str = part.as_ref();
let mut start = 0;
if resolve {
if cfg!(not(windows)) {
if part_ref.starts_with(MAIN_SEPARATOR) {
empty = false;
result = MAIN_SEPARATOR.into();
start = 1;
}
} else {
let starts_with_sep = starts_with_sep(part_ref);
if starts_with_sep {
let (prefix, _) = get_path_prefix(&cwd);
prefix_len = prefix.len();
result = prefix;
empty = false;
result.push(sep);
} else {
let path_buf: PathBuf = PathBuf::from(part_ref);
if path_buf.is_absolute() {
empty = false;
let (prefix, mut components) = get_path_prefix(&path_buf);
if !prefix.is_empty() {
components.next(); }
prefix_len = prefix.len();
result = prefix;
result.push(sep);
resolve_cow = components
.map(|comp| comp.as_os_str().to_str().unwrap_or_default()) .collect::<Vec<&str>>() .join(sep_str)
.into();
part_ref = resolve_cow.as_ref();
}
}
}
} else if starts_with_sep(part_ref) && empty {
empty = false;
result.push(sep);
start = 1;
}
while start < part_ref.len() {
let end = find_next_separator(&part_ref[start..]).map_or(part_ref.len(), |i| i + start);
match &part_ref[start..end] {
".." => {
if let Some(last_index) = index_stack.pop() {
result.truncate(last_index);
} else if empty {
if let Some(last_index) = find_last_sep(&result) {
result.truncate(last_index);
}
}
},
"" | "." => {
},
sub_part => {
let len = result.len();
if !result.ends_with(sep) && !result.is_empty() {
result.push(sep);
}
result.push_str(sub_part);
result.push(sep);
index_stack.push(len);
},
}
start = end + 1;
}
}
if result.len() > prefix_len + 1 && ends_with_sep(&result) {
result.truncate(result.len() - 1);
}
result
}
pub fn resolve(path: Rest<String>) -> Result<String> {
resolve_path(path.iter())
}
fn get_path_prefix(cwd: &Path) -> (String, std::iter::Peekable<std::path::Components<'_>>) {
let mut components = cwd.components().peekable();
let prefix = if let Some(Component::Prefix(prefix)) = components.peek() {
prefix.as_os_str().to_str().unwrap().to_string()
} else {
"".into()
};
(prefix, components)
}
pub fn normalize<P: AsRef<str>>(path: P) -> String {
join_path([path].iter())
}
#[allow(dead_code)] fn starts_with_sep(path: &str) -> bool {
matches!(path.as_bytes().first().unwrap_or(&0), b'/' | b'\\')
}
#[cfg(windows)]
pub fn ends_with_sep(path: &str) -> bool {
matches!(path.as_bytes().last().unwrap_or(&0), b'/' | b'\\')
}
#[cfg(not(windows))]
pub fn ends_with_sep(path: &str) -> bool {
path.ends_with(MAIN_SEPARATOR)
}
#[cfg(windows)]
pub fn is_absolute(path: &str) -> bool {
starts_with_sep(path) || PathBuf::from(path).is_absolute()
}
#[cfg(not(windows))]
pub fn is_absolute(path: &str) -> bool {
path.starts_with(MAIN_SEPARATOR)
}
impl ModuleDef for PathModule {
fn declare(declare: &Declarations) -> Result<()> {
declare.declare("basename")?;
declare.declare("dirname")?;
declare.declare("extname")?;
declare.declare("format")?;
declare.declare("parse")?;
declare.declare("join")?;
declare.declare("resolve")?;
declare.declare("relative")?;
declare.declare("normalize")?;
declare.declare("isAbsolute")?;
declare.declare("delimiter")?;
declare.declare("sep")?;
declare.declare("default")?;
Ok(())
}
fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
export_default(ctx, exports, |default| {
default.set("dirname", Func::from(dirname::<String>))?;
default.set("basename", Func::from(basename))?;
default.set("extname", Func::from(extname))?;
default.set("format", Func::from(format))?;
default.set("parse", Func::from(parse))?;
default.set("join", Func::from(join))?;
default.set("relative", Func::from(relative::<String, String>))?;
default.set("resolve", Func::from(resolve))?;
default.set("normalize", Func::from(normalize::<String>))?;
default.set("isAbsolute", Func::from(|s: String| is_absolute(&s)))?;
default.prop("delimiter", DELIMITER.to_string())?;
default.prop("sep", MAIN_SEPARATOR.to_string())?;
Ok(())
})
}
}
impl From<PathModule> for ModuleInfo<PathModule> {
fn from(val: PathModule) -> Self {
ModuleInfo {
name: "path",
module: val,
}
}
}
#[cfg(test)]
mod tests {
use std::{env::set_current_dir, sync::Mutex};
static THREAD_LOCK: Lazy<Mutex<()>> = Lazy::new(Mutex::default);
use once_cell::sync::Lazy;
use super::*;
#[test]
fn test_relative() {
let _shared = THREAD_LOCK.lock().unwrap();
let cwd = std::env::current_dir().expect("unable to get current working directory");
set_current_dir("/").expect("unable to set working directory to /");
assert_eq!(
relative("a/b/c", "b/c").unwrap(),
"../../../b/c".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb").unwrap(),
"../../impl/bbb".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
relative("/a/b/c", "/a/d").unwrap(),
"../../d".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(relative("/a/b/c", "/a/b/c/d").unwrap(), "d");
assert_eq!(relative("/a/b/c", "/a/b/c").unwrap(), "");
assert_eq!(
relative("a/b", "a/b/c/d").unwrap(),
"c/d".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
relative("a/b/c", "b/c").unwrap(),
"../../../b/c".replace('/', MAIN_SEPARATOR_STR)
);
set_current_dir(cwd).expect("unable to set working directory back");
}
#[test]
fn test_dirname() {
assert_eq!(dirname("/usr/local/bin".to_string()), "/usr/local");
assert_eq!(dirname("/usr/local/".to_string()), "/usr");
assert_eq!(dirname("usr/local/bin".to_string()), "usr/local");
assert_eq!(dirname("/".to_string()), "/");
assert_eq!(dirname("".to_string()), ".");
}
#[test]
fn test_basename() {
assert_eq!(basename("/usr/local/bin".to_string(), Opt(None)), "bin");
assert_eq!(
basename("/usr/local/bin.txt".to_string(), Opt(None)),
"bin.txt"
);
assert_eq!(
basename(
"/usr/local/bin.txt".to_string(),
Opt(Some(".txt".to_string()))
),
"bin"
);
assert_eq!(basename("".to_string(), Opt(None)), "");
assert_eq!(basename("/".to_string(), Opt(None)), "");
}
#[test]
fn test_extname() {
assert_eq!(extname("/usr/local/bin.txt".to_string()), ".txt");
assert_eq!(extname("/usr/local/bin".to_string()), "");
assert_eq!(extname("file.tar.gz".to_string()), ".gz");
assert_eq!(extname(".bashrc".to_string()), "");
assert_eq!(extname("".to_string()), "");
}
#[test]
fn test_join() {
assert_eq!(
join_path(["/usr", "local", "bin"].iter()),
"/usr/local/bin".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
join_path(["/usr", "/local", "bin"].iter()),
"/usr/local/bin".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
join_path(["usr", "local", "bin"].iter()),
"usr/local/bin".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(join_path(["", "bin"].iter()), "bin");
assert_eq!(
join_path(["/usr", "..", "local", "bin"].iter()),
"/local/bin".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
join_path([".", "usr", "local"]),
"usr/local".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
join_path(["/usr", ".", "bin"].iter()),
"/usr/bin".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
join_path(["usr", "local", "bin", ".."].iter()),
"usr/local".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
join_path(["/usr", "local", "", "bin"].iter()),
"/usr/local/bin".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
join_path(["/usr", "local", ".hidden"].iter()),
"/usr/local/.hidden".replace('/', MAIN_SEPARATOR_STR)
); }
#[test]
fn test_resolve_path() {
let _shared = THREAD_LOCK.lock().unwrap();
let prefix = if cfg!(windows) {
if let Some(Component::Prefix(prefix)) =
std::env::current_dir().unwrap().components().next()
{
prefix.as_os_str().to_str().unwrap().to_string()
} else {
"".into()
}
} else {
"".into()
};
assert_eq!(
resolve_path(["", "foo/bar"].iter()).unwrap(),
std::env::current_dir()
.unwrap()
.join("foo/bar".replace('/', MAIN_SEPARATOR_STR))
.to_string_lossy()
.to_string()
);
assert_eq!(
resolve_path(["/"].iter()).unwrap(),
prefix.clone() + MAIN_SEPARATOR_STR
);
assert_eq!(
resolve_path(["/foo/bar", "../baz"].iter()).unwrap(),
prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
resolve_path(["/foo/bar", "./baz"].iter()).unwrap(),
prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
resolve_path(["foo/bar", "/baz"].iter()).unwrap(),
prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
resolve_path(["/foo", "bar", ".", "baz"].iter()).unwrap(),
prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
resolve_path(["/foo", "bar", "..", "baz"].iter()).unwrap(),
prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
resolve_path(["/foo", "bar", "../..", "baz"].iter()).unwrap(),
prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
resolve_path(["/foo", "bar", ".hidden"].iter()).unwrap(),
prefix.clone() + &"/foo/bar/.hidden".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
resolve_path(["/foo", ".", "bar", "."].iter()).unwrap(),
prefix.clone() + &"/foo/bar".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
resolve_path(["/foo", "..", "..", "bar"].iter()).unwrap(),
prefix.clone() + &"/bar".replace('/', MAIN_SEPARATOR_STR)
); assert_eq!(
resolve_path(["/foo/bar", "/..", "baz"].iter()).unwrap(),
prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
resolve_path(["../foo"].iter()).unwrap(),
std::env::current_dir()
.unwrap()
.parent()
.unwrap()
.join("foo".replace('/', MAIN_SEPARATOR_STR))
.to_string_lossy()
.to_string()
);
assert_eq!(
resolve_path(["../".repeat(32)].iter()).unwrap(),
prefix.clone()
); }
#[test]
fn test_normalize() {
assert_eq!(
normalize("/foo//bar//baz"),
"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
normalize("/foo/./bar/../baz"),
"/foo/baz".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(
normalize("foo/bar/"),
"foo/bar".replace('/', MAIN_SEPARATOR_STR)
);
assert_eq!(normalize("./foo"), "foo");
}
#[test]
fn test_is_absolute() {
assert!(is_absolute("/usr/local/bin"));
assert!(!is_absolute("usr/local/bin"));
#[cfg(windows)]
assert!(is_absolute("C:\\Program Files")); assert!(!is_absolute("./local/bin"));
}
#[test]
fn test_replace_backslash() {
assert_eq!(replace_backslash("C:\\Program Files"), "C:/Program Files");
assert_eq!(replace_backslash("/usr/local/bin"), "/usr/local/bin");
assert_eq!(replace_backslash("C:\\Users\\User\\"), "C:/Users/User/");
}
}