mini-static 0.38.8

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
//! Probing for a sidecar before opening the original must change syscall counts and nothing
//! else.
//!
//! `sidecar_before_open` skips the original's `open` when a sidecar wins, which is worth ~60%
//! on the precompressed path. Everything downstream reads `path`, so the risk is not that a
//! sidecar fails to be served — the throughput number would show that — but that some
//! response *differs* from what the old order produced. Each test here pins one such
//! difference, and each corresponds to a guard named in `PLAN-sidecar.md` commit 2.

mod common;

use common::{body_bytes, get, request_with_headers};
use hyper::{Method, StatusCode};
use mini_static::Server;
use std::fs;
use tempfile::TempDir;

const PLAIN: &[u8] = b"body{color:red}\n";
const BROTLI: &[u8] = b"pretend-brotli-bytes";

fn root_with_sidecar() -> TempDir {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("styles.css"), PLAIN).unwrap();
    fs::write(root.path().join("styles.css.br"), BROTLI).unwrap();
    root
}

async fn br_request(server: &Server, path: &str) -> hyper::Response<mini_static::ResponseBody> {
    request_with_headers(server, &Method::GET, path, &[("accept-encoding", "br")]).await
}

/// The baseline: a sidecar is still served, with the original's content type and the
/// sidecar's own bytes.
#[tokio::test]
async fn a_sidecar_is_served_with_the_originals_content_type() {
    let root = root_with_sidecar();
    let server = Server::new(root.path()).unwrap();
    let response = br_request(&server, "/styles.css").await;

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
        response.headers().get("content-encoding").unwrap(),
        "br",
        "the sidecar must be selected"
    );
    assert_eq!(
        response.headers().get("content-type").unwrap(),
        "text/css; charset=utf-8",
        "content type comes from the original's extension, not the sidecar's"
    );
    assert_eq!(body_bytes(response).await.as_ref(), BROTLI);
}

/// **Precondition 2.** A sidecar present while the original is deleted must stay a `404`.
///
/// This is the divergence the grill found: the old order opened the original first, so a
/// missing original produced `404` before any probe ran. Probing first without an existence
/// check would serve the sidecar and turn that `404` into a `200`.
#[tokio::test]
async fn a_sidecar_without_its_original_is_not_served() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("styles.css.br"), BROTLI).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = br_request(&server, "/styles.css").await;

    assert_eq!(
        response.status(),
        StatusCode::NOT_FOUND,
        "no original means 404, whichever order the probe runs in"
    );
}

/// **Precondition 1.** A slashless directory URL still redirects rather than being probed
/// into.
///
/// The redirect is decided after the open, from the resolved path, so `candidate_path`
/// returning `None` for a directory is what keeps this working. A version that probed
/// `assets.br` and then fell through would still redirect, but would pay two failed opens to
/// do it.
#[tokio::test]
async fn a_slashless_directory_still_redirects() {
    let root = TempDir::new().unwrap();
    fs::create_dir(root.path().join("assets")).unwrap();
    fs::write(root.path().join("assets").join("index.html"), b"<p>in</p>").unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = get(&server, "/assets").await;

    assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY);
    assert_eq!(response.headers().get("location").unwrap(), "/assets/");
}

/// A trailing slash resolves to `index.html`, so its sidecar is `index.html.br`.
///
/// This is the case the whole derivation rests on: the target is known without a `stat`
/// because a trailing slash means `index.html` by definition. It is also the most common real
/// request — a site's homepage is `/`, not `/index.html`.
#[tokio::test]
async fn a_directory_index_serves_its_sidecar() {
    let root = TempDir::new().unwrap();
    let assets = root.path().join("assets");
    fs::create_dir(&assets).unwrap();
    fs::write(assets.join("index.html"), b"<p>plain</p>").unwrap();
    fs::write(assets.join("index.html.br"), BROTLI).unwrap();

    let server = Server::new(root.path()).unwrap();

    for path in ["/assets/", "/"] {
        let response = br_request(&server, path).await;
        if path == "/" {
            // No root index in this fixture, so `/` is a miss — asserting that keeps this
            // test honest about which path did the work.
            assert_eq!(response.status(), StatusCode::NOT_FOUND);
            continue;
        }
        assert_eq!(
            response.headers().get("content-encoding").map(|v| v.to_str().unwrap()),
            Some("br"),
            "a trailing slash must find index.html's sidecar"
        );
        assert_eq!(body_bytes(response).await.as_ref(), BROTLI);
    }
}

/// A `Range` request never receives a sidecar, matching the old order exactly.
#[tokio::test]
async fn a_range_request_gets_the_original() {
    let root = root_with_sidecar();
    let server = Server::new(root.path()).unwrap();

    let response = request_with_headers(
        &server,
        &Method::GET,
        "/styles.css",
        &[("accept-encoding", "br"), ("range", "bytes=0-3")],
    )
    .await;

    assert!(
        !response.headers().contains_key("content-encoding"),
        "a ranged response must come from the original, not a sidecar"
    );
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
}

/// **Precondition 3.** With SPA mode on, the old order stands and sidecars still work.
///
/// `wants_sidecar` depends on `html_injection`, which needs the *original's* length and
/// content type — unknowable before it is opened. So the reordering is disabled here, and this
/// test exists to prove the disabled path was not left broken by the change that bypasses it.
#[tokio::test]
async fn spa_mode_keeps_the_old_order_and_still_serves_sidecars() {
    let root = root_with_sidecar();
    let server = Server::new(root.path()).unwrap().with_spa_mode();

    let response = br_request(&server, "/styles.css").await;
    assert_eq!(
        response.headers().get("content-encoding").map(|v| v.to_str().unwrap()),
        Some("br"),
        "a non-HTML asset still gets its sidecar with SPA mode enabled"
    );
    assert_eq!(body_bytes(response).await.as_ref(), BROTLI);
}

/// A symlinked original falls back to the open, which resolves it as it always has.
///
/// `candidate_path` refuses a symlink rather than following it, because the open being skipped
/// returned the symlink-*resolved* path and `cache_control_for` reads it. Serving still works;
/// it simply takes the original route.
#[tokio::test]
async fn a_symlinked_original_still_serves() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("real.css"), PLAIN).unwrap();
    std::os::unix::fs::symlink(root.path().join("real.css"), root.path().join("styles.css"))
        .unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = br_request(&server, "/styles.css").await;

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(body_bytes(response).await.as_ref(), PLAIN);
}

/// Hidden-file policy applies to the pre-open path too.
///
/// `candidate_path` runs `servable_segments` before it builds anything, so this cannot become
/// a second, weaker route to a filename — which is exactly how the content cache once served
/// `/.env` while the disk path refused it.
#[tokio::test]
async fn the_pre_open_path_still_refuses_hidden_files() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join(".env"), b"SECRET=1").unwrap();
    fs::write(root.path().join(".env.br"), BROTLI).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = br_request(&server, "/.env").await;

    assert_eq!(
        response.status(),
        StatusCode::NOT_FOUND,
        "a dot-prefixed segment is refused before any sidecar is considered"
    );
}

/// An encoded separator cannot reach the pre-open path either.
#[tokio::test]
async fn the_pre_open_path_still_refuses_an_encoded_separator() {
    let root = TempDir::new().unwrap();
    let admin = root.path().join("admin");
    fs::create_dir(&admin).unwrap();
    fs::write(admin.join("config"), b"nested").unwrap();
    fs::write(admin.join("config.br"), BROTLI).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = br_request(&server, "/admin%2Fconfig").await;

    assert_eq!(response.status(), StatusCode::NOT_FOUND);
}

/// **Precondition 3, the case that actually depends on it.** With SPA mode on, an HTML page
/// must be served *injected*, not replaced by its precompressed sidecar.
///
/// The earlier SPA test in this file uses a CSS file, where injection never applies and the
/// guard is therefore invisible — a version without the guard passes it. This one fails
/// without the guard, because `html_injection` becomes true for `text/html` and
/// `wants_sidecar` then becomes false, which the pre-open probe cannot know: it would have
/// already decided, using the *sidecar's* metadata, before the original's content type or
/// length were available.
///
/// SPA mode rather than live-reload deliberately. `wants_injection` reads
/// `broadcaster.is_some()`, and the broadcaster is created when the server *runs*, not when
/// it is configured — so an unrun `with_live_reload()` server does not inject at all and
/// would pass this test either way. `spa_mode` is a configuration flag, so it is the half of
/// the same condition that is observable here. The guard checks both.
#[tokio::test]
async fn spa_mode_injects_html_rather_than_serving_its_sidecar() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("index.html"), b"<html><body>hi</body></html>").unwrap();
    fs::write(root.path().join("index.html.br"), BROTLI).unwrap();

    let server = Server::new(root.path()).unwrap().with_spa_mode();
    let response = br_request(&server, "/index.html").await;

    assert!(
        !response.headers().contains_key("content-encoding"),
        "an injectable page must not be replaced by a precompressed sidecar"
    );
    let body = body_bytes(response).await;
    assert_ne!(
        body.as_ref(),
        BROTLI,
        "the sidecar was served instead of the page that needed injecting"
    );
}

/// **The real-path equality guard.** A symlinked *parent directory* makes the constructed
/// path differ from the real one, and anything reading `path` would then answer differently
/// than the open it replaced.
///
/// `symlink_metadata` only declines to follow the *final* component, so a symlinked parent is
/// traversed and `candidate_path` happily returns `/link/styles.css` — where the old order's
/// open returns `/real/styles.css`. The sidecar's verified real path is what catches it:
/// `/real/styles.css.br` is not the `/link/styles.css.br` that was constructed, so the fast
/// path declines and the open runs.
///
/// Observed through `with_immutable_assets`, which is a predicate on the path — the cleanest
/// place the difference becomes a header rather than an internal detail.
#[tokio::test]
async fn a_symlinked_parent_directory_falls_back_to_the_open() {
    let root = TempDir::new().unwrap();
    let real = root.path().join("real");
    fs::create_dir(&real).unwrap();
    fs::write(real.join("styles.css"), PLAIN).unwrap();
    fs::write(real.join("styles.css.br"), BROTLI).unwrap();
    std::os::unix::fs::symlink(&real, root.path().join("link")).unwrap();

    // Keyed on the *real* directory name, which only the resolved path contains.
    let server = Server::new(root.path())
        .unwrap()
        .with_immutable_assets(|path| path.to_string_lossy().contains("/real/"));

    let response = br_request(&server, "/link/styles.css").await;
    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
        response.headers().get("cache-control").map(|v| v.to_str().unwrap()),
        Some("public, max-age=31536000, immutable"),
        "the predicate must see the resolved path, as it did before the reordering"
    );
}

/// A directory with a file sitting where its sidecar would be must still redirect.
///
/// This is the case that makes `candidate_path`'s `is_file()` requirement load-bearing, and it
/// is here because a mutation run proved the earlier tests did not need it: existence is
/// enforced by `symlink_metadata` returning `Err`, so removing `is_file()` broke nothing any
/// fixture covered. The gap needs a sidecar to exist *beside a directory* — then, without the
/// check, `/assets` serves `assets.br`'s bytes as a `200` where it must answer `301` to
/// `/assets/`.
#[tokio::test]
async fn a_directory_beside_a_sidecar_named_file_still_redirects() {
    let root = TempDir::new().unwrap();
    let assets = root.path().join("assets");
    fs::create_dir(&assets).unwrap();
    fs::write(assets.join("index.html"), b"<p>in</p>").unwrap();
    // Not a sidecar for anything — `assets` is a directory. Nothing stops it existing.
    fs::write(root.path().join("assets.br"), BROTLI).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = br_request(&server, "/assets").await;

    assert_eq!(
        response.status(),
        StatusCode::MOVED_PERMANENTLY,
        "a directory must redirect, not be answered from a same-named sidecar"
    );
    assert_eq!(response.headers().get("location").unwrap(), "/assets/");
}