wslpath_rs/
lib.rs

1// Copyright (c) 2025 Jan Holthuis <jan.holthuis@rub.de>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy
4// of the MPL was not distributed with this file, You can obtain one at
5// http://mozilla.org/MPL/2.0/.
6//
7// SPDX-License-Identifier: MPL-2.0
8
9//! Convert paths between WSL guest and Windows host.
10//!
11//! This library aims to offer functionality similar of the `wslpath` conversion tool [added in WSL
12//! build 17046](https://learn.microsoft.com/en-us/windows/wsl/release-notes#wsl-34), but is
13//! implemented in pure Rust.
14//!
15//! Existing crates such as [`wslpath`](https://crates.io/crates/wslpath) call `wsl.exe wslpath`
16//! internally, which may lead to a lot of command invocations when multiple paths need to be
17//! converted.
18
19#![warn(unsafe_code)]
20#![warn(missing_docs)]
21#![cfg_attr(not(debug_assertions), deny(warnings))]
22#![deny(rust_2018_idioms)]
23#![deny(rust_2021_compatibility)]
24#![deny(missing_debug_implementations)]
25#![deny(rustdoc::broken_intra_doc_links)]
26#![deny(clippy::all)]
27#![deny(clippy::explicit_deref_methods)]
28#![deny(clippy::explicit_into_iter_loop)]
29#![deny(clippy::explicit_iter_loop)]
30#![deny(clippy::must_use_candidate)]
31#![cfg_attr(not(test), deny(clippy::panic_in_result_fn))]
32#![cfg_attr(not(debug_assertions), deny(clippy::used_underscore_binding))]
33
34use typed_path::{
35    Utf8UnixComponent, Utf8UnixPath, Utf8UnixPathBuf, Utf8WindowsComponent, Utf8WindowsPath,
36    Utf8WindowsPathBuf, Utf8WindowsPrefix,
37};
38
39/// Represents an error that occurred during conversion.
40#[derive(Debug, PartialEq)]
41pub enum Error {
42    /// The input path is relative and thus cannot be converted.
43    RelativePath,
44    /// The input path prefix is invalid.
45    InvalidPrefix,
46}
47
48/// Convert a Windows path to a WSL path.
49///
50/// The input path needs to be absolute. Path are normalized during conversion. UNC paths
51/// (`\\?\C:\...`) are supported.
52///
53/// # Errors
54///
55/// If the path is not absolute, the method returns an [`Error::RelativePath`]. Paths not starting
56/// with a drive letter will lead to an [`Error::InvalidPrefix`].
57///
58/// # Examples
59///
60/// ```
61/// use wslpath_rs::{windows_to_wsl, Error};
62///
63/// // Regular absolute paths are supported
64/// assert_eq!(windows_to_wsl("C:\\Windows").unwrap(), "/mnt/c/Windows");
65/// assert_eq!(windows_to_wsl("D:\\foo\\..\\bar\\.\\baz.txt").unwrap(), "/mnt/d/bar/baz.txt");
66/// assert_eq!(windows_to_wsl("C:\\Program Files (x86)\\Foo\\bar.txt").unwrap(), "/mnt/c/Program Files (x86)/Foo/bar.txt");
67///
68/// // UNC paths are supported
69/// assert_eq!(windows_to_wsl("\\\\?\\C:\\Windows").unwrap(), "/mnt/c/Windows");
70/// assert_eq!(windows_to_wsl("\\\\?\\D:\\foo\\..\\bar\\.\\baz.txt").unwrap(), "/mnt/d/bar/baz.txt");
71/// assert_eq!(windows_to_wsl("\\\\?\\C:\\Program Files (x86)\\Foo\\bar.txt").unwrap(), "/mnt/c/Program Files (x86)/Foo/bar.txt");
72///
73/// // Relative paths are not supported
74/// assert_eq!(windows_to_wsl("Program Files (x86)\\Foo\\bar.txt").unwrap_err(), Error::RelativePath);
75/// assert_eq!(windows_to_wsl("..\\foo\\bar.txt").unwrap_err(), Error::RelativePath);
76///
77/// // Windows WSL paths are converted to the root
78/// assert_eq!(windows_to_wsl("\\\\?\\UNC\\wsl.localhost\\distro\\home\\user\\file").unwrap(), "/home/user/file");
79///
80/// // Generic network paths are not supported right now
81/// assert_eq!(windows_to_wsl("\\\\?\\UNC\\other.domain\\distro\\home\\user\\file").unwrap_err(), Error::InvalidPrefix);
82/// ```
83pub fn windows_to_wsl(windows_path: &str) -> Result<String, Error> {
84    let path = Utf8WindowsPath::new(windows_path);
85    if !path.is_absolute() {
86        return Err(Error::RelativePath);
87    }
88
89    // "C:\foo" (6 chars) -> "/mnt/c/foo" (10 chars)
90    let expected_length = windows_path.len() + 4;
91    let mut output = Utf8UnixPathBuf::with_capacity(expected_length);
92    for component in path.components() {
93        match component {
94            Utf8WindowsComponent::Prefix(prefix_component) => match prefix_component.kind() {
95                Utf8WindowsPrefix::VerbatimDisk(disk) => {
96                    output.push("/mnt");
97                    output.push(disk.to_ascii_lowercase().to_string());
98                }
99                Utf8WindowsPrefix::Disk(disk) => {
100                    output.push("/mnt");
101                    output.push(disk.to_ascii_lowercase().to_string());
102                }
103                Utf8WindowsPrefix::VerbatimUNC(hostname, _) => {
104                    // Assume that the path is inside the current wsl distro
105                    if hostname == "wsl.localhost" {
106                        output.push("/");
107                    } else {
108                        return Err(Error::InvalidPrefix);
109                    }
110                }
111                _ => {
112                    return Err(Error::InvalidPrefix);
113                }
114            },
115            Utf8WindowsComponent::RootDir => (),
116            Utf8WindowsComponent::CurDir => output.push("."),
117            Utf8WindowsComponent::Normal(name) => output.push(name),
118            Utf8WindowsComponent::ParentDir => output.push(".."),
119        };
120    }
121
122    Ok(output.normalize().into_string())
123}
124
125/// Convert a WSL path to a Windows path.
126///
127/// The input path needs to be absolute. Path are normalized during conversion.
128///
129/// # Errors
130///
131/// If the path is not absolute, the method returns an [`Error::RelativePath`]. Paths not starting
132/// with with `/mnt/<driveletter>` will lead to an [`Error::InvalidPrefix`].
133///
134/// # Examples
135///
136/// ```
137/// use wslpath_rs::{wsl_to_windows, Error};
138///
139/// // Absolute paths are supported
140/// assert_eq!(wsl_to_windows("/mnt/c/Windows").unwrap(), "C:\\Windows");
141/// assert_eq!(wsl_to_windows("/mnt/d/foo/../bar/./baz.txt").unwrap(), "D:\\bar\\baz.txt");
142/// assert_eq!(wsl_to_windows("/mnt/c/Program Files (x86)/Foo/bar.txt").unwrap(), "C:\\Program Files (x86)\\Foo\\bar.txt");
143///
144/// // Absolute paths not starting with `/mnt/<driveletter>` are not supported
145/// assert_eq!(wsl_to_windows("/etc/fstab").unwrap_err(), Error::InvalidPrefix);
146/// assert_eq!(wsl_to_windows("/mnt/my_custom_mount/foo/bar.txt").unwrap_err(), Error::InvalidPrefix);
147///
148/// // Relative paths are not supported
149/// assert_eq!(wsl_to_windows("Program Files (x86)/Foo/bar.txt").unwrap_err(), Error::RelativePath);
150/// assert_eq!(wsl_to_windows("../foo/bar.txt").unwrap_err(), Error::RelativePath);
151/// ```
152pub fn wsl_to_windows(wsl_path: &str) -> Result<String, Error> {
153    let path = Utf8UnixPath::new(wsl_path);
154    if !path.is_absolute() {
155        return Err(Error::RelativePath);
156    }
157
158    let mut components = path.components();
159    if components.next() != Some(Utf8UnixComponent::RootDir) {
160        return Err(Error::InvalidPrefix);
161    }
162    if components.next() != Some(Utf8UnixComponent::Normal("mnt")) {
163        return Err(Error::InvalidPrefix);
164    }
165
166    // "/mnt/c/foo" (10 chars) -> "C:\foo" (6 chars)
167    let expected_length = wsl_path.len();
168    let mut output = Utf8WindowsPathBuf::with_capacity(expected_length);
169    if let Some(Utf8UnixComponent::Normal(drive)) = components.next() {
170        if drive.len() != 1 {
171            return Err(Error::InvalidPrefix);
172        }
173
174        output.push(format!("{}:\\", drive.to_ascii_uppercase()));
175    } else {
176        return Err(Error::InvalidPrefix);
177    }
178
179    for component in components {
180        match component {
181            Utf8UnixComponent::RootDir => (),
182            Utf8UnixComponent::CurDir => output.push("."),
183            Utf8UnixComponent::Normal(name) => output.push(name),
184            Utf8UnixComponent::ParentDir => output.push(".."),
185        };
186    }
187
188    Ok(output.normalize().into_string())
189}