use std::collections::BTreeMap;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, PoisonError};
use flate2::Compression;
use flate2::write::GzEncoder;
pub const IMMUTABLE: &str = "public, max-age=31536000, immutable";
const HASH: usize = 8;
#[derive(Default)]
pub struct Packed {
kept: Mutex<BTreeMap<PathBuf, Arc<[u8]>>>,
}
impl Packed {
pub fn gzipped(&self, path: &Path, hashed: bool) -> io::Result<Arc<[u8]>> {
if hashed {
let kept = self.lock().get(path).cloned();
if let Some(kept) = kept {
return Ok(kept);
}
}
let packed: Arc<[u8]> = gzip(&std::fs::read(path)?)?.into();
if hashed {
self.lock().insert(path.to_owned(), Arc::clone(&packed));
}
Ok(packed)
}
fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<PathBuf, Arc<[u8]>>> {
self.kept.lock().unwrap_or_else(PoisonError::into_inner)
}
}
pub fn gzip(bytes: &[u8]) -> io::Result<Vec<u8>> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(bytes)?;
encoder.finish()
}
#[must_use]
pub fn takes_gzip(accept: Option<&str>) -> bool {
let mut named = None;
let mut any = false;
for item in accept.unwrap_or_default().split(',') {
let mut parts = item.split(';');
let coding = parts.next().unwrap_or_default().trim();
let taken = !parts
.filter_map(|part| part.trim().strip_prefix("q="))
.any(refuses);
if coding.eq_ignore_ascii_case("gzip") || coding.eq_ignore_ascii_case("x-gzip") {
named = Some(taken);
} else if coding == "*" {
any = taken;
}
}
named.unwrap_or(any)
}
fn refuses(quality: &str) -> bool {
let quality = quality.trim();
!quality.is_empty() && quality.bytes().all(|byte| matches!(byte, b'0' | b'.'))
}
#[must_use]
pub fn hashed(path: &str) -> bool {
let Some(name) = path.strip_prefix("/assets/") else {
return false;
};
let file = name.rsplit('/').next().unwrap_or(name);
let Some((stem, extension)) = file.rsplit_once('.') else {
return false;
};
let stem = stem.as_bytes();
let Some(cut) = stem.len().checked_sub(HASH + 1) else {
return false;
};
let (before, hash) = stem.split_at(cut);
!extension.is_empty()
&& !before.is_empty()
&& hash.first() == Some(&b'-')
&& hash
.iter()
.skip(1)
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
}
#[cfg(test)]
#[path = "packed_tests.rs"]
mod tests;