// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A parsed `Content-Disposition` header (RFC 6266).
///
/// `kind` is the lowercased disposition type — `inline`, `attachment`,
/// `form-data` — and `params` the rest, names lowercased and quoted values
/// unquoted.
pub(all) struct Disposition {
kind : String
params : Array[(String, String)]
} derive(Eq, Debug)
///|
pub extend Disposition with Eq::{equal, not_equal}
///|
pub extend Disposition with Debug::{to_repr}
///|
/// Parse a header value. Lenient, like [`media_type`].
pub fn Disposition::parse(s : StringView) -> Disposition {
let semi = index_char(s, 0x3B, 0)
let head = if semi < 0 { s } else { s[0:semi] }
let params : Array[(String, String)] = if semi < 0 {
[]
} else {
parse_params(s[semi + 1:])
}
{ kind: trim(head).to_lower(), params, }
}
///|
/// A parameter by lowercased name.
pub fn Disposition::param(self : Disposition, name : StringView) -> String? {
for pair in self.params {
if pair.0[:] == name {
return Some(pair.1)
}
}
None
}
///|
/// The filename the disposition names, `filename*` first (RFC 6266 §4.3).
///
/// A `filename*` that is not UTF-8 falls back to the plain `filename`.
pub fn Disposition::filename(self : Disposition) -> String? {
match self.param("filename*") {
Some(ext) =>
match decode_ext(ext) {
Some(name) => Some(name)
None => self.param("filename")
}
None => self.param("filename")
}
}
///|
/// The `Content-Disposition` value naming `filename` (RFC 6266).
///
/// A name that is plain ASCII goes out quoted; anything else — a space is not
/// enough, but an accent, a quote, a newline, a byte outside ASCII — goes out
/// as `filename*`, the only form that carries them and the only one a name
/// cannot break out of.
///
/// `inline` asks the browser to show the content rather than save it.
pub fn content_disposition(
filename : StringView,
inline~ : Bool = false,
) -> String {
let kind = if inline { "inline" } else { "attachment" }
if quoted_safe(filename) {
kind + "; filename=\"" + filename.to_owned() + "\""
} else {
kind + "; filename*=utf-8''" + @percent.encode(filename)
}
}
///|
/// The `charset'lang'value` of a `filename*`, decoded when the charset is UTF-8.
///
/// Anything else is `None`, and the caller falls back to `filename`: the value
/// is not UTF-8 text and must not be shown as if it were.
fn decode_ext(ext : String) -> String? {
let first = index_char(ext[:], 0x27, 0)
if first < 0 {
return None
}
let rest = ext[first + 1:]
let second = index_char(rest, 0x27, 0)
if second < 0 {
return None
}
let charset = ext[0:first]
let value = rest[second + 1:]
if charset.to_lower() != "utf-8" {
return None
}
Some(@percent.decode_lossy(value))
}
///|
/// Whether a filename can stand inside a quoted `filename` as it is.
fn quoted_safe(s : StringView) -> Bool {
for i in 0.. 0x7E || c == 0x22 || c == 0x5C {
return false
}
}
true
}