use std::{
env,
ffi::{OsStr, OsString},
process::Command,
};
pub fn command(path: &OsStr) -> Command {
command_with_distro(path, env::var("WSL_DISTRO_NAME").ok().as_deref())
}
fn command_with_distro(path: &OsStr, distro: Option<&str>) -> Command {
let path = interop_path(path, distro);
let mut cmd = Command::new("powershell.exe");
cmd.arg("-NoProfile")
.arg("-Command")
.arg(powershell_start_process_command(&path));
cmd
}
fn powershell_start_process_command(path: &OsStr) -> OsString {
let mut command = OsString::from("Start-Process -FilePath ");
command.push(powershell_single_quoted(path));
command
}
fn powershell_single_quoted(value: &OsStr) -> OsString {
let mut quoted = String::from("'");
for chr in value.to_string_lossy().chars() {
if chr == '\'' {
quoted.push_str("''");
} else {
quoted.push(chr);
}
}
quoted.push('\'');
OsString::from(quoted)
}
fn interop_path(path: &OsStr, distro: Option<&str>) -> OsString {
let path = match path.to_str() {
Some(path) => path,
None => return path.to_owned(),
};
if !path.starts_with('/') {
return OsString::from(path);
}
if let Some(path) = drvfs_path(path) {
return OsString::from(path);
}
match distro {
Some(distro) if !distro.is_empty() => {
OsString::from(format!(r"\\wsl$\{distro}{}", path.replace('/', r"\")))
}
_ => OsString::from(path),
}
}
fn drvfs_path(path: &str) -> Option<String> {
let rest = path.strip_prefix("/mnt/")?;
let mut chars = rest.chars();
let drive = chars.next()?;
if !drive.is_ascii_alphabetic() {
return None;
}
let rest = chars.as_str();
if rest.is_empty() {
return Some(format!("{}:\\", drive.to_ascii_uppercase()));
}
let rest = rest.strip_prefix('/')?;
Some(format!(
"{}:\\{}",
drive.to_ascii_uppercase(),
rest.replace('/', r"\")
))
}
#[cfg(test)]
mod tests {
use std::{ffi::OsStr, process::Command};
use super::{command, command_with_distro, interop_path};
fn command_args(command: &Command) -> Vec<String> {
command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect()
}
#[test]
fn command_starts_with_powershell() {
let command = command(OsStr::new("https://example.com"));
assert_eq!(
command.get_program(),
"powershell.exe",
"avoid wslview as it's unmaintained"
);
assert_eq!(
command_args(&command),
[
"-NoProfile",
"-Command",
"Start-Process -FilePath 'https://example.com'"
]
);
}
#[test]
fn command_quotes_powershell_metacharacters() {
let command = command_with_distro(OsStr::new("https://example.com/a'b;c"), Some("Ubuntu"));
let args = command_args(&command);
assert_eq!(
args,
[
"-NoProfile",
"-Command",
"Start-Process -FilePath 'https://example.com/a''b;c'"
]
);
}
#[test]
fn interop_path_converts_absolute_linux_paths_to_unc_paths() {
assert_eq!(
interop_path(OsStr::new("/home/alice/doc.pdf"), Some("Ubuntu")),
r"\\wsl$\Ubuntu\home\alice\doc.pdf"
);
}
#[test]
fn interop_path_converts_drvfs_paths_to_windows_paths() {
assert_eq!(
interop_path(OsStr::new("/mnt/c/Users/alice/doc.pdf"), Some("Ubuntu")),
r"C:\Users\alice\doc.pdf"
);
assert_eq!(interop_path(OsStr::new("/mnt/d"), Some("Ubuntu")), r"D:\");
}
}