#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use std::borrow::Cow;
use std::path::{Component, Path, PathBuf};
mod native;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PathDialect {
Posix,
Windows,
}
impl PathDialect {
pub const fn native() -> Self {
#[cfg(target_os = "windows")]
{
Self::Windows
}
#[cfg(not(target_os = "windows"))]
{
Self::Posix
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PlatformPathKey {
dialect: PathDialect,
components: Vec<PlatformComponentKey>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum PlatformComponentKey {
Prefix(String),
Root,
Parent,
Normal(String),
}
impl PlatformPathKey {
pub fn new(value: &str, dialect: PathDialect) -> Self {
Self {
dialect,
components: lexical_components(value, dialect),
}
}
}
pub fn is_absolute_text(value: &str, dialect: PathDialect) -> bool {
match dialect {
PathDialect::Posix => value.starts_with('/'),
PathDialect::Windows => {
let bytes = value.as_bytes();
value.starts_with("\\\\")
|| value.starts_with("//")
|| (bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'/' | b'\\'))
}
}
}
pub fn contains_parent_traversal_text(value: &str, dialect: PathDialect) -> bool {
let is_separator = |character: char| match dialect {
PathDialect::Posix => character == '/',
PathDialect::Windows => matches!(character, '/' | '\\'),
};
let mut component = String::new();
for character in value.chars().chain(std::iter::once('/')) {
if is_separator(character) {
if component == ".." {
return true;
}
component.clear();
} else {
component.push(character);
}
}
false
}
fn lexical_components(value: &str, dialect: PathDialect) -> Vec<PlatformComponentKey> {
match dialect {
PathDialect::Posix => lexical_posix_components(value),
PathDialect::Windows => lexical_windows_components(value),
}
}
fn lexical_posix_components(value: &str) -> Vec<PlatformComponentKey> {
let mut components = Vec::new();
if value.starts_with('/') {
components.push(PlatformComponentKey::Root);
}
for component in value.split('/') {
match component {
"" | "." => {}
".." => components.push(PlatformComponentKey::Parent),
value => components.push(PlatformComponentKey::Normal(value.to_owned())),
}
}
components
}
fn lexical_windows_components(value: &str) -> Vec<PlatformComponentKey> {
let mut value = value.replace('\\', "/");
if value
.get(..8)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("//?/unc/"))
|| value
.get(..8)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("//./unc/"))
{
value = format!("//{}", &value[8..]);
} else if value
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("//?/"))
|| value
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("//./"))
{
value = value[4..].to_owned();
}
let mut components = Vec::new();
let mut parts = value.split('/');
if let Some(first) = parts.next()
&& first.len() == 2
&& first.as_bytes()[1] == b':'
&& first.as_bytes()[0].is_ascii_alphabetic()
{
components.push(PlatformComponentKey::Prefix(first.to_ascii_lowercase()));
if value.as_bytes().get(2) == Some(&b'/') {
components.push(PlatformComponentKey::Root);
}
} else {
if value.starts_with("//") {
components.push(PlatformComponentKey::Root);
components.push(PlatformComponentKey::Prefix("unc".to_owned()));
}
parts = value.split('/');
}
for component in parts {
match component {
"" | "." => {}
value
if value.len() == 2
&& value.as_bytes()[1] == b':'
&& value.as_bytes()[0].is_ascii_alphabetic()
&& components
.iter()
.any(|entry| matches!(entry, PlatformComponentKey::Prefix(_))) => {}
".." => components.push(PlatformComponentKey::Parent),
value => components.push(PlatformComponentKey::Normal(value.to_lowercase())),
}
}
components
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct NativePathKey(Vec<native::NativeComponentKey>);
impl NativePathKey {
pub fn new(path: &Path) -> Self {
let path = normalize_lexical_path(path);
Self(
path.components()
.filter(|component| *component != Component::CurDir)
.map(native::component_key)
.collect(),
)
}
}
pub fn contains_parent_traversal(path: &Path) -> bool {
path.components()
.any(|component| component == Component::ParentDir)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathResolutionError {
Empty,
ContainsNul,
ParentTraversal,
}
impl std::fmt::Display for PathResolutionError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
Self::Empty => "path declaration is empty",
Self::ContainsNul => "path declaration contains NUL",
Self::ParentTraversal => "path declaration contains parent traversal",
};
formatter.write_str(message)
}
}
impl std::error::Error for PathResolutionError {}
pub fn resolve_lexical_path(
base: &Path,
declaration: &Path,
) -> Result<PathBuf, PathResolutionError> {
if declaration.as_os_str().is_empty() {
return Err(PathResolutionError::Empty);
}
if declaration.as_os_str().to_string_lossy().contains('\0') {
return Err(PathResolutionError::ContainsNul);
}
if contains_parent_traversal(declaration) {
return Err(PathResolutionError::ParentTraversal);
}
let resolved = if declaration.is_absolute() {
declaration.to_path_buf()
} else {
base.join(declaration)
};
Ok(normalize_lexical_path(&resolved).into_owned())
}
pub fn normalize_lexical_path(path: &Path) -> Cow<'_, Path> {
#[cfg(windows)]
let path = native::normalize_windows_device_path(path);
#[cfg(not(windows))]
let path = Cow::Borrowed(path);
let normalized = path
.components()
.filter(|component| *component != Component::CurDir)
.collect::<PathBuf>();
if normalized.as_os_str() == path.as_os_str() {
path
} else {
Cow::Owned(normalized)
}
}
pub fn paths_equal(left: &Path, right: &Path) -> bool {
#[cfg(windows)]
{
NativePathKey::new(left) == NativePathKey::new(right)
}
#[cfg(not(windows))]
let mut left = left
.components()
.filter(|component| *component != Component::CurDir);
#[cfg(not(windows))]
let mut right = right
.components()
.filter(|component| *component != Component::CurDir);
#[cfg(not(windows))]
loop {
match (left.next(), right.next()) {
(None, None) => return true,
(Some(left), Some(right)) if components_equal(left, right) => {}
_ => return false,
}
}
}
pub fn is_within(path: &Path, root: &Path) -> bool {
if contains_parent_traversal(path) || contains_parent_traversal(root) {
return false;
}
#[cfg(windows)]
{
let path = NativePathKey::new(path);
let root = NativePathKey::new(root);
if root.0.is_empty() {
return !matches!(
path.0.first(),
Some(native::NativeComponentKey::Prefix(_) | native::NativeComponentKey::RootDir)
);
}
path.0.starts_with(&root.0)
}
#[cfg(not(windows))]
let mut path = path
.components()
.filter(|component| *component != Component::CurDir);
#[cfg(not(windows))]
let mut root = root
.components()
.filter(|component| *component != Component::CurDir);
#[cfg(not(windows))]
if root.clone().next().is_none() {
return path.clone().next() != Some(Component::RootDir);
}
#[cfg(not(windows))]
loop {
match (root.next(), path.next()) {
(None, _) => return true,
(Some(root), Some(path)) if components_equal(path, root) => {}
(Some(_), _) => return false,
}
}
}
pub fn contains_component_path(path: &Path, needle: &Path) -> bool {
#[cfg(windows)]
{
let path = NativePathKey::new(path);
let needle = NativePathKey::new(needle);
!needle.0.is_empty()
&& needle.0.len() <= path.0.len()
&& path
.0
.windows(needle.0.len())
.any(|window| window == needle.0)
}
#[cfg(not(windows))]
let path_components: Vec<_> = path
.components()
.filter(|component| *component != Component::CurDir)
.collect();
#[cfg(not(windows))]
let needle_components: Vec<_> = needle
.components()
.filter(|component| *component != Component::CurDir)
.collect();
#[cfg(not(windows))]
if needle_components.is_empty() || needle_components.len() > path_components.len() {
return false;
}
#[cfg(not(windows))]
path_components
.windows(needle_components.len())
.any(|window| {
window
.iter()
.zip(&needle_components)
.all(|(left, right)| components_equal(*left, *right))
})
}
pub fn components_equal(left: Component<'_>, right: Component<'_>) -> bool {
native::components_equal(left, right)
}
pub fn strings_equal(left: &str, right: &str) -> bool {
#[cfg(windows)]
{
case_fold(left) == case_fold(right)
}
#[cfg(not(windows))]
{
left == right
}
}
pub fn case_fold(value: &str) -> String {
#[cfg(windows)]
{
value.to_lowercase()
}
#[cfg(not(windows))]
{
value.to_owned()
}
}