use crate::common::{cache_control::CacheControl, file::File};
use anyhow::Error;
use brotli::enc::BrotliEncoderParams;
use flate2::{Compression, write::GzEncoder};
use sha3::{Digest, Sha3_256};
use std::{
fs,
io::{Cursor, Write},
path::Path,
};
#[derive(Debug)]
pub struct BuildFromPathOptions {
pub use_gzip: bool,
pub use_brotli: bool,
pub content_type_override: Option<String>,
pub cache_control_override: Option<CacheControl>,
}
impl Default for BuildFromPathOptions {
fn default() -> Self {
Self {
use_gzip: true,
use_brotli: true,
content_type_override: None,
cache_control_override: None,
}
}
}
pub fn build_from_path(
path: &Path,
options: &BuildFromPathOptions,
) -> Result<File, Error> {
let content = content_from_path(path)?;
let content_type = if let Some(content_type) = &options.content_type_override {
content_type.clone()
} else {
content_type_from_path(path)
};
let file = build_from_content(
content,
content_type,
&BuildFromContentOptions {
use_gzip: options.use_gzip,
use_brotli: options.use_brotli,
cache_control_override: options.cache_control_override,
},
);
Ok(file)
}
#[derive(Debug)]
pub struct BuildFromContentOptions {
pub use_gzip: bool,
pub use_brotli: bool,
pub cache_control_override: Option<CacheControl>,
}
impl Default for BuildFromContentOptions {
fn default() -> Self {
Self {
use_gzip: true,
use_brotli: true,
cache_control_override: None,
}
}
}
pub fn build_from_content(
content: Box<[u8]>,
content_type: String,
options: &BuildFromContentOptions,
) -> File {
let content_gzip = if options.use_gzip {
content_gzip_from_content(&content)
} else {
None
};
let content_brotli = if options.use_brotli {
content_brotli_from_content(&content)
} else {
None
};
let etag = etag_from_content(&content);
let cache_control = if let Some(cache_control) = &options.cache_control_override {
*cache_control
} else {
CacheControl::MaxCache
};
File {
content,
content_gzip,
content_brotli,
content_type,
etag,
cache_control,
}
}
fn content_from_path(path: &Path) -> Result<Box<[u8]>, Error> {
let content = fs::read(path)?.into_boxed_slice();
Ok(content)
}
fn content_gzip_from_content(content: &[u8]) -> Option<Box<[u8]>> {
if content.is_empty() {
return None;
}
let mut content_gzip = GzEncoder::new(Vec::new(), Compression::best());
content_gzip.write_all(content).unwrap();
let content_gzip = content_gzip.finish().unwrap().into_boxed_slice();
if content_gzip.len() >= content.len() {
return None;
}
Some(content_gzip)
}
fn content_brotli_from_content(content: &[u8]) -> Option<Box<[u8]>> {
if content.is_empty() {
return None;
}
let mut content_cursor = Cursor::new(content);
let mut content_brotli = Vec::new();
let content_brotli_length = brotli::BrotliCompress(
&mut content_cursor,
&mut content_brotli,
&BrotliEncoderParams::default(),
)
.unwrap();
let content_brotli = content_brotli.into_boxed_slice();
assert!(content_brotli.len() == content_brotli_length);
if content_brotli.len() >= content.len() {
return None;
}
Some(content_brotli)
}
fn content_type_from_path(path: &Path) -> String {
let mut content_type = mime_guess::from_path(path)
.first_or_octet_stream()
.as_ref()
.to_owned();
if content_type.starts_with("text/") {
content_type.push_str("; charset=utf-8");
}
content_type
}
fn etag_from_content(content: &[u8]) -> String {
let mut etag = Sha3_256::new();
etag.update(content);
let etag = etag.finalize();
let etag = format!("\"{:x}\"", &etag); etag
}
#[cfg(test)]
mod test {
use super::{
BuildFromContentOptions, build_from_content, content_brotli_from_content,
content_gzip_from_content, content_type_from_path, etag_from_content,
};
use crate::common::file::File;
use std::path::{Path, PathBuf};
use test_case::test_case;
#[test]
fn build_from_content_returns_expected() {
let content_original = b"lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum";
let content_type_original = "text/plain; charset=utf-8";
let file = build_from_content(
Box::new(*content_original),
content_type_original.to_owned(),
&BuildFromContentOptions::default(),
);
let File {
content,
content_gzip,
content_brotli,
content_type,
..
} = file;
assert_eq!(&*content, content_original);
assert_eq!(&*content_gzip.unwrap(), b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x95\xc6\x41\x09\x00\x00\x08\x03\xc0\x2a\x2b\xe7\x43\xd8\x50\x14\xfb\x9b\x61\xbf\x63\x4d\x08\xd9\x7b\x02\x3d\x3f\x1e\x08\x7c\xb8\x3b\x00\x00\x00");
assert_eq!(&*content_brotli.unwrap(), b"\x1b\x3a\x00\xf8\x1d\xa9\x53\x9f\xbb\x70\x9d\xc6\xf6\x06\xa7\xda\xe4\x1a\xa4\x6c\xae\x4e\x18\x15\x0b\x98\x56\x70\x03");
assert_eq!(content_type, content_type_original);
}
#[test]
fn empty_should_not_be_compressed() {
assert!(content_gzip_from_content(&[]).is_none());
assert!(content_brotli_from_content(&[]).is_none());
}
#[test]
fn content_gzip_from_content_returns_expected() {
assert_eq!(
content_gzip_from_content(b"lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum").as_deref(),
Some(b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x95\xc6\x41\x09\x00\x00\x08\x03\xc0\x2a\x2b\xe7\x43\xd8\x50\x14\xfb\x9b\x61\xbf\x63\x4d\x08\xd9\x7b\x02\x3d\x3f\x1e\x08\x7c\xb8\x3b\x00\x00\x00".as_slice())
);
}
#[test]
fn content_brotli_from_content_returns_expected() {
assert_eq!(
content_brotli_from_content(b"lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum").as_deref(),
Some(b"\x1b\x3a\x00\xf8\x1d\xa9\x53\x9f\xbb\x70\x9d\xc6\xf6\x06\xa7\xda\xe4\x1a\xa4\x6c\xae\x4e\x18\x15\x0b\x98\x56\x70\x03".as_slice())
);
}
#[test]
fn etag_from_content_returns_expected() {
assert_eq!(
etag_from_content(b"lorem ipsum"),
etag_from_content(b"lorem ipsum")
);
assert_ne!(
etag_from_content(b"lorem ipsum"),
etag_from_content(b"ipsum lorem")
);
}
#[test_case(
&PathBuf::from("a.html"),
"text/html; charset=utf-8";
"html file"
)]
#[test_case(
&PathBuf::from("directory/styles.css"),
"text/css; charset=utf-8";
"css file in directory"
)]
#[test_case(
&PathBuf::from("/root/dir/script.00ff00.js"),
"text/javascript; charset=utf-8";
"js file, full path, with some hex in stem"
)]
#[test_case(
&PathBuf::from("C:\\Users\\example\\Images\\SomeImage.webp"),
"image/webp";
"webp image in windows style path format"
)]
fn content_type_from_path_returns_expected(
path: &Path,
expected: &str,
) {
assert_eq!(content_type_from_path(path), expected);
}
}