use crate::App;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
const DEFAULT_INDEX_FILE: &str = "index.html";
const DEFAULT_CONTENT_ROOT: &str = "/static";
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct HostEnv {
content_root: PathBuf,
index_path: PathBuf,
fallback_path: Option<PathBuf>,
show_directory: bool,
}
impl Default for HostEnv {
#[inline]
fn default() -> Self {
Self::new(DEFAULT_CONTENT_ROOT)
}
}
impl HostEnv {
#[inline]
pub fn new<T: ?Sized + AsRef<OsStr>>(content_root: &T) -> Self {
let content_root = PathBuf::from(content_root);
warn_if_root_is_fs_root(&content_root);
let index_path = content_root.join(DEFAULT_INDEX_FILE);
Self {
show_directory: false,
fallback_path: None,
content_root,
index_path,
}
}
pub fn with_content_root<T: ?Sized + AsRef<OsStr>>(mut self, root: &T) -> Self {
self.content_root = PathBuf::from(root);
warn_if_root_is_fs_root(&self.content_root);
if let Some(file_name) = self.index_path.file_name() {
self.index_path = self.content_root.join(file_name);
}
if let Some(fallback_file) = self.fallback_path.as_ref().and_then(|p| p.file_name()) {
self.fallback_path = Some(self.content_root.join(fallback_file));
}
self
}
pub fn with_index_file<T: AsRef<Path>>(mut self, index_file: T) -> Self {
let index_path = self.content_root.join(index_file);
self.index_path = index_path;
self
}
pub fn with_fallback_file<T: AsRef<Path>>(mut self, fallback_file: T) -> Self {
let fallback_path = self.content_root.join(fallback_file);
self.fallback_path = Some(fallback_path);
self
}
pub fn with_files_listing(mut self) -> Self {
warn_if_listing_enabled_in_release();
self.show_directory = true;
self
}
#[inline]
pub fn content_root(&self) -> &Path {
&self.content_root
}
#[inline]
pub fn index_path(&self) -> &Path {
&self.index_path
}
#[inline]
pub fn fallback_path(&self) -> Option<&Path> {
match &self.fallback_path {
Some(path) => Some(path),
None => None,
}
}
#[inline]
pub fn show_files_listing(&self) -> bool {
self.show_directory
}
}
impl App {
pub fn with_host_env<T>(mut self, config: T) -> Self
where
T: FnOnce(HostEnv) -> HostEnv,
{
self.host_env = config(self.host_env);
self
}
pub fn set_host_env(mut self, env: HostEnv) -> Self {
self.host_env = env;
self
}
}
#[inline]
fn warn_if_root_is_fs_root(path: &Path) {
#[cfg(feature = "tracing")]
if path == Path::new("/") {
tracing::warn!(
"HostEnv content_root is set to '/', which can expose the entire filesystem. Consider using a dedicated static directory."
);
}
}
#[inline]
fn warn_if_listing_enabled_in_release() {
#[cfg(not(debug_assertions))]
{
#[cfg(feature = "tracing")]
tracing::warn!(
"Static files listing is enabled in release mode; this may leak file metadata. Consider disabling it for production."
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::App;
use std::path::PathBuf;
#[test]
fn it_creates_default_host_env() {
let env = HostEnv::default();
assert_eq!(env.content_root, PathBuf::from(DEFAULT_CONTENT_ROOT));
assert_eq!(env.index_path, PathBuf::from("/static/index.html"));
assert_eq!(env.fallback_path, None);
assert!(!env.show_directory);
}
#[test]
fn it_creates_host_env() {
let env = HostEnv::new("/root");
assert_eq!(env.content_root, PathBuf::from("/root"));
assert_eq!(env.index_path, PathBuf::from("/root/index.html"));
assert_eq!(env.fallback_path, None);
assert!(!env.show_directory);
}
#[test]
fn it_creates_host_env_with_content_root() {
let env = HostEnv::default().with_content_root("/root");
assert_eq!(env.content_root, PathBuf::from("/root"));
assert_eq!(env.index_path, PathBuf::from("/root/index.html"));
assert_eq!(env.fallback_path, None);
assert!(!env.show_directory);
}
#[test]
fn it_creates_with_index_file() {
let env = HostEnv::new("/root").with_index_file("default.html");
assert_eq!(env.content_root, PathBuf::from("/root"));
assert_eq!(env.index_path, PathBuf::from("/root/default.html"));
assert_eq!(env.fallback_path, None);
assert!(!env.show_directory);
}
#[test]
fn it_creates_with_fallback_file() {
let env = HostEnv::new("/root").with_fallback_file("error.html");
assert_eq!(env.content_root, PathBuf::from("/root"));
assert_eq!(env.index_path, PathBuf::from("/root/index.html"));
assert_eq!(env.fallback_path, Some(PathBuf::from("/root/error.html")));
assert!(!env.show_directory);
}
#[test]
fn it_creates_with_file_listing() {
let env = HostEnv::new("/root").with_files_listing();
assert_eq!(env.content_root, PathBuf::from("/root"));
assert_eq!(env.index_path, PathBuf::from("/root/index.html"));
assert_eq!(env.fallback_path, None);
assert!(env.show_directory);
}
#[test]
fn it_updates_content_root() {
let app = App::new().with_host_env(|env| env.with_content_root("tests/resources"));
assert_eq!(app.host_env.content_root, PathBuf::from("tests/resources"));
assert_eq!(
app.host_env.index_path,
PathBuf::from("tests/resources/index.html")
);
assert_eq!(app.host_env.fallback_path, None);
assert!(!app.host_env.show_directory);
}
#[test]
fn it_updates_index_file_with_content_root() {
let app = App::new().with_host_env(|env| {
env.with_content_root("tests/resources")
.with_index_file("default.html")
});
assert_eq!(app.host_env.content_root, PathBuf::from("tests/resources"));
assert_eq!(
app.host_env.index_path,
PathBuf::from("tests/resources/default.html")
);
assert_eq!(app.host_env.fallback_path, None);
assert!(!app.host_env.show_directory);
}
#[test]
fn it_updates_fallback_file_with_content_root() {
let app = App::new().with_host_env(|env| {
env.with_fallback_file("404.html")
.with_content_root("tests/resources")
});
assert_eq!(app.host_env.content_root, PathBuf::from("tests/resources"));
assert_eq!(
app.host_env.index_path,
PathBuf::from("tests/resources/index.html")
);
assert_eq!(
app.host_env.fallback_path,
Some(PathBuf::from("tests/resources/404.html"))
);
assert!(!app.host_env.show_directory);
}
}