// HTTP-header logic for static asset serving: Accept-Encoding parsing,
// ETag / If-None-Match / If-Modified-Since validation, and the header
// maps that go on 200 OK and 304 Not Modified responses.
/// Parses a single Accept-Encoding token, returning the encoding name
/// or `None` if the q-value is 0 (meaning the client explicitly refuses it).
/// Examples:
/// "gzip" -> Some("gzip")
/// "gzip; q=0.8" -> Some("gzip")
/// "gzip;q=0" -> None
/// "br;q=0.0" -> None
///|
fn parse_accept_encoding_token(token : StringView) -> String? {
let (name, rest) = match token.find(";") {
Some(index) =>
(
token[:index].trim(chars=" ").to_owned().to_lower(),
Some(token[index + 1:]),
)
None => (token.trim(chars=" ").to_owned().to_lower(), None)
}
// Check q-value — if explicitly 0, refuse this encoding.
match rest {
Some(params) =>
for param in params.split(";") {
let trimmed = param.trim(chars=" ")
if trimmed.has_prefix("q=") || trimmed.has_prefix("Q=") {
let qval = trimmed[2:].trim(chars=" ").to_owned()
// q=0, q=0.0, q=0.00, etc. means refused.
if qval == "0" || qval == "0.0" || qval == "0.00" || qval == "0.000" {
return None
}
}
}
None => ()
}
Some(name)
}
///|
test "parse_accept_encoding_token strips whitespace and lowercases" {
debug_inspect(parse_accept_encoding_token("gzip"), content="Some(\"gzip\")")
debug_inspect(
parse_accept_encoding_token(" gzip "),
content="Some(\"gzip\")",
)
debug_inspect(
parse_accept_encoding_token("gzip;q=0.8"),
content="Some(\"gzip\")",
)
debug_inspect(parse_accept_encoding_token("BR"), content="Some(\"br\")")
}
///|
test "parse_accept_encoding_token rejects q=0" {
debug_inspect(parse_accept_encoding_token("gzip;q=0"), content="None")
debug_inspect(parse_accept_encoding_token("gzip; q=0.0"), content="None")
debug_inspect(parse_accept_encoding_token("br;q=0.00"), content="None")
// Non-zero q-values are accepted
debug_inspect(
parse_accept_encoding_token("gzip;q=0.5"),
content="Some(\"gzip\")",
)
debug_inspect(
parse_accept_encoding_token("gzip;q=1.0"),
content="Some(\"gzip\")",
)
}
///|
fn normalize_etag_token(token : StringView) -> String {
let trimmed = token.trim(chars=" ")
if trimmed.length() >= 2 &&
(trimmed.has_prefix("W/") || trimmed.has_prefix("w/")) {
trimmed[2:].trim(chars=" ").to_owned()
} else {
trimmed.to_owned()
}
}
///|
test "normalize_etag_token strips weak prefix and whitespace" {
debug_inspect(
normalize_etag_token("\"abc\""),
content=(
#|"\"abc\""
),
)
debug_inspect(
normalize_etag_token("W/\"abc\""),
content=(
#|"\"abc\""
),
)
debug_inspect(
normalize_etag_token("w/\"abc\""),
content=(
#|"\"abc\""
),
)
debug_inspect(
normalize_etag_token(" \"abc\" "),
content=(
#|"\"abc\""
),
)
}
///|
fn if_none_match_matches(header_value : String, etag : String) -> Bool {
for token in header_value.split(",") {
let normalized = normalize_etag_token(token)
if normalized == "*" || normalized == etag {
return true
}
}
false
}
///|
test "if_none_match_matches checks single, wildcard, list, and weak etag" {
// single match
assert_eq(if_none_match_matches("\"abc\"", "\"abc\""), true)
// wildcard
assert_eq(if_none_match_matches("*", "\"abc\""), true)
// comma-separated list with match
assert_eq(if_none_match_matches("\"aaa\", \"abc\", \"zzz\"", "\"abc\""), true)
// no match
assert_eq(if_none_match_matches("\"aaa\", \"bbb\"", "\"abc\""), false)
// weak etag in list
assert_eq(if_none_match_matches("W/\"aaa\", W/\"abc\"", "\"abc\""), true)
}
///|
fn static_asset_headers(
provider : &ServeStaticProvider,
id : String,
meta : StaticAssetMeta,
) -> Map[String, String] {
let headers : Map[String, String] = Map([])
match meta.etag {
Some(etag) => headers.set("ETag", etag)
None => ()
}
match meta.mtime {
Some(mtime) =>
headers.set("Last-Modified", @httputil.format_http_date(mtime))
None => ()
}
match meta.asset_type {
Some(t) => headers.set("Content-Type", t)
None => {
let parts = id.split(".").collect()
if parts.length() > 1 {
match provider.get_type(parts[parts.length() - 1].to_owned()) {
Some(t) => headers.set("Content-Type", t)
None => ()
}
}
}
}
match meta.encoding {
Some(enc) => headers.set("Content-Encoding", enc)
None => ()
}
match meta.size {
Some(size) => headers.set("Content-Length", size.to_string())
None => ()
}
headers
}
///|
fn static_not_modified_headers(
response_headers : Map[String, String],
meta : StaticAssetMeta,
) -> Map[String, String] {
let headers = @httputil.copy_headers(response_headers)
match meta.etag {
Some(etag) => headers.set("ETag", etag)
None => ()
}
match meta.mtime {
Some(mtime) =>
headers.set("Last-Modified", @httputil.format_http_date(mtime))
None => ()
}
headers
}
///|
fn if_modified_since_matches(header_value : String, mtime : Int64) -> Bool {
match @httputil.parse_http_date(header_value) {
Some(if_modified_since) => if_modified_since >= mtime
None => false
}
}