use std::io;
use std::path::Path;
use std::pin::Pin;
use std::task::{Context, Poll, ready};
use bytes::Bytes;
use http::HeaderMap;
use http::header::{IF_RANGE, RANGE};
use http_body::{Body, Frame, SizeHint};
use tokio::fs::File;
use tokio::io::{AsyncRead, ReadBuf};
const MAX_FRAME_LEN: u64 = 64 * 1024;
#[derive(Debug)]
pub struct StaticPublicAssetBody {
buffered: Option<Bytes>,
file: Option<File>,
remaining_file_len: u64,
}
impl StaticPublicAssetBody {
pub(crate) fn from_file(file: File, length: u64) -> Self {
Self {
buffered: None,
file: Some(file),
remaining_file_len: length,
}
}
pub(crate) fn from_length_only(length: u64) -> Self {
Self {
buffered: None,
file: None,
remaining_file_len: length,
}
}
pub(crate) fn from_bytes(bytes: Bytes) -> Self {
Self {
buffered: Some(bytes),
file: None,
remaining_file_len: 0,
}
}
}
impl Body for StaticPublicAssetBody {
type Data = Bytes;
type Error = io::Error;
fn poll_frame(
self: Pin<&mut Self>,
ctx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Bytes>, io::Error>>> {
let body = self.get_mut();
if let Some(bytes) = body.buffered.take() {
return Poll::Ready(Some(Ok(Frame::data(bytes))));
}
let Some(file) = body.file.as_mut() else {
return Poll::Ready(None);
};
if body.remaining_file_len == 0 {
return Poll::Ready(None);
}
let frame_bytes = body.remaining_file_len.min(MAX_FRAME_LEN) as usize;
let mut chunk = vec![0; frame_bytes];
let mut read_buffer = ReadBuf::new(&mut chunk);
ready!(Pin::new(file).poll_read(ctx, &mut read_buffer))?;
let read_bytes = read_buffer.filled().len();
if read_bytes == 0 {
return Poll::Ready(Some(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"the file ended before its length at the start of the response",
))));
}
chunk.truncate(read_bytes);
body.remaining_file_len -= read_bytes as u64;
Poll::Ready(Some(Ok(Frame::data(Bytes::from(chunk)))))
}
fn size_hint(&self) -> SizeHint {
let buffered = self.buffered.as_ref().map_or(0, Bytes::len) as u64;
SizeHint::with_exact(buffered + self.remaining_file_len)
}
}
const CONTENT_TYPES_BY_EXTENSION: [(&str, &str); 17] = [
("cjs", "text/javascript"),
("mjs", "text/javascript"),
("otc", "font/collection"),
("otf", "font/otf"),
("woff", "font/woff"),
("ico", "image/vnd.microsoft.icon"),
("xml", "application/xml"),
("xsl", "application/xslt+xml"),
("xslt", "application/xslt+xml"),
("toml", "application/toml"),
("yaml", "application/yaml"),
("yml", "application/yaml"),
("mkv", "video/matroska"),
("jsx", "text/jsx"),
("map", "application/json"),
("mts", "video/mp2t"),
("ts", "video/mp2t"),
];
pub(crate) fn read_content_type(file_name: &str) -> String {
let extension = Path::new(file_name)
.extension()
.and_then(|extension| extension.to_str())
.map(str::to_ascii_lowercase);
if let Some(extension) = &extension
&& let Some((_, content_type)) = CONTENT_TYPES_BY_EXTENSION
.iter()
.find(|(listed, _)| listed == extension)
{
return (*content_type).to_owned();
}
mime_guess::from_path(file_name)
.first_or_octet_stream()
.essence_str()
.to_owned()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ByteRange {
pub(crate) start: u64,
pub(crate) length: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StaticPublicAssetAnswer {
Whole,
Partial(ByteRange),
RangeNotSatisfiable,
}
pub(crate) fn decide_static_public_asset_answer(
headers: &HeaderMap,
length: u64,
) -> StaticPublicAssetAnswer {
let Some(range_header) = headers.get(RANGE) else {
return StaticPublicAssetAnswer::Whole;
};
if headers.contains_key(IF_RANGE) {
return StaticPublicAssetAnswer::Whole;
}
let Ok(range_header) = range_header.to_str() else {
return StaticPublicAssetAnswer::RangeNotSatisfiable;
};
match parse_ranges(range_header, length) {
Err(RangeError::Invalid) => StaticPublicAssetAnswer::RangeNotSatisfiable,
Err(RangeError::NoOverlap) if length == 0 => StaticPublicAssetAnswer::Whole,
Err(RangeError::NoOverlap) => StaticPublicAssetAnswer::RangeNotSatisfiable,
Ok(ranges) => match ranges.as_slice() {
[range] => StaticPublicAssetAnswer::Partial(*range),
_ => StaticPublicAssetAnswer::Whole,
},
}
}
#[derive(Debug, PartialEq, Eq)]
enum RangeError {
Invalid,
NoOverlap,
}
fn parse_ranges(header: &str, length: u64) -> Result<Vec<ByteRange>, RangeError> {
const UNIT: &str = "bytes=";
let specs = header
.get(..UNIT.len())
.filter(|unit| unit.eq_ignore_ascii_case(UNIT))
.map(|_| &header[UNIT.len()..])
.ok_or(RangeError::Invalid)?;
let mut ranges = Vec::new();
let mut is_any_spec_unsatisfiable = false;
for spec in specs
.split(',')
.map(str::trim)
.filter(|spec| !spec.is_empty())
{
let (first, last) = spec.split_once('-').ok_or(RangeError::Invalid)?;
let (first, last) = (first.trim(), last.trim());
if first.is_empty() {
let suffix_length = parse_decimal(last)?;
if suffix_length == 0 || length == 0 {
is_any_spec_unsatisfiable = true;
continue;
}
let suffix_length = suffix_length.min(length);
ranges.push(ByteRange {
start: length - suffix_length,
length: suffix_length,
});
continue;
}
let start = parse_decimal(first)?;
let end = if last.is_empty() {
None
} else {
Some(parse_decimal(last)?)
};
if end.is_some_and(|end| start > end) {
return Err(RangeError::Invalid);
}
if start >= length {
is_any_spec_unsatisfiable = true;
continue;
}
let end = end.map_or(length - 1, |end| end.min(length - 1));
ranges.push(ByteRange {
start,
length: end - start + 1,
});
}
if ranges.is_empty() {
return Err(if is_any_spec_unsatisfiable {
RangeError::NoOverlap
} else {
RangeError::Invalid
});
}
Ok(ranges)
}
fn parse_decimal(text: &str) -> Result<u64, RangeError> {
if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(RangeError::Invalid);
}
text.parse().map_err(|_| RangeError::Invalid)
}