///|
fn parse_header_with_options(
header : String,
strict? : Bool = false,
) -> Result[Disposition, ParseError] {
let diagnostics : Array[Diagnostic] = []
let trimmed = header.trim(chars=" \t\r\n").to_owned()
if trimmed.is_empty() {
let d = Diagnostic::make(
EmptyHeader,
Error,
"Content-Disposition header is empty",
Some(0),
)
return Err(ParseError::from_diagnostic(d))
}
let (kind_token_view, rest_view) = match trimmed.split_once(";") {
Some((a, b)) => (a.trim(chars=" \t"), Some(b))
None => (trimmed[:].trim(chars=" \t"), None)
}
let kind_token = kind_token_view.to_owned()
if !is_token(kind_token) {
let d = Diagnostic::make(
InvalidDisposition,
Error,
"Disposition type must be a non-empty HTTP token",
Some(0),
)
return Err(ParseError::from_diagnostic(d))
}
let params = match rest_view {
Some(rest) =>
parse_parameters(
rest.to_owned(),
diagnostics,
base_offset=kind_token.length() + 1,
)
None => []
}
let disposition : Disposition = {
kind: DispositionKind::from_token(kind_token),
params,
diagnostics,
}
if strict && disposition.has_errors() {
Err(ParseError::from_diagnostic(first_error(disposition.diagnostics)))
} else {
Ok(disposition)
}
}
///|
fn first_error(diagnostics : Array[Diagnostic]) -> Diagnostic {
for d in diagnostics {
if d.severity == Error {
return d
}
}
Diagnostic::make(InvalidDisposition, Error, "Unknown parse error", None)
}
///|
fn push_diag(
diagnostics : Array[Diagnostic],
code : DiagnosticCode,
severity : Severity,
message : String,
offset : Int?,
) -> Unit {
diagnostics.push(Diagnostic::make(code, severity, message, offset))
}
///|
fn parse_parameters(
input : String,
diagnostics : Array[Diagnostic],
base_offset? : Int = 0,
) -> Array[Parameter] {
let params : Array[Parameter] = []
let mut index = 0
let len = input.length()
while index < len {
index = skip_delimiters(input, index)
if index >= len {
break
}
let name_start = index
while index < len &&
char_at(input, index) != '=' &&
char_at(input, index) != ';' {
index = index + 1
}
let raw_name = input[name_start:index].trim(chars=" \t").to_owned()
if index >= len || char_at(input, index) != '=' {
push_diag(
diagnostics,
MissingEquals,
Error,
"Parameter is missing '=' after its name",
Some(base_offset + name_start),
)
index = skip_to_next_parameter(input, index)
continue
}
index = index + 1
let value_start = index
let parsed = parse_value(input, index, diagnostics, base_offset)
let raw_value = input[value_start:parsed.next].trim(chars=" \t").to_owned()
index = parsed.next
let extended = raw_name.has_suffix("*")
let normalized = normalize_param_name(raw_name)
if !is_token(normalized) {
push_diag(
diagnostics,
InvalidParameterName,
Error,
"Parameter name must be a HTTP token",
Some(base_offset + name_start),
)
index = skip_to_next_parameter(input, index)
continue
}
if has_duplicate(params, normalized, extended) {
push_diag(
diagnostics,
DuplicateParameter,
Warning,
"Duplicate parameter with the same normalized name",
Some(base_offset + name_start),
)
}
let decoded = if extended {
decode_extended_value(
parsed.value,
diagnostics,
base_offset + value_start,
)
} else {
{ value: parsed.value, charset: None, language: None }
}
params.push({
name: normalized,
value: decoded.value,
raw_name,
raw_value,
extended,
charset: decoded.charset,
language: decoded.language,
position: params.length(),
})
index = skip_to_next_parameter(input, index)
}
params
}
///|
struct ParsedValue {
value : String
next : Int
} derive(Eq, Debug)
///|
fn parse_value(
input : String,
start : Int,
diagnostics : Array[Diagnostic],
base_offset : Int,
) -> ParsedValue {
let mut index = skip_ows(input, start)
if index >= input.length() {
push_diag(
diagnostics,
MissingValue,
Error,
"Parameter value is missing",
Some(base_offset + start),
)
return { value: "", next: index }
}
if char_at(input, index) == '"' {
parse_quoted_value(input, index, diagnostics, base_offset)
} else {
let value_start = index
while index < input.length() && char_at(input, index) != ';' {
index = index + 1
}
{
value: input[value_start:index].trim(chars=" \t").to_owned(),
next: index,
}
}
}
///|
fn parse_quoted_value(
input : String,
quote_start : Int,
diagnostics : Array[Diagnostic],
base_offset : Int,
) -> ParsedValue {
let buf = StringBuilder(size_hint=32)
let mut index = quote_start + 1
let mut closed = false
while index < input.length() {
let c = char_at(input, index)
if c == '"' {
closed = true
index = index + 1
break
} else if c == '\\' {
if index + 1 >= input.length() {
push_diag(
diagnostics,
BadEscape,
Error,
"Quoted string ends after a backslash escape",
Some(base_offset + index),
)
index = index + 1
break
}
let escaped = char_at(input, index + 1)
if escaped == '\r' || escaped == '\n' {
push_diag(
diagnostics,
BadEscape,
Error,
"Quoted string escape cannot contain a line break",
Some(base_offset + index),
)
} else {
buf.write_char(escaped)
}
index = index + 2
} else {
if c == '\r' || c == '\n' {
push_diag(
diagnostics,
ControlCharacter,
Error,
"Quoted string cannot contain a raw line break",
Some(base_offset + index),
)
} else {
buf.write_char(c)
}
index = index + 1
}
}
if !closed {
push_diag(
diagnostics,
UnterminatedQuote,
Error,
"Quoted string is not terminated",
Some(base_offset + quote_start),
)
}
while index < input.length() && char_at(input, index) != ';' {
if !is_ows(char_at(input, index)) {
push_diag(
diagnostics,
InvalidParameterName,
Warning,
"Ignoring non-whitespace characters after quoted value",
Some(base_offset + index),
)
}
index = index + 1
}
{ value: buf.to_string(), next: index }
}
///|
fn skip_ows(input : String, index : Int) -> Int {
let mut i = index
while i < input.length() && is_ows(char_at(input, i)) {
i = i + 1
}
i
}
///|
fn skip_delimiters(input : String, index : Int) -> Int {
let mut i = index
while i < input.length() {
let c = char_at(input, i)
if c == ';' || is_ows(c) {
i = i + 1
} else {
break
}
}
i
}
///|
fn skip_to_next_parameter(input : String, index : Int) -> Int {
let mut i = index
while i < input.length() && char_at(input, i) != ';' {
i = i + 1
}
if i < input.length() && char_at(input, i) == ';' {
i + 1
} else {
i
}
}
///|
fn has_duplicate(
params : Array[Parameter],
name : String,
extended : Bool,
) -> Bool {
params.any(p => p.name == name && p.extended == extended)
}
///|
struct DecodedValue {
value : String
charset : String?
language : String?
} derive(Eq, Debug)
///|
fn decode_extended_value(
raw : String,
diagnostics : Array[Diagnostic],
offset : Int,
) -> DecodedValue {
match raw.split_once("'") {
Some((charset_view, rest_view)) =>
match rest_view.split_once("'") {
Some((language_view, data_view)) => {
let charset = charset_view.to_owned()
let language = language_view.to_owned()
let data = data_view.to_owned()
let decoded = decode_percent_data(data, charset, diagnostics, offset)
{ value: decoded, charset: Some(charset), language: Some(language) }
}
None => {
push_diag(
diagnostics,
InvalidExtendedValue,
Error,
"Extended value must have charset'language'value sections",
Some(offset),
)
{ value: raw, charset: Some(charset_view.to_owned()), language: None }
}
}
None => {
push_diag(
diagnostics,
InvalidExtendedValue,
Error,
"Extended value must start with a charset section",
Some(offset),
)
{ value: raw, charset: None, language: None }
}
}
}
///|
fn decode_percent_data(
data : String,
charset : String,
diagnostics : Array[Diagnostic],
offset : Int,
) -> String {
let bytes = percent_decode_bytes(data, diagnostics, offset)
let lower = charset.to_lower()
if lower == "utf-8" || lower == "utf8" {
@utf8.decode_lossy(bytes)
} else if lower == "iso-8859-1" || lower == "latin1" || lower == "latin-1" {
let chars : Array[Char] = []
for byte in bytes.iter() {
chars.push(byte.to_char())
}
String::from_array(chars)
} else {
push_diag(
diagnostics,
UnsupportedCharset,
Error,
"Unsupported extended filename charset: " + charset,
Some(offset),
)
data
}
}
///|
fn percent_decode_bytes(
data : String,
diagnostics : Array[Diagnostic],
offset : Int,
) -> Bytes {
let values : Array[Byte] = []
let mut index = 0
while index < data.length() {
let c = char_at(data, index)
if c == '%' {
if index + 2 >= data.length() ||
!is_hex(char_at(data, index + 1)) ||
!is_hex(char_at(data, index + 2)) {
push_diag(
diagnostics,
InvalidPercentEncoding,
Error,
"Percent escape must contain two hexadecimal digits",
Some(offset + index),
)
values.push('%'.to_int().to_byte())
index = index + 1
} else {
let hi = hex_value(char_at(data, index + 1))
let lo = hex_value(char_at(data, index + 2))
values.push(((hi << 4) + lo).to_byte())
index = index + 3
}
} else {
let bs = @utf8.encode(c.to_string())
for b in bs.iter() {
values.push(b)
}
index = index + c.utf16_len()
}
}
Bytes::from_array(values)
}