parse-net 0.9.3

Grammar for Internet message fields: RFC 9110 field syntax, media types, RFC 9651 Structured Fields, HTTP and RFC 5322 dates, negotiation, entity tags, ranges, Content-Disposition, Cache-Control, Forwarded, auth challenges, and the email lexical layer. no_std, no allocation, sans-IO, zero dependencies.
Documentation
# parse-net v0.9.3 - Review fixes

**Date:** 2026-11-08
**Compare:** [`v0.9.2...v0.9.3`](https://github.com/jamesgober/parse-net/compare/v0.9.2...v0.9.3)

**A review of the code added since `0.9.0`.** The whole crate had three independent reviews at `0.9.0`. What came after, `param::parse_optional`, the `B` codec with `encode_words_b`, `decode_phrase`, and `write_mime_param_at`, had property tests and 24 hours of fuzzing but no review. One independent review read it against the RFCs and the documented contract and proved each finding by running it. It found one defect and one misuse left open; `0.9.3` fixes both.

**No call changes its signature.** Two inputs read differently, both listed below.

## Fixed

- `mail::decode_phrase` decoded an encoded word that had a comment or a quoted string right before it (D-084).
  - `(c)=?UTF-8?Q?a?=` read as `a`, and `"x"=?UTF-8?Q?a?=` as `x a`. RFC 2047 section 5 wants whitespace between an encoded word in a phrase and any adjacent word or special, on both sides; `0.9.2` checked only the side after the word, so `=?UTF-8?Q?a?=(c)` was already text.
  - Both now read as text: `=?UTF-8?Q?a?=` and `x =?UTF-8?Q?a?=`. With whitespace between, `(c) =?UTF-8?Q?a?=` still reads as `a`.
  - A name sent to be shown literally can no longer be decoded into other text.
- `mail::write_mime_param_at` read any room it was given (D-084).
  - The room is 78 less what the line holds, so it is at most 78; a larger one put a whole value on the first line, past 78 columns, and past the 998-byte line limit for a long value.
  - A room past 78 is now read as 78, as `encode_words` caps its own room.

| Input | `0.9.2` | `0.9.3` |
|---|---|---|
| `decode_phrase(b"(c)=?UTF-8?Q?a?=")` | `a` | `=?UTF-8?Q?a?=` |
| `decode_phrase(b"\"x\"=?UTF-8?Q?a?=")` | `x a` | `x =?UTF-8?Q?a?=` |
| `write_mime_param_at(name, value, room > 78, out)` | one line of any length | as with a room of 78 |

## Performance

- `response_cache_control` and `request_cache_control` compare directive names as two or three overlapping words instead of byte by byte, once the length has picked the candidate: 881 to 823 instructions (-6.6%) for `public, max-age=31536000, immutable`. A test checks the word compare against the byte rule for every directive name, at every position, with every byte value.
- The `decode_phrase` fix reads one byte, only for an atom that starts with `=?`: no measurable cost (3,179 to 3,180 instructions on two `Q` words).
- Wall-clock rows wait for a quiet machine (`docs/PERFORMANCE.md`).

## Documentation

- `decode_phrase`: folds are read between words and inside comments; a quoted string must be unfolded first (`mail::unfold`), as for `parse_mime_params` (D-078). An empty word, `""` or an encoded word with no text, counts as a word, so `x "" y` reads as `x  y`. Both were already so in `0.9.2`; the documentation now says it.
- An empty encoded word, such as `=?UTF-8?B??=`, still decodes to empty text. RFC 2047's `encoded-text` is `1*`, but encoders send it for empty subjects and Python's `email` package reads it as empty; reading it as text instead would show `=?UTF-8?B??=` on a screen (D-084).
- The question the roadmap left for web-net, whether `Strict-Transport-Security` needs the optional-parameter shape without a leading `;`, is closed: web-net writes HSTS and reads none, so its grammar stays the P3 item PN-18, an addition for 1.x (D-083).

## Testing

- Regressions for each finding in `tests/review_mail.rs`, and unit tests for both sides of an encoded word in a phrase and for rooms of 79, 500, and `usize::MAX`.
- The three fuzz targets over the changed code, `cache_control`, `mail_phrase`, and `mime_params`, ran 24 hours each on the release commit (3.3 billion inputs) with no finding. All 38 targets ran a two-minute smoke on it; the smoke caught the `mime_params` harness still expecting a room past 78 to be used as given, fixed with the cap. The other 35 targets cover code that did not change since their 24 hours on `0.9.2`.

## What's next

`0.9.x`: smtp-net and web-net on parse-net, then the release candidates. `1.0.0` is James's decision; `dev/READINESS-1.0.md` lists what stands open.

**Full Changelog:** https://github.com/jamesgober/parse-net/compare/v0.9.2...v0.9.3