use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
use serialize_to_javascript::{default_template, DefaultTemplate, Template};
use super::{BaseDirectory, Error, PathResolver, Result};
use crate::{
command,
plugin::{Builder, TauriPlugin},
AppHandle, Manager, Runtime, State,
};
fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
fn normalize_path_no_absolute(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
let mut p = ret.to_string_lossy().to_string();
if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
p.push(MAIN_SEPARATOR);
}
if let Some(c) = c.to_str() {
p.push_str(c);
}
ret = PathBuf::from(p);
}
}
}
ret
}
#[command(root = "crate")]
pub fn resolve_directory<R: Runtime>(
_app: AppHandle<R>,
resolver: State<'_, PathResolver<R>>,
directory: BaseDirectory,
path: Option<PathBuf>,
) -> Result<PathBuf> {
super::resolve_path(&resolver, directory, path).map(|p| dunce::simplified(&p).to_path_buf())
}
#[command(root = "crate")]
pub fn resolve(paths: Vec<String>) -> Result<PathBuf> {
let mut path = std::env::current_dir().map_err(Error::CurrentDir)?;
for p in paths {
path.push(p);
}
Ok(dunce::simplified(&normalize_path(&path)).to_path_buf())
}
#[command(root = "crate")]
pub fn normalize(path: String) -> String {
let mut p = dunce::simplified(&normalize_path_no_absolute(Path::new(&path)))
.to_string_lossy()
.to_string();
if p.is_empty() && path == ".." {
"..".into()
} else if p.is_empty() && path == "." {
".".into()
} else {
if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) {
p.push(MAIN_SEPARATOR);
}
p
}
}
#[command(root = "crate")]
pub fn join(paths: Vec<String>) -> String {
let path = PathBuf::from(
paths
.into_iter()
.map(|mut p| {
if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
p.push(MAIN_SEPARATOR);
}
p
})
.collect::<String>(),
);
let p = dunce::simplified(&normalize_path_no_absolute(&path))
.to_string_lossy()
.to_string();
if p.is_empty() {
".".into()
} else {
p
}
}
#[command(root = "crate")]
pub fn dirname(path: String) -> Result<PathBuf> {
match Path::new(&path).parent() {
Some(p) => Ok(dunce::simplified(p).to_path_buf()),
None => Err(Error::NoParent),
}
}
#[command(root = "crate")]
pub fn extname<R: Runtime>(app: AppHandle<R>, path: String) -> Result<String> {
let file_name = app.path().file_name(&path).ok_or(Error::NoExtension)?;
match Path::new(&file_name)
.extension()
.and_then(std::ffi::OsStr::to_str)
{
Some(p) => Ok(p.to_string()),
None => Err(Error::NoExtension),
}
}
#[command(root = "crate")]
pub fn basename<R: Runtime>(app: AppHandle<R>, path: &str, ext: Option<&str>) -> Result<String> {
let file_name = app.path().file_name(path);
match file_name {
Some(p) => {
let maybe_stripped = if let Some(ext) = ext {
p.strip_suffix(ext).unwrap_or(&p).to_string()
} else {
p
};
Ok(maybe_stripped)
}
None => Err(Error::NoBasename),
}
}
#[command(root = "crate")]
pub fn is_absolute(path: String) -> bool {
Path::new(&path).is_absolute()
}
#[derive(Template)]
#[default_template("./init.js")]
struct InitJavascript {
sep: &'static str,
delimiter: &'static str,
}
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
#[cfg(windows)]
let (sep, delimiter) = ("\\", ";");
#[cfg(not(windows))]
let (sep, delimiter) = ("/", ":");
let init_js = InitJavascript { sep, delimiter }
.render_default(&Default::default())
.unwrap();
Builder::new("path")
.invoke_handler(crate::generate_handler![
#![plugin(path)]
resolve_directory,
resolve,
normalize,
join,
dirname,
extname,
basename,
is_absolute
])
.js_init_script(init_js.to_string())
.setup(|app, _api| {
#[cfg(target_os = "android")]
{
let handle = _api.register_android_plugin("app.tauri", "PathPlugin")?;
app.manage(PathResolver(handle));
}
#[cfg(not(target_os = "android"))]
{
app.manage(PathResolver(app.clone()));
}
Ok(())
})
.build()
}
#[cfg(test)]
mod tests {
use crate::test::mock_app;
#[test]
fn basename() {
let app = mock_app();
let path = "/path/to/some-json-file.json";
assert_eq!(
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
"some-json-file"
);
let path = "/path/to/some-json-file.json";
assert_eq!(
super::basename(app.handle().clone(), path, Some("json")).unwrap(),
"some-json-file."
);
let path = "/path/to/some-json-file.html.json";
assert_eq!(
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
"some-json-file.html"
);
let path = "/path/to/some-json-file.json.json";
assert_eq!(
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
"some-json-file.json"
);
let path = "/path/to/some-json-file.json.html";
assert_eq!(
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
"some-json-file.json.html"
);
}
#[test]
fn join() {
fn check(paths: Vec<&str>, expected_unix: &str, expected_windows: &str) {
let expected = if cfg!(windows) {
expected_windows
} else {
expected_unix
};
let paths = paths.into_iter().map(String::from).collect();
assert_eq!(super::join(paths), expected);
}
check(vec![""], ".", ".");
check(vec!["", ""], ".", ".");
check(vec!["a"], "a", "a");
check(vec!["", "a"], "a", "a");
check(vec!["a", "b"], "a/b", r"a\b");
check(vec!["a", "", "b"], "a/b", r"a\b");
check(vec!["a", "/b", "c"], "a/b/c", r"a\b\c");
check(vec!["a", "b/c", "d"], "a/b/c/d", r"a\b\c\d");
check(vec!["a/", "b"], "a/b", r"a\b");
}
}