mini-static 0.14.4

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
# PATCH: benchmarking + performance/security hardening

Tracking document for a multi-phase effort to (1) stand up real benchmarking for
`mini-static` and (2) push it toward "best in class" speed and security. Check items
off as commits land. Design rationale lives in the plan this was generated from — this
file is just the live checklist.

Current version: `0.13.0`. Versioning policy for this work: patch bumps for
bench/test/doc-only commits; a minor bump (`0.14.0`) on the commit where `206`/`416`
Range support first becomes reachable via `handle_request` (a genuine behavior change).

No commits will be made automatically — the user commits manually.

---

## Phase A — Benchmark infrastructure

- [ ] Add `criterion` (`default-features = false`) as a dev-dependency; `[[bench]]`
      entries with `harness = false`.
- [ ] `benches/path_resolution.rs``resolve_with_canonical_root`/`resolve` across
      shallow/deep/percent-decoded/traversal-rejected/directory-index paths.
- [ ] `benches/streaming.rs``Server::handle_request` throughput (`Throughput::Bytes`)
      across fixture file sizes.
- [ ] `benches/minify_cache.rs` — cache-miss vs. cache-hit cost via `with_minify()`.
- [ ] `benches/fixtures.rs` — shared fixture builder (`tempfile::TempDir`, generated at
      run time, not committed): small (1KB/4KB ×20), medium (256KB/1MB ×5), large
      (10MB/100MB ×2), many-small-files (500 × ~2KB / 10 subdirs), precompressed
      `.br`/`.gz` sidecars (shell out to system `gzip`/`brotli`, degrade to gzip-only if
      `brotli` binary absent), fingerprinted filenames for immutable-cache-control.
- [ ] `examples/serve_fixture.rs` — binary serving the fixture tree via `Server::run_*`,
      driven by CLI flags (`--root`, `--port`).
- [ ] `benches/load/run.sh``oha`-driven load test matrix (small/large/precompressed/
      many-small-files × concurrency 10/50/200 × 10s), JSON output to
      `benches/load/results/<timestamp>/`.
- [ ] `benches/load/README.md` — setup + result format docs.
- [ ] `benches/load/comparison/``nginx.conf`, `static-web-server.toml`,
      `run_comparison.sh` (sequential runs, local binaries not Docker, same fixture dir
      for all three servers, environment fingerprint recorded per result). Manual/local
      only — never wired into `cargo ci`.

## Phase B — Range / If-Range support (206 Partial Content)

Scope: single-range only. A `Range` header with a comma (multi-range) is treated as
absent and falls through to full 200 (RFC 9110-legal, matches common server behavior).

- [ ] Hoist `file_size` computation earlier in `handle_request` (`src/server.rs`) so
      range validation has it available before the body-construction branch.
- [ ] Add `parse_range_header(header: &str, file_size: u64) -> RangeOutcome` (enum:
      `NoRange | Satisfiable(u64, u64) | Unsatisfiable | MultiRangeIgnored`) next to
      `is_etag_match`/`generate_etag`.
- [ ] `If-Range` validation: exact strong ETag match only (not `is_etag_match`'s weak
      comparison) — stale `If-Range` ignores `Range`, serves full 200.
- [ ] `416 Range Not Satisfiable` + `Content-Range: bytes */<file_size>` for
      out-of-bounds/unsatisfiable ranges.
- [ ] `206 Partial Content` + `Content-Range` + adjusted `Content-Length` for
      satisfiable ranges; clamp `end` to `file_size - 1` if it overshoots.
- [ ] `Accept-Ranges: bytes` on every 200/304/206 response.
- [ ] `FileBody::new_ranged(file, start, len)` in `src/handler.rs``remaining:
      Option<u64>` field capping `poll_frame` reads; seek via
      `file.seek(SeekFrom::Start(start)).await` in `handle_request` before construction
      (needs `tokio::io::AsyncSeekExt` import).
- [ ] Range + precompressed-sidecar interaction: skip sidecar, serve range from original
      file when `Range` is satisfiable (mirrors existing `html_injection` bypass).
- [ ] `HEAD + Range` → same `206`/`Content-Range`/`Content-Length` headers, empty body.
- [ ] `tests/range.rs` — satisfiable/suffix/open-ended ranges, out-of-bounds → 416,
      clamped end, If-Range match/mismatch, multi-range → 200, range+sidecar →
      uncompressed, HEAD+Range, Accept-Ranges on plain 200.
- [ ] Inline unit tests for `parse_range_header` pure edge cases.
- [ ] Update README's Range-limitation paragraph + `DEV_PLAN.md`'s Phase 0.2.0 entry.
- [ ] **Version bump to 0.14.0** on the commit where 206/416 first becomes reachable.

## Phase C — Security hardening

- [ ] Document `Server::with_max_connections`/header-timeout as connection-level
      protection only; point consumers wanting request-level throttling/blocking at
      `mini-guard` (implemented — `Policy`/`decide()`/`GuardMiddleware`, wired via
      `mini-unified`). No new rate-limiting code in mini-static itself.
- [ ] README "Security headers" section: keep `X-Content-Type-Options: nosniff`
      unconditionally; document HSTS/CSP/X-Frame-Options as the embedder's
      responsibility (context-dependent, not mini-static's to default) with guidance on
      where to add them via mini-serve/mini-unified middleware.
- [ ] Add `proptest` as a dev-dependency.
- [ ] `tests/resolve_property.rs` — arbitrary path-like strings (`..`, null bytes,
      percent-encoding, deep nesting, mixed slashes) asserting: any `Ok` result has
      `root_canon` as a prefix.
- [ ] Extract pure helper from `read_header_prefix`'s `\r\n\r\n`-scan logic (e.g.
      `contains_header_terminator(buf: &[u8], newly_read: usize) -> bool`) so it's
      property-testable without a live socket per case.
- [ ] Property test for the extracted helper + one full-socket proptest confirming the
      end-to-end `MAX_HEADER_BYTES` bound holds under generated inputs.
- [ ] Set explicit `ProptestConfig { cases: N, .. }` rather than relying on the default.

## Phase D — Re-benchmark + comparison

- [ ] Re-run `cargo bench -p mini-static` post-Range/hardening, compare against Phase A
      baseline.
- [ ] Re-run `benches/load/run.sh` and `comparison/run_comparison.sh`, produce updated
      markdown comparison table.
- [ ] Confirm `cargo ci` and `cargo lint` still pass.

---

## Verification checklist (recurring, check at every phase boundary)

- [ ] `cargo bench -p mini-static` completes cleanly, stable (non-bimodal) timings.
- [ ] `benches/load/run.sh` output: nonzero req/s, `p99 > p50 > 0`, consistent across two
      consecutive runs.
- [ ] Comparison table generated with environment fingerprint (OS/CPU/tool versions).
- [ ] `curl -r 0-99 http://127.0.0.1:<port>/big.bin -o partial.bin` byte-exact against
      `dd`-extracted expected bytes.
- [ ] `cargo test -p mini-static` — new proptest cases + existing traversal/header-
      injection adversarial tests all pass.
- [ ] `cargo ci` and `cargo lint` pass at every commit.

## Open tradeoffs

1. nginx/static-web-server comparison: manual-only, never in `cargo ci` (recommended).
2. Fixture sidecar generation: shell out to system `gzip`/`brotli` vs. new dev-deps
   (recommended: shell out).
3. Phase B minor-bump commit boundary: first-206-reachable vs. fully-documented commit
   (recommended: first-reachable, per existing `DEV_PLAN.md` precedent).