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}