Ferrit Explore
中文·繁體·EN·日本語 Sign in Register
cielxl / veld / src / handler / index.rs
//! Index file handler.
//!
//! Resolves directory URIs to their index files (e.g. `index.html`,
//! `index.htm`).  Works as a focused helper alongside [`StaticFileHandler`]:
//! when a request maps to a directory, `IndexHandler` tries each configured
//! index file name in order and returns the first one that exists on disk.

use std::fs;
use std::path::{Path, PathBuf};

use tracing::debug;

use crate::http::request::Request;

// ---------------------------------------------------------------------------
// Defaults
// ---------------------------------------------------------------------------

const DEFAULT_INDEX_FILES: &[&str] = &["index.html", "index.htm"];

// ---------------------------------------------------------------------------
// IndexHandler
// ---------------------------------------------------------------------------

/// Resolves directory requests to index files.
#[derive(Debug, Clone)]
pub struct IndexHandler {
    /// Document root -- all index paths are resolved under this tree.
    root: PathBuf,
    /// Index file names to try, in priority order.
    index_files: Vec<String>,
}

impl IndexHandler {
    // ------------------------------------------------------------------
    // Construction
    // ------------------------------------------------------------------

    /// Create a new handler with an explicit root and index file list.
    pub fn new(root: PathBuf, index_files: Vec<String>) -> Self {
        Self { root, index_files }
    }

    /// Create a handler with sensible defaults rooted at `root`.
    pub fn with_defaults(root: PathBuf) -> Self {
        Self {
            root,
            index_files: DEFAULT_INDEX_FILES.iter().map(|s| s.to_string()).collect(),
        }
    }

    // ------------------------------------------------------------------
    // Index resolution
    // ------------------------------------------------------------------

    /// Walk the configured index file names and return the path to the first
    /// regular file found inside `directory`.
    ///
    /// Returns `None` when no index file exists or when `directory` itself
    /// is not a directory.
    pub fn find_index(&self, directory: &Path) -> Option<PathBuf> {
        // Quick check: the caller should already know this is a directory,
        // but guard against misuse.
        if !directory.is_dir() {
            return None;
        }

        for name in &self.index_files {
            let candidate = directory.join(name);

            // Must be a regular file (not a symlink-to-directory, etc.).
            match fs::metadata(&candidate) {
                Ok(meta) if meta.is_file() => {
                    debug!(path = %candidate.display(), "index file matched");
                    return Some(candidate);
                }
                _ => continue,
            }
        }

        debug!(dir = %directory.display(), "no index file found");
        None
    }

    // ------------------------------------------------------------------
    // Request handling
    // ------------------------------------------------------------------

    /// Handle a request whose URI maps to a directory.
    ///
    /// If an index file is found the caller (typically [`StaticFileHandler`])
    /// should serve that file.  When no index file exists this method returns
    /// a `403 Forbidden` response.
    ///
    /// `uri` is the original request URI (used only for error messages and
    /// for building a redirect when the URI lacks a trailing slash).
    pub fn handle(&self, request: &Request) -> Option<PathBuf> {
        let uri = request.uri();

        // Ensure the URI ends with a slash -- directories without a trailing
        // slash should be redirected by the caller before reaching here.
        if !uri.ends_with('/') {
            return None;
        }

        // Map the URI to a filesystem path.
        let directory = self.root.join(uri.trim_start_matches('/'));
        let canonical_root = self.root.canonicalize().ok()?;
        let canonical_dir = match directory.canonicalize() {
            Ok(c) => c,
            Err(_) => return None,
        };

        // Security: refuse to serve anything outside the document root.
        if !canonical_dir.starts_with(&canonical_root) {
            return None;
        }

        self.find_index(&canonical_dir)
    }

    // ------------------------------------------------------------------
    // Accessors
    // ------------------------------------------------------------------

    /// The document root.
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// The configured index file names.
    pub fn index_files(&self) -> &[String] {
        &self.index_files
    }
}

// ===========================================================================
// Tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn find_index_returns_first_match() {
        let dir = std::env::temp_dir().join("veld_idx_test_1");
        let _ = fs::create_dir_all(&dir);
        // Only create index.htm, not index.html -- so htm wins.
        fs::write(dir.join("index.htm"), "<h1>hi</h1>").unwrap();

        let handler = IndexHandler::new(dir.clone(), vec!["index.html".into(), "index.htm".into()]);

        let result = handler.find_index(&dir);
        assert!(result.is_some());
        assert_eq!(result.unwrap().file_name().unwrap(), "index.htm");

        let _ = fs::remove_dir_all(&dir);
    }

    #[test]
    fn find_index_returns_none_for_empty_dir() {
        let dir = std::env::temp_dir().join("veld_idx_test_2");
        let _ = fs::create_dir_all(&dir);

        let handler = IndexHandler::with_defaults(dir.clone());
        assert!(handler.find_index(&dir).is_none());

        let _ = fs::remove_dir_all(&dir);
    }

    #[test]
    fn find_index_skips_subdirectories() {
        let dir = std::env::temp_dir().join("veld_idx_test_3");
        let _ = fs::create_dir_all(&dir);
        // Create a *directory* named index.html -- should be skipped.
        fs::create_dir_all(dir.join("index.html")).unwrap();

        let handler = IndexHandler::with_defaults(dir.clone());
        assert!(handler.find_index(&dir).is_none());

        let _ = fs::remove_dir_all(&dir);
    }

    #[test]
    fn find_index_respects_priority_order() {
        let dir = std::env::temp_dir().join("veld_idx_test_4");
        let _ = fs::create_dir_all(&dir);
        fs::write(dir.join("index.html"), "html").unwrap();
        fs::write(dir.join("index.htm"), "htm").unwrap();

        let handler = IndexHandler::new(dir.clone(), vec!["index.htm".into(), "index.html".into()]);

        let result = handler.find_index(&dir).unwrap();
        assert_eq!(result.file_name().unwrap(), "index.htm");

        let _ = fs::remove_dir_all(&dir);
    }

    #[test]
    fn find_index_returns_none_for_non_directory() {
        let handler = IndexHandler::with_defaults(PathBuf::from("/tmp"));
        assert!(handler.find_index(Path::new("/nonexistent/path")).is_none());
    }
}