///|
/// Content negotiation that mirrors the `accepts`/`negotiator` packages
/// Express relies on: quality values, wildcards, specificity ranking and the
/// per-kind defaults for `Accept`, `Accept-Charset`, `Accept-Encoding` and
/// `Accept-Language`.
///|
/// One parsed media range / charset / encoding / language from an accept
/// header, in header order.
priv struct AcceptSpec {
value : String
q : Double
order : Int32
params : Array[(String, String)]
}
///|
priv enum AcceptKind {
Media
Charset
Encoding
Language
}
///|
priv struct AcceptCandidate {
q : Double
specificity : Int32
spec_order : Int32
}
///|
/// Parses a q parameter such as `0.8` or `1`. Anything unparsable is `1.0`.
fn parse_q(value : String) -> Double {
let text = value.trim()
let mut integer : Double = 0.0
let mut seen_dot = false
let mut scale : Double = 0.1
let mut fraction : Double = 0.0
for ch in text {
let digit = ch.to_int() - 48
if digit >= 0 && digit <= 9 {
if seen_dot {
fraction = fraction + digit.to_double() * scale
scale = scale * 0.1
} else {
integer = integer * 10.0 + digit.to_double()
}
} else if ch == '.' {
if seen_dot {
break
}
seen_dot = true
} else {
break
}
}
integer + fraction
}
///|
fn strip_quotes(value : String) -> String {
if value.length() >= 2 && value.has_prefix("\"") && value.has_suffix("\"") {
value[1:value.length() - 1].to_owned()
} else {
value
}
}
///|
fn parse_accept_specs(header : String) -> Array[AcceptSpec] {
let specs : Array[AcceptSpec] = []
let mut order = 0
for raw in header.split(",") {
let item = raw.to_owned().trim().to_owned()
if item == "" {
continue
}
let segments = item.split(";")
let mut value = ""
let params : Array[(String, String)] = []
let mut q = 1.0
let mut index = 0
for part in segments {
let part = part.to_owned().trim().to_owned()
if index == 0 {
value = part.to_lower()
} else {
match part.split_once("=") {
Some((key, raw_value)) => {
let key = key.to_owned().trim().to_lower().to_owned()
let raw_value = strip_quotes(raw_value.to_owned().trim().to_owned())
if key == "q" {
q = parse_q(raw_value)
} else {
params.push((key, raw_value.to_lower()))
}
}
None => ()
}
}
index = index + 1
}
if value != "" {
specs.push({ value, q, order, params, })
}
order = order + 1
}
specs
}
///|
/// Splits `type/subtype` into its two parts (`*` yields `("*", "*")`).
fn split_media(value : String) -> (String, String) {
match value.split_once("/") {
Some((major, minor)) => (major.to_owned(), minor.to_owned())
None => (value, "*")
}
}
///|
/// The specificity of `provided` against one spec, following each negotiator
/// kind's `specify()`: `None` when the spec rejects the value.
fn spec_specificity(
spec : AcceptSpec,
provided : String,
kind : AcceptKind,
) -> Int32? {
let provided_lower = provided.to_lower()
match kind {
Charset | Encoding =>
if spec.value == provided_lower {
Some(1)
} else if spec.value == "*" {
Some(0)
} else {
None
}
Media => {
let (spec_type, spec_subtype) = split_media(spec.value)
let (provided_type, provided_subtype) = split_media(provided_lower)
let mut score = 0
if spec_type == provided_type {
score = score | 4
} else if spec_type != "*" {
return None
}
if spec_subtype == provided_subtype {
score = score | 2
} else if spec_subtype != "*" {
return None
}
if spec.params.length() > 0 {
let params = media_params(provided_lower)
for pair in spec.params {
let mut matched = false
for candidate in params {
if candidate.0 == pair.0 && (pair.1 == "*" || candidate.1 == pair.1) {
matched = true
}
}
if !matched {
return None
}
}
score = score | 1
}
Some(score)
}
Language => {
let (spec_full, spec_prefix) = language_parts(spec.value)
let (provided_full, provided_prefix) = language_parts(provided_lower)
if spec_full == provided_full {
Some(4)
} else if spec_prefix == provided_full {
Some(2)
} else if spec_full == provided_prefix {
Some(1)
} else if spec_full == "*" {
Some(0)
} else {
None
}
}
}
}
///|
/// The parameters of a media type such as `text/html;level=1`, `q` excluded.
fn media_params(value : String) -> Array[(String, String)] {
let params : Array[(String, String)] = []
let segments = value.split(";")
let mut index = 0
for raw in segments {
if index > 0 {
let part = raw.to_owned().trim().to_owned()
match part.split_once("=") {
Some((key, raw_value)) => {
let key = key.to_owned().trim().to_lower().to_owned()
if key != "q" {
params.push(
(key, strip_quotes(raw_value.to_owned().trim().to_owned())),
)
}
}
None => ()
}
}
index = index + 1
}
params
}
///|
/// `en-US` becomes `("en-us", "en")`.
fn language_parts(value : String) -> (String, String) {
match value.split_once("-") {
Some((prefix, _)) => (value, prefix.to_owned())
None => (value, value)
}
}
///|
/// The best-matching spec for one provided value, ranked like negotiator's
/// `getPriority`: specificity first, then q, then header order.
fn candidate_priority(
specs : Array[AcceptSpec],
provided : String,
kind : AcceptKind,
) -> AcceptCandidate {
let mut best : AcceptCandidate = { q: 0.0, specificity: 0, spec_order: -1, }
for spec in specs {
match spec_specificity(spec, provided, kind) {
Some(specificity) =>
if specificity > best.specificity ||
(
specificity == best.specificity &&
(
spec.q > best.q ||
(spec.q == best.q && spec.order > best.spec_order)
)
) {
best = { q: spec.q, specificity, spec_order: spec.order, }
}
None => ()
}
}
best
}
///|
/// Index of the winning provided value, or `-1`. Ordering follows
/// negotiator's `compareSpecs`: q desc, specificity desc, spec order asc,
/// provided order asc.
fn pick_best(
specs : Array[AcceptSpec],
provided : Array[String],
kind : AcceptKind,
) -> Int32 {
let mut best_index = -1
let mut best_q = 0.0
let mut best_specificity = 0
let mut best_spec_order = 0
let mut index = 0
while index < provided.length() {
let candidate = candidate_priority(specs, provided[index], kind)
if candidate.q > 0.0 {
let better = if best_index == -1 {
true
} else {
candidate.q > best_q ||
(candidate.q == best_q && candidate.specificity > best_specificity) ||
(
candidate.q == best_q &&
candidate.specificity == best_specificity &&
candidate.spec_order < best_spec_order
)
}
if better {
best_index = index
best_q = candidate.q
best_specificity = candidate.specificity
best_spec_order = candidate.spec_order
}
}
index = index + 1
}
best_index
}
///|
/// `req.accepts` for media types. Extensions such as `html` are mapped to
/// their media type before matching, and the caller's original string is
/// returned. An empty `accept` means "no header".
pub fn negotiate_media(accept : String, provided : Array[String]) -> String {
if accept.trim() == "" {
return if provided.length() > 0 { provided[0] } else { "" }
}
let mimes : Array[String] = []
let originals : Array[String] = []
for type_ in provided {
let mime = mime_for_type_name(type_)
if mime != "" {
mimes.push(mime)
originals.push(type_)
}
}
if mimes.length() == 0 {
return ""
}
let specs = parse_accept_specs(accept)
let index = pick_best(specs, mimes, Media)
if index == -1 {
return ""
}
originals[index]
}
///|
/// `req.acceptsCharsets`. With no header every charset is acceptable, so the
/// first provided one wins.
pub fn negotiate_charsets(header : String, provided : Array[String]) -> String {
let specs = if header == "" {
parse_accept_specs("*")
} else {
parse_accept_specs(header)
}
let index = pick_best(specs, provided, Charset)
if index == -1 {
""
} else {
provided[index]
}
}
///|
/// `req.acceptsEncodings`. With no header `identity` is the only acceptable
/// encoding, like negotiator's implicit `identity` entry.
pub fn negotiate_encodings(header : String, provided : Array[String]) -> String {
let specs = parse_accept_specs(header)
let mut has_identity = false
let mut min_quality = 1.0
for spec in specs {
if spec.value == "identity" || spec.value == "*" {
has_identity = true
}
let quality = if spec.q == 0.0 { 1.0 } else { spec.q }
if quality < min_quality {
min_quality = quality
}
}
if !has_identity {
specs.push({
value: "identity",
q: min_quality,
order: specs.length(),
params: [],
})
}
let index = pick_best(specs, provided, Encoding)
if index == -1 {
""
} else {
provided[index]
}
}
///|
/// `req.acceptsLanguages`. With no header every language is acceptable.
pub fn negotiate_languages(header : String, provided : Array[String]) -> String {
let specs = if header == "" {
parse_accept_specs("*")
} else {
parse_accept_specs(header)
}
let index = pick_best(specs, provided, Language)
if index == -1 {
""
} else {
provided[index]
}
}