wslpath_rs/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// Copyright (c) 2024 Jan Holthuis <jan.holthuis@rub.de>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy
// of the MPL was not distributed with this file, You can obtain one at
// http://mozilla.org/MPL/2.0/.
//
// SPDX-License-Identifier: MPL-2.0

//! Convert paths between WSL guest and Windows host.
//!
//! This library aims to offer functionality similar of the `wslpath` conversion tool [added in WSL
//! build 17046](https://learn.microsoft.com/en-us/windows/wsl/release-notes#wsl-34), but is
//! implemented in pure Rust.
//!
//! Existing crates such as [`wslpath`](https://crates.io/crates/wslpath) call `wsl.exe wslpath`
//! internally, which may lead to a lot of command invocations when multiple paths need to be
//! converted.

#![warn(unsafe_code)]
#![warn(missing_docs)]
#![cfg_attr(not(debug_assertions), deny(warnings))]
#![deny(rust_2018_idioms)]
#![deny(rust_2021_compatibility)]
#![deny(missing_debug_implementations)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(clippy::all)]
#![deny(clippy::explicit_deref_methods)]
#![deny(clippy::explicit_into_iter_loop)]
#![deny(clippy::explicit_iter_loop)]
#![deny(clippy::must_use_candidate)]
#![cfg_attr(not(test), deny(clippy::panic_in_result_fn))]
#![cfg_attr(not(debug_assertions), deny(clippy::used_underscore_binding))]

use typed_path::{
    Utf8UnixComponent, Utf8UnixPath, Utf8UnixPathBuf, Utf8WindowsComponent, Utf8WindowsPath,
    Utf8WindowsPathBuf, Utf8WindowsPrefix,
};

/// Represents an error that occurred during conversion.
#[derive(Debug, PartialEq)]
pub enum Error {
    /// The input path is relative and thus cannot be converted.
    RelativePath,
    /// The input path prefix is invalid.
    InvalidPrefix,
}

/// Convert a Windows path to a WSL path.
///
/// The input path needs to be absolute. Path are normalized during conversion. UNC paths
/// (`\\?\C:\...`) are supported.
///
/// # Errors
///
/// If the path is not absolute, the method returns an [`Error::RelativePath`]. Paths not starting
/// with a drive letter will lead to an [`Error::InvalidPrefix`].
///
/// # Examples
///
/// ```
/// use wslpath_rs::{windows_to_wsl, Error};
///
/// // Regular absolute paths are supported
/// assert_eq!(windows_to_wsl("C:\\Windows").unwrap(), "/mnt/c/Windows");
/// assert_eq!(windows_to_wsl("D:\\foo\\..\\bar\\.\\baz.txt").unwrap(), "/mnt/d/bar/baz.txt");
/// assert_eq!(windows_to_wsl("C:\\Program Files (x86)\\Foo\\bar.txt").unwrap(), "/mnt/c/Program Files (x86)/Foo/bar.txt");
///
/// // UNC paths are supported
/// assert_eq!(windows_to_wsl("\\\\?\\C:\\Windows").unwrap(), "/mnt/c/Windows");
/// assert_eq!(windows_to_wsl("\\\\?\\D:\\foo\\..\\bar\\.\\baz.txt").unwrap(), "/mnt/d/bar/baz.txt");
/// assert_eq!(windows_to_wsl("\\\\?\\C:\\Program Files (x86)\\Foo\\bar.txt").unwrap(), "/mnt/c/Program Files (x86)/Foo/bar.txt");
///
/// // Relative paths are not supported
/// assert_eq!(windows_to_wsl("Program Files (x86)\\Foo\\bar.txt").unwrap_err(), Error::RelativePath);
/// assert_eq!(windows_to_wsl("..\\foo\\bar.txt").unwrap_err(), Error::RelativePath);
/// ```
pub fn windows_to_wsl(windows_path: &str) -> Result<String, Error> {
    let path = Utf8WindowsPath::new(windows_path);
    if !path.is_absolute() {
        return Err(Error::RelativePath);
    }

    // "C:\foo" (6 chars) -> "/mnt/c/foo" (10 chars)
    let expected_length = windows_path.len() + 4;
    let mut output = Utf8UnixPathBuf::with_capacity(expected_length);
    for component in path.components() {
        match component {
            Utf8WindowsComponent::Prefix(prefix_component) => match prefix_component.kind() {
                Utf8WindowsPrefix::VerbatimDisk(disk) => {
                    output.push("/mnt");
                    output.push(disk.to_ascii_lowercase().to_string());
                }
                Utf8WindowsPrefix::Disk(disk) => {
                    output.push("/mnt");
                    output.push(disk.to_ascii_lowercase().to_string());
                }
                _ => {
                    return Err(Error::InvalidPrefix);
                }
            },
            Utf8WindowsComponent::RootDir => (),
            Utf8WindowsComponent::CurDir => output.push("."),
            Utf8WindowsComponent::Normal(name) => output.push(name),
            Utf8WindowsComponent::ParentDir => output.push(".."),
        };
    }

    Ok(output.normalize().into_string())
}

/// Convert a WSL path to a Windows path.
///
/// The input path needs to be absolute. Path are normalized during conversion.
///
/// # Errors
///
/// If the path is not absolute, the method returns an [`Error::RelativePath`]. Paths not starting
/// with with `/mnt/<driveletter>` will lead to an [`Error::InvalidPrefix`].
///
/// # Examples
///
/// ```
/// use wslpath_rs::{wsl_to_windows, Error};
///
/// // Absolute paths are supported
/// assert_eq!(wsl_to_windows("/mnt/c/Windows").unwrap(), "C:\\Windows");
/// assert_eq!(wsl_to_windows("/mnt/d/foo/../bar/./baz.txt").unwrap(), "D:\\bar\\baz.txt");
/// assert_eq!(wsl_to_windows("/mnt/c/Program Files (x86)/Foo/bar.txt").unwrap(), "C:\\Program Files (x86)\\Foo\\bar.txt");
///
/// // Absolute paths not starting with `/mnt/<driveletter>` are not supported
/// assert_eq!(wsl_to_windows("/etc/fstab").unwrap_err(), Error::InvalidPrefix);
/// assert_eq!(wsl_to_windows("/mnt/my_custom_mount/foo/bar.txt").unwrap_err(), Error::InvalidPrefix);
///
/// // Relative paths are not supported
/// assert_eq!(wsl_to_windows("Program Files (x86)/Foo/bar.txt").unwrap_err(), Error::RelativePath);
/// assert_eq!(wsl_to_windows("../foo/bar.txt").unwrap_err(), Error::RelativePath);
/// ```
pub fn wsl_to_windows(wsl_path: &str) -> Result<String, Error> {
    let path = Utf8UnixPath::new(wsl_path);
    if !path.is_absolute() {
        return Err(Error::RelativePath);
    }

    let mut components = path.components();
    if components.next() != Some(Utf8UnixComponent::RootDir) {
        return Err(Error::InvalidPrefix);
    }
    if components.next() != Some(Utf8UnixComponent::Normal("mnt")) {
        return Err(Error::InvalidPrefix);
    }

    // "/mnt/c/foo" (10 chars) -> "C:\foo" (6 chars)
    let expected_length = wsl_path.len();
    let mut output = Utf8WindowsPathBuf::with_capacity(expected_length);
    if let Some(Utf8UnixComponent::Normal(drive)) = components.next() {
        if drive.len() != 1 {
            return Err(Error::InvalidPrefix);
        }

        output.push(format!("{}:\\", drive.to_ascii_uppercase()));
    } else {
        return Err(Error::InvalidPrefix);
    }

    for component in components {
        match component {
            Utf8UnixComponent::RootDir => (),
            Utf8UnixComponent::CurDir => output.push("."),
            Utf8UnixComponent::Normal(name) => output.push(name),
            Utf8UnixComponent::ParentDir => output.push(".."),
        };
    }

    Ok(output.normalize().into_string())
}