rustpathwindows-subsystem-for-linux

How to handle path on Windows WSL2 in rust?


Background

I tried to develop a Rust lib to assist with swapping filenames for any two files, but I had problems when I tried to dealing with paths on WSL.

Examples

Normal Situation

File1 File2
Before D:\test\1.ext1 D:\test\2.ext2
After D:\test\2.ext1 D:\test\1.ext2

Relative Path

File1 File2
Before ./1.ext1 ~/2.ext2
After D:\test\2.ext1 C:\Users\xxx\1.ext2

WSL Situation

File1 File2
Before \\wsl.localhost\Debian\home\user1\1.ext1 \\wsl.localhost\Debian\home\user1\2.ext2
After (WHAT I WANT) \\wsl.localhost\Debian\home\user1\2.ext1 \\wsl.localhost\Debian\home\user1\1.ext2

What I've done

MWE:

use std::path::{Path, PathBuf};

fn main() {
    // Please create this file in your wsl for test.
    // `~/1.ext1`
    // MODIFY THIS with your wsl user name
    let wsl_user_name = "";

    // Please create this on working dir (windows) for test.
    // `./2.ext2`

    let current_exe = std::env::current_exe().unwrap();
    let base_dir = current_exe.parent().unwrap();

    let wsl_file = PathBuf::from(format!(
        r"\\wsl.localhost\Debian\home\{}\1.ext1",
        wsl_user_name
    ));
    let win_file = PathBuf::from(r"./2.ext2");

    let result_wsl = path_get_absolute_exist(&wsl_file, base_dir);
    dbg!(result_wsl);

    let result_win = path_get_absolute_exist(&win_file, base_dir);
    dbg!(result_win);
}

fn path_get_absolute_exist(path: &Path, base_dir: &Path) -> (bool, PathBuf) {
    if path.as_os_str().is_empty() {
        return (false, path.to_path_buf());
    }

    let mut path = path.to_path_buf();

    #[cfg(windows)]
    {
        use std::path::{Component, Prefix};

        path = {
            let temp = path.to_str().unwrap_or("").replace("/", "\\");
            PathBuf::from(temp)
        };

        let is_absolute = {
            let mut components = path.components();
            if let Some(Component::Prefix(prefix_component)) = components.next() {
                let has_root_dir = matches!(components.next(), Some(Component::RootDir));
                dbg!(has_root_dir);
                if !has_root_dir {
                    false
                } else {
                    dbg!(prefix_component.kind());
                    matches!(
                        prefix_component.kind(), //Useful for judge `\\wsl.localhost\` but useless when test exist()
                        Prefix::VerbatimUNC(..)
                            | Prefix::UNC(..)
                            | Prefix::VerbatimDisk(..)
                            | Prefix::Disk(_)
                            | Prefix::DeviceNS(..)
                            | Prefix::Verbatim(_)
                    )
                }
            } else {
                dbg!(path.is_absolute());
                path.is_absolute()
            }
        };

        if !is_absolute {
            if path.starts_with("~") {
                if let Ok(home_dir) = std::env::var("USERPROFILE") {
                    let mut new_path = PathBuf::from(home_dir);
                    let remaining = path.strip_prefix("~/").ok();
                    if let Some(rem) = remaining {
                        new_path.push(rem);
                        path = new_path;
                    } else if path.to_string_lossy() == "~" {
                        path = new_path;
                    } else {
                        // "~something"
                        path = base_dir.join(path);
                    }
                }
            } else {
                path = base_dir.join(path);
            }
        }
        dbg!(format!("Path Final: {}", &path.display()));
    }

    #[cfg(not(windows))]
    {
        path = {
            let temp = path.to_str().unwrap_or("").replace("\\", "/");
            PathBuf::from(temp)
        };

        if !path.is_absolute() {
            if path.starts_with("~") {
                if let Ok(home_dir) = std::env::var("HOME") {
                    let mut new_path = PathBuf::from(home_dir);
                    if let Some(remaining) = path.strip_prefix("~/") {
                        new_path.push(remaining);
                    } else if path.to_string_lossy() == "~" {
                        // Just "~", so it's the home directory
                    }
                    path = new_path;
                }
            } else {
                path = base_dir.join(path);
            }
        }
        dbg!(format!("Path Final: {}", &path.display()));
    }

    let canonical = path.canonicalize();
    match canonical {
        Ok(x) => (x.exists(), x),
        Err(e) => {
            eprintln!("{}", e);
            (path.exists(), path)
        }
    }
}

error info

[src\exchange.rs:161:13] is_absolute = true
[src\exchange.rs:190:9] &path = "\\\\wsl.localhost\\Debian\\home\\user1\\1.ext1" 
[src\exchange.rs:161:13] is_absolute = true
[src\exchange.rs:190:9] &path = "\\\\wsl.localhost\\Debian\\home\\user1\\2.ext2"
File does not exist

I didn't give all the code here for length reasons, if you need all the code, please check it out at https://github.com/Mikachu2333/exchange_name_lib


Solution

    1. For recognize WSL path, one should use std::path::{Component, Prefix} in order to recognize WSL path such as \\\\wsl.localhost\\Debian\\home\\LinkChou\\1.ext1

    2. Although WSL path is "absolute" in the view of users, rust would not recognize it as 'absolute', but PathBuf.join(new) would replace the old as the following example shows.

      let dir = =std::env::current_dir().unwrap();
      let file = r"\\\\wsl.localhost\\Debian\\home\\LinkChou\\1.ext1";
      let path = PathBuf::from(file);
      assert(path.is_absolute());//panic
      
      let result = dir.join(path);
      assert(result.to_path_buf(),dir.to_path_buf())//true
      
    3. Complete code as the following shows.

    use std::path::{Path, PathBuf};
    
    fn main() {
        // Please create this file in your wsl for test before run.
        // `~/1.ext1`
        // MODIFY THIS with your wsl user name
        let wsl_user_name = "LinkChou";
        let wsl_type = "Debian";
    
        // Please create this on working dir (windows) for test.
        // `./2.ext2`
    
        let current_exe = std::env::current_exe().unwrap();
        let base_dir = current_exe.parent().unwrap();
        
        // test files path
        let wsl_file = PathBuf::from(format!(
            r"\\wsl.localhost\{}\home\{}\1.ext1",
            wsl_type, wsl_user_name
        ));
        let win_file = PathBuf::from(r"2.ext2");
    
        let result_wsl = path_get_absolute_exist(&wsl_file, base_dir);
        dbg!(&result_wsl);//true, wsl file path(same as original str)
        assert!(result_wsl.1.exists());//true
    
        let result_win = path_get_absolute_exist(&win_file, base_dir);
        dbg!(result_win);// false, return absolute path
    }
    
    fn path_get_absolute_exist(path: &Path, base_dir: &Path) -> (bool, PathBuf) {
        if path.as_os_str().is_empty() {
            return (false, path.to_path_buf());
        }
    
        let mut path = path.to_path_buf();
        
        //only judge wsl,... .etc on win
        #[cfg(windows)]
        {
            use std::path::{Component, Prefix};
    
            path = {
                let temp = path.to_str().unwrap_or("").replace("/", "\\");
                PathBuf::from(temp)
            };
    
            let is_absolute = {
                let mut components = path.components();
                if let Some(Component::Prefix(prefix_component)) = components.next() {
                    let has_root_dir = matches!(components.next(), Some(Component::RootDir));
                    dbg!(has_root_dir);
                    if !has_root_dir {
                        false
                    } else {
                        dbg!(prefix_component.kind());
                        matches!(
                            prefix_component.kind(), //Useful for judge `\\wsl.localhost\` but useless when test exist()
                            Prefix::VerbatimUNC(..)
                                | Prefix::UNC(..)
                                | Prefix::VerbatimDisk(..)
                                | Prefix::Disk(_)
                                | Prefix::DeviceNS(..)
                                | Prefix::Verbatim(_)
                        )
                    }
                } else {
                    dbg!(path.is_absolute());
                    path.is_absolute()
                }
            };
    
            if !is_absolute {
                if path.starts_with("~") {
                    if let Ok(home_dir) = std::env::var("USERPROFILE") {
                        let mut new_path = PathBuf::from(home_dir);
                        let remaining = path.strip_prefix("~/").ok();
                        if let Some(rem) = remaining {
                            new_path.push(rem);
                            path = new_path;
                        } else if path.to_string_lossy() == "~" {
                            path = new_path;
                        } else {
                            // "~something"
                            path = base_dir.join(path);
                        }
                    }
                } else if path.starts_with(".") {
                    let remaining = path.strip_prefix(".\\").ok();
                    path = base_dir.join(remaining.unwrap());
                } else {
                    path = base_dir.join(path);
                }
            }
            dbg!(format!("Path Final: {}", &path.display()));
        }
    
        #[cfg(not(windows))]
        {
            path = {
                let temp = path.to_str().unwrap_or("").replace("\\", "/");
                PathBuf::from(temp)
            };
    
            if !path.is_absolute() {
                if path.starts_with("~") {
                    if let Ok(home_dir) = std::env::var("HOME") {
                        let mut new_path = PathBuf::from(home_dir);
                        if let Some(remaining) = path.strip_prefix("~/") {
                            new_path.push(remaining);
                        } else if path.to_string_lossy() == "~" {
                            // Just "~", so it's the home directory
                        }
                        path = new_path;
                    }
                } else if path.starts_with(".") {
                    let remaining = path.strip_prefix("./").ok();
                    path = base_dir.join(remaining.unwrap());
                } else {
                    path = base_dir.join(path);
                }
            }
            dbg!(format!("Path Final: {}", &path.display()));
        }
    
        let canonical = path.canonicalize();//we have to use canonicalize here because it will panic if path's type is wsl
        match canonical {
            Ok(x) => (x.exists(), x),
            Err(e) => {
                eprintln!("{}", e);
                (path.exists(), path)
            }
        }
    }
    
    
    1. Use the code shown above to replace path.is_absolute() and path.canonicalize() for avoiding panic and resolve wsl like odd path.