use std::io::ErrorKind;
use super::error::MountError;
const PATH_MAX: usize = 4096;
const NAME_MAX: usize = 255;
const DEPTH_MAX: usize = 64;
#[derive(Debug)]
pub(super) struct MountRelativePath {
relative: String,
}
impl MountRelativePath {
pub fn for_dir_op(&self) -> &str {
if self.relative.is_empty() { "." } else { &self.relative }
}
pub fn is_mount_root(&self) -> bool {
self.relative.is_empty()
}
}
pub(super) fn resolve_virtual_path(
virtual_path: &str,
mount_virtual_path: &str,
) -> Result<MountRelativePath, MountError> {
reject_null_bytes(virtual_path)?;
let normalized = normalize_virtual_path(virtual_path);
let relative = strip_mount_prefix(&normalized, mount_virtual_path)
.ok_or_else(|| MountError::NoMountPoint(virtual_path.to_owned()))?
.to_owned();
reject_drive_or_unc_segments(&relative, &normalized)?;
Ok(MountRelativePath { relative })
}
#[must_use]
pub(super) fn normalize_virtual_path(path: &str) -> String {
if is_already_normalized_absolute_path(path) {
return path.to_owned();
}
let mut components = Vec::new();
for part in path.split('/') {
match part {
"" | "." => {}
".." => {
components.pop();
}
_ => components.push(part),
}
}
if components.is_empty() {
"/".to_owned()
} else {
format!("/{}", components.join("/"))
}
}
#[must_use]
pub(super) fn strip_mount_prefix<'a>(normalized_path: &'a str, mount_virtual_path: &str) -> Option<&'a str> {
if mount_virtual_path == "/" {
return Some(normalized_path.strip_prefix('/').unwrap_or(normalized_path));
}
if normalized_path == mount_virtual_path {
return Some("");
}
normalized_path
.strip_prefix(mount_virtual_path)
.and_then(|rest| rest.strip_prefix('/'))
}
pub(super) fn reject_drive_or_unc_segments(relative: &str, normalized_virtual_path: &str) -> Result<(), MountError> {
let has_escape_prefix = relative.contains('\\') || relative.split('/').any(is_windows_drive_prefix);
if has_escape_prefix {
Err(MountError::PathEscape {
virtual_path: normalized_virtual_path.to_owned(),
})
} else {
Ok(())
}
}
fn is_windows_drive_prefix(segment: &str) -> bool {
let bytes = segment.as_bytes();
bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
}
pub(super) fn contains_null_byte(virtual_path: &str) -> bool {
virtual_path.contains('\0')
}
pub(super) fn reject_null_bytes(virtual_path: &str) -> Result<(), MountError> {
if contains_null_byte(virtual_path) {
Err(MountError::EmbeddedNullByte("embedded null byte"))
} else {
Ok(())
}
}
pub(super) fn reject_overlong_path(path: &str) -> Result<(), MountError> {
let mut components = path.split('/');
let too_long = path.len() > PATH_MAX
|| components.by_ref().take(DEPTH_MAX).any(|c| c.len() > NAME_MAX)
|| components.next().is_some();
if too_long {
Err(MountError::io_err(
ErrorKind::InvalidFilename,
"File name too long",
elide_middle(path).as_deref().unwrap_or(path),
))
} else {
Ok(())
}
}
const ERROR_PATH_EDGE: usize = 20;
fn elide_middle(path: &str) -> Option<String> {
let mut boundaries = path.char_indices().map(|(index, _)| index);
let head_end = boundaries.nth(ERROR_PATH_EDGE)?;
let tail_start = boundaries.nth_back(ERROR_PATH_EDGE - 1)?;
Some(format!("{}…{}", &path[..head_end], &path[tail_start..]))
}
fn is_already_normalized_absolute_path(path: &str) -> bool {
if !path.starts_with('/') {
return false;
}
if path == "/" {
return true;
}
if path.ends_with('/') {
return false;
}
!path
.split('/')
.skip(1)
.any(|component| component.is_empty() || component == "." || component == "..")
}