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.2 - Display names and mid-line parameters

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

**Two gaps smtp-net met adopting `0.9.1`.** `parse_mailbox` returns a display name raw, and turning it into text needs the quotes removed, quoted pairs unescaped, comments dropped, and encoded words decoded, but only those outside quoted strings: no existing call could do all of that. And `write_mime_param` assumed its parameter starts a line, so after `Content-Type: text/plain` a long or non-ASCII value could pass 78 columns. `0.9.2` adds `mail::decode_phrase` and `mail::write_mime_param_at`. The third request, `class::is_atext`, has been public since `0.5`.

**Nothing changes for existing callers.** Every `0.9.1` call keeps its signature and its output; `write_mime_param` writes the same bytes, faster.

## Added

- `mail::decode_phrase(value, limits, out)` reads a `phrase` (RFC 5322 section 3.2.5), such as `Mailbox::display_name`, as text (D-081).
  - Comments are dropped, quoted strings lose their quotes and their quoted pairs are unescaped, and the words are joined by one space; `CRLF WSP` folds count as whitespace.
  - RFC 2047 encoded words are decoded where section 5 allows them in a phrase: a whole atom with whitespace on both sides. Inside a quoted string, or next to a special or a comment, they are text. Two encoded words with only whitespace between them are joined with none (section 6.2).
  - The text holds no control character but HTAB, whether it came raw, quoted, escaped, or encoded: C0, DEL, and C1 are refused, so a display name cannot carry a terminal escape or a header injection into a screen or a log.
  - A phrase with no word is `Empty`; a special such as `,` outside a quoted string is `InvalidByte`. Nothing is written on error.
- `mail::write_mime_param_at(name, value, room_first_line, out)` writes a MIME parameter after other text on the line and returns the columns left on the line it ends on, so parameters chain (D-082).
  - The form is `write_mime_param`'s. A one-line form that does not fit in the room goes on a new line as `;CRLF name=value`, before the value is split into sections; the `;` always goes on the current line.
  - With a room of 78 it writes exactly what `write_mime_param` writes, which a property test checks.

```rust
use parse_net::{mail, Limits, SliceSink};

let m = mail::parse_mailbox(b"\"Doe, Jane\" (Sales) <jane@example.com>", &Limits::DEFAULT)?;
let mut name = [0u8; 64];
assert_eq!(mail::decode_phrase(m.display_name.unwrap_or_default(), &Limits::DEFAULT, &mut name)?, "Doe, Jane");

let mut buf = [0u8; 128];
let mut out = SliceSink::new(&mut buf);
// After `Content-Disposition: attachment`, 47 columns are left.
let room = mail::write_mime_param_at("filename", "quarterly report for the northern region.pdf", 47, &mut out).expect("room");
assert_eq!(out.written(), &b";\r\n filename=\"quarterly report for the northern region.pdf\""[..]);
assert_eq!(room, 22);
# Ok::<(), parse_net::ParseError>(())
```

## Performance

- `write_mime_param` on a quoted value: 30% fewer instructions (3,271 to 2,292), from writing the runs between escapes whole and checking an ASCII value's controls a byte at a time; 5.5% fewer on an extended value.
- `decode_words` on `Q` words: 3% fewer instructions (5,462 to 5,295), from writing runs of plain bytes whole.
- `parse_mailbox` and `decode_phrase` together took 31% to 47% of the time `mailparse::addrparse` takes for a mailbox and its display name, read on a shared machine under heavy load (load average 350 on 32 threads); the wall-clock rows wait for a quiet run (`docs/PERFORMANCE.md`).

## Testing

- `decode_phrase`: unit tests for every form above and every error at its offset; a property test against a plain reference reader (atoms, quoted strings with any escapes, whitespace, folds, and comments); a no-panic property over arbitrary bytes and over every display name `parse_mailbox` accepts; threat cases for controls in each form; complexity cases for many words, many encoded words, and encoded-word candidates that never close; counting-allocator coverage.
- `write_mime_param_at`: unit tests at each boundary of the room; a property test that the first line keeps to the room, every other line to 78 columns, the room returned matches the last line, the value parses back, and a room of 78 gives `write_mime_param`'s bytes.
- A new fuzz target, `mail_phrase`, and `mime_params` now writing mid-line as well. All 38 targets ran 24 hours each on the release commit (3.6 billion inputs) with no finding; three stopped on WSL wall-clock steps, replayed their inputs clean, and served the rest of their time.
- The `list` fuzz harness found its split point by parsing the value up to every comma, quadratic on a comment holding many escaped commas (one 3.5 KB input took 130 ms a run). It now finds the commas outside quotes and comments in one pass (1.5 ms on that input). The library took 10 microseconds on that input; only the harness changed.

## What's next

`0.9.x`: smtp-net on `decode_phrase` and `write_mime_param_at`, 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.1...v0.9.2