use std::path::{Path, PathBuf};
use super::transport::HttpResponse;
use super::ui_bridge::content_type_for;
fn safe_relative_path(request_path: &str) -> Option<PathBuf> {
let rel = request_path.trim_start_matches('/');
let rel = if rel.is_empty() { "index.html" } else { rel };
if rel
.split('/')
.any(|seg| seg == ".." || seg == "." || seg.is_empty())
{
return None;
}
Some(PathBuf::from(rel))
}
pub(crate) fn bundle_asset_exists(ui_dir: &Path, request_path: &str) -> bool {
match safe_relative_path(request_path) {
Some(rel) => ui_dir.join(rel).is_file(),
None => false,
}
}
pub(crate) fn serve_bundle_asset(ui_dir: &Path, request_path: &str) -> Option<HttpResponse> {
let rel = safe_relative_path(request_path)?;
let content_type = content_type_for(&rel.to_string_lossy());
let body = std::fs::read(ui_dir.join(&rel)).ok()?;
Some(HttpResponse {
status: 200,
body,
content_type,
extra_headers: Vec::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn bundle() -> TempDir {
let dir = TempDir::new().expect("tempdir");
fs::write(dir.path().join("index.html"), b"<html>red-ui</html>").unwrap();
fs::write(dir.path().join("app.js"), b"console.log('ui')").unwrap();
fs::create_dir(dir.path().join("assets")).unwrap();
fs::write(dir.path().join("assets/style.css"), b"body{}").unwrap();
dir
}
#[test]
fn root_serves_index_html() {
let dir = bundle();
let resp = serve_bundle_asset(dir.path(), "/").expect("index served");
assert_eq!(resp.status, 200);
assert_eq!(resp.content_type, "text/html; charset=utf-8");
assert_eq!(resp.body, b"<html>red-ui</html>");
}
#[test]
fn named_asset_is_served_with_content_type() {
let dir = bundle();
let resp = serve_bundle_asset(dir.path(), "/app.js").expect("js served");
assert_eq!(resp.content_type, "text/javascript; charset=utf-8");
assert_eq!(resp.body, b"console.log('ui')");
let css = serve_bundle_asset(dir.path(), "/assets/style.css").expect("css served");
assert_eq!(css.content_type, "text/css; charset=utf-8");
}
#[test]
fn missing_asset_returns_none() {
let dir = bundle();
assert!(serve_bundle_asset(dir.path(), "/nope.js").is_none());
}
#[test]
fn traversal_is_refused() {
let dir = bundle();
assert!(serve_bundle_asset(dir.path(), "/../secret").is_none());
assert!(serve_bundle_asset(dir.path(), "/assets/../../etc/passwd").is_none());
assert!(safe_relative_path("/a/./b").is_none());
}
#[test]
fn bundle_asset_exists_tracks_real_files_only() {
let dir = bundle();
assert!(bundle_asset_exists(dir.path(), "/"));
assert!(bundle_asset_exists(dir.path(), "/index.html"));
assert!(bundle_asset_exists(dir.path(), "/app.js"));
assert!(bundle_asset_exists(dir.path(), "/assets/style.css"));
assert!(!bundle_asset_exists(dir.path(), "/query"));
assert!(!bundle_asset_exists(
dir.path(),
"/collections/foo/documents/bar"
));
assert!(!bundle_asset_exists(dir.path(), "/../secret"));
}
}