// parser.mbt — RFC 6266 Content-Disposition header value parser.
//
// Grammar (RFC 6266 Section 4.2, applied to the header field value, i.e.
// the part after the field name and colon):
//
// disposition-type = "inline" / "attachment" / disp-ext-type
// disposition-parm = filename-parm / disp-ext-parm
// filename-parm = "filename" "=" value
// / "filename*" "=" ext-value
// disp-ext-parm = token "=" value / ext-token "=" ext-value
// value = token / quoted-string
//
// Optional whitespace (SP / HTAB) is permitted around the `;` separators
// and around the `=` sign, matching RFC 7230 field-value conventions; this
// is documented in `docs/compatibility.md`. CR and LF are never treated as
// whitespace.
//
// Duplicate parameter names are rejected in strict mode (RFC 6266 leaves
// the behaviour undefined; strict mode refuses to guess) and preserved
// with a recorded duplicate in compatible mode. Every public entry point
// returns `Result[T, DispositionError]`; internal functions raise.
///|
/// The detailed result of parsing a Content-Disposition header value:
/// the model plus the duplicate names and compatible recoveries observed.
pub struct DispositionParse {
content_disposition : ContentDisposition
duplicates : Array[String]
recoveries : Array[String]
mode : ParseMode
}
///|
/// The parsed Content-Disposition value.
pub fn DispositionParse::content_disposition(self : DispositionParse) -> ContentDisposition {
self.content_disposition
}
///|
/// The names of duplicated parameters (compatible mode), in detection
/// order, each at most once.
pub fn DispositionParse::duplicates(self : DispositionParse) -> Array[String] {
self.duplicates
}
///|
/// The compatible recoveries applied during the parse, in application
/// order, each at most once. Empty in strict mode.
pub fn DispositionParse::recoveries(self : DispositionParse) -> Array[String] {
self.recoveries
}
///|
/// The parse mode used for this parse.
pub fn DispositionParse::mode(self : DispositionParse) -> ParseMode {
self.mode
}
///|
/// Parses a Content-Disposition header field value in strict mode with
/// default limits.
///
/// Errors: `Input::EmptyInput` (empty value), `Input::LimitExceeded`
/// (input larger than `max_input_bytes`), `Limit::LimitExceeded`, plus the
/// `DispositionType`, `ParameterName`, `ParameterValue`, `QuotedString`,
/// `ExtendedValue`, `Charset` and `PercentEncoding` errors raised while
/// parsing.
pub fn parse_content_disposition(input : String) -> Result[ContentDisposition, DispositionError] {
parse_content_disposition_with_options(input, ParseOptions::default())
}
///|
/// Parses a Content-Disposition header field value with explicit options.
pub fn parse_content_disposition_with_options(
input : String,
options : ParseOptions
) -> Result[ContentDisposition, DispositionError] {
match parse_content_disposition_detailed(input, options) {
Ok(p) => Ok(p.content_disposition)
Err(e) => Err(e)
}
}
///|
/// Parses a Content-Disposition header field value with explicit options,
/// also returning duplicate names and compatible recoveries.
pub fn parse_content_disposition_detailed(
input : String,
options : ParseOptions
) -> Result[DispositionParse, DispositionError] {
let limits = options.limits()
let mode = options.mode()
let bytes = @utf8.encode(input)
if bytes.length() > limits.max_input_bytes() {
return Err(
disposition_error(
Input,
LimitExceeded,
"input exceeds max_input_bytes (\{limits.max_input_bytes()} bytes)",
),
)
}
let cursor = Scanner::new(input)
cursor.skip_ows()
if cursor.eof() {
return Err(disposition_error(Input, EmptyInput, "empty Content-Disposition value"))
}
let collector = ParseCollector::new()
let (dt, raw_type) = try {
unwrap_or_raise(parse_disposition_type(cursor))
} catch {
e => return Err(unwrap_disposition_error(e))
}
let cd = content_disposition_with_raw(dt, raw_type)
let mut done = false
let mut last_was_empty = false
while !done {
cursor.skip_ows()
if cursor.eof() {
done = true
} else if cursor.consume_char(59) {
// ';'
cursor.skip_ows()
if cursor.eof() {
// trailing ';' with no following parameter. When the separator run
// was already reported as an empty element (';;' at the end), there
// is no separate trailing-semicolon recovery.
if mode == Compatible {
if !last_was_empty {
collector.record_recovery("trailing-semicolon")
}
done = true
} else {
return Err(
disposition_error_at(
ParameterName,
ExpectedToken,
cursor.position(),
"expected a parameter after ';'",
),
)
}
} else {
match cursor.peek_byte() {
Some(b) if b == 59 => {
// empty parameter element (';;'): no name
if mode == Compatible {
collector.record_recovery("skip-empty-parameter")
last_was_empty = true
} else {
return Err(
disposition_error_at(
ParameterName,
ExpectedToken,
cursor.position(),
"expected a parameter after ';'",
),
)
}
}
_ => {
last_was_empty = false
let param_start = cursor.position()
let param = try {
unwrap_or_raise(parse_parameter(cursor, limits, mode, collector))
} catch {
e => return Err(unwrap_disposition_error(e))
}
if collector.has_seen(param.name) {
if mode == Strict {
return Err(
disposition_error_at(
ParameterName,
DuplicateParameter,
param_start,
"duplicate parameter '\{param.name}'",
),
)
}
collector.record_duplicate(param.name)
} else {
collector.mark_seen(param.name)
}
cd.parameters.push(param)
if cd.parameters.length() > limits.max_parameters() {
return Err(
disposition_error_at(
Limit,
LimitExceeded,
param_start,
"too many parameters (exceeds max_parameters)",
),
)
}
}
}
}
} else {
return Err(
disposition_error_at(Input, TrailingInput, cursor.position(), cursor.context_string()),
)
}
}
Ok({
content_disposition: cd,
duplicates: collector.duplicates(),
recoveries: collector.recoveries(),
mode,
})
}
// Parses the leading disposition type token, returning the typed value
// together with the raw token exactly as it appeared on the wire so that a
// preserve-case serialisation can reproduce the original spelling.
fn parse_disposition_type(
cursor : Scanner
) -> Result[(DispositionType, String), DispositionError] raise {
let (ts, te) = cursor.consume_token()
if te == ts {
raise disposition_error_at(
DispositionType,
InvalidDispositionType,
cursor.position(),
"expected a disposition type token",
)
}
let name = cursor.take_string(ts, te)
let dt = if name.equal_ignore_ascii_case("inline") {
Inline
} else if name.equal_ignore_ascii_case("attachment") {
Attachment
} else {
Extension(name)
}
Ok((dt, name))
}