Repository: sharkdp/fd (language: rust)

Issue:
fd cannot search files under a RAM disk
**Describe the bug you encountered:**

when performing search (e.g. `fd somepattern`) under a ramdisk (using ImDisk as RAM disk utility), the following error is returned immediately:

`[fd error] Cannot retrieve current directory (has it been deleted?)`

Instead, the builtin windows prompt command:
`dir /S /P somepattern`
works correctly.


**What version of `fd` are you using?**
8.2.1

**Which operating system / distribution are you on?**
Windows 8.1 x64

fd doesn't show substituted drive on Windows
fd on Windows shows the real path on a substituted drive, not the substituted one:
```
pwsh> subst
F:\: => C:\data\flash drive

pwsh> cd \
pwsh> pwd

Path
----
F:\

pwsh> fd.exe --absolute-path
C:\data\flash drive\ARCHIVE
[...]
```


Discussion / hints:
Also, it does not work with virtual disks created by apps encrypting folders (e.g. [cppcryptfs][1]).

[1]: https://github.com/bailey27/cppcryptfs
Thank you for reporting this.

This is due to this check in `main.rs`:
```rs
    let current_directory = Path::new(".");
    if !filesystem::is_dir(current_directory) {
        return Err(anyhow!(
            "Could not retrieve current directory (has it been deleted?)."
        ));
    }
```

We probably shouldn't check whether `.` is a directory... just if it exists(?). Apparently on these special file systems, `.` is not a directory? Can someone confirm this?
I did a quick check using the `os.stat` command within python, and the `st_mode` integer is identical for folders under the RAM disk, and folders on the main hard drive. Also, the rest of the `os.stat` output does not present notable differences, so the issue might be located somewhere (or it could be due to a bad implementation of `filesystem::is_dir`)...

I'll try to run the `filesystem::is_dir` snippet under the two kinds of directories as well, and let you know.
Well, it seems that the culprit is the `path.canonicalize().is_ok()` part in the `is_dir` implementation, which returns false in the RAM disk case. The other check within the parenthesis (`path.file_name().is_some()`) returns false for both filesystems, at least when providing the current directory `"."`
Python has run into very similar issues with ImDisk before: https://bugs.python.org/issue26658

I'm guessing that `canonicalize()` is trying to do the same thing as Python used to, i.e. figure out the drive letter associated with the mount point.  That seems surprisingly aggressive to me -- I expect it to resolve symlinks, but not other junction/reparse points.  Might be worth reporting as a Rust bug.
Having the same issue.  I'm using `ImDisk` for a virtual `R:\` drive and `fd` can't do a thing in it.
Thank you for the feedback. I'm not a Windows user myself, so I cannot judge the significance of this. How do other tools behave? How big of a problem is this?
Say you would bind mount `/usr/local/bin` to `~/bin`. Then you do an `absolute-path` search in `~/bin` but all the results are prefixed with `/usr/local/bin` instead of `/home/user/bin`. Highly irritating.
Cygwin `readlink` uses the substituted drive:
```
> cd /cygdrive/f
> readlink.exe -f Autorun.inf
/cygdrive/f/Autorun.inf
```
Does this work if the CWD is `F:\`?
It does not work when I'm on `F:\`. As described in my first note:
```
[...]
pwsh> pwd

Path
----
F:\
[...]
```
Ok, thank you.

I'm not going to work on this myself as I don't have a Windows machine to develop on. Also, I don't have an idea on how to fix this.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6dcf8821d..dede4b7ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,5 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "aho-corasick"
 version = "0.7.18"
@@ -166,6 +168,7 @@ dependencies = [
  "lazy_static",
  "libc",
  "lscolors",
+ "normpath",
  "num_cpus",
  "regex",
  "regex-syntax",
@@ -330,6 +333,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "normpath"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27e6e8f70e9fbbe3752d330d769e3424f24b9458ce266df93a3b456902fd696a"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "num_cpus"
 version = "1.13.0"
diff --git a/Cargo.toml b/Cargo.toml
index 7c9322475..a4d479d79 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,6 +47,7 @@ lscolors = "0.7"
 globset = "0.4"
 anyhow = "1.0"
 dirs-next = "2.0"
+normpath = "0.3"
 
 [dependencies.clap]
 version = "2.31.2"
diff --git a/src/filesystem.rs b/src/filesystem.rs
index 19fc0e643..6680ae5c7 100644
--- a/src/filesystem.rs
+++ b/src/filesystem.rs
@@ -7,6 +7,8 @@ use std::io;
 use std::os::unix::fs::{FileTypeExt, PermissionsExt};
 use std::path::{Path, PathBuf};
 
+use normpath::PathExt;
+
 use crate::walk;
 
 pub fn path_absolute_form(path: &Path) -> io::Result<PathBuf> {
@@ -33,10 +35,10 @@ pub fn absolute_path(path: &Path) -> io::Result<PathBuf> {
     Ok(path_buf)
 }
 
-// Path::is_dir() is not guaranteed to be intuitively correct for "." and ".."
-// See: https://github.com/rust-lang/rust/issues/45302
-pub fn is_dir(path: &Path) -> bool {
-    path.is_dir() && (path.file_name().is_some() || path.canonicalize().is_ok())
+pub fn is_existing_directory(path: &Path) -> bool {
+    // Note: we do not use `.exists()` here, as `.` always exists, even if
+    // the CWD has been deleted.
+    path.is_dir() && (path.file_name().is_some() || path.normalize().is_ok())
 }
 
 #[cfg(any(unix, target_os = "redox"))]
diff --git a/src/main.rs b/src/main.rs
index 8a315a0b6..8654688ad 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,6 +21,7 @@ use atty::Stream;
 use globset::GlobBuilder;
 use lscolors::LsColors;
 use regex::bytes::{RegexBuilder, RegexSetBuilder};
+use normpath::PathExt;
 
 use crate::error::print_error;
 use crate::exec::CommandTemplate;
@@ -55,7 +56,7 @@ fn run() -> Result<ExitCode> {
     // Set the current working directory of the process
     if let Some(base_directory) = matches.value_of_os("base-directory") {
         let base_directory = Path::new(base_directory);
-        if !filesystem::is_dir(base_directory) {
+        if !filesystem::is_existing_directory(base_directory) {
             return Err(anyhow!(
                 "The '--base-directory' path '{}' is not a directory.",
                 base_directory.to_string_lossy()
@@ -70,7 +71,7 @@ fn run() -> Result<ExitCode> {
     }
 
     let current_directory = Path::new(".");
-    if !filesystem::is_dir(current_directory) {
+    if !filesystem::is_existing_directory(current_directory) {
         return Err(anyhow!(
             "Could not retrieve current directory (has it been deleted?)."
         ));
@@ -95,7 +96,7 @@ fn run() -> Result<ExitCode> {
         let mut directories = vec![];
         for path in paths {
             let path_buffer = PathBuf::from(path);
-            if filesystem::is_dir(&path_buffer) {
+            if filesystem::is_existing_directory(&path_buffer) {
                 directories.push(path_buffer);
             } else {
                 print_error(format!(
@@ -120,7 +121,7 @@ fn run() -> Result<ExitCode> {
             .iter()
             .map(|path_buffer| {
                 path_buffer
-                    .canonicalize()
+                    .normalize()
                     .and_then(|pb| filesystem::absolute_path(pb.as_path()))
                     .unwrap()
             })
@@ -130,7 +131,7 @@ fn run() -> Result<ExitCode> {
     // Detect if the user accidentally supplied a path instead of a search pattern
     if !matches.is_present("full-path")
         && pattern.contains(std::path::MAIN_SEPARATOR)
-        && filesystem::is_dir(Path::new(pattern))
+        && Path::new(pattern).is_dir()
     {
         return Err(anyhow!(
             "The search pattern '{pattern}' contains a path-separation character ('{sep}') \


Test patch:
diff --git a/tests/tests.rs b/tests/tests.rs
index 783420ad6..7fe2c7068 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -5,6 +5,7 @@ use std::io::Write;
 use std::path::Path;
 use std::time::{Duration, SystemTime};
 
+use normpath::PathExt;
 use regex::escape;
 
 use crate::testenv::TestEnv;
@@ -26,8 +27,9 @@ static DEFAULT_FILES: &[&str] = &[
 fn get_absolute_root_path(env: &TestEnv) -> String {
     let path = env
         .test_root()
-        .canonicalize()
+        .normalize()
         .expect("absolute path")
+        .as_path()
         .to_str()
         .expect("string")
         .to_string();
@@ -1090,16 +1092,19 @@ fn test_symlink_as_root() {
 fn test_symlink_and_absolute_path() {
     let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
 
+    let expected_path = if cfg!(windows) { "symlink" } else { "one/two" };
+
     te.assert_output_subdirectory(
         "symlink",
         &["--absolute-path"],
         &format!(
-            "{abs_path}/one/two/c.foo
-            {abs_path}/one/two/C.Foo2
-            {abs_path}/one/two/three
-            {abs_path}/one/two/three/d.foo
-            {abs_path}/one/two/three/directory_foo",
-            abs_path = &abs_path
+            "{abs_path}/{expected_path}/c.foo
+            {abs_path}/{expected_path}/C.Foo2
+            {abs_path}/{expected_path}/three
+            {abs_path}/{expected_path}/three/d.foo
+            {abs_path}/{expected_path}/three/directory_foo",
+            abs_path = &abs_path,
+            expected_path = expected_path
         ),
     );
 }
@@ -1127,6 +1132,8 @@ fn test_symlink_and_full_path() {
     let root = te.system_root();
     let prefix = escape(&root.to_string_lossy());
 
+    let expected_path = if cfg!(windows) { "symlink" } else { "one/two" };
+
     te.assert_output_subdirectory(
         "symlink",
         &[
@@ -1135,10 +1142,11 @@ fn test_symlink_and_full_path() {
             &format!("^{prefix}.*three", prefix = prefix),
         ],
         &format!(
-            "{abs_path}/one/two/three
-            {abs_path}/one/two/three/d.foo
-            {abs_path}/one/two/three/directory_foo",
-            abs_path = &abs_path
+            "{abs_path}/{expected_path}/three
+            {abs_path}/{expected_path}/three/d.foo
+            {abs_path}/{expected_path}/three/directory_foo",
+            abs_path = &abs_path,
+            expected_path = expected_path
         ),
     );
 }