const WINDOWS_RESERVED: &[&str] = &[
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];
pub fn check_component(component: &str) -> Result<(), &'static str> {
if component.is_empty() {
return Err("empty path component");
}
if component.contains('\0') {
return Err("path contains a null byte");
}
if component.contains(':') {
return Err("path contains a stream separator");
}
if component.ends_with('.') || component.ends_with(' ') {
return Err("path component ends with a dot or space");
}
let stem = component
.split('.')
.next()
.unwrap_or(component)
.to_ascii_uppercase();
if WINDOWS_RESERVED.contains(&stem.as_str()) {
return Err("path component is a reserved device name");
}
Ok(())
}
#[cfg(windows)]
pub fn filesystem_anchors() -> Vec<std::path::PathBuf> {
(b'A'..=b'Z')
.map(|letter| std::path::PathBuf::from(format!("{}:\\", letter as char)))
.filter(|anchor| anchor.is_dir())
.filter_map(|anchor| anchor.canonicalize().ok())
.collect()
}
#[cfg(not(windows))]
pub fn filesystem_anchors() -> Vec<std::path::PathBuf> {
vec![std::path::PathBuf::from("/")]
}
#[cfg(unix)]
pub fn file_identity(meta: &std::fs::Metadata) -> u64 {
use std::os::unix::fs::MetadataExt;
meta.ino()
}
#[cfg(not(unix))]
pub fn file_identity(_meta: &std::fs::Metadata) -> u64 {
0
}
#[cfg(unix)]
pub fn remove_entry(path: &std::path::Path, _meta: &std::fs::Metadata) -> std::io::Result<()> {
std::fs::remove_file(path)
}
#[cfg(windows)]
pub fn remove_entry(path: &std::path::Path, meta: &std::fs::Metadata) -> std::io::Result<()> {
if meta.is_symlink() {
if let Ok(target) = std::fs::metadata(path) {
if target.is_dir() {
return std::fs::remove_dir(path);
}
}
}
std::fs::remove_file(path)
}
#[cfg(not(any(unix, windows)))]
pub fn remove_entry(path: &std::path::Path, _meta: &std::fs::Metadata) -> std::io::Result<()> {
std::fs::remove_file(path)
}
#[cfg(unix)]
pub fn is_out_of_space(err: &std::io::Error) -> bool {
matches!(err.raw_os_error(), Some(code) if code == libc::ENOSPC || code == libc::EDQUOT)
}
#[cfg(windows)]
pub fn is_out_of_space(err: &std::io::Error) -> bool {
const ERROR_DISK_FULL: i32 = 112;
const ERROR_HANDLE_DISK_FULL: i32 = 39;
matches!(
err.raw_os_error(),
Some(code) if code == ERROR_DISK_FULL || code == ERROR_HANDLE_DISK_FULL
)
}
#[cfg(not(any(unix, windows)))]
pub fn is_out_of_space(_err: &std::io::Error) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ordinary_names_pass() {
assert!(check_component("config.json").is_ok());
assert!(check_component("my-app_v2").is_ok());
}
#[test]
fn a_name_containing_two_dots_is_not_traversal() {
assert!(check_component("my..file.txt").is_ok());
assert!(check_component("..config").is_ok());
}
#[test]
fn reserved_device_names_are_refused() {
assert!(check_component("CON").is_err());
assert!(check_component("con").is_err());
assert!(check_component("NUL.txt").is_err());
assert!(check_component("COM1.log").is_err());
assert!(check_component("CONSOLE").is_ok());
}
#[test]
fn stream_separators_are_refused() {
assert!(check_component("file.txt:secret").is_err());
}
#[test]
fn trailing_dot_or_space_is_refused() {
assert!(check_component("name.").is_err());
assert!(check_component("name ").is_err());
}
#[test]
fn null_bytes_are_refused() {
assert!(check_component("na\0me").is_err());
}
#[test]
fn empty_components_are_refused() {
assert!(check_component("").is_err());
}
#[cfg(unix)]
#[test]
fn is_out_of_space_matches_enospc_and_edquot_only() {
let enospc = std::io::Error::from_raw_os_error(libc::ENOSPC);
assert!(is_out_of_space(&enospc));
let edquot = std::io::Error::from_raw_os_error(libc::EDQUOT);
assert!(is_out_of_space(&edquot));
let enoent = std::io::Error::from_raw_os_error(libc::ENOENT);
assert!(!is_out_of_space(&enoent));
let other = std::io::Error::other("not an os error");
assert!(!is_out_of_space(&other));
}
#[cfg(windows)]
#[test]
fn is_out_of_space_matches_disk_full_codes_only() {
const ERROR_DISK_FULL: i32 = 112;
const ERROR_HANDLE_DISK_FULL: i32 = 39;
let disk_full = std::io::Error::from_raw_os_error(ERROR_DISK_FULL);
assert!(is_out_of_space(&disk_full));
let handle_disk_full = std::io::Error::from_raw_os_error(ERROR_HANDLE_DISK_FULL);
assert!(is_out_of_space(&handle_disk_full));
let not_found = std::io::Error::from_raw_os_error(2);
assert!(!is_out_of_space(¬_found));
let other = std::io::Error::other("not an os error");
assert!(!is_out_of_space(&other));
}
}