///|
/// One normalized Cache-Control directive. Unknown extensions are represented
/// exactly like registered directives so callers can inspect them.
pub(all) struct CacheDirective {
name : String
value : String?
quoted : Bool
raw : String
} derive(Debug, Eq)
///|
/// Loss-tolerant parse result for one or more Cache-Control field lines.
pub(all) struct CacheControl {
directives : Array[CacheDirective]
diagnostics : Array[Diagnostic]
} derive(Debug, Eq)
///|
/// Result of reading a numeric delta-seconds directive.
pub(all) enum DeltaDirective {
DeltaMissing
DeltaValid(Int64)
DeltaInvalid
DeltaRepeated
} derive(Debug, Eq)
///|
/// Parse every Cache-Control field line. Syntax faults are captured as
/// diagnostics and valid neighboring directives remain available.
pub fn parse_cache_control(headers : Headers) -> CacheControl {
let directives : Array[CacheDirective] = []
let diagnostics = headers.diagnostics()
for field_value in headers.values("cache-control") {
let split = @lex.split_quoted_list(field_value)
if !split.valid {
diagnostics.push(
diagnostic(
Error,
"CACHE_CONTROL_UNCLOSED_QUOTE",
"Cache-Control contains an unclosed quoted string",
),
)
}
for raw in split.parts {
parse_directive(raw, directives, diagnostics)
}
}
append_duplicate_diagnostics(directives, diagnostics)
{ directives, diagnostics, }
}
///|
fn parse_directive(
raw : String,
directives : Array[CacheDirective],
diagnostics : Array[Diagnostic],
) -> Unit {
let text = @lex.trim_ows(raw)
if text.length() == 0 {
diagnostics.push(
diagnostic(
Warning,
"CACHE_CONTROL_EMPTY_MEMBER",
"empty Cache-Control list member was ignored",
),
)
return
}
match @lex.find_unquoted_equals(text) {
None => {
let name = @lex.lower(text)
if !@lex.is_token(name) {
diagnostics.push(
diagnostic(
Error,
"CACHE_CONTROL_NAME_INVALID",
"directive name is not a valid HTTP token",
),
)
return
}
directives.push({ name, value: None, quoted: false, raw: text, })
}
Some(offset) => {
let name = @lex.lower(text[:offset].trim(chars=" \t").to_owned())
let encoded = text[offset + 1:].trim(chars=" \t").to_owned()
if !@lex.is_token(name) {
diagnostics.push(
diagnostic(
Error,
"CACHE_CONTROL_NAME_INVALID",
"directive name is not a valid HTTP token",
),
)
return
}
if encoded.length() == 0 {
diagnostics.push(
diagnostic(
Error,
"CACHE_CONTROL_VALUE_MISSING",
"directive has an equals sign but no value",
),
)
directives.push({ name, value: None, quoted: false, raw: text, })
return
}
if encoded.has_prefix("\"") {
let decoded = @lex.decode_quoted(encoded)
if !decoded.valid {
diagnostics.push(
diagnostic(
Error,
"CACHE_CONTROL_QUOTED_VALUE_INVALID",
"directive contains an invalid quoted-string",
),
)
}
directives.push({
name,
value: Some(decoded.value),
quoted: true,
raw: text,
})
} else if @lex.is_token(encoded) {
directives.push({
name,
value: Some(encoded),
quoted: false,
raw: text,
})
} else {
diagnostics.push(
diagnostic(
Error,
"CACHE_CONTROL_TOKEN_VALUE_INVALID",
"unquoted directive value is not a valid HTTP token",
),
)
directives.push({
name,
value: Some(encoded),
quoted: false,
raw: text,
})
}
}
}
}
///|
fn append_duplicate_diagnostics(
directives : Array[CacheDirective],
diagnostics : Array[Diagnostic],
) -> Unit {
for index, directive in directives {
let mut seen_before = false
for prior = 0; prior < index; prior = prior + 1 {
if directives[prior].name == directive.name {
seen_before = true
break
}
}
if seen_before {
diagnostics.push(
diagnostic(
Warning,
"CACHE_CONTROL_DUPLICATE_DIRECTIVE",
"duplicate Cache-Control directive: " + directive.name,
),
)
}
}
}
///|
fn diagnostic(
level : DiagnosticLevel,
code : String,
message : String,
) -> Diagnostic {
{ level, code, message, field_name: Some("cache-control"), }
}
///|
pub fn CacheControl::contains(self : CacheControl, name : String) -> Bool {
let normalized = @lex.lower(name)
for directive in self.directives {
if directive.name == normalized {
return true
}
}
false
}
///|
pub fn CacheControl::occurrences(
self : CacheControl,
name : String,
) -> Array[CacheDirective] {
let normalized = @lex.lower(name)
let matches : Array[CacheDirective] = []
for directive in self.directives {
if directive.name == normalized {
matches.push(directive)
}
}
matches
}
///|
/// Read a single delta-seconds directive with RFC 9111 overflow saturation.
pub fn CacheControl::delta(
self : CacheControl,
name : String,
) -> DeltaDirective {
let matches = self.occurrences(name)
if matches.length() == 0 {
return DeltaMissing
}
if matches.length() > 1 {
return DeltaRepeated
}
match matches[0].value {
None => DeltaInvalid
Some(value) =>
match @lex.parse_saturating_decimal(value, 2147483648L) {
Some(seconds) => DeltaValid(seconds)
None => DeltaInvalid
}
}
}
///|
/// Return field-name arguments from directives such as private="set-cookie".
pub fn CacheControl::field_names(
self : CacheControl,
name : String,
) -> Array[String] {
let names : Array[String] = []
for directive in self.occurrences(name) {
match directive.value {
None => ()
Some(value) => {
let split = @lex.split_quoted_list(value)
for item in split.parts {
let normalized = @lex.lower(@lex.trim_ows(item))
if @lex.is_token(normalized) {
names.push(normalized)
}
}
}
}
}
names
}