1 | //! # Mime |
2 | //! |
3 | //! Mime is now Media Type, technically, but `Mime` is more immediately |
4 | //! understandable, so the main type here is `Mime`. |
5 | //! |
6 | //! ## What is Mime? |
7 | //! |
8 | //! Example mime string: `text/plain` |
9 | //! |
10 | //! ``` |
11 | //! let plain_text: mime::Mime = "text/plain" .parse().unwrap(); |
12 | //! assert_eq!(plain_text, mime::TEXT_PLAIN); |
13 | //! ``` |
14 | //! |
15 | //! ## Inspecting Mimes |
16 | //! |
17 | //! ``` |
18 | //! let mime = mime::TEXT_PLAIN; |
19 | //! match (mime.type_(), mime.subtype()) { |
20 | //! (mime::TEXT, mime::PLAIN) => println!("plain text!" ), |
21 | //! (mime::TEXT, _) => println!("structured text" ), |
22 | //! _ => println!("not text" ), |
23 | //! } |
24 | //! ``` |
25 | |
26 | #![doc (html_root_url = "https://docs.rs/mime/0.3.17" )] |
27 | #![deny (warnings)] |
28 | #![deny (missing_docs)] |
29 | #![deny (missing_debug_implementations)] |
30 | |
31 | |
32 | use std::cmp::Ordering; |
33 | use std::error::Error; |
34 | use std::fmt; |
35 | use std::hash::{Hash, Hasher}; |
36 | use std::str::FromStr; |
37 | use std::slice; |
38 | |
39 | mod parse; |
40 | |
41 | /// A parsed mime or media type. |
42 | #[derive (Clone)] |
43 | pub struct Mime { |
44 | source: Source, |
45 | slash: usize, |
46 | plus: Option<usize>, |
47 | params: ParamSource, |
48 | } |
49 | |
50 | /// An iterator of parsed mime |
51 | #[derive (Clone, Debug)] |
52 | pub struct MimeIter<'a> { |
53 | pos: usize, |
54 | source: &'a str, |
55 | } |
56 | |
57 | /// A section of a `Mime`. |
58 | /// |
59 | /// For instance, for the Mime `image/svg+xml`, it contains 3 `Name`s, |
60 | /// `image`, `svg`, and `xml`. |
61 | /// |
62 | /// In most cases, `Name`s are compared ignoring case. |
63 | #[derive (Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
64 | pub struct Name<'a> { |
65 | // TODO: optimize with an Atom-like thing |
66 | // There a `const` Names, and so it is possible for the statis strings |
67 | // to havea different memory address. Additionally, when used in match |
68 | // statements, the strings are compared with a memcmp, possibly even |
69 | // if the address and length are the same. |
70 | // |
71 | // Being an enum with an Atom variant that is a usize (and without a |
72 | // string pointer and boolean) would allow for faster comparisons. |
73 | source: &'a str, |
74 | insensitive: bool, |
75 | } |
76 | |
77 | /// An error when parsing a `Mime` from a string. |
78 | #[derive (Debug)] |
79 | pub struct FromStrError { |
80 | inner: parse::ParseError, |
81 | } |
82 | |
83 | impl FromStrError { |
84 | fn s(&self) -> &str { |
85 | "mime parse error" |
86 | } |
87 | } |
88 | |
89 | impl fmt::Display for FromStrError { |
90 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
91 | write!(f, " {}: {}" , self.s(), self.inner) |
92 | } |
93 | } |
94 | |
95 | impl Error for FromStrError { |
96 | // Minimum Rust is 1.15, Error::description was still required then |
97 | #[allow (deprecated)] |
98 | fn description(&self) -> &str { |
99 | self.s() |
100 | } |
101 | } |
102 | |
103 | #[derive (Clone)] |
104 | enum Source { |
105 | Atom(u8, &'static str), |
106 | Dynamic(String), |
107 | } |
108 | |
109 | impl Source { |
110 | fn as_ref(&self) -> &str { |
111 | match *self { |
112 | Source::Atom(_, s: &str) => s, |
113 | Source::Dynamic(ref s: &String) => s, |
114 | } |
115 | } |
116 | } |
117 | |
118 | #[derive (Clone)] |
119 | enum ParamSource { |
120 | Utf8(usize), |
121 | Custom(usize, Vec<(Indexed, Indexed)>), |
122 | None, |
123 | } |
124 | |
125 | #[derive (Clone, Copy)] |
126 | struct Indexed(usize, usize); |
127 | |
128 | impl Mime { |
129 | /// Get the top level media type for this `Mime`. |
130 | /// |
131 | /// # Example |
132 | /// |
133 | /// ``` |
134 | /// let mime = mime::TEXT_PLAIN; |
135 | /// assert_eq!(mime.type_(), "text" ); |
136 | /// assert_eq!(mime.type_(), mime::TEXT); |
137 | /// ``` |
138 | #[inline ] |
139 | pub fn type_(&self) -> Name { |
140 | Name { |
141 | source: &self.source.as_ref()[..self.slash], |
142 | insensitive: true, |
143 | } |
144 | } |
145 | |
146 | /// Get the subtype of this `Mime`. |
147 | /// |
148 | /// # Example |
149 | /// |
150 | /// ``` |
151 | /// let mime = mime::TEXT_PLAIN; |
152 | /// assert_eq!(mime.subtype(), "plain" ); |
153 | /// assert_eq!(mime.subtype(), mime::PLAIN); |
154 | /// ``` |
155 | #[inline ] |
156 | pub fn subtype(&self) -> Name { |
157 | let end = self.plus.unwrap_or_else(|| { |
158 | return self.semicolon().unwrap_or(self.source.as_ref().len()) |
159 | }); |
160 | Name { |
161 | source: &self.source.as_ref()[self.slash + 1..end], |
162 | insensitive: true, |
163 | } |
164 | } |
165 | |
166 | /// Get an optional +suffix for this `Mime`. |
167 | /// |
168 | /// # Example |
169 | /// |
170 | /// ``` |
171 | /// let svg = "image/svg+xml" .parse::<mime::Mime>().unwrap(); |
172 | /// assert_eq!(svg.suffix(), Some(mime::XML)); |
173 | /// assert_eq!(svg.suffix().unwrap(), "xml" ); |
174 | /// |
175 | /// |
176 | /// assert!(mime::TEXT_PLAIN.suffix().is_none()); |
177 | /// ``` |
178 | #[inline ] |
179 | pub fn suffix(&self) -> Option<Name> { |
180 | let end = self.semicolon().unwrap_or(self.source.as_ref().len()); |
181 | self.plus.map(|idx| Name { |
182 | source: &self.source.as_ref()[idx + 1..end], |
183 | insensitive: true, |
184 | }) |
185 | } |
186 | |
187 | /// Look up a parameter by name. |
188 | /// |
189 | /// # Example |
190 | /// |
191 | /// ``` |
192 | /// let mime = mime::TEXT_PLAIN_UTF_8; |
193 | /// assert_eq!(mime.get_param(mime::CHARSET), Some(mime::UTF_8)); |
194 | /// assert_eq!(mime.get_param("charset" ).unwrap(), "utf-8" ); |
195 | /// assert!(mime.get_param("boundary" ).is_none()); |
196 | /// |
197 | /// let mime = "multipart/form-data; boundary=ABCDEFG" .parse::<mime::Mime>().unwrap(); |
198 | /// assert_eq!(mime.get_param(mime::BOUNDARY).unwrap(), "ABCDEFG" ); |
199 | /// ``` |
200 | pub fn get_param<'a, N>(&'a self, attr: N) -> Option<Name<'a>> |
201 | where N: PartialEq<Name<'a>> { |
202 | self.params().find(|e| attr == e.0).map(|e| e.1) |
203 | } |
204 | |
205 | /// Returns an iterator over the parameters. |
206 | #[inline ] |
207 | pub fn params<'a>(&'a self) -> Params<'a> { |
208 | let inner = match self.params { |
209 | ParamSource::Utf8(_) => ParamsInner::Utf8, |
210 | ParamSource::Custom(_, ref params) => { |
211 | ParamsInner::Custom { |
212 | source: &self.source, |
213 | params: params.iter(), |
214 | } |
215 | } |
216 | ParamSource::None => ParamsInner::None, |
217 | }; |
218 | |
219 | Params(inner) |
220 | } |
221 | |
222 | /// Return a `&str` of the Mime's ["essence"][essence]. |
223 | /// |
224 | /// [essence]: https://mimesniff.spec.whatwg.org/#mime-type-essence |
225 | pub fn essence_str(&self) -> &str { |
226 | let end = self.semicolon().unwrap_or(self.source.as_ref().len()); |
227 | |
228 | &self.source.as_ref()[..end] |
229 | } |
230 | |
231 | #[cfg (test)] |
232 | fn has_params(&self) -> bool { |
233 | match self.params { |
234 | ParamSource::None => false, |
235 | _ => true, |
236 | } |
237 | } |
238 | |
239 | #[inline ] |
240 | fn semicolon(&self) -> Option<usize> { |
241 | match self.params { |
242 | ParamSource::Utf8(i) | |
243 | ParamSource::Custom(i, _) => Some(i), |
244 | ParamSource::None => None, |
245 | } |
246 | } |
247 | |
248 | fn atom(&self) -> u8 { |
249 | match self.source { |
250 | Source::Atom(a, _) => a, |
251 | _ => 0, |
252 | } |
253 | } |
254 | } |
255 | |
256 | // Mime ============ |
257 | |
258 | fn eq_ascii(a: &str, b: &str) -> bool { |
259 | // str::eq_ignore_ascii_case didn't stabilize until Rust 1.23. |
260 | // So while our MSRV is 1.15, gotta import this trait. |
261 | #[allow (deprecated, unused)] |
262 | use std::ascii::AsciiExt; |
263 | |
264 | a.eq_ignore_ascii_case(b) |
265 | } |
266 | |
267 | fn mime_eq_str(mime: &Mime, s: &str) -> bool { |
268 | if let ParamSource::Utf8(semicolon: usize) = mime.params { |
269 | if mime.source.as_ref().len() == s.len() { |
270 | eq_ascii(a:mime.source.as_ref(), b:s) |
271 | } else { |
272 | params_eq(semicolon, a:mime.source.as_ref(), b:s) |
273 | } |
274 | } else if let Some(semicolon: usize) = mime.semicolon() { |
275 | params_eq(semicolon, a:mime.source.as_ref(), b:s) |
276 | } else { |
277 | eq_ascii(a:mime.source.as_ref(), b:s) |
278 | } |
279 | } |
280 | |
281 | fn params_eq(semicolon: usize, a: &str, b: &str) -> bool { |
282 | if b.len() < semicolon + 1 { |
283 | false |
284 | } else if !eq_ascii(&a[..semicolon], &b[..semicolon]) { |
285 | false |
286 | } else { |
287 | // gotta check for quotes, LWS, and for case senstive names |
288 | let mut a = &a[semicolon + 1..]; |
289 | let mut b = &b[semicolon + 1..]; |
290 | let mut sensitive; |
291 | |
292 | loop { |
293 | a = a.trim(); |
294 | b = b.trim(); |
295 | |
296 | match (a.is_empty(), b.is_empty()) { |
297 | (true, true) => return true, |
298 | (true, false) | |
299 | (false, true) => return false, |
300 | (false, false) => (), |
301 | } |
302 | |
303 | //name |
304 | if let Some(a_idx) = a.find('=' ) { |
305 | let a_name = { |
306 | #[allow (deprecated)] |
307 | { a[..a_idx].trim_left() } |
308 | }; |
309 | if let Some(b_idx) = b.find('=' ) { |
310 | let b_name = { |
311 | #[allow (deprecated)] |
312 | { b[..b_idx].trim_left() } |
313 | }; |
314 | if !eq_ascii(a_name, b_name) { |
315 | return false; |
316 | } |
317 | sensitive = a_name != CHARSET; |
318 | a = &a[..a_idx]; |
319 | b = &b[..b_idx]; |
320 | } else { |
321 | return false; |
322 | } |
323 | } else { |
324 | return false; |
325 | } |
326 | //value |
327 | let a_quoted = if a.as_bytes()[0] == b'"' { |
328 | a = &a[1..]; |
329 | true |
330 | } else { |
331 | false |
332 | }; |
333 | let b_quoted = if b.as_bytes()[0] == b'"' { |
334 | b = &b[1..]; |
335 | true |
336 | } else { |
337 | false |
338 | }; |
339 | |
340 | let a_end = if a_quoted { |
341 | if let Some(quote) = a.find('"' ) { |
342 | quote |
343 | } else { |
344 | return false; |
345 | } |
346 | } else { |
347 | a.find(';' ).unwrap_or(a.len()) |
348 | }; |
349 | |
350 | let b_end = if b_quoted { |
351 | if let Some(quote) = b.find('"' ) { |
352 | quote |
353 | } else { |
354 | return false; |
355 | } |
356 | } else { |
357 | b.find(';' ).unwrap_or(b.len()) |
358 | }; |
359 | |
360 | if sensitive { |
361 | if !eq_ascii(&a[..a_end], &b[..b_end]) { |
362 | return false; |
363 | } |
364 | } else { |
365 | if &a[..a_end] != &b[..b_end] { |
366 | return false; |
367 | } |
368 | } |
369 | a = &a[a_end..]; |
370 | b = &b[b_end..]; |
371 | } |
372 | } |
373 | } |
374 | |
375 | impl PartialEq for Mime { |
376 | #[inline ] |
377 | fn eq(&self, other: &Mime) -> bool { |
378 | match (self.atom(), other.atom()) { |
379 | // TODO: |
380 | // This could optimize for when there are no customs parameters. |
381 | // Any parsed mime has already been lowercased, so if there aren't |
382 | // any parameters that are case sensistive, this can skip the |
383 | // eq_ascii, and just use a memcmp instead. |
384 | (0, _) | |
385 | (_, 0) => mime_eq_str(self, s:other.source.as_ref()), |
386 | (a: u8, b: u8) => a == b, |
387 | } |
388 | } |
389 | } |
390 | |
391 | impl Eq for Mime {} |
392 | |
393 | impl PartialOrd for Mime { |
394 | fn partial_cmp(&self, other: &Mime) -> Option<Ordering> { |
395 | Some(self.cmp(other)) |
396 | } |
397 | } |
398 | |
399 | impl Ord for Mime { |
400 | fn cmp(&self, other: &Mime) -> Ordering { |
401 | self.source.as_ref().cmp(other.source.as_ref()) |
402 | } |
403 | } |
404 | |
405 | impl Hash for Mime { |
406 | fn hash<T: Hasher>(&self, hasher: &mut T) { |
407 | hasher.write(self.source.as_ref().as_bytes()); |
408 | } |
409 | } |
410 | |
411 | impl<'a> PartialEq<&'a str> for Mime { |
412 | #[inline ] |
413 | fn eq(&self, s: & &'a str) -> bool { |
414 | mime_eq_str(self, *s) |
415 | } |
416 | } |
417 | |
418 | impl<'a> PartialEq<Mime> for &'a str { |
419 | #[inline ] |
420 | fn eq(&self, mime: &Mime) -> bool { |
421 | mime_eq_str(mime, *self) |
422 | } |
423 | } |
424 | |
425 | impl FromStr for Mime { |
426 | type Err = FromStrError; |
427 | |
428 | fn from_str(s: &str) -> Result<Mime, Self::Err> { |
429 | parse::parse(s).map_err(|e: ParseError| FromStrError { inner: e }) |
430 | } |
431 | } |
432 | |
433 | impl AsRef<str> for Mime { |
434 | #[inline ] |
435 | fn as_ref(&self) -> &str { |
436 | self.source.as_ref() |
437 | } |
438 | } |
439 | |
440 | impl fmt::Debug for Mime { |
441 | #[inline ] |
442 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
443 | fmt::Debug::fmt(self.source.as_ref(), f) |
444 | } |
445 | } |
446 | |
447 | impl fmt::Display for Mime { |
448 | #[inline ] |
449 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
450 | fmt::Display::fmt(self.source.as_ref(), f) |
451 | } |
452 | } |
453 | |
454 | // Name ============ |
455 | |
456 | fn name_eq_str(name: &Name, s: &str) -> bool { |
457 | if name.insensitive { |
458 | eq_ascii(a:name.source, b:s) |
459 | } else { |
460 | name.source == s |
461 | } |
462 | } |
463 | |
464 | impl<'a> Name<'a> { |
465 | /// Get the value of this `Name` as a string. |
466 | /// |
467 | /// Note that the borrow is not tied to `&self` but the `'a` lifetime, allowing the |
468 | /// string to outlive `Name`. Alternately, there is an `impl<'a> From<Name<'a>> for &'a str` |
469 | /// which isn't rendered by Rustdoc, that can be accessed using `str::from(name)` or `name.into()`. |
470 | pub fn as_str(&self) -> &'a str { |
471 | self.source |
472 | } |
473 | } |
474 | |
475 | impl<'a, 'b> PartialEq<&'b str> for Name<'a> { |
476 | #[inline ] |
477 | fn eq(&self, other: & &'b str) -> bool { |
478 | name_eq_str(self, *other) |
479 | } |
480 | } |
481 | |
482 | impl<'a, 'b> PartialEq<Name<'a>> for &'b str { |
483 | #[inline ] |
484 | fn eq(&self, other: &Name<'a>) -> bool { |
485 | name_eq_str(name:other, *self) |
486 | } |
487 | } |
488 | |
489 | impl<'a> AsRef<str> for Name<'a> { |
490 | #[inline ] |
491 | fn as_ref(&self) -> &str { |
492 | self.source |
493 | } |
494 | } |
495 | |
496 | impl<'a> From<Name<'a>> for &'a str { |
497 | #[inline ] |
498 | fn from(name: Name<'a>) -> &'a str { |
499 | name.source |
500 | } |
501 | } |
502 | |
503 | impl<'a> fmt::Debug for Name<'a> { |
504 | #[inline ] |
505 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
506 | fmt::Debug::fmt(self.source, f) |
507 | } |
508 | } |
509 | |
510 | impl<'a> fmt::Display for Name<'a> { |
511 | #[inline ] |
512 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
513 | fmt::Display::fmt(self.source, f) |
514 | } |
515 | } |
516 | |
517 | // Params =================== |
518 | |
519 | enum ParamsInner<'a> { |
520 | Utf8, |
521 | Custom { |
522 | source: &'a Source, |
523 | params: slice::Iter<'a, (Indexed, Indexed)>, |
524 | }, |
525 | None, |
526 | } |
527 | |
528 | /// An iterator over the parameters of a MIME. |
529 | pub struct Params<'a>(ParamsInner<'a>); |
530 | |
531 | impl<'a> fmt::Debug for Params<'a> { |
532 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
533 | fmt.debug_struct(name:"Params" ).finish() |
534 | } |
535 | } |
536 | |
537 | impl<'a> Iterator for Params<'a> { |
538 | type Item = (Name<'a>, Name<'a>); |
539 | |
540 | #[inline ] |
541 | fn next(&mut self) -> Option<(Name<'a>, Name<'a>)> { |
542 | match self.0 { |
543 | ParamsInner::Utf8 => { |
544 | let value = (CHARSET, UTF_8); |
545 | self.0 = ParamsInner::None; |
546 | Some(value) |
547 | } |
548 | ParamsInner::Custom { source, ref mut params } => { |
549 | params.next().map(|&(name, value)| { |
550 | let name = Name { |
551 | source: &source.as_ref()[name.0..name.1], |
552 | insensitive: true, |
553 | }; |
554 | let value = Name { |
555 | source: &source.as_ref()[value.0..value.1], |
556 | insensitive: name == CHARSET, |
557 | }; |
558 | (name, value) |
559 | }) |
560 | } |
561 | ParamsInner::None => None |
562 | } |
563 | } |
564 | |
565 | #[inline ] |
566 | fn size_hint(&self) -> (usize, Option<usize>) { |
567 | match self.0 { |
568 | ParamsInner::Utf8 => (1, Some(1)), |
569 | ParamsInner::Custom { ref params, .. } => params.size_hint(), |
570 | ParamsInner::None => (0, Some(0)), |
571 | } |
572 | } |
573 | } |
574 | |
575 | macro_rules! names { |
576 | ($($id:ident, $e:expr;)*) => ( |
577 | $( |
578 | #[doc = $e] |
579 | pub const $id: Name<'static> = Name { |
580 | source: $e, |
581 | insensitive: true, |
582 | }; |
583 | )* |
584 | |
585 | #[test] |
586 | fn test_names_macro_consts() { |
587 | #[allow(unused, deprecated)] |
588 | use std::ascii::AsciiExt; |
589 | $( |
590 | assert_eq!($id.source.to_ascii_lowercase(), $id.source); |
591 | )* |
592 | } |
593 | ) |
594 | } |
595 | |
596 | names! { |
597 | STAR, "*" ; |
598 | |
599 | TEXT, "text" ; |
600 | IMAGE, "image" ; |
601 | AUDIO, "audio" ; |
602 | VIDEO, "video" ; |
603 | APPLICATION, "application" ; |
604 | MULTIPART, "multipart" ; |
605 | MESSAGE, "message" ; |
606 | MODEL, "model" ; |
607 | FONT, "font" ; |
608 | |
609 | // common text/ * |
610 | PLAIN, "plain" ; |
611 | HTML, "html" ; |
612 | XML, "xml" ; |
613 | JAVASCRIPT, "javascript" ; |
614 | CSS, "css" ; |
615 | CSV, "csv" ; |
616 | EVENT_STREAM, "event-stream" ; |
617 | VCARD, "vcard" ; |
618 | |
619 | // common application/* |
620 | JSON, "json" ; |
621 | WWW_FORM_URLENCODED, "x-www-form-urlencoded" ; |
622 | MSGPACK, "msgpack" ; |
623 | OCTET_STREAM, "octet-stream" ; |
624 | PDF, "pdf" ; |
625 | |
626 | // common font/* |
627 | WOFF, "woff" ; |
628 | WOFF2, "woff2" ; |
629 | |
630 | // multipart/* |
631 | FORM_DATA, "form-data" ; |
632 | |
633 | // common image/* |
634 | BMP, "bmp" ; |
635 | GIF, "gif" ; |
636 | JPEG, "jpeg" ; |
637 | PNG, "png" ; |
638 | SVG, "svg" ; |
639 | |
640 | // audio/* |
641 | BASIC, "basic" ; |
642 | MPEG, "mpeg" ; |
643 | MP4, "mp4" ; |
644 | OGG, "ogg" ; |
645 | |
646 | // parameters |
647 | CHARSET, "charset" ; |
648 | BOUNDARY, "boundary" ; |
649 | UTF_8, "utf-8" ; |
650 | } |
651 | |
652 | macro_rules! mimes { |
653 | ($($id:ident, $($piece:expr),*;)*) => ( |
654 | #[allow(non_camel_case_types)] |
655 | enum __Atoms { |
656 | __Dynamic, |
657 | $( |
658 | $id, |
659 | )* |
660 | } |
661 | |
662 | $( |
663 | mime_constant! { |
664 | $id, $($piece),* |
665 | } |
666 | )* |
667 | |
668 | #[test] |
669 | fn test_mimes_macro_consts() { |
670 | let _ = [ |
671 | $( |
672 | mime_constant_test! { |
673 | $id, $($piece),* |
674 | } |
675 | ),* |
676 | ].iter().enumerate().map(|(pos, &atom)| { |
677 | assert_eq!(pos + 1, atom as usize, "atom {} in position {}" , atom, pos + 1); |
678 | }).collect::<Vec<()>>(); |
679 | } |
680 | ) |
681 | } |
682 | |
683 | macro_rules! mime_constant { |
684 | ($id:ident, $src:expr, $slash:expr) => ( |
685 | mime_constant!($id, $src, $slash, None); |
686 | ); |
687 | ($id:ident, $src:expr, $slash:expr, $plus:expr) => ( |
688 | mime_constant!(FULL $id, $src, $slash, $plus, ParamSource::None); |
689 | ); |
690 | |
691 | ($id:ident, $src:expr, $slash:expr, $plus:expr, $params:expr) => ( |
692 | mime_constant!(FULL $id, $src, $slash, $plus, ParamSource::Utf8($params)); |
693 | ); |
694 | |
695 | |
696 | (FULL $id:ident, $src:expr, $slash:expr, $plus:expr, $params:expr) => ( |
697 | #[doc = "`" ] |
698 | #[doc = $src] |
699 | #[doc = "`" ] |
700 | pub const $id: Mime = Mime { |
701 | source: Source::Atom(__Atoms::$id as u8, $src), |
702 | slash: $slash, |
703 | plus: $plus, |
704 | params: $params, |
705 | }; |
706 | ) |
707 | } |
708 | |
709 | |
710 | #[cfg (test)] |
711 | macro_rules! mime_constant_test { |
712 | ($id:ident, $src:expr, $slash:expr) => ( |
713 | mime_constant_test!($id, $src, $slash, None); |
714 | ); |
715 | ($id:ident, $src:expr, $slash:expr, $plus:expr) => ( |
716 | mime_constant_test!(FULL $id, $src, $slash, $plus, ParamSource::None); |
717 | ); |
718 | |
719 | ($id:ident, $src:expr, $slash:expr, $plus:expr, $params:expr) => ( |
720 | mime_constant_test!(FULL $id, $src, $slash, $plus, ParamSource::Utf8($params)); |
721 | ); |
722 | |
723 | (FULL $id:ident, $src:expr, $slash:expr, $plus:expr, $params:expr) => ({ |
724 | let __mime = $id; |
725 | let __slash = __mime.as_ref().as_bytes()[$slash]; |
726 | assert_eq!(__slash, b'/' , "{:?} has {:?} at slash position {:?}" , __mime, __slash as char, $slash); |
727 | if let Some(plus) = __mime.plus { |
728 | let __c = __mime.as_ref().as_bytes()[plus]; |
729 | assert_eq!(__c, b'+' , "{:?} has {:?} at plus position {:?}" , __mime, __c as char, plus); |
730 | } else { |
731 | assert!(!__mime.as_ref().as_bytes().contains(&b'+' ), "{:?} forgot plus" , __mime); |
732 | } |
733 | if let ParamSource::Utf8(semicolon) = __mime.params { |
734 | assert_eq!(__mime.as_ref().as_bytes()[semicolon], b';' ); |
735 | assert_eq!(&__mime.as_ref()[semicolon..], "; charset=utf-8" ); |
736 | } else if let ParamSource::None = __mime.params { |
737 | assert!(!__mime.as_ref().as_bytes().contains(&b';' )); |
738 | } else { |
739 | unreachable!(); |
740 | } |
741 | __mime.atom() |
742 | }) |
743 | } |
744 | |
745 | |
746 | mimes! { |
747 | STAR_STAR, "*/*" , 1; |
748 | |
749 | TEXT_STAR, "text/*" , 4; |
750 | TEXT_PLAIN, "text/plain" , 4; |
751 | TEXT_PLAIN_UTF_8, "text/plain; charset=utf-8" , 4, None, 10; |
752 | TEXT_HTML, "text/html" , 4; |
753 | TEXT_HTML_UTF_8, "text/html; charset=utf-8" , 4, None, 9; |
754 | TEXT_CSS, "text/css" , 4; |
755 | TEXT_CSS_UTF_8, "text/css; charset=utf-8" , 4, None, 8; |
756 | TEXT_JAVASCRIPT, "text/javascript" , 4; |
757 | TEXT_XML, "text/xml" , 4; |
758 | TEXT_EVENT_STREAM, "text/event-stream" , 4; |
759 | TEXT_CSV, "text/csv" , 4; |
760 | TEXT_CSV_UTF_8, "text/csv; charset=utf-8" , 4, None, 8; |
761 | TEXT_TAB_SEPARATED_VALUES, "text/tab-separated-values" , 4; |
762 | TEXT_TAB_SEPARATED_VALUES_UTF_8, "text/tab-separated-values; charset=utf-8" , 4, None, 25; |
763 | TEXT_VCARD, "text/vcard" , 4; |
764 | |
765 | IMAGE_STAR, "image/*" , 5; |
766 | IMAGE_JPEG, "image/jpeg" , 5; |
767 | IMAGE_GIF, "image/gif" , 5; |
768 | IMAGE_PNG, "image/png" , 5; |
769 | IMAGE_BMP, "image/bmp" , 5; |
770 | IMAGE_SVG, "image/svg+xml" , 5, Some(9); |
771 | |
772 | FONT_WOFF, "font/woff" , 4; |
773 | FONT_WOFF2, "font/woff2" , 4; |
774 | |
775 | APPLICATION_JSON, "application/json" , 11; |
776 | APPLICATION_JAVASCRIPT, "application/javascript" , 11; |
777 | APPLICATION_JAVASCRIPT_UTF_8, "application/javascript; charset=utf-8" , 11, None, 22; |
778 | APPLICATION_WWW_FORM_URLENCODED, "application/x-www-form-urlencoded" , 11; |
779 | APPLICATION_OCTET_STREAM, "application/octet-stream" , 11; |
780 | APPLICATION_MSGPACK, "application/msgpack" , 11; |
781 | APPLICATION_PDF, "application/pdf" , 11; |
782 | |
783 | MULTIPART_FORM_DATA, "multipart/form-data" , 9; |
784 | } |
785 | |
786 | #[deprecated (since="0.3.1" , note="please use `TEXT_JAVASCRIPT` instead" )] |
787 | #[doc (hidden)] |
788 | pub const TEXT_JAVSCRIPT: Mime = TEXT_JAVASCRIPT; |
789 | |
790 | |
791 | #[cfg (test)] |
792 | mod tests { |
793 | use std::str::FromStr; |
794 | use super::*; |
795 | |
796 | #[test ] |
797 | fn test_type_() { |
798 | assert_eq!(TEXT_PLAIN.type_(), TEXT); |
799 | } |
800 | |
801 | |
802 | #[test ] |
803 | fn test_subtype() { |
804 | assert_eq!(TEXT_PLAIN.subtype(), PLAIN); |
805 | assert_eq!(TEXT_PLAIN_UTF_8.subtype(), PLAIN); |
806 | let mime = Mime::from_str("text/html+xml" ).unwrap(); |
807 | assert_eq!(mime.subtype(), HTML); |
808 | } |
809 | |
810 | #[test ] |
811 | fn test_matching() { |
812 | match (TEXT_PLAIN.type_(), TEXT_PLAIN.subtype()) { |
813 | (TEXT, PLAIN) => (), |
814 | _ => unreachable!(), |
815 | } |
816 | } |
817 | |
818 | #[test ] |
819 | fn test_suffix() { |
820 | assert_eq!(TEXT_PLAIN.suffix(), None); |
821 | let mime = Mime::from_str("text/html+xml" ).unwrap(); |
822 | assert_eq!(mime.suffix(), Some(XML)); |
823 | } |
824 | |
825 | #[test ] |
826 | fn test_mime_fmt() { |
827 | let mime = TEXT_PLAIN; |
828 | assert_eq!(mime.to_string(), "text/plain" ); |
829 | let mime = TEXT_PLAIN_UTF_8; |
830 | assert_eq!(mime.to_string(), "text/plain; charset=utf-8" ); |
831 | } |
832 | |
833 | #[test ] |
834 | fn test_mime_from_str() { |
835 | assert_eq!(Mime::from_str("text/plain" ).unwrap(), TEXT_PLAIN); |
836 | assert_eq!(Mime::from_str("TEXT/PLAIN" ).unwrap(), TEXT_PLAIN); |
837 | assert_eq!(Mime::from_str("text/plain;charset=utf-8" ).unwrap(), TEXT_PLAIN_UTF_8); |
838 | assert_eq!(Mime::from_str("text/plain;charset= \"utf-8 \"" ).unwrap(), TEXT_PLAIN_UTF_8); |
839 | |
840 | // spaces |
841 | assert_eq!(Mime::from_str("text/plain; charset=utf-8" ).unwrap(), TEXT_PLAIN_UTF_8); |
842 | |
843 | // quotes + semi colon |
844 | Mime::from_str("text/plain;charset= \"utf-8 \"; foo=bar" ).unwrap(); |
845 | Mime::from_str("text/plain;charset= \"utf-8 \" ; foo=bar" ).unwrap(); |
846 | |
847 | let upper = Mime::from_str("TEXT/PLAIN" ).unwrap(); |
848 | assert_eq!(upper, TEXT_PLAIN); |
849 | assert_eq!(upper.type_(), TEXT); |
850 | assert_eq!(upper.subtype(), PLAIN); |
851 | |
852 | |
853 | let extended = Mime::from_str("TEXT/PLAIN; CHARSET=UTF-8; FOO=BAR" ).unwrap(); |
854 | assert_eq!(extended, "text/plain; charset=utf-8; foo=BAR" ); |
855 | assert_eq!(extended.get_param("charset" ).unwrap(), "utf-8" ); |
856 | assert_eq!(extended.get_param("foo" ).unwrap(), "BAR" ); |
857 | |
858 | Mime::from_str("multipart/form-data; boundary=--------foobar" ).unwrap(); |
859 | |
860 | // stars |
861 | assert_eq!("*/*" .parse::<Mime>().unwrap(), STAR_STAR); |
862 | assert_eq!("image/*" .parse::<Mime>().unwrap(), "image/*" ); |
863 | assert_eq!("text/*; charset=utf-8" .parse::<Mime>().unwrap(), "text/*; charset=utf-8" ); |
864 | |
865 | // parse errors |
866 | Mime::from_str("f o o / bar" ).unwrap_err(); |
867 | Mime::from_str("text \n/plain" ).unwrap_err(); |
868 | Mime::from_str("text \r/plain" ).unwrap_err(); |
869 | Mime::from_str("text/ \r\nplain" ).unwrap_err(); |
870 | Mime::from_str("text/plain; \r\ncharset=utf-8" ).unwrap_err(); |
871 | Mime::from_str("text/plain; charset= \r\nutf-8" ).unwrap_err(); |
872 | Mime::from_str("text/plain; charset= \"\r\nutf-8 \"" ).unwrap_err(); |
873 | } |
874 | |
875 | #[test ] |
876 | fn test_mime_from_str_empty_parameter_list() { |
877 | static CASES: &'static [&'static str] = &[ |
878 | "text/event-stream;" , |
879 | "text/event-stream; " , |
880 | "text/event-stream; " , |
881 | ]; |
882 | |
883 | for case in CASES { |
884 | let mime = Mime::from_str(case).expect(case); |
885 | assert_eq!(mime, TEXT_EVENT_STREAM, "case = {:?}" , case); |
886 | assert_eq!(mime.type_(), TEXT, "case = {:?}" , case); |
887 | assert_eq!(mime.subtype(), EVENT_STREAM, "case = {:?}" , case); |
888 | assert!(!mime.has_params(), "case = {:?}" , case); |
889 | } |
890 | |
891 | } |
892 | |
893 | #[test ] |
894 | fn test_case_sensitive_values() { |
895 | let mime = Mime::from_str("multipart/form-data; charset=BASE64; boundary=ABCDEFG" ).unwrap(); |
896 | assert_eq!(mime.get_param(CHARSET).unwrap(), "bAsE64" ); |
897 | assert_eq!(mime.get_param(BOUNDARY).unwrap(), "ABCDEFG" ); |
898 | assert_ne!(mime.get_param(BOUNDARY).unwrap(), "abcdefg" ); |
899 | } |
900 | |
901 | #[test ] |
902 | fn test_get_param() { |
903 | assert_eq!(TEXT_PLAIN.get_param("charset" ), None); |
904 | assert_eq!(TEXT_PLAIN.get_param("baz" ), None); |
905 | |
906 | assert_eq!(TEXT_PLAIN_UTF_8.get_param("charset" ), Some(UTF_8)); |
907 | assert_eq!(TEXT_PLAIN_UTF_8.get_param("baz" ), None); |
908 | |
909 | let mime = Mime::from_str("text/plain; charset=utf-8; foo=bar" ).unwrap(); |
910 | assert_eq!(mime.get_param(CHARSET).unwrap(), "utf-8" ); |
911 | assert_eq!(mime.get_param("foo" ).unwrap(), "bar" ); |
912 | assert_eq!(mime.get_param("baz" ), None); |
913 | |
914 | |
915 | let mime = Mime::from_str("text/plain;charset= \"utf-8 \"" ).unwrap(); |
916 | assert_eq!(mime.get_param(CHARSET), Some(UTF_8)); |
917 | } |
918 | |
919 | #[test ] |
920 | fn test_name_eq() { |
921 | assert_eq!(TEXT, TEXT); |
922 | assert_eq!(TEXT, "text" ); |
923 | assert_eq!("text" , TEXT); |
924 | assert_eq!(TEXT, "TEXT" ); |
925 | |
926 | let param = Name { |
927 | source: "ABC" , |
928 | insensitive: false, |
929 | }; |
930 | |
931 | assert_eq!(param, param); |
932 | assert_eq!(param, "ABC" ); |
933 | assert_eq!("ABC" , param); |
934 | assert_ne!(param, "abc" ); |
935 | assert_ne!("abc" , param); |
936 | } |
937 | |
938 | #[test ] |
939 | fn test_essence_str() { |
940 | assert_eq!(TEXT_PLAIN.essence_str(), "text/plain" ); |
941 | assert_eq!(TEXT_PLAIN_UTF_8.essence_str(), "text/plain" ); |
942 | assert_eq!(IMAGE_SVG.essence_str(), "image/svg+xml" ); |
943 | } |
944 | } |
945 | |