use std::{
borrow::Cow,
ffi::{OsStr, OsString},
fmt::Write,
fs,
io::ErrorKind,
path::{Path, PathBuf},
str,
};
use gxhash::gxhash64;
use super::FsError;
pub const MAX_PATH: usize = 32_767;
const NAME_MAX: usize = 255;
pub(crate) const TMP_EXT: &str = ".s0tmp";
pub(crate) const PART_EXT: &str = ".s0part";
pub(crate) fn to_slash_cow(rel: &Path) -> Cow<'_, str> {
let s = rel.to_string_lossy();
if s.contains('\\') {
Cow::Owned(s.replace('\\', "/"))
} else {
s
}
}
pub(crate) fn to_slash(rel: &Path) -> String {
to_slash_cow(rel).into_owned()
}
fn rel_path_str(b: &[u8]) -> Result<&str, FsError> {
if b.is_empty() || b.len() > MAX_PATH {
return Err(FsError::BadPath("长度非法".to_owned()));
}
let s = str::from_utf8(b).map_err(|_| FsError::BadPath("非 UTF-8".to_owned()))?;
if s
.split('/')
.any(|c| matches!(c, "" | "." | "..") || c.contains(['\0', '\\', ':']))
{
return Err(FsError::BadPath("空段、绝对路径、.. 或非法字符".to_owned()));
}
Ok(s)
}
pub fn rel_path(b: &[u8]) -> Result<&Path, FsError> {
rel_path_str(b).map(Path::new)
}
pub fn safe_join(root: &Path, rel: &[u8]) -> Result<PathBuf, FsError> {
let s = rel_path_str(rel)?;
let mut cur = PathBuf::with_capacity(root.as_os_str().len() + 1 + s.len());
cur.push(root);
let mut it = s.split('/').peekable();
let mut check_fs = true;
while let Some(seg) = it.next() {
cur.push(seg);
if check_fs {
match fs::symlink_metadata(&cur) {
Ok(m) => {
if m.file_type().is_symlink() {
return Err(FsError::BadPath("路径含符号链接".to_owned()));
}
if it.peek().is_some() && !m.is_dir() {
return Err(FsError::BadPath("父路径非目录".to_owned()));
}
}
Err(e) if e.kind() == ErrorKind::NotFound => {
check_fs = false;
}
Err(e) => return Err(e.into()),
}
}
}
Ok(cur)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Staging {
pub tmp: PathBuf,
pub part: PathBuf,
}
impl Staging {
pub fn of(path: &Path) -> Result<Self, FsError> {
let name = path
.file_name()
.ok_or_else(|| FsError::BadPath("无文件名".to_owned()))?;
let dir = path.parent().unwrap_or(Path::new(""));
Ok(Self {
tmp: staged(dir, name, TMP_EXT),
part: staged(dir, name, PART_EXT),
})
}
pub fn clean(&self) -> Result<(), FsError> {
remove(&self.tmp)?;
remove(&self.part)
}
}
fn staged(dir: &Path, name: &OsStr, ext: &str) -> PathBuf {
let mut s = OsString::with_capacity(1 + name.len().min(NAME_MAX) + ext.len());
s.push(".");
if 1 + name.len() + PART_EXT.len() > NAME_MAX {
let _ = write!(s, "s0-{:016x}", gxhash64(name.as_encoded_bytes(), 0));
} else {
s.push(name);
}
s.push(ext);
if dir.as_os_str().is_empty() {
PathBuf::from(s)
} else {
dir.join(s)
}
}
pub fn remove(p: &Path) -> Result<(), FsError> {
match fs::remove_file(p) {
Err(e) if e.kind() != ErrorKind::NotFound => Err(e.into()),
_ => Ok(()),
}
}