use std::path::{Path, PathBuf};
use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use crate::wire::ui::WireRevealReq;
use super::state::AppState;
#[derive(Debug, PartialEq, Eq)]
pub(super) enum RevealError {
InvalidSegment,
NotFound,
Outside,
}
pub(super) fn resolve_target(
scan_root: &Path,
segments: &[String],
) -> Result<PathBuf, RevealError> {
let mut path = scan_root.to_path_buf();
for seg in segments {
if seg.is_empty() || seg == "." || seg == ".." || seg.contains('/') || seg.contains('\\') {
return Err(RevealError::InvalidSegment);
}
path.push(seg);
}
let canonical = path.canonicalize().map_err(|_| RevealError::NotFound)?;
if !canonical.starts_with(scan_root) {
return Err(RevealError::Outside);
}
Ok(canonical)
}
pub(super) async fn reveal(
State(state): State<Arc<AppState>>,
axum::Json(req): axum::Json<WireRevealReq>,
) -> Response {
let canonical = match resolve_target(&state.scan_root, &req.segments) {
Ok(p) => p,
Err(RevealError::InvalidSegment) => {
return (StatusCode::BAD_REQUEST, "invalid segment").into_response()
}
Err(RevealError::NotFound) => {
return (StatusCode::NOT_FOUND, "path does not exist").into_response()
}
Err(RevealError::Outside) => {
return (StatusCode::FORBIDDEN, "outside scan root").into_response()
}
};
if let Err(e) = reveal_in_filer(&canonical) {
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
}
StatusCode::OK.into_response()
}
#[cfg(target_os = "macos")]
fn reveal_in_filer(path: &Path) -> std::io::Result<()> {
let status = std::process::Command::new("open")
.arg("-R")
.arg(path)
.status()?;
if !status.success() {
return Err(std::io::Error::other(format!(
"`open -R` exited with {status}"
)));
}
Ok(())
}
#[cfg(target_os = "windows")]
fn reveal_in_filer(path: &Path) -> std::io::Result<()> {
std::process::Command::new("explorer")
.arg(format!("/select,{}", path.display()))
.status()?;
Ok(())
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn reveal_in_filer(path: &Path) -> std::io::Result<()> {
let target = if path.is_file() {
path.parent().unwrap_or(path)
} else {
path
};
open::that(target).map_err(std::io::Error::other)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn accepts_file_inside_canonicalized_root() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
fs::write(root.join("hello.txt"), b"").unwrap();
let target = resolve_target(&root, &["hello.txt".to_string()]).unwrap();
assert_eq!(target, root.join("hello.txt"));
}
#[test]
fn rejects_non_canonical_root_that_would_escape_starts_with() {
let dir = tempfile::tempdir().unwrap();
let canonical_root = dir.path().canonicalize().unwrap();
if dir.path() == canonical_root {
return;
}
fs::write(canonical_root.join("hello.txt"), b"").unwrap();
let err = resolve_target(dir.path(), &["hello.txt".to_string()]).unwrap_err();
assert_eq!(err, RevealError::Outside);
}
#[test]
fn rejects_traversal_segments() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
for bad in ["..", ".", "", "a/b", "a\\b"] {
assert_eq!(
resolve_target(&root, &[bad.to_string()]),
Err(RevealError::InvalidSegment),
"expected InvalidSegment for {bad:?}",
);
}
}
#[test]
fn rejects_missing_segment_with_not_found() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
assert_eq!(
resolve_target(&root, &["does-not-exist".to_string()]),
Err(RevealError::NotFound),
);
}
}