mini-static 0.18.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"
            );
        }
    }
}