parse-net 0.5.0

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
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# Grammar

For each field parse-net reads: the ABNF from its RFC, how case is treated,
any leniency parse-net applies or declines and the RFC text behind it, and the
function that parses it. ABNF is quoted from the RFCs (IETF Trust; code
components under the Revised BSD License). Where a parse-net rule is stricter
or looser than the ABNF, the row says so and why; nothing is lenient without a
sentence from the RFC that asks for it.

Rules every parser shares:

- Field values arrive without surrounding whitespace, and every parser strips
  SP and HTAB (only those) from both ends anyway: "A field value does not
  include leading or trailing whitespace. When a specific version of HTTP
  allows such whitespace to appear in a message, a field parsing
  implementation MUST exclude such whitespace prior to evaluating the field
  value." (RFC 9110 section 5.5). VT, FF, CR, LF, and NUL are refused, never
  stripped (threat T6).
- A control byte is reported as `BareCr`, `LineFeed`, `Nul`, or `Control`
  wherever it occurs, including inside quoted strings and comments (D-037).
- Bytes above 0x7F are accepted only where the grammar has `obs-text`.

Contents: [lexical elements](#lexical-elements), [lists](#lists),
[parameters](#parameters), [media types](#media-types-and-ranges),
[negotiation](#accept-fields), [dates](#http-date), [Host](#host),
[percent-encoding](#percent-encoding), [entity tags](#entity-tags-and-conditions),
[ranges](#ranges), [ext-value](#ext-value), [Content-Disposition](#content-disposition),
[Cache-Control](#cache-control), [Forwarded](#forwarded), [X-Forwarded-*](#x-forwarded-),
[authentication](#authorization-and-www-authenticate), [Origin](#origin),
[Structured Fields](#structured-field-values), [Link](#link),
[email dates](#email-dates), [email header values](#email-header-values),
[encoded words](#encoded-words), [MIME parameters](#mime-parameters),
[addresses and message identifiers](#addresses-and-message-identifiers).

## Lexical elements

RFC 9110 sections 5.6.2 to 5.6.5.

```abnf
token          = 1*tchar
tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
               / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
               / DIGIT / ALPHA
OWS            = *( SP / HTAB )
RWS            = 1*( SP / HTAB )
BWS            = OWS
quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
qdtext         = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
quoted-pair    = "\" ( HTAB / SP / VCHAR / obs-text )
comment        = "(" *( ctext / quoted-pair / comment ) ")"
ctext          = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
obs-text       = %x80-FF
```

- Case: tokens are compared as the field defines; `Quoted::eq_ignore_ascii_case`
  and `Word::eq_ignore_ascii_case` exist for fields that say so.
- A token and a quoted string with the same content are equal: "The quoted
  and unquoted values are equivalent." (section 5.6.6).
- Comments nest; the depth is limited by `Limits::max_comment_depth`
  (`CommentTooDeep`), because the grammar is recursive and the input is not
  trusted.
- Functions: `text::parse_token`, `parse_quoted`, `parse_word`,
  `parse_comment`, `trim_ows`, `skip_ows`, `is_token`.

## Lists

RFC 9110 section 5.6.1.

```abnf
#element => [ element ] *( OWS "," OWS [ element ] )
```

- Empty elements are skipped, up to `Limits::max_empty_elements` (default
  16), then `TooManyEmptyElements`: "A recipient MUST parse and ignore a
  reasonable number of empty list elements: enough to handle common mistakes
  by senders that merge values, but not so much that they could be used as a
  denial-of-service mechanism." (section 5.6.1.2).
- Commas inside quoted strings and comments do not split.
- Several field lines are read as one list joined by commas (section 5.3);
  `list::combined` does this without copying.
- Functions: `list`, `list::combined`, `list::contains_token`,
  `list::for_each`.

## Parameters

RFC 9110 section 5.6.6.

```abnf
parameters      = *( OWS ";" OWS [ parameter ] )
parameter       = parameter-name "=" parameter-value
parameter-name  = token
parameter-value = ( token / quoted-string )
```

- Case: "Parameter names are case-insensitive. Parameter values might or
  might not be case-sensitive, depending on the semantics of the parameter
  name."
- No whitespace around `=`: "Parameters do not allow whitespace (not even
  "bad" whitespace) around the "=" character." `a = b` is refused.
- Empty parameters (`;;`, a trailing `;`) are accepted, as the `[ parameter ]`
  in the rule allows.
- A repeated name is refused by default (`DuplicateParameter`, threat T4);
  `DuplicatePolicy::First` and `Last` exist for fields whose RFC picks one.
- Function: `param::parse`, with `Params::get`, `iter`, `len`.

## Media types and ranges

RFC 9110 sections 8.3.1 and 12.5.1.

```abnf
media-type  = type "/" subtype parameters
type        = token
subtype     = token
media-range = ( "*/*" / ( type "/*" ) / ( type "/" subtype ) ) parameters
```

- Case: "The type and subtype tokens are case-insensitive." The `charset`
  value is compared case-insensitively too (section 8.3.1, citing RFC 2046
  section 4.1.2).
- `parse_media_type` refuses `*` as type or subtype (`Wildcard`): a
  `Content-Type` names one type. `media::parse_range` accepts `*/*` and
  `type/*` and refuses `*/subtype`, which the rule does not produce.
- In a media range, parameters after `q` are accept extensions, ignored for
  matching.
- `boundary` follows RFC 2046 section 5.1.1 (1 to 70 characters from its
  `bchars`, not ending in a space): `MediaType::boundary`.
- Functions: `parse_media_type`, `media::parse`, `media::parse_range`,
  `MediaRange::matches`, `media::write`.

## Accept fields

RFC 9110 sections 12.4.2 and 12.5; RFC 4647 section 2.1.

```abnf
weight          = OWS ";" OWS "q=" qvalue
qvalue          = ( "0" [ "." *3DIGIT ] ) / ( "1" [ "." *3"0" ] )
Accept          = #( media-range [ weight ] )
Accept-Charset  = #( ( token / "*" ) [ weight ] )
Accept-Encoding = #( codings [ weight ] )
codings         = content-coding / "identity" / "*"
Accept-Language = #( language-range [ weight ] )
language-range  = (1*8ALPHA *("-" 1*8alphanum)) / "*"
```

- `q` is matched case-insensitively ("a common parameter, named "q"
  (case-insensitive)", section 12.4.2); `1.5`, `0.1234`, `.5` are refused
  (`InvalidQValue`).
- A malformed `Accept*` field is treated as absent by default
  (`MalformedPolicy::TreatAsAbsent`, reported as `Outcome::HeaderIgnored`).
  Section 12.4.1 lets an origin server "disregard the header field" when no
  representation is acceptable; parse-net applies the same choice to a field
  that does not parse, and says so. `MalformedPolicy::Refuse` reports
  `Outcome::Refused` for callers that answer 400.
- Language tags are compared case-insensitively; matching is RFC 4647 lookup
  (section 3.4) by default, basic filtering (section 3.3.1) on request.
- `x-gzip` and `x-compress` match `gzip` and `compress` (section 8.4.1.1,
  8.4.1.3).
- Functions: `best_media`, `best_language`, `best_encoding`, `best_charset`,
  `negotiate::Negotiator`, `negotiate::parse_accept`, `parse_weighted`.

## HTTP-date

RFC 9110 section 5.6.7.

```abnf
HTTP-date    = IMF-fixdate / obs-date
IMF-fixdate  = day-name "," SP date1 SP time-of-day SP GMT
obs-date     = rfc850-date / asctime-date
rfc850-date  = day-name-l "," SP date2 SP time-of-day SP GMT
asctime-date = day-name SP date3 SP time-of-day SP year
date1        = day SP month SP year
date2        = day "-" month "-" 2DIGIT
date3        = month SP ( 2DIGIT / ( SP DIGIT ) )
GMT          = %s"GMT"
```

- All three formats are read: "A recipient that parses a timestamp value in
  an HTTP field MUST accept all three HTTP-date formats." Writers produce
  IMF-fixdate only.
- Names are case-sensitive, as the `%s` strings in the RFC grammar are; only
  `GMT` is accepted.
- RFC 850 two-digit years: "Recipients of a timestamp value in rfc850-date
  format, which uses a two-digit year, MUST interpret a timestamp that appears
  to be more than 50 years in the future as representing the most recent year
  in the past that had the same last two digits." (D-039)
- The weekday must match the date (`WeekdayMismatch`).
- Stricter than the ABNF: the grammar's comment allows second `60`
  (`; 00:00:00 - 23:59:60 (leap second)`), but parse-net refuses it unless
  `Limits::allow_leap_second`, which reads it as `59`. No HTTP sender
  generates one (senders write `Date` from a Unix clock), and Unix time has
  no 60th second to hold it (D-053).
- Years 1 to 9999; dates before 1970 are valid (verdict V-2).
- Functions: `http_date` (`std`), `http_date_at`, `HttpDate::parse_at`,
  `parse_imf_fixdate`, `HttpDate::format`, `write_to`.

## Host

RFC 9110 section 7.2; RFC 3986 section 3.2.2.

```abnf
Host        = uri-host [ ":" port ]
host        = IP-literal / IPv4address / reg-name
IP-literal  = "[" ( IPv6address / IPvFuture  ) "]"
IPvFuture   = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
reg-name    = *( unreserved / pct-encoded / sub-delims )
port        = *DIGIT
```

- Case: "The scheme and host are case-insensitive" (RFC 9110 section 4.2.3);
  `Host::eq_ignore_ascii_case` compares names that way and IP literals by
  value.
- Stricter than the ABNF, for the reasons in D-031 and D-040: `@` anywhere is
  `InvalidHost` (userinfo is not part of `Host`, threat T15); a name whose
  last label is all digits or `0x` hex must be canonical dotted-decimal IPv4,
  so `127.1` and `0x7f.0.0.1` are refused rather than read as names that a
  later resolver turns into addresses (the WHATWG URL "ends in a number"
  rule).
- `port` is `*DIGIT`; an empty port (`a.com:`) is reported as
  `HostPort::empty_port`, and a value above 65535 is `InvalidPort`.
- `parse_host_port` strips OWS like every field parser; `parse_uri_host_port`
  strips nothing, for request targets and URL authorities (D-044).
- Functions: `parse_host_port`, `host::parse_uri_host_port`,
  `HostPort::write_to`.

## Percent-encoding

RFC 3986 section 2.1.

```abnf
pct-encoded = "%" HEXDIG HEXDIG
```

- Either hex case decodes: "The uppercase hexadecimal digits 'A' through
  'F' are equivalent to the lowercase digits 'a' through 'f', respectively."
  `pct::decode_lower_only_into` accepts lowercase only, for RFC 9651 Display
  Strings.
- `decode_utf8_into` refuses decoded bytes that are not UTF-8, overlong forms
  and surrogates included.
- Functions: `pct::validate`, `validate_with`, `decoded_len`, `decode_into`,
  `decode_utf8_into`, `decode_lower_only_into`, `encode`.

## Entity tags and conditions

RFC 9110 sections 8.8.3, 13.1.1, and 13.1.2.

```abnf
entity-tag    = [ weak ] opaque-tag
weak          = %x57.2F ; W/
opaque-tag    = DQUOTE *etagc DQUOTE
etagc         = "!" / %x23-7E ; '#'-'~'
              / obs-text
If-Match      = "*" / #entity-tag
If-None-Match = "*" / #entity-tag
```

- `W/` is case-sensitive ("its opaque value with "W/" (case-sensitive)"):
  `w/"x"` is `InvalidEntityTag`. An unquoted tag is refused.
- Comparison follows the section 8.8.3.2 table: `strong_eq`, `weak_eq`;
  `If-Match` uses strong comparison, `If-None-Match` weak.
- Functions: `EntityTag::parse`, `strong`, `weak`, `etag::parse_condition`,
  `if_match`, `if_none_match`.

## Ranges

RFC 9110 sections 14.1.2, 14.4, and 13.1.5.

```abnf
Range             = ranges-specifier
ranges-specifier  = range-unit "=" range-set
range-set         = range-spec *( OWS "," OWS range-spec )
range-spec        = int-range / suffix-range / other-range
int-range         = first-pos "-" [ last-pos ]
suffix-range      = "-" suffix-length
Content-Range     = range-unit SP ( range-resp / unsatisfied-range )
range-resp        = incl-range "/" ( complete-length / "*" )
unsatisfied-range = "*/" complete-length
If-Range          = entity-tag / HTTP-date
```

- "Range units are compared in a case-insensitive fashion." Another unit
  parses (its set is kept raw) and resolves to `RangeOutcome::NotBytes`.
- Resolution refuses abusive sets as section 14.2 allows ("A server that
  supports range requests MAY ignore or reject a Range header field that
  contains [...] a ranges-specifier with more than two overlapping ranges, or
  a set of many small ranges that are not listed in ascending order"): more
  ranges than
  `Limits::max_ranges` (default 16), more than two overlapping ranges, or
  more than two descents in order give `RangeOutcome::Ignore` (serve 200).
- `If-Range` dates are IMF-fixdate only (D-035): the date is compared exactly
  with the server's own `Last-Modified`.
- Functions: `range::parse`, `resolve`, `byte_ranges`, `parse_content_range`,
  `write_content_range`, `write_unsatisfied`, `parse_if_range`.

## ext-value

RFC 8187 section 3.2.1.

```abnf
ext-value     = charset  "'" [ language ] "'" value-chars
charset       = "UTF-8" / mime-charset
mime-charset  = 1*mime-charsetc
value-chars   = *( pct-encoded / attr-char )
attr-char     = ALPHA / DIGIT
              / "!" / "#" / "$" / "&" / "+" / "-" / "."
              / "^" / "_" / "`" / "|" / "~"
```

- Charset names are case-insensitive. UTF-8 and ISO-8859-1 are decoded
  (ISO-8859-1 transcoded to UTF-8, D-010); another charset is
  `UnsupportedCharset`. Writers produce UTF-8 only, as section 3.2.1 requires
  of producers.
- Decoded C0 controls, DEL, and C1 controls are refused (`ControlInDecoded`):
  a file name is shown to people and handed to file systems.
- Functions: `ExtValue::parse`, `decode_into`, `max_decoded_len`,
  `write_ext_value`.

## Content-Disposition

RFC 6266 section 4.1 (and RFC 7578 section 4.2 for `form-data`).

```abnf
content-disposition = "Content-Disposition" ":"
                       disposition-type *( ";" disposition-parm )
disposition-type    = "inline" | "attachment" | disp-ext-type
                    ; case-insensitive
disp-ext-type       = token
disposition-parm    = filename-parm | disp-ext-parm
filename-parm       = "filename" "=" value
                    | "filename*" "=" ext-value
disp-ext-parm       = token "=" value
                    | ext-token "=" ext-value
```

- The parameters are read with the RFC 9110 rule above (so no whitespace
  around `=`, empty parameters accepted); tc2231 classes C-1 and C-2.
- `filename*` is preferred over `filename` (section 4.3) and must decode at
  parse time; an undecodable `filename*` refuses the field (tc2231 C-5).
- RFC 2231 continuations (`filename*0=`) are not joined: RFC 6266 takes the
  RFC 5987 profile, which leaves them out (tc2231 C-3). They belong to the
  mail layer.
- A plain `filename` decodes as UTF-8 or not at all; no charset is guessed
  for `obs-text` (tc2231 C-4).
- The name is returned as sent: `Filename::has_path_separator` and
  `has_bidi_control` report what a caller must check (section 4.3: "Recipients
  SHOULD strip or replace character sequences that are known to cause
  confusion"); the policy is the caller's.
- Every case of the greenbytes tc2231 suite has a verdict in
  `tests/differential/tc2231.md`.
- Functions: `parse_content_disposition`, `disposition::parse`,
  `ContentDisposition::filename`, `Filename::decode_into`,
  `write_attachment`.

## Cache-Control

RFC 9111 section 5.2.

```abnf
Cache-Control   = #cache-directive
cache-directive = token [ "=" ( token / quoted-string ) ]
delta-seconds   = 1*DIGIT
```

- "Cache directives are identified by a token, to be compared
  case-insensitively".
- Both argument forms are read for every directive.
- `delta-seconds` saturates at 2147483648 (RFC 9111 section 1.2.2).
- A repeated directive keeps its first value and sets `duplicates`: "When
  there is more than one value present for a given directive [...] either
  the first occurrence should be used or the response should be considered
  stale" (section 4.2.1). An argument that does not parse leaves the field
  `None` and sets `invalid_values`. Conflicting directives are all reported;
  the same section says "the most restrictive directive should be honored",
  which is the cache's decision.
- Functions: `response_cache_control`, `request_cache_control`,
  `cache::parse`, `response_lines`, `request_lines`, `cache::Writer`.

## Forwarded

RFC 7239 sections 4 and 6.

```abnf
Forwarded         = 1#forwarded-element
forwarded-element = [ forwarded-pair ] *( ";" [ forwarded-pair ] )
forwarded-pair    = token "=" value
value             = token / quoted-string
node              = nodename [ ":" node-port ]
nodename          = IPv4address / "[" IPv6address "]" /
                    "unknown" / obfnode
obfnode           = "_" 1*( ALPHA / DIGIT / "." / "_" / "-")
node-port         = port / obfport
port              = 1*5DIGIT
obfport           = "_" 1*(ALPHA / DIGIT / "." / "_" / "-")
```

- Parameter names are case-insensitive; "Each parameter MUST NOT occur more
  than once per field-value": a repeat is refused.
- An IPv6 address or a node with a port must be quoted ("an IPv6 address and
  any nodename with node-port specified MUST be quoted"); unquoted, it is
  refused.
- `client_address` walks the chain from the right through trusted proxies
  only, and stops at `unknown`, an obfuscated node, or an element without
  `for` (`ClientAddress::Unknown`). A malformed chain is an error: use the
  peer address.
- Functions: `forwarded::parse`, `client_address`,
  `client_address_detailed`.

## X-Forwarded-*

No RFC: the de facto forms. `X-Forwarded-For` is a comma-separated list of IP
addresses, each optionally with a port (IPv6 in brackets when it has one);
`X-Forwarded-Proto` is `http` or `https`; `X-Forwarded-Host` follows the
`Host` grammar. The leftmost address is whatever the client sent; only
addresses appended by trusted proxies are meaningful, which is why the
resolver walks from the right.

- Functions: `forwarded::parse_x_forwarded_for`, `parse_x_forwarded_proto`,
  `parse_x_forwarded_host`, `client_address` with
  `ForwardedSource::XForwardedFor`.

## Authorization and WWW-Authenticate

RFC 9110 section 11.

```abnf
credentials      = auth-scheme [ 1*SP ( token68 / [ auth-param *( OWS ","
                   OWS auth-param ) ] ) ]
challenge        = auth-scheme [ 1*SP ( token68 / [ auth-param *( OWS ","
                   OWS auth-param ) ] ) ]
WWW-Authenticate = #challenge
auth-scheme      = token
auth-param       = token BWS "=" BWS ( token / quoted-string )
token68          = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" )
                   *"="
```

- The scheme "uses a case-insensitive token"; parameter names are "matched
  case-insensitively and each parameter name MUST only occur once per
  challenge": a repeat is refused, and `write_challenge` refuses one too
  (`WriteError::DuplicateName`).
- `BWS` around `=` is accepted here, as the rule says, unlike section 5.6.6
  parameters.
- `Debug` prints the scheme and parameter names only: credentials are
  secrets.
- Functions: `auth::parse_credentials`, `bearer`, `basic`,
  `auth::parse_challenges`, `write_challenge`, `write_challenge_token68`.

## Origin

RFC 6454 section 7.1.

```abnf
origin              = "Origin:" OWS origin-list-or-null OWS
origin-list-or-null = %x6E %x75 %x6C %x6C / origin-list
origin-list         = serialized-origin *( SP serialized-origin )
serialized-origin   = scheme "://" host [ ":" port ]
```

- `null` is case-sensitive (the `%x` form). The scheme is compared
  case-insensitively; the host by the `Host` rules.
- One origin only: a space-separated `origin-list` is refused. Browsers send
  one (the Fetch standard serializes a single origin), and a list would leave
  the caller to pick which origin to trust; refusing fails closed for CSRF
  checks.
- A path, query, fragment, trailing `/`, userinfo, or empty port is refused
  (`InvalidOrigin`, `InvalidHost`).
- Functions: `parse_origin`, `Origin::same_origin`, `effective_port`,
  `write_to`.

## Structured Field Values

RFC 9651. The RFC's ABNF is non-normative ("If there is disagreement between
the parsing algorithms and ABNF, the specified algorithms take precedence"),
so parse-net implements the section 4.2 algorithms; the ABNF is quoted for
reference.

```abnf
sf-list       = list-member *( OWS "," OWS list-member )
list-member   = sf-item / inner-list
inner-list    = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")"
                parameters
parameters    = *( ";" *SP parameter )
parameter     = param-key [ "=" param-value ]
key           = ( lcalpha / "*" )
                *( lcalpha / DIGIT / "_" / "-" / "." / "*" )
sf-dictionary = dict-member *( OWS "," OWS dict-member )
dict-member   = member-key ( parameters / ( "=" member-value ))
sf-item       = bare-item parameters
bare-item     = sf-integer / sf-decimal / sf-string / sf-token
                / sf-binary / sf-boolean / sf-date / sf-displaystring
sf-integer       = ["-"] 1*15DIGIT
sf-decimal       = ["-"] 1*12DIGIT "." 1*3DIGIT
sf-string        = DQUOTE *( unescaped / "%" / bs-escaped ) DQUOTE
sf-token         = ( ALPHA / "*" ) *( tchar / ":" / "/" )
sf-binary        = ":" base64 ":"
sf-boolean       = "?" ( "0" / "1" )
sf-date          = "@" sf-integer
sf-displaystring = "%" DQUOTE *( unescaped / "\" / pct-encoded )
                   DQUOTE
base64           = *( ALPHA / DIGIT / "+" / "/" ) *"="
pct-encoded      = "%" lc-hexdig lc-hexdig
```

- Keys are lowercase; everything else is case-sensitive.
- The field is accepted or refused whole: "If parsing fails, either the
  entire field value MUST be ignored (i.e., treated as if the field were not
  present in the section), or alternatively the complete HTTP message MUST be
  treated as malformed."
- Only SP is discarded around the value and inside inner lists; OWS (SP and
  HTAB) around list and dictionary commas.
- Byte sequences: missing padding and non-zero pad bits are accepted ("parsers
  SHOULD NOT fail when "=" padding is not present" and "SHOULD NOT fail when
  non-zero pad bits are present"); padding that does not complete the last
  quantum, or extra padding, is refused (`InvalidBase64`).
- Dates are limited to years 1 to 9999, the `HttpDate` range (D-030); the
  RFC sets no range of its own.
- Display Strings: lowercase hex only, and the decoded bytes must be UTF-8.
- Limits: each `Limits` default meets the section 3 minimum (1024 list and
  dictionary members, 256 inner-list members and parameters, keys of at least
  64 characters with a default of 256, 1024-character strings, 512-character
  tokens, 16384-byte byte sequences).
- Conformance: the httpwg structured-field-tests suite runs whole, and every
  list and dictionary again split into one line per member
  (`tests/sfv_conformance.rs`); the 4 optional cases parse-net refuses are
  listed in `tests/sfv/README.md`.
- Functions: `sfv::parse_item`, `parse_list`, `parse_dictionary`,
  `parse_list_lines`, `parse_dictionary_lines`, `sfv::Parser` (limits and
  `visit_*`), `Dictionary::get`, `index_into`.
- The writer (`sfv::Writer`) produces the canonical form of section 4.1;
  every accepted parse case is serialized again and compared with the
  suite's `canonical`, and the four serialisation-only files run whole.

## Link

RFC 8288 section 3.

```abnf
Link       = #link-value
link-value = "<" URI-Reference ">" *( OWS ";" OWS link-param )
link-param = token BWS [ "=" BWS ( token / quoted-string ) ]
```

- Parameter names are case-insensitive; a parameter value is optional
  (`; crossorigin`), and `BWS` around `=` is accepted, as the rule says.
- `rel`: "The rel parameter MUST be present but MUST NOT appear more than
  once in a given link-value; occurrences after the first MUST be ignored by
  parsers." `rel` and the other single-valued parameters (`anchor`,
  `media`, `title`, `title*`, `type`) return the first occurrence;
  `hreflang` may repeat and returns every one.
- The target is checked as RFC 3986 characters with well-formed `%XX` and
  returned as sent. RFC 8288 section 3.1 says a relative reference must be
  resolved; parse-net leaves resolution to the caller (url-lib), since it
  needs the base URI.
- `title*` is an RFC 8187 value decoded at parse time, as `filename*` is.
- Functions: `parse_link`, `LinkValue`, `link::write_link`.

## Email dates

RFC 5322 section 3.3, and section 4.3 for the obsolete forms.

```abnf
date-time   =   [ day-of-week "," ] date time [CFWS]
day-of-week =   ([FWS] day-name) / obs-day-of-week
date        =   day month year
day         =   ([FWS] 1*2DIGIT FWS) / obs-day
year        =   (FWS 4*DIGIT FWS) / obs-year
time        =   time-of-day zone
time-of-day =   hour ":" minute [ ":" second ]
zone        =   (FWS ( "+" / "-" ) 4DIGIT) / obs-zone
```

- Names are case-insensitive (RFC 5234 quoted strings); comments and folding
  whitespace may separate the tokens; the weekday, when present, must match.
- `-0000` sets `offset_unknown`: the time is UTC and the local zone is not
  known (section 3.3). Second `60` is read as `59` with `leap_second` set.
- Years outside 1 to 9999 are `OutOfRange`, the range every date type here
  shares.
- With `obsolete`: two-digit years below 50 are 20xx and others 19xx, and
  three-digit years add 1900 (section 4.3, D-036); `UT`, `GMT`, and the North
  American zones; the military zones are read as `-0000`, because "they
  SHOULD all be considered equivalent to "-0000" unless there is out-of-band
  information confirming their meaning." `J` is not a zone.
- Writers always produce the weekday, a two-digit day, a four-digit year,
  seconds, and a numeric zone.
- Functions: `MailDate::parse`, `new`, `format`, `write_to`.

## Email header values

RFC 5322 sections 2.1.1, 2.2.3, and 3.6.8.

```abnf
field-name = 1*ftext
ftext      = %d33-57 / %d59-126
```

- A header value for writing has no CR, LF, or NUL and is at most 998
  bytes (the line limit of section 2.1.1, which `Name: ` also counts
  toward): `mail::check_header_value`.
- Unfolding removes a CRLF only when WSP follows it (section 2.2.3); any
  other CR or LF is refused: `mail::unfold`.
- Folding inserts CRLF before existing whitespace so lines stay within 78
  columns where the words allow, never makes a line of only whitespace, and
  refuses a word that cannot fit in 998: `mail::fold`.
- `mail::is_field_name`.

## Encoded words

RFC 2047 sections 2 to 6.

```abnf
encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
charset      = token
encoding     = token
```

- An encoded word is decoded only when it stands between whitespace (or
  the start or end of the text) and has the whole shape; anything else is
  ordinary text. Inside a quoted string nothing is decoded (section 5).
- "any 'linear-white-space' that separates a pair of adjacent
  'encoded-word's is ignored" (section 6.2); other whitespace is kept.
- Q: `_` is a space and `=XX` a byte (either hex case read); each UTF-8 word
  must hold whole characters. Charsets UTF-8, US-ASCII, and ISO-8859-1
  (transcoded), with an optional RFC 2231 `*language`. Decoded CR, LF, and
  NUL are refused. `B` waits for encode-lib (D-002).
- Encoding: "An 'encoded-word' may not be more than 75 characters long";
  words split only between characters and are separated by CRLF SP.
  Printable ASCII without `=?` is written as it is.
- Functions: `mail::decode_words`, `mail::encode_words`.

## MIME parameters

RFC 2045 section 5.1 and RFC 2231 sections 3 to 7.

```abnf
parameter := attribute "=" value
attribute := token
value     := token / quoted-string

regular-parameter-name := attribute [section]
section                := initial-section / other-sections
initial-section        := "*0"
other-sections         := "*" ("1" / "2" / "3" / "4" / "5" /
                               "6" / "7" / "8" / "9") *DIGIT)
extended-initial-name  := attribute [initial-section] "*"
extended-other-names   := attribute other-sections "*"
```

- Tokens use the MIME class (which allows `{` and `}`, unlike HTTP
  `tchar`); names are case-insensitive; comments and folding whitespace may
  separate the parts; a name given twice (sections counted one by one) is
  refused.
- A section number with a leading zero (`*01`) is not a section (the
  grammar excludes it), so that parameter is an ordinary one.
- Continuations are validated at parse time: sections run from 0 with no
  gap, at most `Limits::max_rfc2231_sections` (32 by default), a charset
  only on section 0, and the joined value decodes. Charsets: UTF-8,
  US-ASCII, ISO-8859-1 (transcoded).
- Writing picks a token, a quoted string, a single `name*=UTF-8''...`, or
  `name*0*`, `name*1*` sections on folded lines, whichever keeps the line
  within 78 columns.
- Functions: `mail::parse_mime_params`, `MimeParams::get`, `decode_into`,
  `Continuation`, `mail::write_mime_param`.

## Addresses and message identifiers

RFC 5322 sections 3.2.3, 3.4, and 3.6.4; RFC 6532; RFC 5321 section
4.5.3.1.

```abnf
atext          = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" /
                 "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" /
                 "{" / "|" / "}" / "~"
dot-atom-text  = 1*atext *("." 1*atext)
addr-spec      = local-part "@" domain
local-part     = dot-atom / quoted-string / obs-local-part
domain         = dot-atom / domain-literal / obs-domain
dtext          = %d33-90 / %d94-126 / obs-dtext
mailbox        = name-addr / addr-spec
name-addr      = [display-name] angle-addr
msg-id         = [CFWS] "<" id-left "@" id-right ">" [CFWS]
id-left        = dot-atom-text / obs-id-left
id-right       = dot-atom-text / no-fold-literal / obs-id-right
no-fold-literal = "[" *dtext "]"
```

- Stricter than the ABNF, for login validation (D-008): no obsolete forms,
  no comments, and no folding inside an `addr-spec`; the local part is at
  most 64 octets and the address at most 254 (RFC 5321).
- Non-ASCII is accepted only as valid UTF-8 (RFC 6532), never as raw
  obs-text; `AddrSpec::is_ascii` says whether SMTPUTF8 is needed.
- A mailbox's display name is checked as a phrase (atoms, `.`, quoted
  strings, comments, UTF-8) and returned raw, encoded words included.
- Functions: `mail::parse_addr_spec`, `parse_mailbox`, `parse_msg_id`,
  `write_msg_id`.