use std::{ffi::OsString, path::Path};
use crate::cli::Failure;
pub fn display(path: &Path) -> String {
let text = path.to_string_lossy();
if cfg!(windows) {
text.replace('\\', "/")
} else {
text.into_owned()
}
}
pub fn to_archive(path: &Path) -> Result<Vec<u8>, Failure> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Ok(path.as_os_str().as_bytes().to_vec())
}
#[cfg(not(unix))]
{
path.to_str()
.map(|text| text.as_bytes().to_vec())
.ok_or_else(|| Failure::new(format!("`{}` is not valid Unicode", display(path))))
}
}
pub fn from_archive(name: &[u8]) -> Result<OsString, Failure> {
let traversal = name.is_empty()
|| name == b"."
|| name == b".."
|| name.contains(&b'/')
|| name.contains(&b'\\')
|| name.contains(&0);
let reserved = cfg!(windows)
&& (name.contains(&b':') || name.last().is_some_and(|byte| matches!(byte, b'.' | b' ')));
if traversal || reserved {
return Err(Failure::new(format!(
"archive contains an unsafe entry name: `{}`",
String::from_utf8_lossy(name)
)));
}
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
Ok(OsString::from_vec(name.to_vec()))
}
#[cfg(not(unix))]
Ok(OsString::from(String::from_utf8_lossy(name).into_owned()))
}
pub fn sibling(base: &Path, name: OsString) -> std::path::PathBuf {
base.parent().unwrap_or_else(|| Path::new("")).join(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_traversal_and_reserved_names() {
for name in [&b""[..], b".", b"..", b"a/b", b"a\\b", b"a\0b"] {
assert!(from_archive(name).is_err(), "accepted {name:?}");
}
assert!(from_archive(b"ok.txt").is_ok());
}
#[test]
#[cfg(windows)]
fn rejects_windows_stream_and_trailing_dot_names() {
for name in [&b"file:stream"[..], b"trailing.", b"trailing "] {
assert!(from_archive(name).is_err(), "accepted {name:?}");
}
}
#[test]
fn display_never_mixes_separators() {
let joined = Path::new("a/b").join("c.zar");
assert!(!display(&joined).contains('\\'));
}
}