mini-static 0.29.0

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
use mini_static::resolve_with_canonical_root;
use proptest::prelude::*;
use tempfile::TempDir;

proptest! {
    #[test]
    fn any_resolved_path_is_under_root_canonical(
        path_segments in prop::collection::vec("[a-zA-Z0-9._-]{1,20}", 0..10)
    ) {
        let root = TempDir::new().unwrap();
        let root_canon = root.path().canonicalize().unwrap();

        // Build a request path from segments
        let request_path = format!("/{}", path_segments.join("/"));

        // Attempt to resolve
        if let Ok(resolved) = resolve_with_canonical_root(&root_canon, &request_path) {
            // If it resolves successfully, it MUST be under root_canon
            assert!(
                resolved.starts_with(&root_canon),
                "resolved path {:?} is not under root {:?}",
                resolved,
                root_canon
            );
        }
    }

    #[test]
    fn traversal_attempts_always_rejected(
        _segments_count in 1..20usize,
        leading_traversals in 1..10usize,
    ) {
        let root = TempDir::new().unwrap();
        let root_canon = root.path().canonicalize().unwrap();

        // Build a traversal attack path: /../../../etc/passwd
        let traversals = (0..leading_traversals)
            .map(|_| "..")
            .collect::<Vec<_>>()
            .join("/");
        let request_path = format!("/{}/etc/passwd", traversals);

        // This MUST be rejected
        let result = resolve_with_canonical_root(&root_canon, &request_path);
        assert!(
            result.is_err(),
            "traversal attempt {:?} should be rejected",
            request_path
        );
    }

    #[test]
    fn null_bytes_in_path_rejected(
        prefix in "[a-zA-Z0-9._-]{0,20}",
        suffix in "[a-zA-Z0-9._-]{0,20}",
    ) {
        let root = TempDir::new().unwrap();
        let root_canon = root.path().canonicalize().unwrap();

        // Try to inject null bytes
        let request_path = format!("/{}\0{}", prefix, suffix);

        // This MUST be rejected
        let result = resolve_with_canonical_root(&root_canon, &request_path);
        assert!(
            result.is_err(),
            "path with null bytes should be rejected"
        );
    }

    #[test]
    fn percent_encoded_path_decoded_safely(
        path_segments in prop::collection::vec("[a-zA-Z0-9]{1,20}", 0..5)
    ) {
        let root = TempDir::new().unwrap();
        let root_canon = root.path().canonicalize().unwrap();

        // Build a request path with percent-encoded segments
        let request_path = format!(
            "/{}",
            path_segments
                .iter()
                .map(|s| percent_encoding::percent_encode(s.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string())
                .collect::<Vec<_>>()
                .join("/")
        );

        // Attempt to resolve - should either succeed (if path is safe) or fail
        // But it must never escape the root
        if let Ok(resolved) = resolve_with_canonical_root(&root_canon, &request_path) {
            assert!(
                resolved.starts_with(&root_canon),
                "percent-decoded path {:?} escaped root {:?}",
                resolved,
                root_canon
            );
        }
    }

    #[test]
    fn deep_nesting_bounded_safely(
        depth in 0..100usize,
    ) {
        let root = TempDir::new().unwrap();
        let root_canon = root.path().canonicalize().unwrap();

        // Build a very deeply nested path
        let nested = (0..depth)
            .map(|i| format!("d{}", i))
            .collect::<Vec<_>>()
            .join("/");
        let request_path = format!("/{}/file.txt", nested);

        // Resolve should handle deep paths safely (not stack overflow, not escape root)
        if let Ok(resolved) = resolve_with_canonical_root(&root_canon, &request_path) {
            assert!(
                resolved.starts_with(&root_canon),
                "deeply nested path escaped root"
            );
        }
    }

    #[test]
    fn mixed_slash_directions_rejected_or_contained(
        forward_segments in prop::collection::vec("[a-zA-Z0-9]{1,10}", 0..3),
        mixed_slashes in "[/\\\\]{1,5}",
    ) {
        let root = TempDir::new().unwrap();
        let root_canon = root.path().canonicalize().unwrap();

        // Build a path with backslashes mixed in (potential Windows escape)
        let request_path = if forward_segments.is_empty() {
            format!("/{}", mixed_slashes)
        } else {
            format!("/{}{}", forward_segments[0], mixed_slashes)
        };

        // Resolve should handle it safely
        if let Ok(resolved) = resolve_with_canonical_root(&root_canon, &request_path) {
            assert!(
                resolved.starts_with(&root_canon),
                "path with mixed slashes escaped root"
            );
        }
    }

    /// No request carrying a hidden segment may ever resolve under the default policy,
    /// whatever else the path contains — the exception being a leading `.well-known`.
    /// This is the generated-input counterpart to `tests/hidden_files.rs`, which pins
    /// the specific names that motivated the rule.
    #[test]
    fn hidden_segments_never_resolve(
        prefix in prop::collection::vec("[a-zA-Z0-9_-]{1,10}", 0..3),
        hidden_name in "\\.[a-zA-Z0-9_-]{1,10}",
        suffix in prop::collection::vec("[a-zA-Z0-9_-]{1,10}", 0..3),
    ) {
        let root = TempDir::new().unwrap();
        let root_canon = root.path().canonicalize().unwrap();

        // Materialize the whole chain on disk, so a resolve can only fail because the
        // policy rejected it — not because the file happened not to exist.
        let mut dir = root_canon.clone();
        for segment in prefix.iter().chain(std::iter::once(&hidden_name)).chain(suffix.iter()) {
            dir = dir.join(segment);
            std::fs::create_dir_all(&dir).unwrap();
        }

        let mut segments = prefix.clone();
        segments.push(hidden_name.clone());
        segments.extend(suffix.iter().cloned());
        let request_path = format!("/{}", segments.join("/"));

        let resolved = resolve_with_canonical_root(&root_canon, &request_path);

        // `.well-known` as the first segment is the documented exception; every other
        // hidden segment must be refused.
        let is_excepted = prefix.is_empty() && hidden_name == ".well-known";
        prop_assert!(
            resolved.is_err() || is_excepted,
            "hidden segment {hidden_name:?} resolved via {request_path:?}"
        );
    }
}