Skip to content

Commit ef761b3

Browse files
authored
fix: relative path handling in canonicalize_path_maybe_not_exists (#20)
1 parent a22a74d commit ef761b3

2 files changed

Lines changed: 97 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@ jobs:
3636
if: contains(matrix.os, 'ubuntu')
3737
run: |
3838
cargo fmt -- --check
39-
39+
4040
- name: Check builds Wasm
4141
if: contains(matrix.os, 'ubuntu')
4242
run: |
4343
rustup target add wasm32-unknown-unknown
4444
cargo check --all-features --target wasm32-unknown-unknown
4545
46+
# Some tests mutate the current working directory while some others rely
47+
# on it; we run them sequentially to avoid flakiness.
4648
- name: Cargo test
47-
run: cargo test --locked --release --all-features --bins --tests --examples
49+
run: cargo test --locked --release --all-features --bins --tests --examples -- --test-threads=1
4850

4951
- name: Lint
5052
if: contains(matrix.os, 'ubuntu')

src/fs.rs

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

3-
use std::borrow::Cow;
43
use std::io::Error;
54
use std::io::ErrorKind;
65
use std::io::Write;
@@ -18,7 +17,6 @@ use sys_traits::SystemRandom;
1817
use sys_traits::ThreadSleep;
1918

2019
use crate::get_atomic_path;
21-
use crate::normalize_path;
2220

2321
/// Canonicalizes a path which might be non-existent by going up the
2422
/// ancestors until it finds a directory that exists, canonicalizes
@@ -28,10 +26,8 @@ use crate::normalize_path;
2826
/// subsequently be created along this path by some other code.
2927
pub fn canonicalize_path_maybe_not_exists(
3028
sys: &impl FsCanonicalize,
31-
path: &Path,
29+
mut path: &Path,
3230
) -> std::io::Result<PathBuf> {
33-
let path = normalize_path(Cow::Borrowed(path));
34-
let mut path = path.as_ref();
3531
let mut names_stack = Vec::new();
3632
loop {
3733
match sys.fs_canonicalize(path) {
@@ -47,6 +43,13 @@ pub fn canonicalize_path_maybe_not_exists(
4743
None => return Err(err),
4844
});
4945
path = match path.parent() {
46+
// When the provided path is a relative path (e.g. `foo/bar.txt`),
47+
// `path.parent()` ends up being the empty string as documented in
48+
// `std::path::Path::parent()` after going up the ancestor path.
49+
// In this case, what this function should return is a path joining
50+
// the current path with the provided path i.e. `{cwd}/foo/bar.txt`,
51+
// so we set `path` to `.` to indicate the current directory.
52+
Some(parent) if parent.as_os_str().is_empty() => Path::new("."),
5053
Some(parent) => parent,
5154
None => return Err(err),
5255
};
@@ -168,29 +171,110 @@ fn add_file_context_to_err(file_path: &Path, err: Error) -> Error {
168171

169172
#[cfg(test)]
170173
mod test {
174+
use std::path::Path;
171175
use std::path::PathBuf;
172176

173177
use sys_traits::impls::InMemorySys;
174178
use sys_traits::impls::RealSys;
179+
use sys_traits::EnvCurrentDir;
175180
use sys_traits::EnvSetCurrentDir;
181+
use sys_traits::FsCanonicalize;
176182
use sys_traits::FsCreateDirAll;
177183
use sys_traits::FsRead;
184+
use sys_traits::FsSymlinkDir;
178185

179186
use super::atomic_write_file_with_retries;
180187
use super::canonicalize_path_maybe_not_exists;
181188

182189
#[test]
183-
fn test_canonicalize_path_maybe_not_exists() {
190+
fn test_canonicalize_path_maybe_not_exists_in_memory() {
184191
let sys = InMemorySys::default();
192+
193+
// .
194+
// └── a
195+
// └── b (cwd)
196+
// └── c
185197
sys.fs_create_dir_all("/a/b/c").unwrap();
186198
sys.env_set_current_dir("/a/b").unwrap();
199+
200+
let path = canonicalize_path_maybe_not_exists(&sys, Path::new("")).unwrap();
201+
assert_eq!(path, PathBuf::from("/a/b"));
202+
let path =
203+
canonicalize_path_maybe_not_exists(&sys, Path::new(".")).unwrap();
204+
assert_eq!(path, PathBuf::from("/a/b"));
205+
let path =
206+
canonicalize_path_maybe_not_exists(&sys, Path::new("d")).unwrap();
207+
assert_eq!(path, PathBuf::from("/a/b/d"));
187208
let path =
188-
canonicalize_path_maybe_not_exists(&sys, &PathBuf::from("./c")).unwrap();
209+
canonicalize_path_maybe_not_exists(&sys, Path::new("./d")).unwrap();
210+
assert_eq!(path, PathBuf::from("/a/b/d"));
211+
let path =
212+
canonicalize_path_maybe_not_exists(&sys, Path::new("c")).unwrap();
213+
assert_eq!(path, PathBuf::from("/a/b/c"));
214+
let path =
215+
canonicalize_path_maybe_not_exists(&sys, Path::new("./c")).unwrap();
189216
assert_eq!(path, PathBuf::from("/a/b/c"));
190217
let path =
191-
canonicalize_path_maybe_not_exists(&sys, &PathBuf::from("./c/d/e"))
192-
.unwrap();
218+
canonicalize_path_maybe_not_exists(&sys, Path::new("c/d/e")).unwrap();
193219
assert_eq!(path, PathBuf::from("/a/b/c/d/e"));
220+
let path =
221+
canonicalize_path_maybe_not_exists(&sys, Path::new("./c/d/e")).unwrap();
222+
assert_eq!(path, PathBuf::from("/a/b/c/d/e"));
223+
}
224+
225+
// Note: this test mutates the current working directory. Although it will be
226+
// restored at the end, if other tests relying on the cwd are run in parallel
227+
// to this test, they may fail.
228+
#[test]
229+
fn test_canonicalize_path_maybe_not_exists_real() {
230+
let sys = RealSys;
231+
let temp_dir = tempfile::tempdir().unwrap();
232+
233+
// Save the original working directory to restore it later
234+
let original_cwd = sys.env_current_dir().unwrap();
235+
236+
// .
237+
// ├── a
238+
// │ └── b
239+
// │ └── c
240+
// └── link -> a/b/c (cwd)
241+
sys
242+
.fs_create_dir_all(temp_dir.path().join("a/b/c"))
243+
.unwrap();
244+
sys
245+
.fs_symlink_dir(
246+
temp_dir.path().join("a/b/c"),
247+
temp_dir.path().join("link"),
248+
)
249+
.unwrap();
250+
let cwd = temp_dir.path().join("link");
251+
sys.env_set_current_dir(&cwd).unwrap();
252+
253+
let canonicalized_temp_dir_path =
254+
sys.fs_canonicalize(temp_dir.path()).unwrap();
255+
256+
let path =
257+
canonicalize_path_maybe_not_exists(&sys, Path::new(".")).unwrap();
258+
assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c"));
259+
260+
let path =
261+
canonicalize_path_maybe_not_exists(&sys, &PathBuf::from("d")).unwrap();
262+
assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c/d"));
263+
264+
let path =
265+
canonicalize_path_maybe_not_exists(&sys, Path::new("./d")).unwrap();
266+
assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c/d"));
267+
268+
let path =
269+
canonicalize_path_maybe_not_exists(&sys, Path::new("d/e")).unwrap();
270+
assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c/d/e"));
271+
272+
let path =
273+
canonicalize_path_maybe_not_exists(&sys, Path::new("./d/e")).unwrap();
274+
assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c/d/e"));
275+
276+
// Restore the original working directory
277+
sys.env_set_current_dir(&original_cwd).unwrap();
194278
}
195279

196280
#[test]

0 commit comments

Comments
 (0)