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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
<h1 align="center">
    <img width="90px" height="auto" src="https://raw.githubusercontent.com/jamesgober/jamesgober/main/media/icons/hexagon-3.svg" alt="Triple Hexagon">
    <br><b>CHANGELOG</b>
</h1>
<p>
  All notable changes to <code>parse-net</code> will be documented in this file. The format is based on <a href="https://keepachangelog.com/en/1.1.0/">Keep a Changelog</a>,
  and this project adheres to <a href="https://semver.org/spec/v2.0.0.html/">Semantic Versioning</a>.
</p>

---

## [Unreleased]

---

## [0.9.3] - 2026-11-08

Fixes from a review of the code added since 0.9.0, and a faster
`Cache-Control` reader. No call changes its signature.

### Fixed

- `mail::decode_phrase` decoded an encoded word with a comment or a quoted
  string right before it: `(c)=?UTF-8?Q?a?=` read as `a`, and
  `"x"=?UTF-8?Q?a?=` as `x a`. RFC 2047 section 5 wants whitespace on both
  sides of an encoded word in a phrase, and 0.9.2 checked only the side
  after it. Both are now text, as the mirror cases were (D-084).
- `mail::write_mime_param_at` with a room past 78 could put a whole value on
  the first line, past 78 columns, and past 998 bytes for a long one. A room
  past 78 is now read as 78, as `encode_words` caps its own (D-084).

### Changed

- `response_cache_control` and `request_cache_control` compare directive
  names as two or three words instead of byte by byte: 881 to 823
  instructions (-6.6%) for `public, max-age=31536000, immutable`.

### 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.
- The question left open for web-net, whether `Strict-Transport-Security`
  needs the optional-parameter shape without a leading `;`, is closed:
  web-net writes HSTS and reads none, and its grammar stays PN-18 for 1.x
  (D-083).

---

## [0.9.2] - 2026-11-07

Two additions smtp-net asked for while adopting 0.9.1, and faster MIME
parameter writing. No 0.9.1 call changes its signature or its output.

### Added

- `mail::decode_phrase`: the text of a `phrase` (RFC 5322 section 3.2.5),
  such as the display name `parse_mailbox` returns raw. Comments are
  dropped, quoted strings lose their quotes and their quoted pairs are
  unescaped, and the words are joined by one space; RFC 2047 encoded words
  are decoded where section 5 allows them in a phrase, a whole atom between
  whitespace, and two of them with only whitespace between are joined with
  none (section 6.2). The text meets `decode_words`' rules: UTF-8, no control
  character but HTAB, C1 included. For smtp-net and gate-net (D-081).
- `mail::write_mime_param_at(name, value, room_first_line, out)`:
  `write_mime_param` for a parameter that does not start a line. A one-line
  form that does not fit in the room goes on a new line, `;CRLF name=value`,
  before the value is split into sections, and the call returns the columns
  left on its last line, so parameters chain. With a room of 78 it writes
  what `write_mime_param` writes (D-082).
- Fuzz target `mail_phrase` (38 targets): plain phrases read back as their
  words, any text through a quoted string reads back unchanged, and every
  display name `parse_mailbox` accepts decodes or fails with a listed error;
  `mime_params` now writes mid-line too and checks the room returned.
  Property tests for both functions, threat cases for display names, complexity cases,
  counting-allocator coverage, and benchmark rows.

### 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% on an extended value.
- `decode_words` on `Q` words: 3% fewer instructions (5,462 to 5,295), from
  writing runs of plain bytes whole.

### Notes

- `class::is_atext`, which smtp-net asked for, has been public since 0.5;
  nothing changed.
- The `list` fuzz target found its split point by parsing the value up to
  every comma, which is quadratic on a comment that holds 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 it; only the harness changed.

---

## [0.9.1] - 2026-11-05

Two additions consumers asked for while adopting 0.9.0. No 0.9.0 call
changes its signature; `decode_words` now reads `B` words it refused.

### Added

- `param::parse_optional`, with `OptionalParams` and `OptionalParam`: RFC
  6455 section 9.1 extension parameters, whose value may be left out.
  permessage-deflate (RFC 7692) sends `client_max_window_bits` and
  `server_no_context_takeover` that way, and `param::parse` refuses them with
  `MissingValue`. Every other rule is `param::parse`'s: empty parameters,
  bytes used, no whitespace around `=`, the duplicate policy, and the
  64-name limit under `Refuse`. `find` returns the parameter, so its `value`
  tells a bare name from one with a value; `contains` asks whether it is
  there. Found by ws-net adopting 0.9.0 (D-079).
- `mail::encode_words_b`: RFC 2047 `B` encoded words, `=?UTF-8?B?...?=`, of
  at most 75 bytes, split only between characters, folded as
  `encode_words` folds. For text that is mostly not ASCII it is the shorter
  form: a Japanese subject is one 64-byte word against two words, 144 bytes,
  in `Q`. For smtp-net (D-080).
- Fuzz targets `param_optional`, `mail_words_b`, and `diff_b64` (against the
  `base64` crate's canonical engine; 37 targets, four differential, the
  weekly workflow runs it); property tests against a plain reference reader,
  against `param::parse`, and for the `B` round trip; complexity cases for
  bare names and `B` words; counting-allocator coverage; benchmark rows.

### Changed

- `mail::decode_words` reads `B` words, which it refused with
  `InvalidEncodedWord`: a widening, allowed in a 0.9.x patch. The base64 is
  strict, so every byte string has one encoding: whole groups of four, `=`
  only as the last one or two characters with zero bits under it, nothing
  outside the alphabet, no whitespace inside the word, and at most 75 bytes
  to a word. Every decoded byte meets the Q rules: no control character (C0,
  DEL, C1), whole UTF-8 characters, ASCII for US-ASCII. A word that breaks
  one is `InvalidEncodedWord`, `ControlInDecoded`, or `InvalidUtf8`, at the
  group of four that holds it.
- The base64 is a private codec for one word at a time, with no public API:
  this reverses D-002 for `B` words, decided for smtp-net and ws-net and
  recorded for James (D-080, `dev/READINESS-1.0.md`). The `encode-lib`
  feature stays, as a documented no-op, so builds that name it keep working;
  `basic_decode_into`, `ByteSeq::decode_into`, and `sfv::Writer::bytes` still
  wait for encode-lib.

### Performance

- A full benchmark run on a quiet machine, the first since 0.7: every row is
  25% to 81% faster than its competitor, and both estimates are met
  (`client_address` 37.6 ns against 60, `encode_words` 176 ns against 200).
  `response_cache_control` reads 25% (33% on the loaded 0.9.0 run).
- `decode_words` on one `B` word is 61% faster than `mailparse` (145 ns
  against 373 ns): a clean word goes out in one piece.
- `param::parse` and the Q paths stay within 1.5% of 0.9.0 in instructions,
  the size of the moves code layout makes in unchanged functions.

---

## [0.9.0] - 2026-11-03

Beta: the pre-1.0 polish pass. Three independent reviews of the whole crate
found 46 defects; all are fixed, each with a regression test. `0.9.0` refuses
some inputs that `0.8.0` accepted, and accepts some it refused.

### Security

- `write_element` quotes every host that is not a token: a `Host` holding a
  comma or a semicolon, legal in a reg-name, forged a `Forwarded` hop or pair
  that the next proxy read.
- `Host`, `Origin`, and `write_element` refuse bytes no parsed value holds, so
  their public fields cannot carry CR, LF, or NUL through a writer.
- The numeric-name rule (D-040) reads percent-decoded bytes: `127.0.0.%31` and
  `127%2e1` passed as names, and a browser reads them as addresses.
- `decode_words` refuses raw controls in its text, and is linear: failed
  candidates each scanned to the end of the value, 62 seconds for 1 MiB.
- RFC 2231 continuations refuse C1 controls, across sections too, and a
  section given twice, whose value depended on the parameters' order.
- A malformed `max-stale` no longer reads as any staleness.
- `Debug` of a bare credential hides it (it may be a key sent without a
  scheme), and `OwnedMediaType`'s `Debug` hides parameter values.
- `check_header_value`, `fold`, and `pct::encode` refuse or encode every
  control but HTAB (D-078).

### Added

- `range::parse_if_range_at`: `If-Range` in all three date forms; without a
  clock, `parse_if_range` reads IMF-fixdate and asctime (D-035).
- `forwarded::Proto::{Ws, Wss, Other}`: `proto` takes any URI scheme, where
  `proto=wss` failed whole chains closed (D-077).
- `dev/READINESS-1.0.md`: what stands between parse-net and 1.0.

### Changed

- `byte_ranges` reads the one-range shape by position (815 to 197
  instructions); `client_address` decides and validates in one pass (3,193 to
  1,289); the repeated-name table zeroes nothing for short lists;
  `Content-Disposition` stops revalidating `filename*` (D-074). Every compared
  row is at least 30% faster than its competitor.
- Numerals past `u64::MAX` in `Range` saturate (RFC 9110 section 14.1.2,
  D-075); a non-zero suffix on an empty representation is `Ignore`, not 416.
- `*` stands alone in `If-Match` and `If-None-Match`.
- `Negotiator` refuses what the fields' grammar forbids (`language-range`,
  parameters other than `q`), never chooses an invalid offer, and places
  `Refused` errors by line and offset (D-076).
- Control bytes keep their kind in ranges, entity tags, and after a backslash
  (D-037); `InvalidQuotedPair` is now for grammars that allow no such pair.
- `for=UNKNOWN` reads as `unknown`; a repeated pair of any name in a
  `Forwarded` element is refused.
- `Link` reads a quoted `title*` and ignores `title*` after the first;
  `write_link` and `write_challenge` refuse what their parsers would.
- Leading empty list elements are read in credentials and challenges.
- `Cache-Control` budgets span field lines, and a trailing comma counts as an
  empty element, in `Cache-Control`, entity-tag lists, and ranges.
- `fold` folds after the colon and splits long whitespace runs, so values that
  fit are no longer refused; `write_mime_param`, `write_msg_id`, and
  `encode_words` keep the 998-byte line limit.
- `decode_words` reads folds as whitespace, treats a lone `"` as text, and
  never errors on what does not stand alone as an encoded word.
- `parse_mailbox` reads comments around an angle address; comments in mail
  fields may fold. RFC 2231 section numbers past three digits count, values
  keep `{` and `}`, and US-ASCII holds in every section.
- Display String triples count toward `max_string_len`; `civil_from_days`
  clamps to real dates; many error offsets now point at the failure; refusals
  in `decode_words` and `MimeParams::decode_into` write nothing.

### Testing

- `tests/review_core.rs`, `tests/review_security.rs`, `tests/review_mail.rs`:
  a regression for each finding.
- Property tests: the one-pass `client_address`, `xff_member`, and the
  `byte_ranges` path against the code they replaced; no host can forge a
  `Forwarded` pair through `write_element`.
- `tests/complexity.rs` gains the adversarial `decode_words` shapes, and its
  time bound on an optimised build is 2.0 of the size ratio (D-073).

---

## [0.8.0] - 2026-10-29

Alpha: parse-net in use by published consumers. http-net 0.8.0 and 0.9.0
depend on parse-net 0.7, cookie-net 0.3.0 to 0.7.0 and form-net 0.3.0 to
0.5.0 on 0.6.1, all with green CI; none has an API change open. No API
change.

### Changed

- The complexity tests check timing on an optimised build only (the CI job
  `Complexity (release)`, at the 1.2 bound); an unoptimised build checks the
  limit errors and prints the times. The 2.0 bound for debug builds failed
  on a macOS runner on a single-loop walker (D-073).
- The weekly differential fuzz workflow builds for
  `x86_64-unknown-linux-gnu`: the installed cargo-fuzz is a musl build and
  defaulted to its own target, where the address sanitizer cannot link.

---

## [0.7.0] - 2026-10-24

Hardening: every walker checked against adversarial input, every threat
tested, every fuzz target run for 24 hours.

### Added

- `param::MAX_CHECKED_PARAMS` (64): the most parameters one element may have
  where repeated names are refused (D-072).
- `tests/threats.rs`: an attack test for each row of the threat table that
  had none, and where the others live.
- `tests/complexity.rs`: every walker fed 1 KiB, 64 KiB, and 1 MiB of
  adversarial input; the default limits give the expected limit error, and
  with every limit raised the time grows with the input, within 1.2 of the
  size ratio in a release build (a CI job) and 2.0 in a debug build (D-073).
- `docs/SECURITY.md` (the threat model, tests, costs, reporting) and
  `docs/PLATFORM-NOTES.md` (features, `no_std`, stack use).
- A weekly CI workflow runs the differential fuzz targets for 30 minutes
  each (`.github/workflows/fuzz.yml`).

### Changed

- The repeated-name check of RFC 9110 parameters under
  `DuplicatePolicy::Refuse`, authentication parameters, and MIME parameters
  compares names from a table of up to 64 offsets on the stack. It read every
  earlier parameter again, so a field of many parameters cost the square of
  its length: 8.8 seconds for 1 MiB with `max_params` raised, and about 100
  times a normal parse at the default limits. Those lists now hold at most
  64 parameters per element; `max_params` keeps its default of 256 for
  Structured Fields (D-072).
- `decode_words` refuses every decoded control character (C0 including
  HTAB, DEL, C1), as ext-values do; it refused only CR, LF, and NUL, so an
  encoded ESC reached the decoded text (D-071).
- `param::parse` walks a shrinking slice; `Params::get` and the parameter
  iterator read validated text without validating it again;
  `parse_u32_saturating` reads up to nine digits without saturating steps.
  `parse_media_type` with `charset` went from 32.0 ns to 26.1 ns (30% faster
  than `mime`) and `response_cache_control` from 47.8 ns to 41.2 ns (31%
  faster than `headers`), so every compared row meets the 20% target
  (D-069).
- The `homepage` key, which repeated `repository`, is gone from the
  manifest.

### Fixed

- `mail::fold` wrote whitespace that ends a value past the 998-byte line
  limit when the last word filled the line (a 989-byte word and a TAB after
  `Subject: ` made 999 bytes). It is refused with `OutOfRange`, like a word
  that does not fit: a fold before it would leave a line of only
  whitespace. Found by the 24-hour fuzz run.

### Testing

- Property tests compare the new parameter walker with the 0.6 walker
  (kept as a test-only reference), `Params::get` and the iterator with the
  checked word parser, and `parse_u32_saturating` with exact arithmetic. One
  found a bug in a draft of the new `get` before it shipped: a name could
  match across an empty parameter (`;;a=""` and `;a`).
- The `pct_decode` and `sfv_roundtrip` fuzz targets check every error they
  see, and every fuzz target now checks that an error prints exactly its
  kind's message and position.

---

## [0.6.1] - 2026-10-22

### Fixed

- `parse_condition`, `if_none_match`, and `if_match` counted the comma after
  each entity tag as an empty list element, so a list of more than 16 tags
  was refused with `TooManyEmptyElements`. The one-pass walker of 0.6.0 left
  the separator for its next pass to read. A unit test now reads lists of 17,
  100, and 1024 tags, and a property test compares the walker with the
  general list walker on long lists.

---

## [0.6.0] - 2026-10-22

Optimisation: every operation measured against the Rust crates that do the
same work.

### Added

- `benches/`: criterion benchmarks against `mime`, `httpdate`, `sfv`,
  `headers`, `content_disposition`, and `mailparse` on real traffic shapes,
  as their own crate so the competitors never enter parse-net's dependency
  tree (D-066); a CI job keeps them building.
- `docs/PERFORMANCE.md`: the results, what changed, the method, the machine,
  and the commands.
- Property tests: the `MailDate` fast path agrees with the general parser;
  the `X-Forwarded-For` IPv4 reader agrees with `core::net::Ipv4Addr`.

### Changed

- Entity-tag lists, `Cache-Control`, and range sets are validated and read in
  one pass by a walker for their own grammar (D-067): `if_none_match` 63 ns
  to 18 ns, `byte_ranges` 40 ns to 28 ns.
- `best_media` validates and scores in the same pass and compares exact
  forms first: 861 ns to 461 ns.
- `MailDate::parse` reads the canonical form by position (D-068): 54 ns to
  22 ns. `client_address` reads IPv4 with a byte-level parser: 196 ns to
  110 ns. `encode_words` builds each word on the stack from a length table.
- `Cache-Control` names are dispatched on length; `Params::get` and
  `MediaRange::matches` compare exact bytes before ignoring case.
- `parse_media_type` and `response_cache_control` are 17% and 19% faster
  than their competitors, below the 20% target; the plan is in
  `docs/PERFORMANCE.md` (D-069).

---

## [0.5.0] - 2026-10-22

Feature complete: links, Structured Field writing, the email lexical layer,
and owned forms.

### Added

- `link`: `parse_link`, `Links`, `LinkValue` (`target`, `rel`, `rels`,
  `has_rel`, `anchor`, `hreflang`, `title` with `title*`, `media`, `type_`,
  `params`), `LinkParams`, `write_link` (RFC 8288).
- `sfv::Writer`: canonical Structured Field serialization (RFC 9651 section
  4.1) for Items, Lists, and Dictionaries, with typed value methods, `bare`,
  `param`, `param_key`, `key`, inner lists, and `finish`.
- `MailDate`: RFC 5322 `date-time` with `new`, `parse` (strict, or with the
  obsolete forms of section 4.3), `format`, `write_to`, `utc`,
  `offset_minutes`, `offset_unknown`, `leap_second`.
- `mail`: `check_header_value`, `is_field_name`, `unfold`, `fold`,
  `encode_words` and `decode_words` (RFC 2047 Q), `parse_mime_params` and
  `MimeParams` with RFC 2231 continuations (`MimeValue`, `Continuation`),
  `write_mime_param`, `parse_addr_spec` (`AddrSpec`, `LocalPart`, `Domain`),
  `parse_mailbox` (`Mailbox`), `parse_msg_id` and `write_msg_id` (`MsgId`).
- `forwarded::write_element`.
- With `alloc`: `OwnedMediaType`, `OwnedEntityTag`, `OwnedHostPort`, and
  `sfv::owned` (`OwnedBareItem`, `OwnedItem`, `OwnedList`,
  `OwnedDictionary`), each with `to_owned` on the view and `as_view` back.
- `ext_value::Charset::UsAscii`.
- Tests: the httpwg serialisation tests (547 cases), and every accepted
  parse case serialized again and compared with its canonical form; property
  tests for every 0.5 parser (no panic) and writer (its output parses back);
  counting-allocator tests over every 0.5 parse and write; size and trait
  checks for every 0.4 and 0.5 view; an example for every public item.
- Fuzz targets: `link`, `mail_date`, `mail_words`, `mime_params`,
  `addr_spec`, `fold_unfold`, `sfv_roundtrip`.
- `docs/ARCHITECTURE.md`; `docs/GRAMMAR.md` extended to Link and the email
  layer.

### Changed

- `docs/API.md`: `Writer::param_key` (D-059), `Charset::UsAscii` (D-058),
  the iterator types (D-064); the `MailDate` example said "Tue, 21 Oct 2026",
  which is a Wednesday.
- The base64 decode helpers (ATH-3, MAIL-5, `ByteSeq::decode_into`,
  `sfv::Writer::bytes`, `B` encoded words) move to 0.6, as D-002 provides,
  because encode-lib is not published (D-062).

---

## [0.4.0] - 2026-10-22

Extended: the web layer and what form-net needs.

### Added

- `media`: `parse_range` and `MediaRange` (`matches`, `specificity`,
  `weight`, `params`) for `Accept` members.
- `negotiate`: `QValue`, `best_media`, `best_language`, `best_encoding`,
  `best_charset`; `Negotiator` with `LanguageMatching` (RFC 4647 lookup or
  basic filtering), `MalformedPolicy`, and `Outcome`, including
  `media_lines`; `parse_accept` (`AcceptList`) and `parse_weighted`
  (`WeightedList`, `Weighted`) for other selection rules.
- `etag`: `EntityTag` (`parse`, `strong`, `weak`, `strong_eq`, `weak_eq`,
  `write_to`), `Condition`, `TagList`, `parse_condition`, `if_match`,
  `if_none_match`.
- `range`: `parse`, `RangeHeader`, `RangeSpec`, `resolve` and `byte_ranges`
  with `RangeOutcome` (overlap and order rules against range abuse),
  `ByteRange`, `parse_content_range`, `ContentRange`, `write_content_range`,
  `write_unsatisfied`, `parse_if_range`, `IfRange`.
- `ext_value`: RFC 8187 `ExtValue` (UTF-8 and ISO-8859-1, decoded controls
  refused), `Charset`, `write_ext_value`.
- `disposition`: `parse_content_disposition`, `parse`, `ContentDisposition`,
  `DispositionType`, `Filename` (`decode_into`, `decode_percent_into`,
  `max_decoded_len`, `has_path_separator`, `has_bidi_control`),
  `write_attachment` with an ASCII fallback.
- `cache`: `parse`, `Directive`, `Directives`, `response_cache_control`,
  `request_cache_control`, `response_lines`, `request_lines`,
  `ResponseDirectives`, `RequestDirectives`, `Writer`.
- `forwarded`: `client_address` and `client_address_detailed` with `Trust`,
  `ForwardedSource`, `ClientAddress`, `Resolved`; `parse`, `Element`, `Node`,
  `NodePort`, `Proto`; `parse_x_forwarded_for` (`XffChain`),
  `parse_x_forwarded_proto`, `parse_x_forwarded_host`.
- `auth`: `parse_credentials`, `Credentials`, `AuthPayload`, `AuthParams`
  (redacted `Debug`), `bearer`, `basic`, `parse_challenges`, `Challenges`,
  `Challenge`, `write_challenge`, `write_challenge_token68`.
- `origin`: `parse_origin`, `OriginValue`, `Origin` (`same_origin`,
  `effective_port`, `write_to`).
- `sfv`: RFC 9651 parsing: `parse_item`, `parse_list`, `parse_dictionary`,
  `parse_list_lines`, `parse_dictionary_lines`, `Parser` with `Limits` and the
  single-pass `visit_item`, `visit_list`, `visit_dictionary` through
  `Visitor`; `BareItem` (all eight types, Date and Display String included),
  `Decimal`, `SfString`, `ByteSeq`, `DisplayString`, `Item`, `Member`,
  `InnerList`, `List`, `Dictionary` (`get`, `index_into`), `SfParams`,
  `ListLines`, `DictionaryLines`, and their iterators.
- `WriteError::DuplicateName`.
- Tests: the httpwg structured-field-tests suite in full (1598 cases), again
  with every list and dictionary split into one line per member; every
  greenbytes tc2231 case with a recorded verdict
  (`tests/differential/tc2231.md`); property tests for every 0.4 parser and
  the writer-parser closure check for every 0.4 writer; counting-allocator
  tests over every 0.4 parse and write.
- Fuzz targets: `media_range`, `accept_negotiate`, `etag_condition`, `range`,
  `content_range`, `ext_value`, `content_disposition`, `cache_control`,
  `forwarded`, `xff`, `authorization`, `www_authenticate`, `origin`,
  `sfv_item`, `sfv_list`, `sfv_dictionary`, and the differential oracle
  `diff_sfv` (against `sfv`), with verdicts V-5 to V-7.
- `docs/GRAMMAR.md`: for every field, the ABNF from its RFC, the case rules,
  each leniency or strictness and the RFC text behind it, and the function
  that parses it.

### Changed

- `media::write` refuses a parameter name given twice (`DuplicateName`), so
  its output always parses (D-052).
- `docs/API.md`: `Node` ports (D-047), `AuthParams` (D-048), the `sfv`
  additions (D-055); `parse_challenges`, `write_challenge_token68`, and
  `has_bidi_control` shipped in 0.4, ahead of plan (D-049).
- `RangeHeader::len` is documented as the size of the range set whatever the
  unit.

---

## [0.3.0] - 2026-10-22

The core: what http-net, ws-net, and cookie-net need.

### Added

- Errors: `ParseError` (8 bytes: kind, field line, offset; never input bytes)
  with `kind`, `offset`, `line`, `new`; `ErrorKind` with 49 variants and
  `message`; `WriteError` (`Full`, `Forbidden`, `OutOfRange`, `NotToken`,
  `InvalidSequence`); `Full`; `Display` and `core::error::Error` for all.
- `Limits` with every bound and its setter, RFC-sourced defaults, and
  `DuplicatePolicy` (`Refuse`, `First`, `Last`).
- `class`: the byte-class table (25 public classes in one 1 KiB table), the
  `is_*` predicates, `hex_value`, `Class` with `contains` and `union`, `all`.
- `scan`: `find_byte`, `find_either`, `check_field_value` (SWAR, eight bytes
  per step), `first_not_in`.
- `num`: `parse_u64`, `parse_u32_saturating` (the `delta-seconds` rule).
- `write`: `Sink`, `SliceSink`, `CountSink`, `Sink for Vec<u8>` (`alloc`),
  `Sink for &mut S`; `write_token`, `write_quoted`, `write_word`,
  `write_param`, `write_field_value`, `write_decimal_u64`, `write_hex_u64`.
- `text`: `is_token`, `is_ows`, `trim_ows`, `skip_ows`, `parse_token`,
  `parse_quoted`, `parse_word`, `parse_comment`; `Quoted` (zero-copy, buffer-free
  comparison, `unescape_into`), `Word`, `Comment`.
- `list`: `list`, `parse`, `for_each`, `combined` (several field lines as one
  list, no copy), `contains_token`; `List`, `Elements`, `CombinedList`.
- `param`: `parse`, `Params` (`get`, `iter`, names-only `Debug`), `Param`.
- `media`: `parse_media_type`, `parse`, `write`; `MediaType` with `essence_is`,
  `is`, `charset`, `boundary` (RFC 2046), `suffix`, `is_json`,
  `is_form_urlencoded`, `is_multipart_form_data`, `write_canonical`; `Boundary`.
- `date`: `HttpDate` (`from_unix`, `unix`, `MIN`, `MAX`, `parse_at`,
  `parse_at_with`, `parse_imf_fixdate`, `parse_detailed`, `format`, `write_to`,
  `civil`; `now`, `from_system_time`, `to_system_time` with `std`),
  `http_date_at`, `http_date` (`std`), `Civil`, `DateFormat`,
  `days_from_civil`, `civil_from_days`, `is_leap_year`, `days_in_month`,
  `weekday_from_days`.
- `host`: `parse_host_port`, `parse_uri_host_port` (strips nothing, for
  targets and URLs), `parse_host`; `HostPort`, `Host` with
  `eq_ignore_ascii_case` and `write_to`.
- `pct`: `validate`, `validate_with`, `decoded_len`, `decode_into`,
  `decode_utf8_into`, `decode_lower_only_into`, `encoded_len`, `encode`;
  `EncodeSet` with three named sets; `HexCase`.
- Tests: unit tests on every path; property tests (no parser panics, writer
  round trips, SWAR equals bytewise, combined lines equal joined); counting
  allocator tests over every parse and every write into a `SliceSink`; RFC
  vectors in `tests/vectors/`; compile-time `Send + Sync + Unpin` and size
  checks.
- Fuzz targets: `text_quoted`, `text_comment`, `list`, `params`, `media_type`,
  `http_date`, `host`, `pct_decode`, and the differential oracles
  `diff_http_date` (against `httpdate`) and `diff_media_type` (against `mime`),
  with the verdicts in `tests/differential/verdicts.md`.
- `docs/MIGRATION.md` for the http-net and ws-net maintainers.

### Changed

- `docs/API.md`: control bytes are reported with the same four kinds by every
  parser (D-037); `HttpDate::to_system_time` returns `Option` (D-038); the
  RFC 850 century window (D-039); numeric host names (D-040);
  `host::parse_uri_host_port` (D-044); `parse_u32_saturating`,
  `decode_lower_only_into`, and the `First` and `Last` duplicate policies
  shipped in 0.3, ahead of plan (D-042).

---

## [0.2.0] - 2026-10-22

The design lock: the public API through 1.0, written down before the code. No
parsing code yet.

### Added

- `docs/API.md`: every public item from `0.3.0` to `1.0.0` with its id, grade,
  tier, shipping version, signature, behaviour, every `ErrorKind` it can
  return, examples, and the edge cases its tests cover; the `ErrorKind` table
  with what each variant refuses and what to do; the `Limits` defaults and
  their RFC sources; one coverage checklist per consumer (http-net, ws-net,
  cookie-net, form-net, smtp-net).
- `dev/PLAN.md` decisions D-026 to D-036 made at the design lock (ISO weekday
  numbering, a public `ParseError::new`, no second quoting mode for
  `multipart/form-data`, `WriteError::InvalidSequence`, the Structured Field
  Date range, `@` checked first in `Host`, `mail::unfold` validating first,
  `If-Range` dates as IMF-fixdate, the RFC 5322 obsolete-year rule, and the
  names added beyond the specification's sketch).

### Changed

- `dev/ROADMAP.md`: 0.1.0 marked done; 0.2.0 items checked.

## [0.1.0] - 2026-10-21

The scaffold and the plan. No parsing code yet.

### Added

- Crate manifest (edition 2024, MSRV 1.85, no dependencies, features `alloc`,
  `std`, and `encode-lib` declared ahead of their code), `Apache-2.0 OR MIT`
  license, README, REPS, `deny.toml`, `clippy.toml`, `rustfmt.toml`,
  `.gitattributes`.
- Crate root: `VERSION`, `#![no_std]`, `#![forbid(unsafe_code)]`, missing docs
  denied, and `unwrap`, `expect`, `panic`, slice indexing, and printing denied
  in library code.
- CI: Linux, macOS, and Windows on stable and 1.85 (format, clippy and tests in
  three feature modes, examples, documentation with default and all features);
  a bare-metal `thumbv7em-none-eabi` build with no features and with `alloc`; a
  check that the default build has zero runtime dependencies; `cargo package`;
  `cargo audit` and `cargo deny`.
- `dev/PLAN.md` (purpose, scope, requirements, design, byte-class table,
  algorithms, API outline, threats, performance targets, testing, competitors,
  decision log D-001 to D-025), `dev/DIRECTIVES.md`, and `dev/ROADMAP.md`
  (0.1 to 1.0 with exit criteria).
- `fuzz/` crate skeleton; targets arrive with the parsers they cover.

[Unreleased]: https://github.com/jamesgober/parse-net/compare/v0.9.3...HEAD
[0.9.3]: https://github.com/jamesgober/parse-net/compare/v0.9.2...v0.9.3
[0.9.2]: https://github.com/jamesgober/parse-net/compare/v0.9.1...v0.9.2
[0.9.1]: https://github.com/jamesgober/parse-net/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/jamesgober/parse-net/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/jamesgober/parse-net/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/jamesgober/parse-net/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/jamesgober/parse-net/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/jamesgober/parse-net/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/jamesgober/parse-net/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/jamesgober/parse-net/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/jamesgober/parse-net/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/jamesgober/parse-net/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/jamesgober/parse-net/releases/tag/v0.1.0