#![deny(clippy::print_stderr, clippy::print_stdout)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use bytes::Buf;
use futures_core::Stream;
use http::header::{self, HeaderMap, HeaderValue};
use std::ops::Range;
use std::str::FromStr;
use std::time::SystemTime;
macro_rules! unsafe_fmt_ascii_val {
($max_len:expr, $fmt:expr, $($arg:tt)+) => {{
let mut buf = bytes::BytesMut::with_capacity($max_len);
use std::fmt::Write;
write!(buf, $fmt, $($arg)*).expect("fmt_val fits within provided max len");
unsafe {
http::header::HeaderValue::from_maybe_shared_unchecked(buf.freeze())
}
}}
}
mod chunker;
#[cfg(feature = "dir")]
#[cfg_attr(docsrs, doc(cfg(feature = "dir")))]
pub mod dir;
mod etag;
mod file;
mod gzip;
mod platform;
mod range;
mod serving;
pub use crate::file::ChunkedReadFile;
pub use crate::gzip::BodyWriter;
pub use crate::serving::serve;
pub trait Entity: 'static + Send + Sync {
type Error: 'static + Send + Sync;
type Data: 'static + Send + Sync + Buf + From<Vec<u8>> + From<&'static [u8]>;
fn len(&self) -> u64;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn get_range(
&self,
range: Range<u64>,
) -> Box<dyn Stream<Item = Result<Self::Data, Self::Error>> + Send + Sync>;
fn add_headers(&self, _: &mut HeaderMap);
fn etag(&self) -> Option<HeaderValue>;
fn last_modified(&self) -> Option<SystemTime>;
}
fn parse_qvalue(s: &str) -> Result<u16, ()> {
match s {
"1" | "1." | "1.0" | "1.00" | "1.000" => return Ok(1000),
"0" | "0." => return Ok(0),
s if !s.starts_with("0.") => return Err(()),
_ => {}
};
let v = &s[2..];
let factor = match v.len() {
1 => 100,
2 => 10,
3 => 1,
_ => return Err(()),
};
let v = u16::from_str(v).map_err(|_| ())?;
let q = v * factor;
Ok(q)
}
pub fn should_gzip(headers: &HeaderMap) -> bool {
let v = match headers.get(header::ACCEPT_ENCODING) {
None => return false,
Some(v) => v,
};
let (mut gzip_q, mut identity_q, mut star_q) = (None, None, None);
let parts = match v.to_str() {
Ok(s) => s.split(','),
Err(_) => return false,
};
for qi in parts {
let qi = qi.trim();
let mut parts = qi.rsplitn(2, ';').map(|p| p.trim());
let last_part = parts
.next()
.expect("rsplitn should return at least one part");
let coding;
let quality;
match parts.next() {
None => {
coding = last_part;
quality = 1000;
}
Some(c) => {
if !last_part.starts_with("q=") {
return false; }
let q = &last_part[2..];
match parse_qvalue(q) {
Ok(q) => {
coding = c;
quality = q;
}
Err(_) => return false, };
}
}
if coding == "gzip" {
gzip_q = Some(quality);
} else if coding == "identity" {
identity_q = Some(quality);
} else if coding == "*" {
star_q = Some(quality);
}
}
let gzip_q = gzip_q.or(star_q).unwrap_or(0);
let identity_q = identity_q.or(star_q).unwrap_or(1);
gzip_q > 0 && gzip_q >= identity_q
}
pub struct StreamingBodyBuilder {
chunk_size: usize,
gzip_level: u32,
should_gzip: bool,
body_needed: bool,
}
pub fn streaming_body<T>(req: &http::Request<T>) -> StreamingBodyBuilder {
StreamingBodyBuilder {
chunk_size: 4096,
gzip_level: 6,
should_gzip: should_gzip(req.headers()),
body_needed: *req.method() != http::method::Method::HEAD,
}
}
impl StreamingBodyBuilder {
pub fn with_chunk_size(self, chunk_size: usize) -> Self {
StreamingBodyBuilder { chunk_size, ..self }
}
pub fn with_gzip_level(self, gzip_level: u32) -> Self {
StreamingBodyBuilder { gzip_level, ..self }
}
pub fn build<P, D, E>(self) -> (http::Response<P>, Option<BodyWriter<D, E>>)
where
D: From<Vec<u8>> + Send + Sync,
E: Send + Sync,
P: From<Box<dyn Stream<Item = Result<D, E>> + Send>>,
{
let (w, stream) = chunker::BodyWriter::with_chunk_size(self.chunk_size);
let mut resp = http::Response::new(stream.into());
resp.headers_mut()
.append(header::VARY, HeaderValue::from_static("accept-encoding"));
if self.should_gzip && self.gzip_level > 0 {
resp.headers_mut()
.append(header::CONTENT_ENCODING, HeaderValue::from_static("gzip"));
}
if !self.body_needed {
return (resp, None);
}
let w = match self.should_gzip && self.gzip_level > 0 {
true => BodyWriter::gzipped(w, flate2::Compression::new(self.gzip_level)),
false => BodyWriter::raw(w),
};
(resp, Some(w))
}
}
#[cfg(test)]
mod tests {
use http::header::HeaderValue;
use http::{self, header};
fn ae_hdrs(value: &'static str) -> http::HeaderMap {
let mut h = http::HeaderMap::new();
h.insert(header::ACCEPT_ENCODING, HeaderValue::from_static(value));
h
}
#[test]
fn parse_qvalue() {
use super::parse_qvalue;
assert_eq!(parse_qvalue("0"), Ok(0));
assert_eq!(parse_qvalue("0."), Ok(0));
assert_eq!(parse_qvalue("0.0"), Ok(0));
assert_eq!(parse_qvalue("0.00"), Ok(0));
assert_eq!(parse_qvalue("0.000"), Ok(0));
assert_eq!(parse_qvalue("0.0000"), Err(()));
assert_eq!(parse_qvalue("0.2"), Ok(200));
assert_eq!(parse_qvalue("0.23"), Ok(230));
assert_eq!(parse_qvalue("0.234"), Ok(234));
assert_eq!(parse_qvalue("1"), Ok(1000));
assert_eq!(parse_qvalue("1."), Ok(1000));
assert_eq!(parse_qvalue("1.0"), Ok(1000));
assert_eq!(parse_qvalue("1.1"), Err(()));
assert_eq!(parse_qvalue("1.00"), Ok(1000));
assert_eq!(parse_qvalue("1.000"), Ok(1000));
assert_eq!(parse_qvalue("1.001"), Err(()));
assert_eq!(parse_qvalue("1.0000"), Err(()));
assert_eq!(parse_qvalue("2"), Err(()));
}
#[test]
fn should_gzip() {
assert!(!super::should_gzip(&header::HeaderMap::new()));
assert!(super::should_gzip(&ae_hdrs("gzip")));
assert!(super::should_gzip(&ae_hdrs("gzip;q=0.001")));
assert!(!super::should_gzip(&ae_hdrs("gzip;q=0")));
assert!(!super::should_gzip(&ae_hdrs("")));
assert!(super::should_gzip(&ae_hdrs("*")));
assert!(!super::should_gzip(&ae_hdrs("gzip;q=0, *")));
assert!(super::should_gzip(&ae_hdrs("identity=q=0, *")));
assert!(super::should_gzip(&ae_hdrs("identity;q=0.5, gzip;q=1.0")));
assert!(!super::should_gzip(&ae_hdrs("identity;q=1.0, gzip;q=0.5")));
assert!(!super::should_gzip(&ae_hdrs("*;q=0")));
}
}