Skip to content

Commit fe381a0

Browse files
authored
fix: join no longer cares if path doesn't exist, closes #2499 (#2548)
1 parent 36367dd commit fe381a0

File tree

2 files changed

+140
-34
lines changed

2 files changed

+140
-34
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri": patch
3+
"api": patch
4+
---
5+
6+
Now `resolve()`, `join()` and `normalize()` from the `path` module, won't throw errors if the path doesn't exist, which matches NodeJS behavior.

core/tauri/src/endpoints/path.rs

Lines changed: 134 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use super::InvokeResponse;
66
use crate::{api::path::BaseDirectory, Config, PackageInfo};
77
use serde::Deserialize;
88
#[cfg(path_all)]
9-
use std::path::{Path, PathBuf};
9+
use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
1010
use std::sync::Arc;
1111
/// The API descriptor.
1212
#[derive(Deserialize)]
@@ -77,48 +77,74 @@ pub fn resolve_path_handler(
7777

7878
#[cfg(path_all)]
7979
fn resolve(paths: Vec<String>) -> crate::Result<String> {
80-
// start with the current directory
81-
let mut resolved_path = PathBuf::new().join(".");
82-
83-
for path in paths {
84-
let path_buf = PathBuf::from(path);
85-
86-
// if we encounter an absolute path, we use it as the starting path for next iteration
87-
if path_buf.is_absolute() {
88-
resolved_path = path_buf;
89-
} else {
90-
resolved_path = resolved_path.join(&path_buf);
91-
}
80+
// Start with current directory path because users might pass empty or vec!["."]
81+
// then start adding paths from the vector one by one using path.push()
82+
// so if an absolute path is encountered in the iteration, we use it as the current full path
83+
// examples:
84+
// 1. vec!["."] or vec![] will be equal to std::env::current_dir()
85+
// 2. vec!["/foo/bar", "/tmp/file", "baz"] will be equal to PathBuf::from("/tmp/file/baz")
86+
let mut path = std::env::current_dir()?;
87+
for p in paths {
88+
path.push(p);
9289
}
93-
94-
normalize(resolved_path.to_string_lossy().to_string())
90+
Ok(normalize_path(&path).to_string_lossy().to_string())
9591
}
9692

9793
#[cfg(path_all)]
98-
fn normalize(path: String) -> crate::Result<String> {
99-
let path = std::fs::canonicalize(path)?;
100-
let path = path.to_string_lossy().to_string();
101-
102-
// remove `\\\\?\\` on windows, UNC path
103-
#[cfg(target_os = "windows")]
104-
let path = path.replace("\\\\?\\", "");
94+
fn join(paths: Vec<String>) -> crate::Result<String> {
95+
let path = PathBuf::from(
96+
paths
97+
.iter()
98+
.map(|p| {
99+
// Add MAIN_SEPARATOR if this is not the first element in the vector
100+
// and if it doesn't already have a spearator.
101+
// Doing this to ensure that the vector elements are separated in
102+
// the resulting string so path.components() can work correctly when called
103+
// in normalize_path_no_absolute() later
104+
if !p.starts_with('/') && !p.starts_with('\\') && p != &paths[0] {
105+
let mut tmp = String::from(MAIN_SEPARATOR);
106+
tmp.push_str(p);
107+
tmp
108+
} else {
109+
p.to_string()
110+
}
111+
})
112+
.collect::<String>(),
113+
);
105114

106-
Ok(path)
115+
let p = normalize_path_no_absolute(&path)
116+
.to_string_lossy()
117+
.to_string();
118+
Ok(if p.is_empty() { ".".into() } else { p })
107119
}
108120

109121
#[cfg(path_all)]
110-
fn join(paths: Vec<String>) -> crate::Result<String> {
111-
let mut joined_path = PathBuf::new();
112-
for path in paths {
113-
joined_path = joined_path.join(path);
114-
}
115-
normalize(joined_path.to_string_lossy().to_string())
122+
fn normalize(path: String) -> crate::Result<String> {
123+
let mut p = normalize_path_no_absolute(Path::new(&path))
124+
.to_string_lossy()
125+
.to_string();
126+
Ok(if p.is_empty() {
127+
// Nodejs will return ".." if we used normalize("..")
128+
// and will return "." if we used normalize("") or normalize(".")
129+
if path == ".." {
130+
path
131+
} else {
132+
".".into()
133+
}
134+
} else {
135+
// If the path passed to this function contains a trailing separator,
136+
// we make sure to perserve it. That's how NodeJS works
137+
if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) {
138+
p.push(MAIN_SEPARATOR);
139+
}
140+
p
141+
})
116142
}
117143

118144
#[cfg(path_all)]
119145
fn dirname(path: String) -> crate::Result<String> {
120146
match Path::new(&path).parent() {
121-
Some(path) => Ok(path.to_string_lossy().to_string()),
147+
Some(p) => Ok(p.to_string_lossy().to_string()),
122148
None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path(
123149
"Couldn't get the parent directory".into(),
124150
))),
@@ -131,7 +157,7 @@ fn extname(path: String) -> crate::Result<String> {
131157
.extension()
132158
.and_then(std::ffi::OsStr::to_str)
133159
{
134-
Some(path) => Ok(path.to_string()),
160+
Some(p) => Ok(p.to_string()),
135161
None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path(
136162
"Couldn't get the extension of the file".into(),
137163
))),
@@ -144,13 +170,87 @@ fn basename(path: String, ext: Option<String>) -> crate::Result<String> {
144170
.file_name()
145171
.and_then(std::ffi::OsStr::to_str)
146172
{
147-
Some(path) => Ok(if let Some(ext) = ext {
148-
path.replace(ext.as_str(), "")
173+
Some(p) => Ok(if let Some(ext) = ext {
174+
p.replace(ext.as_str(), "")
149175
} else {
150-
path.to_string()
176+
p.to_string()
151177
}),
152178
None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path(
153179
"Couldn't get the basename".into(),
154180
))),
155181
}
156182
}
183+
184+
/// Resolve ".." and "." if there is any , this snippet is taken from cargo's paths util
185+
/// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106
186+
#[cfg(path_all)]
187+
fn normalize_path(path: &Path) -> PathBuf {
188+
let mut components = path.components().peekable();
189+
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
190+
components.next();
191+
PathBuf::from(c.as_os_str())
192+
} else {
193+
PathBuf::new()
194+
};
195+
196+
for component in components {
197+
match component {
198+
Component::Prefix(..) => unreachable!(),
199+
Component::RootDir => {
200+
ret.push(component.as_os_str());
201+
}
202+
Component::CurDir => {}
203+
Component::ParentDir => {
204+
ret.pop();
205+
}
206+
Component::Normal(c) => {
207+
ret.push(c);
208+
}
209+
}
210+
}
211+
ret
212+
}
213+
214+
/// Resolve ".." and "." if there is any , this snippet is taken from cargo's paths util but
215+
/// slightly modified to not resolve absolute paths
216+
/// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106
217+
#[cfg(path_all)]
218+
fn normalize_path_no_absolute(path: &Path) -> PathBuf {
219+
let mut components = path.components().peekable();
220+
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
221+
components.next();
222+
PathBuf::from(c.as_os_str())
223+
} else {
224+
PathBuf::new()
225+
};
226+
227+
for component in components {
228+
match component {
229+
Component::Prefix(..) => unreachable!(),
230+
Component::RootDir => {
231+
ret.push(component.as_os_str());
232+
}
233+
Component::CurDir => {}
234+
Component::ParentDir => {
235+
ret.pop();
236+
}
237+
Component::Normal(c) => {
238+
// Using PathBuf::push here will replace the whole path if an absolute path is encountered
239+
// which is not the intended behavior, so instead of that, convert the current resolved path
240+
// to a string and do simple string concatenation with the current component then convert it
241+
// back to a PathBuf
242+
let mut p = ret.to_string_lossy().to_string();
243+
// Don't add the separator if the resolved path is empty,
244+
// otherwise we are gonna have unwanted leading separator
245+
if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
246+
p.push(MAIN_SEPARATOR);
247+
}
248+
if let Some(c) = c.to_str() {
249+
p.push_str(c);
250+
}
251+
ret = PathBuf::from(p);
252+
}
253+
}
254+
}
255+
ret
256+
}

0 commit comments

Comments
 (0)