use std::borrow::Cow;
pub(crate) fn split_segments(path: &str) -> (bool, Option<String>, Vec<String>) {
let posix = path.replace('\\', "/");
if is_opaque_device_path(&posix) {
return (true, Some(posix), Vec::new());
}
let mut prefix = String::new();
let mut rest = posix.as_str();
let mut unc = false;
if let Some(after_prefix) = rest.strip_prefix("//?/") {
prefix.push_str("//?/");
rest = after_prefix;
if let Some((server, share, remainder)) =
rest.strip_prefix("UNC/").and_then(split_unc_share)
{
prefix.push_str("UNC/");
prefix.push_str(server);
prefix.push('/');
prefix.push_str(share);
rest = remainder;
unc = true;
}
} else if let Some((server, share, remainder)) =
rest.strip_prefix("//").and_then(split_unc_share)
{
prefix.push_str("//");
prefix.push_str(server);
prefix.push('/');
prefix.push_str(share);
rest = remainder;
unc = true;
}
let bytes = rest.as_bytes();
if !unc && bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
let (drive_prefix, remainder) = rest.split_at(2);
prefix.push_str(drive_prefix);
rest = remainder;
}
let drive = (!prefix.is_empty()).then_some(prefix);
let absolute = unc || rest.starts_with('/');
let segments = rest
.split('/')
.filter(|segment| !segment.is_empty())
.map(|segment| segment.to_string())
.collect();
(absolute, drive, segments)
}
pub(crate) fn is_opaque_device_path(posix: &str) -> bool {
if posix.starts_with("//./") {
return true;
}
let Some(rest) = posix.strip_prefix("//?/") else {
return posix.strip_prefix("//").is_some_and(|rest| {
let mut components = rest.split('/');
let server = components.next().unwrap_or_default();
let share = components.next().unwrap_or_default();
!server.is_empty()
&& !share.is_empty()
&& (!valid_unc_component(server) || !valid_unc_component(share))
});
};
let bytes = rest.as_bytes();
let drive =
bytes.len() >= 3 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' && bytes[2] == b'/';
!drive
&& rest
.strip_prefix("UNC/")
.and_then(split_unc_share)
.is_none()
}
fn split_unc_share(path: &str) -> Option<(&str, &str, &str)> {
let mut components = path.splitn(3, '/');
let server = components.next()?;
let share = components.next()?;
if !valid_unc_component(server) || !valid_unc_component(share) {
return None;
}
Some((server, share, components.next().unwrap_or_default()))
}
fn valid_unc_component(component: &str) -> bool {
!component.is_empty() && !matches!(component, "." | "..")
}
pub fn strip_windows_verbatim_prefix(text: &str) -> Cow<'_, str> {
if let Some(rest) = text.strip_prefix(r"\\?\UNC\") {
Cow::Owned(format!(r"\\{rest}"))
} else if let Some(rest) = text.strip_prefix(r"\\?\") {
Cow::Borrowed(rest)
} else {
Cow::Borrowed(text)
}
}
#[cfg(windows)]
pub(crate) fn wide_maybe_verbatim(path: &std::path::Path) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;
use std::path::{Component, Prefix};
fn nul_terminated(mut wide: Vec<u16>) -> Vec<u16> {
wide.push(0);
wide
}
let raw: Vec<u16> = path.as_os_str().encode_wide().collect();
const BACKSLASH: u16 = b'\\' as u16;
const SLASH: u16 = b'/' as u16;
const QUESTION: u16 = b'?' as u16;
const DOT: u16 = b'.' as u16;
if raw.starts_with(&[BACKSLASH, BACKSLASH, QUESTION, BACKSLASH])
|| raw.starts_with(&[BACKSLASH, BACKSLASH, DOT, BACKSLASH])
{
return nul_terminated(raw);
}
if raw.contains(&SLASH) {
return nul_terminated(raw);
}
let mut components = path.components();
let is_supported_prefix = matches!(
components.next(),
Some(Component::Prefix(prefix))
if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::UNC(_, _))
);
if !is_supported_prefix || !matches!(components.next(), Some(Component::RootDir)) {
return nul_terminated(raw);
}
if components.any(|component| !matches!(component, Component::Normal(_))) {
return nul_terminated(raw);
}
let prefix_kind = path
.components()
.next()
.and_then(|component| match component {
Component::Prefix(prefix) => Some(prefix.kind()),
_ => None,
});
let prefixed = match prefix_kind {
Some(Prefix::Disk(_)) => {
let mut out: Vec<u16> = r"\\?\".encode_utf16().collect();
out.extend_from_slice(&raw);
out
}
Some(Prefix::UNC(_, _)) => {
let mut out: Vec<u16> = r"\\?\UNC".encode_utf16().collect();
out.extend_from_slice(&raw[1..]);
out
}
_ => raw,
};
nul_terminated(prefixed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_drive_and_unc_verbatim_prefixes() {
assert_eq!(
strip_windows_verbatim_prefix(r"\\?\C:\Users\runner\Temp\harn-abc"),
r"C:\Users\runner\Temp\harn-abc"
);
assert_eq!(
strip_windows_verbatim_prefix(r"\\?\UNC\server\share\dir"),
r"\\server\share\dir"
);
}
#[test]
fn passes_through_non_verbatim_paths_unchanged() {
assert_eq!(
strip_windows_verbatim_prefix(r"C:\Temp\harn-abc"),
r"C:\Temp\harn-abc"
);
assert_eq!(
strip_windows_verbatim_prefix("/tmp/harn-abc/child"),
"/tmp/harn-abc/child"
);
assert_eq!(
strip_windows_verbatim_prefix(r"\\.\PhysicalDrive0"),
r"\\.\PhysicalDrive0"
);
assert_eq!(
strip_windows_verbatim_prefix(r"relative\dir"),
r"relative\dir"
);
assert_eq!(strip_windows_verbatim_prefix(r"C:\a\..\b"), r"C:\a\..\b");
assert_eq!(
strip_windows_verbatim_prefix("C:/forward/slash"),
"C:/forward/slash"
);
}
#[test]
fn strips_prefix_from_paths_longer_than_max_path() {
let deep = format!(r"\\?\C:\{}", "segment\\".repeat(40));
let stripped = strip_windows_verbatim_prefix(&deep);
assert!(stripped.len() > 260, "test path must exceed MAX_PATH");
#[expect(clippy::string_slice, reason = "test input is ASCII")]
let expected = &deep[r"\\?\".len()..];
assert_eq!(stripped, expected);
assert!(!stripped.starts_with(r"\\?\"));
}
}