use std::path::{Path, PathBuf};
use super::name::safe_name;
#[derive(Debug)]
pub enum InboxError {
Escaped,
SymlinkInPath,
Io(String),
}
impl std::fmt::Display for InboxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InboxError::Escaped => write!(f, "destination is outside the inbox"),
InboxError::SymlinkInPath => write!(f, "path contains a symbolic link"),
InboxError::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for InboxError {}
pub struct Inbox {
root: PathBuf,
}
impl Inbox {
pub fn new(root: impl Into<PathBuf>) -> Inbox {
Inbox { root: root.into() }
}
pub fn root(&self) -> &Path {
&self.root
}
pub async fn resolve(&self, peer_slug: &str, proposed: &str) -> Result<PathBuf, InboxError> {
let parent = self.root.join(safe_name(peer_slug));
tokio::fs::create_dir_all(&parent).await.map_err(|e| InboxError::Io(e.to_string()))?;
let root = tokio::fs::canonicalize(&self.root)
.await
.map_err(|e| InboxError::Io(e.to_string()))?;
ensure_no_symlink(&self.root, &parent).await?;
let real_parent =
tokio::fs::canonicalize(&parent).await.map_err(|e| InboxError::Io(e.to_string()))?;
if !real_parent.starts_with(&root) {
return Err(InboxError::Escaped);
}
let path = real_parent.join(safe_name(proposed));
if !path.starts_with(&root) {
return Err(InboxError::Escaped);
}
if let Ok(info) = tokio::fs::symlink_metadata(&path).await {
if info.file_type().is_symlink() {
return Err(InboxError::SymlinkInPath);
}
}
Ok(path)
}
}
async fn ensure_no_symlink(root: &Path, path: &Path) -> Result<(), InboxError> {
let rest = path.strip_prefix(root).unwrap_or(path);
let mut current = root.to_path_buf();
for component in rest.components() {
current.push(component);
let info = tokio::fs::symlink_metadata(¤t)
.await
.map_err(|e| InboxError::Io(e.to_string()))?;
if info.file_type().is_symlink() {
return Err(InboxError::SymlinkInPath);
}
}
Ok(())
}