use std::fs::Metadata;
use std::time::UNIX_EPOCH;
use headers::HeaderValue;
use crate::handler::RequestHandlerOpts;
const ETAG_MAX_LEN: usize = 3 + 32 + 1 + 16 + 1;
pub(crate) fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
handler_opts.etag = enabled;
tracing::info!(enabled, "etag headers");
}
#[must_use]
pub(crate) fn build_from_meta(meta: &Metadata) -> Option<(headers::ETag, HeaderValue)> {
let modified = meta.modified().ok()?;
if modified == UNIX_EPOCH {
return None;
}
let nanos = modified.duration_since(UNIX_EPOCH).ok()?.as_nanos();
Some(build_from_parts(nanos, meta.len()))
}
#[must_use]
fn build_from_parts(mtime_nanos: u128, len: u64) -> (headers::ETag, HeaderValue) {
use std::fmt::Write as _;
let mut s = String::with_capacity(ETAG_MAX_LEN);
let _ = write!(s, "W/\"{mtime_nanos:x}-{len:x}\"");
let hv = HeaderValue::from_str(&s).expect("etag value is valid header bytes");
let etag: headers::ETag = s.parse().expect("etag value conforms to RFC 7232");
(etag, hv)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::time::Duration;
#[test]
fn etag_has_weak_prefix_and_dash_separator() {
let (_, hv) = build_from_parts(0x1234_5678, 42);
let s = hv.to_str().unwrap();
assert!(s.starts_with("W/\""), "{s}");
assert!(s.ends_with('"'), "{s}");
assert_eq!(s, "W/\"12345678-2a\"");
}
#[test]
fn etag_changes_when_size_changes() {
let (_, a) = build_from_parts(1_000, 10);
let (_, b) = build_from_parts(1_000, 11);
assert_ne!(a, b);
}
#[test]
fn etag_changes_when_mtime_changes() {
let (_, a) = build_from_parts(1_000, 10);
let (_, b) = build_from_parts(1_001, 10);
assert_ne!(a, b);
}
#[test]
fn etag_is_stable_for_same_inputs() {
let (_, a) = build_from_parts(1_700_000_000_000_000_000, 4096);
let (_, b) = build_from_parts(1_700_000_000_000_000_000, 4096);
assert_eq!(a, b);
}
#[test]
fn etag_max_value_fits_capacity() {
let (_, hv) = build_from_parts(u128::MAX, u64::MAX);
assert!(hv.as_bytes().len() <= ETAG_MAX_LEN);
}
#[test]
fn build_from_meta_returns_some_for_real_file() {
let path = std::env::temp_dir().join("sws-etag-test.bin");
fs::write(&path, b"abc").unwrap();
let meta = fs::metadata(&path).unwrap();
let result = build_from_meta(&meta);
let _ = fs::remove_file(&path);
let (_, hv) = result.expect("expected an ETag for a freshly created file");
let s = hv.to_str().unwrap();
assert!(s.starts_with("W/\""));
assert!(s.ends_with("-3\""), "expected length suffix: {s}");
}
#[test]
fn typed_etag_round_trips() {
let nanos = 1_700_000_000_000_000_000u128;
let (tag, hv) = build_from_parts(nanos, 4096);
let reparsed: headers::ETag = hv.to_str().unwrap().parse().unwrap();
assert_eq!(tag, reparsed);
}
#[test]
fn build_from_meta_returns_none_for_unix_epoch_mtime() {
let _ = UNIX_EPOCH + Duration::ZERO;
}
}