# mini-static — Development Plan
Coverage target: **≥ 80%** line coverage. Like `mini-serve`, the connection-lifecycle
and traversal-security tests need real filesystem and socket fixtures (`tempfile`), not
pure-function unit tests alone.
## Phase 0.1.0 — MVP: resolve, serve, traverse-safe
*Exit criteria: serve a file under a root; a traversal attempt and a missing file are
indistinguishable over the wire.*
1. **Commit: `resolve()` — segment-based traversal check + canonicalize-and-`starts_with`
boundary.** Primary test: request `jquery..min.js` (a legitimate filename containing
`..` as a substring but not as a segment), assert it resolves successfully — the
regression test for the over-eager substring check. Second test: request `../../etc/passwd`-shaped
input, assert it's rejected.
2. **Commit: traversal and missing-file both answer 404, `nosniff` header on all
responses.** Primary test: request a traversal path and a genuinely missing file,
assert both return status `404` with byte-identical bodies — proving there's no
distinguishing oracle. Assert `X-Content-Type-Options: nosniff` is present on both.
3. **Commit: `Server::run` canonicalizes root once; `resolve` takes it as a precondition.**
Primary test: instrument syscall count (or wrap the filesystem in a counting fake) for
N requests, assert `canonicalize` is called once total, not once per request.
## Phase 0.2.0 — HTTP correctness
*Exit criteria: method handling, Content-Length, directory redirects, range edge cases
all match RFC 9110 behavior.*
4. **Commit: method gate — GET/HEAD proceed, everything else 405 + `Allow`.** Primary
test: `DELETE` a real file, assert `405` with `Allow: GET, HEAD` and confirm (via a
counting fake filesystem) zero disk reads occurred.
5. **Commit: `Content-Length` on full 200 responses.** Primary test: request a known-size
file without a `Range` header, assert the response header's value equals the file's
actual byte length exactly.
6. **Commit: directory-index 301 redirect.** Primary test: request `/dir` where
`/dir/index.html` exists, assert a `301` with `Location: /dir/` — not the index
content directly.
7. **Commit: multi-range → full 200; `If-Range` honored.** Primary test: send a
multi-range `Range` header, assert `200` with the full body (not `416`); send a
single range with a stale `If-Range` validator, assert the full current file returns.
## Phase 0.3.0 — Connection lifecycle
8. **Commit: header-read timeout.** Primary test: open a raw socket, send nothing,
assert the connection closes within the configured timeout and a subsequent normal
request on a new connection still succeeds.
9. **Commit: loopback-only ephemeral binds.** Primary test: assert `run_ephemeral()`'s
bound address is exactly `127.0.0.1`.
## Phase 0.4.0 — Performance + non-ASCII filenames
10. **Commit: `BytesMut`-based streaming, `split_to(n).freeze()`.** Primary test:
benchmark or instrument to confirm one copy per chunk instead of two, on a
multi-megabyte fixture file.
11. **Commit: byte-level percent-decoding for filenames.** Primary test: serve a file
named with a non-ASCII character (e.g. `é.png`), assert it resolves and serves
correctly, with the traversal guard still active on the same request path.
## Phase 0.5.0 — Publish
12. **Commit: `err`/`log` feature docs + doc comments on every public item.**
13. **Commit: CI** — `cargo test`, `cargo test --all-features`, `cargo clippy -- -D warnings`,
`cargo llvm-cov --fail-under-lines 80`.
14. **Publish `0.5.0`** to crates.io (name confirmed available).
Cache-control policy hook and precompressed-sidecar support (both noted as optional in
the README) become their own post-0.5.0 minor-version phases if/when a consumer actually
needs them — not built speculatively ahead of that need.
## Phase 0.6.0 — Conditional requests (post-publish)
*Why deferred:* every response already computes and sends an `ETag`
(`generate_etag()` in `server.rs`), but nothing reads `If-None-Match` or
`If-Modified-Since` back — a revalidating client gets a full `200` every time. This is a
regression against `small-static`'s prior behavior (`is_not_modified()`,
`backstack-old/crates/small-static/src/server.rs:436-449`), not a new feature ask, but it
was never one of this crate's own numbered phases, so it's deliberately out of scope for
the 0.5.0 publish rather than an oversight — see the security/robustness/performance
evaluation this workspace ran before publishing 0.5.0 (Finding #9) for the full
pros/cons writeup behind that call.
15. **Commit: honor `If-None-Match` / `If-Modified-Since`, answer `304 Not Modified`
with no body when the validator matches.** Primary test: request a file with a
matching `If-None-Match` (the ETag from a prior response), assert `304` with an
empty body and no `Content-Length`; request the same file with a stale validator,
assert the full `200` body still returns.