Skip to content

Commit 4fef61b

Browse files
authored
Merge pull request #132 from http-rs/infer-type
Add mime sniffing to Body::from_file
2 parents 8626786 + e17731e commit 4fef61b

File tree

10 files changed

+207
-4
lines changed

10 files changed

+207
-4
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ serde_urlencoded = "0.6.1"
4242

4343
[dev-dependencies]
4444
http = "0.2.0"
45+
async-std = { version = "1.4.0", features = ["unstable", "attributes"] }

src/body.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,26 @@ impl Body {
358358
/// # Ok(()) }) }
359359
/// ```
360360
#[cfg(feature = "async_std")]
361-
pub async fn from_file<P>(file: P) -> io::Result<Self>
361+
pub async fn from_file<P>(path: P) -> io::Result<Self>
362362
where
363363
P: AsRef<Path>,
364364
{
365-
let file = fs::read(file.as_ref()).await?;
366-
Ok(file.into())
365+
let path = path.as_ref();
366+
let mut file = fs::File::open(path).await?;
367+
let len = file.metadata().await?.len();
368+
369+
// Look at magic bytes first, look at extension second, fall back to
370+
// octet stream.
371+
let mime = peek_mime(&mut file)
372+
.await?
373+
.or_else(|| guess_ext(path))
374+
.unwrap_or(mime::BYTE_STREAM);
375+
376+
Ok(Self {
377+
mime,
378+
length: Some(len as usize),
379+
reader: Box::new(io::BufReader::new(file)),
380+
})
367381
}
368382

369383
/// Get the length of the body in bytes.
@@ -448,3 +462,32 @@ impl BufRead for Body {
448462
Pin::new(&mut self.reader).consume(amt)
449463
}
450464
}
465+
466+
/// Look at first few bytes of a file to determine the mime type.
467+
/// This is used for various binary formats such as images and videos.
468+
#[cfg(feature = "async_std")]
469+
async fn peek_mime(file: &mut async_std::fs::File) -> io::Result<Option<Mime>> {
470+
// We need to read the first 300 bytes to correctly infer formats such as tar.
471+
let mut buf = [0_u8; 300];
472+
file.read(&mut buf).await?;
473+
let mime = Mime::sniff(&buf).ok();
474+
475+
// Reset the file cursor back to the start.
476+
file.seek(io::SeekFrom::Start(0)).await?;
477+
Ok(mime)
478+
}
479+
480+
/// Look at the extension of a file to determine the mime type.
481+
/// This is useful for plain-text formats such as HTML and CSS.
482+
#[cfg(feature = "async_std")]
483+
fn guess_ext(path: &Path) -> Option<Mime> {
484+
let ext = path.extension().map(|p| p.to_str()).flatten();
485+
match ext {
486+
Some("html") => Some(mime::HTML),
487+
Some("js") | Some("mjs") | Some("jsonp") => Some(mime::JAVASCRIPT),
488+
Some("json") => Some(mime::JSON),
489+
Some("css") => Some(mime::CSS),
490+
Some("svg") => Some(mime::SVG),
491+
None | Some(_) => None,
492+
}
493+
}

src/mime/constants.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,77 @@ pub const HTML: Mime = Mime {
8686
static_subtype: Some("html"),
8787
};
8888

89+
/// Content-Type for SVG.
90+
///
91+
/// # Mime Type
92+
///
93+
/// ```txt
94+
/// image/svg+xml
95+
/// ```
96+
pub const SVG: Mime = Mime {
97+
static_essence: Some("image/svg+xml"),
98+
essence: String::new(),
99+
basetype: String::new(),
100+
subtype: String::new(),
101+
params: None,
102+
static_basetype: Some("image"),
103+
static_subtype: Some("svg+xml"),
104+
};
105+
106+
/// Content-Type for ICO icons.
107+
///
108+
/// # Mime Type
109+
///
110+
/// ```txt
111+
/// image/x-icon
112+
/// ```
113+
// There are multiple `.ico` mime types known, but `image/x-icon`
114+
// is what most browser use. See:
115+
// https://en.wikipedia.org/wiki/ICO_%28file_format%29#MIME_type
116+
pub const ICO: Mime = Mime {
117+
static_essence: Some("image/x-icon"),
118+
essence: String::new(),
119+
basetype: String::new(),
120+
subtype: String::new(),
121+
params: None,
122+
static_basetype: Some("image"),
123+
static_subtype: Some("x-icon"),
124+
};
125+
126+
/// Content-Type for PNG images.
127+
///
128+
/// # Mime Type
129+
///
130+
/// ```txt
131+
/// image/png
132+
/// ```
133+
pub const PNG: Mime = Mime {
134+
static_essence: Some("image/png"),
135+
essence: String::new(),
136+
basetype: String::new(),
137+
subtype: String::new(),
138+
params: None,
139+
static_basetype: Some("image"),
140+
static_subtype: Some("png"),
141+
};
142+
143+
/// Content-Type for JPEG images.
144+
///
145+
/// # Mime Type
146+
///
147+
/// ```txt
148+
/// image/jpeg
149+
/// ```
150+
pub const JPEG: Mime = Mime {
151+
static_essence: Some("image/jpeg"),
152+
essence: String::new(),
153+
basetype: String::new(),
154+
subtype: String::new(),
155+
params: None,
156+
static_basetype: Some("image"),
157+
static_subtype: Some("jpeg"),
158+
};
159+
89160
/// Content-Type for Server Sent Events
90161
///
91162
/// # Mime Type
@@ -170,3 +241,20 @@ pub const MULTIPART_FORM: Mime = Mime {
170241
static_subtype: Some("form-data"),
171242
params: None,
172243
};
244+
245+
/// Content-Type for webassembly.
246+
///
247+
/// # Mime Type
248+
///
249+
/// ```txt
250+
/// application/wasm
251+
/// ```
252+
pub const WASM: Mime = Mime {
253+
static_essence: Some("application/wasm"),
254+
essence: String::new(),
255+
basetype: String::new(),
256+
subtype: String::new(),
257+
static_basetype: Some("application"),
258+
static_subtype: Some("wasm"),
259+
params: None,
260+
};

src/mime/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ impl Mime {
9898
}
9999
}
100100

101+
impl PartialEq<Mime> for Mime {
102+
fn eq(&self, other: &Mime) -> bool {
103+
let left = match self.static_essence {
104+
Some(essence) => essence,
105+
None => &self.essence,
106+
};
107+
let right = match other.static_essence {
108+
Some(essence) => essence,
109+
None => &other.essence,
110+
};
111+
left == right
112+
}
113+
}
114+
101115
impl Display for Mime {
102116
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103117
parse::format(self, f)
@@ -188,7 +202,7 @@ impl PartialEq<str> for ParamValue {
188202

189203
/// This is a hack that allows us to mark a trait as utf8 during compilation. We
190204
/// can remove this once we can construct HashMap during compilation.
191-
#[derive(Debug, Clone)]
205+
#[derive(Debug, Clone, PartialEq, Eq)]
192206
pub(crate) enum ParamKind {
193207
Utf8,
194208
Vec(Vec<(ParamName, ParamValue)>),

src/response.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,11 @@ impl Response {
367367
}
368368
}
369369

370+
/// Get the current content type
371+
pub fn content_type(&self) -> Option<Mime> {
372+
self.header(CONTENT_TYPE)?.last().as_str().parse().ok()
373+
}
374+
370375
/// Get the length of the body stream, if it has been set.
371376
///
372377
/// This value is set when passing a fixed-size object into as the body. E.g. a string, or a

tests/fixtures/empty.custom

Whitespace-only changes.

tests/fixtures/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<html>
2+
3+
<head></head>
4+
5+
<body>This is a fixture!</body>
6+
7+
</html>

tests/fixtures/nori.png

1.03 MB
Loading

tests/fixtures/unknown.custom

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
this is an unknown text format

tests/mime.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use async_std::fs;
2+
use async_std::io;
3+
use http_types::{mime, Body, Response};
4+
5+
#[async_std::test]
6+
async fn guess_plain_text_mime() -> io::Result<()> {
7+
let body = Body::from_file("tests/fixtures/index.html").await?;
8+
let mut res = Response::new(200);
9+
res.set_body(body);
10+
assert_eq!(res.content_type(), Some(mime::HTML));
11+
Ok(())
12+
}
13+
14+
#[async_std::test]
15+
async fn guess_binary_mime() -> http_types::Result<()> {
16+
let body = Body::from_file("tests/fixtures/nori.png").await?;
17+
let mut res = Response::new(200);
18+
res.set_body(body);
19+
assert_eq!(res.content_type(), Some(mime::PNG));
20+
21+
// Assert the file is correctly reset after we've peeked the bytes
22+
let left = fs::read("tests/fixtures/nori.png").await?;
23+
let right = res.body_bytes().await?;
24+
assert_eq!(left, right);
25+
Ok(())
26+
}
27+
28+
#[async_std::test]
29+
async fn guess_mime_fallback() -> io::Result<()> {
30+
let body = Body::from_file("tests/fixtures/unknown.custom").await?;
31+
let mut res = Response::new(200);
32+
res.set_body(body);
33+
assert_eq!(res.content_type(), Some(mime::BYTE_STREAM));
34+
Ok(())
35+
}
36+
37+
#[async_std::test]
38+
async fn parse_empty_files() -> http_types::Result<()> {
39+
let body = Body::from_file("tests/fixtures/empty.custom").await?;
40+
let mut res = Response::new(200);
41+
res.set_body(body);
42+
assert_eq!(res.content_type(), Some(mime::BYTE_STREAM));
43+
Ok(())
44+
}

0 commit comments

Comments
 (0)