Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/media.rs

29.5 KiB, 137 runs

created by r1870400018:591, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The specification for Media Types is:
2//! https://www.iana.org/assignments/media-types/media-types.xhtml
3//! RFC 2046 Section 1:
4//! "In general, the top-level media type is used to declare the general
5//! type of data, while the subtype specifies a specific format for that
6//! type of data. Thus, a media type of "image/xyz" is enough to tell a
7//! user agent that the data is an image, even if the user agent has no
8//! knowledge of the specific image format "xyz". Such information can
9//! be used, for example, to decide whether or not to show a user the raw
10//! data from an unrecognized subtype -- such an action might be
11//! reasonable for unrecognized subtypes of "text", but not for
12//! unrecognized subtypes of "image" or "audio". For this reason,
13//! registered subtypes of "text", "image", "audio", and "video" should
14//! not contain embedded information that is really of a different type.
15//! Such compound formats should be represented using the "multipart" or
16//! "application" types."
17//!
18//! TODO Complete types.
19use crate::charset::Charset;
20
21use oxedyne_fe2o3_core::prelude::*;
22
23use std::{
24 fmt::{
25 self,
26 Display,
27 },
28 str::FromStr,
29};
30
31
32pub const MEDIA_PLAIN_TEXT: ContentTypeValue =
33 ContentTypeValue::MediaType((
34 MediaType::Text(Text::Plain),
35 Some(Charset::Utf_8),
36 ));
37
38/// Encapsulator for "Content-Type" header.
39#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
40pub enum ContentTypeValue {
41 MediaType((MediaType, Option<Charset>)),
42 Multipart((Multipart, String)),
43}
44
45impl Display for ContentTypeValue {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 Self::MediaType((mt, cs_opt)) => match cs_opt {
49 Some(cs) => write!(f, "{}; charset={}", mt, cs),
50 None => write!(f, "{}", mt),
51 },
52 // `mt` here is the SUBTYPE, whose Display is "form-data", so the top
53 // level has to be written. Without it a parsed multipart header was
54 // re-emitted as `form-data; boundary=...`, which the next hop cannot
55 // parse -- see the note on `Multipart`'s own Display.
56 Self::Multipart((mt, b)) => write!(f, "multipart/{}; boundary={}", mt, b),
57 }
58 }
59}
60
61/// ╭────────────────────────────╮
62/// │ IANA Top Level Media Types │
63/// ╰────────────────────────────╯
64///
65/// RFC 2046 Section 2
66/// https://www.rfc-editor.org/rfc/rfc2046.html#section-2
67///
68#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
69pub enum MediaType {
70 // Discrete
71 Application(Application),
72 Audio(Audio),
73 Font(Font),
74 Image(Image),
75 Model(Model),
76 Text(Text),
77 Video(Video),
78 // Composite
79 //Message(Message),
80 Multipart(Multipart),
81}
82
83impl Display for MediaType {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, "{}", match self {
86 Self::Application(inner) => fmt!("application/{}", inner),
87 Self::Audio(inner) => fmt!("audio/{}", inner),
88 Self::Font(inner) => fmt!("font/{}", inner),
89 Self::Image(inner) => fmt!("image/{}", inner),
90 Self::Model(inner) => fmt!("model/{}", inner),
91 Self::Text(inner) => fmt!("text/{}", inner),
92 Self::Video(inner) => fmt!("video/{}", inner),
93 Self::Multipart(inner) => fmt!("multipart/{}", inner),
94 })
95 }
96}
97
98impl FromStr for MediaType {
99 type Err = Error<ErrTag>;
100
101 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
102 Ok(match s.split_once('/') {
103 Some((left, right)) => match left {
104 "application" => Self::Application(res!(Application::from_str(right))),
105 "audio" => Self::Audio(res!(Audio::from_str(right))),
106 "font" => Self::Font(res!(Font::from_str(right))),
107 "image" => Self::Image(res!(Image::from_str(right))),
108 "model" => Self::Model(res!(Model::from_str(right))),
109 "text" => Self::Text(res!(Text::from_str(right))),
110 "video" => Self::Video(res!(Video::from_str(right))),
111 "multipart" => Self::Multipart(res!(Multipart::from_str(right))),
112 _ => return Err(err!(
113 "Unrecognised Media type '{}' in '{}'.", left, s;
114 IO, Network, Unknown, Input)),
115 },
116 _ => return Err(err!(
117 "Invalid Media type '{}', '/' character not found.", s;
118 IO, Network, Invalid, Input)),
119 })
120 }
121}
122
123impl MediaType {
124 pub fn is_text(&self) -> bool {
125 match self {
126 Self::Text(_) |
127 Self::Image(Image::SvgXml) |
128 Self::Application(Application::Json) |
129 Self::Application(Application::JsonLd) |
130 Self::Application(Application::ManifestJson) |
131 Self::Application(Application::FormUrlEncoded) |
132 Self::Application(Application::Xml) => true,
133 // Structured syntax suffixes per RFC 6838 §4.2.8: anything
134 // that looks like `foo+json` or `foo+xml` is text-shaped, so
135 // body dumps like `application/problem+json` (RFC 7807) and
136 // `application/jose+json` (RFC 7515) can be logged as text
137 // rather than binary.
138 Self::Application(Application::Other(s)) => {
139 s.ends_with("+json") || s.ends_with("+xml")
140 },
141 // TODO complete list
142 _ => false,
143 }
144 }
145
146 /// Is a body of this type worth compressing on the way out?
147 ///
148 /// Text of every shape is, and so is WebAssembly, whose binary format is
149 /// mostly indices and opcodes and halves under DEFLATE. Everything already
150 /// compressed is not: a second pass over a PNG, a JPEG, a WebP, an AVIF, a
151 /// WOFF2, an Opus stream or a Zip spends the processor and *adds* bytes,
152 /// since a compressor that finds no redundancy still pays for its own
153 /// framing. RFC 9110 §8.4.1 names content coding as an aid to transfer, not
154 /// a second encoding of an already-encoded representation.
155 ///
156 /// The default is `false`, so a type this crate does not model is sent as it
157 /// is. That is the safe way round: a missed saving costs bandwidth, whereas
158 /// compressing something already compressed costs the processor and gains
159 /// nothing.
160 pub fn is_compressible(&self) -> bool {
161 match self {
162 // Every text subtype, plus the text-shaped application subtypes and
163 // SVG, which `is_text` already recognises.
164 _ if self.is_text() => true,
165 // Binary, but highly redundant: a module is mostly LEB128 indices.
166 Self::Application(Application::Wasm) => true,
167 // Structured documents that travel uncompressed.
168 Self::Application(Application::Sql) |
169 Self::Application(Application::OpenDocument) => true,
170 // The uncompressed font formats. WOFF and WOFF2 carry their own
171 // compression and are deliberately absent.
172 Self::Font(Font::Collection) |
173 Self::Font(Font::Otf) |
174 Self::Font(Font::Sfnt) |
175 Self::Font(Font::Ttf) => true,
176 // An uncompressed raster format, unlike every other image type.
177 Self::Image(Image::Tiff) => true,
178 Self::Model(Model::Obj) => true,
179 _ => false,
180 }
181 }
182}
183
184/// ╭────────────────────────────────────────────╮
185/// │ IANA Top Level Media Type: Application │
186/// │ Subtypes │
187/// ╰────────────────────────────────────────────╯
188///
189/// The `Other(String)` variant accepts any subtype we do not have a named
190/// variant for -- e.g. `application/problem+json` (RFC 7807, used by ACME
191/// CAs for error responses), `application/jose+json` (RFC 7515), or any
192/// future IANA subtype. This matters for clients like the ACME driver
193/// in [`crate::acme::client`] which must be able to receive and parse
194/// responses with arbitrary Content-Type values; a strict enum would
195/// refuse the response at parse time and the caller would never see the
196/// body. Keeping known subtypes as dedicated variants preserves the
197/// ergonomics of pattern matching for code that cares.
198#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
199pub enum Application {
200 Json,
201 JsonLd,
202 /// `application/manifest+json`, registered with IANA by the W3C Web
203 /// Application Manifest specification.
204 ManifestJson,
205 Pdf,
206 Sql,
207 MicrosoftDocument,
208 MicrosoftPresentation,
209 MicrosoftSpreadsheet,
210 OpenDocument,
211 OpenXmlDocument,
212 OpenXmlPresentation,
213 OpenXmlSpreadsheet,
214 FormUrlEncoded,
215 /// `application/wasm`, registered with IANA by the WebAssembly Core
216 /// specification. A browser compiles a module as it arrives only when the
217 /// server says this; under any other type `compileStreaming` refuses the
218 /// response and the whole module must be buffered first.
219 Wasm,
220 Xml,
221 Zip,
222 Zstd,
223 /// Subtype not explicitly modelled by this crate; the contained
224 /// string is the raw subtype text after the `application/` prefix.
225 Other(String),
226}
227
228impl Display for Application {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 match self {
231 Self::Json => write!(f, "json"),
232 Self::JsonLd => write!(f, "ld+json"),
233 Self::ManifestJson => write!(f, "manifest+json"),
234 Self::Pdf => write!(f, "pdf"),
235 Self::Sql => write!(f, "sql"),
236 Self::MicrosoftDocument => write!(f, "msword"),
237 Self::MicrosoftPresentation => write!(f, "vnd.ms-powerpoint"),
238 Self::MicrosoftSpreadsheet => write!(f, "vnd.ms-excel"),
239 Self::OpenDocument => write!(f, "vnd.oasis.opendocument.text"),
240 Self::OpenXmlDocument => write!(f, "vnd.openxmlformats-officedocument.wordprocessingml.document"),
241 Self::OpenXmlPresentation => write!(f, "vnd.openxmlformats-officedocument.presentationml.presentation"),
242 Self::OpenXmlSpreadsheet => write!(f, "vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
243 Self::FormUrlEncoded => write!(f, "x-www-form-urlencoded"),
244 Self::Wasm => write!(f, "wasm"),
245 Self::Xml => write!(f, "xml"),
246 Self::Zip => write!(f, "zip"),
247 Self::Zstd => write!(f, "zstd"),
248 Self::Other(s) => write!(f, "{}", s),
249 }
250 }
251}
252
253impl FromStr for Application {
254 type Err = Error<ErrTag>;
255
256 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
257 Ok(match s {
258 "json" => Self::Json,
259 "ld+json" => Self::JsonLd,
260 "manifest+json" => Self::ManifestJson,
261 "pdf" => Self::Pdf,
262 "sql" => Self::Sql,
263 "msword" => Self::MicrosoftDocument,
264 "vnd.ms-powerpoint" => Self::MicrosoftPresentation,
265 "vnd.ms-excel" => Self::MicrosoftSpreadsheet,
266 "vnd.oasis.opendocument.text" => Self::OpenDocument,
267 "vnd.openxmlformats-officedocument.wordprocessingml.document" => Self::OpenXmlDocument,
268 "vnd.openxmlformats-officedocument.presentationml.presentation" => Self::OpenXmlPresentation,
269 "vnd.openxmlformats-officedocument.spreadsheetml.sheet" => Self::OpenXmlSpreadsheet,
270 "x-www-form-urlencoded" => Self::FormUrlEncoded,
271 "wasm" => Self::Wasm,
272 "xml" => Self::Xml,
273 "zip" => Self::Zip,
274 "zstd" => Self::Zstd,
275 // Any other IANA subtype: stored verbatim so callers that do
276 // care about it can still read the raw string, and the HTTP
277 // message parser can construct a complete HttpMessage instead
278 // of failing the whole response. Structured JSON-ish subtypes
279 // like `problem+json` and `jose+json` arrive here.
280 other => Self::Other(other.to_string()),
281 })
282 }
283}
284
285/// ╭────────────────────────────────────────────╮
286/// │ IANA Top Level Media Type: Audio │
287/// │ Subtypes │
288/// ╰────────────────────────────────────────────╯
289#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
290pub enum Audio {
291 Aac,
292 Flac,
293 Mp4,
294 Mpeg,
295 Ogg,
296 Wav,
297 Webm,
298}
299
300/// The subtype alone, because [`MediaType`] writes the `audio/` before it.
301/// Writing it here as well once put `audio/audio/mpeg` on the wire, which no
302/// player recognises as anything.
303impl Display for Audio {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 write!(f, "{}", match self {
306 Self::Aac => "aac",
307 Self::Flac => "flac",
308 Self::Mp4 => "mp4",
309 Self::Mpeg => "mpeg",
310 Self::Ogg => "ogg",
311 Self::Wav => "wav",
312 Self::Webm => "webm",
313 })
314 }
315}
316
317impl FromStr for Audio {
318 type Err = Error<ErrTag>;
319
320 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
321 Ok(match s {
322 "aac" => Self::Aac,
323 "flac" => Self::Flac,
324 "mp4" => Self::Mp4,
325 "mpeg" => Self::Mpeg,
326 "ogg" => Self::Ogg,
327 "wav" => Self::Wav,
328 // WAVE has never had one name. IANA registers `audio/vnd.wave`
329 // (RFC 2361) and lists `audio/wav`, `audio/wave` and `audio/x-wav`
330 // as the names in use; the WHATWG mime sniffing standard emits
331 // `audio/wave`. All three are read, and `audio/wav` is written.
332 "wave" | "x-wav" | "vnd.wave"
333 => Self::Wav,
334 "webm" => Self::Webm,
335 _ => return Err(err!(
336 "Unrecognised Audio Media subtype '{}'.", s;
337 IO, Network, Unknown, Input)),
338 })
339 }
340}
341
342/// ╭────────────────────────────────────────────╮
343/// │ IANA Top Level Media Type: Video │
344/// │ Subtypes │
345/// ╰────────────────────────────────────────────╯
346///
347/// A recording served under the wrong type is a recording no browser will play,
348/// however well the bytes are delivered, so the seekable media this crate exists
349/// to serve needs its types named.
350#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
351pub enum Video {
352 Mp4,
353 Mpeg,
354 Ogg,
355 Quicktime,
356 Webm,
357 /// Matroska, which IANA has not registered but every player expects.
358 XMatroska,
359 /// AVI, likewise unregistered and likewise expected.
360 XMsVideo,
361}
362
363impl Display for Video {
364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365 write!(f, "{}", match self {
366 Self::Mp4 => "mp4",
367 Self::Mpeg => "mpeg",
368 Self::Ogg => "ogg",
369 Self::Quicktime => "quicktime",
370 Self::Webm => "webm",
371 Self::XMatroska => "x-matroska",
372 Self::XMsVideo => "x-msvideo",
373 })
374 }
375}
376
377impl FromStr for Video {
378 type Err = Error<ErrTag>;
379
380 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
381 Ok(match s {
382 "mp4" => Self::Mp4,
383 "mpeg" => Self::Mpeg,
384 "ogg" => Self::Ogg,
385 "quicktime" => Self::Quicktime,
386 "webm" => Self::Webm,
387 "x-matroska" => Self::XMatroska,
388 "x-msvideo" => Self::XMsVideo,
389 _ => return Err(err!(
390 "Unrecognised Video Media subtype '{}'.", s;
391 IO, Network, Unknown, Input)),
392 })
393 }
394}
395
396/// ╭────────────────────────────────────────────╮
397/// │ IANA Top Level Media Type: Font │
398/// │ Subtypes │
399/// ╰────────────────────────────────────────────╯
400#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
401pub enum Font {
402 Collection,
403 Otf,
404 Sfnt,
405 Ttf,
406 Woff,
407 Woff2,
408}
409
410impl Display for Font {
411 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 write!(f, "{}", match self {
413 Self::Collection => "collection",
414 Self::Otf => "otf",
415 Self::Sfnt => "sfnt",
416 Self::Ttf => "ttf",
417 Self::Woff => "woff",
418 Self::Woff2 => "woff2",
419 })
420 }
421}
422
423impl FromStr for Font {
424 type Err = Error<ErrTag>;
425
426 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
427 Ok(match s {
428 "collection" => Self::Collection,
429 "otf" => Self::Otf,
430 "sfnt" => Self::Sfnt,
431 "ttf" => Self::Ttf,
432 "woff" => Self::Woff,
433 "woff2" => Self::Woff2,
434 _ => return Err(err!(
435 "Unrecognised Font Media subtype '{}'.", s;
436 IO, Network, Unknown, Input)),
437 })
438 }
439}
440
441/// ╭────────────────────────────────────────────╮
442/// │ IANA Top Level Media Type: Image │
443/// │ Subtypes │
444/// ╰────────────────────────────────────────────╯
445#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
446pub enum Image {
447 Avif,
448 Gif,
449 /// `image/vnd.microsoft.icon`, the IANA registration for the favicon
450 /// format; `image/x-icon` is the unregistered name browsers also accept.
451 Icon,
452 Jpeg,
453 Png,
454 SvgXml,
455 Tiff,
456 /// `image/webp`, registered with IANA by RFC 9649.
457 Webp,
458}
459
460impl Display for Image {
461 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462 write!(f, "{}", match self {
463 Self::Avif => "avif",
464 Self::Gif => "gif",
465 Self::Icon => "vnd.microsoft.icon",
466 Self::Jpeg => "jpeg",
467 Self::Png => "png",
468 Self::SvgXml => "svg+xml",
469 Self::Tiff => "tiff",
470 Self::Webp => "webp",
471 })
472 }
473}
474
475impl FromStr for Image {
476 type Err = Error<ErrTag>;
477
478 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
479 Ok(match s {
480 "avif" => Self::Avif,
481 "gif" => Self::Gif,
482 "vnd.microsoft.icon" => Self::Icon,
483 // The unregistered name browsers have always sent and accepted, so
484 // a proxy forwarding one upstream's favicon type does not fail.
485 "x-icon" => Self::Icon,
486 "jpeg" => Self::Jpeg,
487 "png" => Self::Png,
488 "svg+xml" => Self::SvgXml,
489 "tiff" => Self::Tiff,
490 "webp" => Self::Webp,
491 _ => return Err(err!(
492 "Unrecognised Image Media subtype '{}'.", s;
493 IO, Network, Unknown, Input)),
494 })
495 }
496}
497
498/// ╭────────────────────────────────────────────╮
499/// │ IANA Top Level Media Type: Model │
500/// │ Subtypes │
501/// ╰────────────────────────────────────────────╯
502#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
503pub enum Model {
504 Obj,
505}
506
507impl Display for Model {
508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509 write!(f, "{}", match self {
510 Self::Obj => "obj",
511 })
512 }
513}
514
515impl FromStr for Model {
516 type Err = Error<ErrTag>;
517
518 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
519 Ok(match s {
520 "obj" => Self::Obj,
521 _ => return Err(err!(
522 "Unrecognised Model Media subtype '{}'.", s;
523 IO, Network, Unknown, Input)),
524 })
525 }
526}
527
528/// ╭────────────────────────────────────────────╮
529/// │ IANA Top Level Media Type: Multipart │
530/// │ Subtypes │
531/// ╰────────────────────────────────────────────╯
532#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
533pub enum Multipart {
534 FormData,
535}
536
537impl Display for Multipart {
538 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539 write!(f, "{}", match self {
540 Self::FormData => "form-data",
541 })
542 }
543}
544
545impl FromStr for Multipart {
546 type Err = Error<ErrTag>;
547
548 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
549 Ok(match s {
550 "form-data" => Self::FormData,
551 _ => return Err(err!(
552 "Unrecognised Multipart Media subtype '{}'.", s;
553 IO, Network, Unknown, Input)),
554 })
555 }
556}
557
558/// ╭────────────────────────────────────────────╮
559/// │ IANA Top Level Media Type: Text │
560/// │ Subtypes │
561/// ╰────────────────────────────────────────────╯
562#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
563pub enum Text {
564 Plain,
565 Css,
566 Csv,
567 Html,
568 Javascript,
569 Xml,
570}
571
572impl Display for Text {
573 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574 write!(f, "{}", match self {
575 Self::Plain => "plain",
576 Self::Css => "css",
577 Self::Csv => "csv",
578 Self::Html => "html",
579 Self::Javascript => "javascript",
580 Self::Xml => "xml",
581 })
582 }
583}
584
585impl FromStr for Text {
586 type Err = Error<ErrTag>;
587
588 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
589 Ok(match s {
590 "plain" => Self::Plain,
591 "css" => Self::Css,
592 "csv" => Self::Csv,
593 "html" => Self::Html,
594 "javascript" => Self::Javascript,
595 "xml" => Self::Xml,
596 _ => return Err(err!(
597 "Unrecognised Text Media subtype '{}'.", s;
598 IO, Network, Unknown, Input)),
599 })
600 }
601}
602
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 /// `MediaType` writes the top-level type, so a subtype that writes it again
609 /// puts `audio/audio/mpeg` on the wire, which no player recognises.
610 #[test]
611 fn test_a_media_type_names_its_top_level_once() {
612 assert_eq!(fmt!("{}", MediaType::Audio(Audio::Mpeg)), "audio/mpeg");
613 assert_eq!(fmt!("{}", MediaType::Video(Video::Mp4)), "video/mp4");
614 assert_eq!(fmt!("{}", MediaType::Font(Font::Woff2)), "font/woff2");
615 assert_eq!(fmt!("{}", MediaType::Text(Text::Html)), "text/html");
616 }
617
618 /// Every type this crate writes it can also read back, which is what a proxy
619 /// forwarding a `Content-Type` needs of it.
620 #[test]
621 fn test_every_recording_type_survives_a_round_trip() -> Outcome<()> {
622 let types = [
623 MediaType::Video(Video::Mp4),
624 MediaType::Video(Video::Mpeg),
625 MediaType::Video(Video::Ogg),
626 MediaType::Video(Video::Quicktime),
627 MediaType::Video(Video::Webm),
628 MediaType::Video(Video::XMatroska),
629 MediaType::Video(Video::XMsVideo),
630 MediaType::Audio(Audio::Aac),
631 MediaType::Audio(Audio::Flac),
632 MediaType::Audio(Audio::Mp4),
633 MediaType::Audio(Audio::Mpeg),
634 MediaType::Audio(Audio::Ogg),
635 MediaType::Audio(Audio::Wav),
636 MediaType::Audio(Audio::Webm),
637 ];
638 for mt in types {
639 let written = fmt!("{}", mt);
640 let read = res!(MediaType::from_str(&written));
641 assert_eq!(read, mt, "{:?} did not survive being written as {:?}", mt, written);
642 }
643 Ok(())
644 }
645
646 /// The exact strings in the IANA media types registry. A type spelled a
647 /// little differently is a type the receiver does not recognise, and the
648 /// registry is the only authority on the spelling.
649 #[test]
650 fn test_a_registered_type_is_spelled_as_the_registry_spells_it() {
651 for (mt, registered) in [
652 // WebAssembly Core, IANA registered.
653 (MediaType::Application(Application::Wasm), "application/wasm"),
654 // W3C Web Application Manifest, IANA registered.
655 (MediaType::Application(Application::ManifestJson), "application/manifest+json"),
656 // RFC 9649.
657 (MediaType::Image(Image::Webp), "image/webp"),
658 // IANA registered; `image/x-icon` is the unregistered name.
659 (MediaType::Image(Image::Icon), "image/vnd.microsoft.icon"),
660 // AVIF: IANA registered by the AV1 Image File Format specification.
661 (MediaType::Image(Image::Avif), "image/avif"),
662 // TIFF: RFC 3302.
663 (MediaType::Image(Image::Tiff), "image/tiff"),
664 ] {
665 assert_eq!(fmt!("{}", mt), registered);
666 match MediaType::from_str(registered) {
667 Ok(read) => assert_eq!(read, mt, "{} was read back as {:?}", registered, read),
668 Err(e) => panic!("{} was refused: {}", registered, e),
669 }
670 }
671 }
672
673 /// A format that carries its own compression gains nothing from a second
674 /// pass and pays for the framing, so the default is to leave a type alone.
675 #[test]
676 fn test_only_a_type_that_gains_by_it_is_compressible() {
677 for mt in [
678 MediaType::Text(Text::Html),
679 MediaType::Text(Text::Css),
680 MediaType::Text(Text::Javascript),
681 MediaType::Application(Application::Json),
682 MediaType::Application(Application::Wasm),
683 MediaType::Image(Image::SvgXml),
684 MediaType::Font(Font::Ttf),
685 MediaType::Image(Image::Tiff),
686 ] {
687 assert!(mt.is_compressible(), "{} gains by being compressed", mt);
688 }
689 for mt in [
690 MediaType::Image(Image::Png),
691 MediaType::Image(Image::Jpeg),
692 MediaType::Image(Image::Webp),
693 MediaType::Image(Image::Avif),
694 MediaType::Font(Font::Woff2),
695 MediaType::Audio(Audio::Ogg),
696 MediaType::Video(Video::Mp4),
697 MediaType::Application(Application::Zip),
698 MediaType::Application(Application::Zstd),
699 MediaType::Application(Application::Pdf),
700 ] {
701 assert!(!mt.is_compressible(), "{} is already compressed", mt);
702 }
703 }
704
705 /// One format, several registered names. A proxy that refuses the name an
706 /// upstream chose forwards nothing, so all of them are read even though only
707 /// one is written.
708 #[test]
709 fn test_the_alternative_names_for_one_format_are_all_read() -> Outcome<()> {
710 for name in ["audio/wav", "audio/wave", "audio/x-wav", "audio/vnd.wave"] {
711 let read = res!(MediaType::from_str(name));
712 assert_eq!(read, MediaType::Audio(Audio::Wav), "reading {}", name);
713 }
714 assert_eq!(res!(MediaType::from_str("image/x-icon")), MediaType::Image(Image::Icon));
715 Ok(())
716 }
717}