diff --git a/.changes/core-path-endpoint-path-doesnt-exist-error.md b/.changes/core-path-endpoint-path-doesnt-exist-error.md new file mode 100644 index 00000000000..2a2d24bd0cc --- /dev/null +++ b/.changes/core-path-endpoint-path-doesnt-exist-error.md @@ -0,0 +1,6 @@ +--- +"tauri": patch +"api": patch +--- + +Now `resolve()`, `join()` and `normalize()` from the `path` module, won't throw errors if the path doesn't exist, which matches NodeJS behavior. diff --git a/core/tauri/src/endpoints/path.rs b/core/tauri/src/endpoints/path.rs index 8d06b9a0de7..91f6e03aa37 100644 --- a/core/tauri/src/endpoints/path.rs +++ b/core/tauri/src/endpoints/path.rs @@ -6,7 +6,7 @@ use super::InvokeResponse; use crate::{api::path::BaseDirectory, Config, PackageInfo}; use serde::Deserialize; #[cfg(path_all)] -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR}; use std::sync::Arc; /// The API descriptor. #[derive(Deserialize)] @@ -77,48 +77,74 @@ pub fn resolve_path_handler( #[cfg(path_all)] fn resolve(paths: Vec) -> crate::Result { - // start with the current directory - let mut resolved_path = PathBuf::new().join("."); - - for path in paths { - let path_buf = PathBuf::from(path); - - // if we encounter an absolute path, we use it as the starting path for next iteration - if path_buf.is_absolute() { - resolved_path = path_buf; - } else { - resolved_path = resolved_path.join(&path_buf); - } + // Start with current directory path because users might pass empty or vec!["."] + // then start adding paths from the vector one by one using path.push() + // so if an absolute path is encountered in the iteration, we use it as the current full path + // examples: + // 1. vec!["."] or vec![] will be equal to std::env::current_dir() + // 2. vec!["/foo/bar", "/tmp/file", "baz"] will be equal to PathBuf::from("/tmp/file/baz") + let mut path = std::env::current_dir()?; + for p in paths { + path.push(p); } - - normalize(resolved_path.to_string_lossy().to_string()) + Ok(normalize_path(&path).to_string_lossy().to_string()) } #[cfg(path_all)] -fn normalize(path: String) -> crate::Result { - let path = std::fs::canonicalize(path)?; - let path = path.to_string_lossy().to_string(); - - // remove `\\\\?\\` on windows, UNC path - #[cfg(target_os = "windows")] - let path = path.replace("\\\\?\\", ""); +fn join(paths: Vec) -> crate::Result { + let path = PathBuf::from( + paths + .iter() + .map(|p| { + // Add MAIN_SEPARATOR if this is not the first element in the vector + // and if it doesn't already have a spearator. + // Doing this to ensure that the vector elements are separated in + // the resulting string so path.components() can work correctly when called + // in normalize_path_no_absolute() later + if !p.starts_with('/') && !p.starts_with('\\') && p != &paths[0] { + let mut tmp = String::from(MAIN_SEPARATOR); + tmp.push_str(p); + tmp + } else { + p.to_string() + } + }) + .collect::(), + ); - Ok(path) + let p = normalize_path_no_absolute(&path) + .to_string_lossy() + .to_string(); + Ok(if p.is_empty() { ".".into() } else { p }) } #[cfg(path_all)] -fn join(paths: Vec) -> crate::Result { - let mut joined_path = PathBuf::new(); - for path in paths { - joined_path = joined_path.join(path); - } - normalize(joined_path.to_string_lossy().to_string()) +fn normalize(path: String) -> crate::Result { + let mut p = normalize_path_no_absolute(Path::new(&path)) + .to_string_lossy() + .to_string(); + Ok(if p.is_empty() { + // Nodejs will return ".." if we used normalize("..") + // and will return "." if we used normalize("") or normalize(".") + if path == ".." { + path + } else { + ".".into() + } + } else { + // If the path passed to this function contains a trailing separator, + // we make sure to perserve it. That's how NodeJS works + if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) { + p.push(MAIN_SEPARATOR); + } + p + }) } #[cfg(path_all)] fn dirname(path: String) -> crate::Result { match Path::new(&path).parent() { - Some(path) => Ok(path.to_string_lossy().to_string()), + Some(p) => Ok(p.to_string_lossy().to_string()), None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path( "Couldn't get the parent directory".into(), ))), @@ -131,7 +157,7 @@ fn extname(path: String) -> crate::Result { .extension() .and_then(std::ffi::OsStr::to_str) { - Some(path) => Ok(path.to_string()), + Some(p) => Ok(p.to_string()), None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path( "Couldn't get the extension of the file".into(), ))), @@ -144,13 +170,87 @@ fn basename(path: String, ext: Option) -> crate::Result { .file_name() .and_then(std::ffi::OsStr::to_str) { - Some(path) => Ok(if let Some(ext) = ext { - path.replace(ext.as_str(), "") + Some(p) => Ok(if let Some(ext) = ext { + p.replace(ext.as_str(), "") } else { - path.to_string() + p.to_string() }), None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path( "Couldn't get the basename".into(), ))), } } + +/// Resolve ".." and "." if there is any , this snippet is taken from cargo's paths util +/// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106 +#[cfg(path_all)] +fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + 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 +} + +/// Resolve ".." and "." if there is any , this snippet is taken from cargo's paths util but +/// slightly modified to not resolve absolute paths +/// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106 +#[cfg(path_all)] +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().cloned() { + 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) => { + // Using PathBuf::push here will replace the whole path if an absolute path is encountered + // which is not the intended behavior, so instead of that, convert the current resolved path + // to a string and do simple string concatenation with the current component then convert it + // back to a PathBuf + let mut p = ret.to_string_lossy().to_string(); + // Don't add the separator if the resolved path is empty, + // otherwise we are gonna have unwanted leading separator + 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 +}