///|
/// Content-Type header parsing errors (RFC 9110 Section 8.3)
pub(all) enum ContentTypeError {
Empty
InvalidMediaType
InvalidParameter
UnterminatedQuote
} derive(Show, Eq)
///|
/// Content-Type header (RFC 9110 Section 8.3)
pub(all) struct ContentType {
media_type : String
subtype : String
parameters : Array[(String, String)]
} derive(Eq)
///|
pub fn ContentType::parse(
input : String,
) -> Result[ContentType, ContentTypeError] {
let s = ct_trim_string(input)
if s.length() == 0 {
return Err(Empty)
}
let (media_part, rest) = ct_split_at_semicolon(s)
let parse_result = ct_parse_media_type(media_part)
match parse_result {
Err(e) => return Err(e)
Ok((mt, st)) => {
let params_result = ct_parse_parameters(rest)
match params_result {
Err(e) => return Err(e)
Ok(params) =>
Ok({
media_type: ct_to_lower_case(mt),
subtype: ct_to_lower_case(st),
parameters: params,
})
}
}
}
}
///|
pub fn ContentType::new(media_type : String, subtype : String) -> ContentType {
{
media_type: ct_to_lower_case(media_type),
subtype: ct_to_lower_case(subtype),
parameters: [],
}
}
///|
pub fn ContentType::with_parameter(
self : ContentType,
name : String,
value : String,
) -> ContentType {
let new_params = []
let mut idx = 0
while idx < self.parameters.length() {
new_params.push(self.parameters[idx])
idx = idx + 1
}
new_params.push((ct_to_lower_case(name), value))
{ media_type: self.media_type, subtype: self.subtype, parameters: new_params }
}
///|
pub fn ContentType::media_type(self : ContentType) -> String {
self.media_type
}
///|
pub fn ContentType::subtype(self : ContentType) -> String {
self.subtype
}
///|
pub fn ContentType::mime_type(self : ContentType) -> String {
self.media_type + "/" + self.subtype
}
///|
pub fn ContentType::parameter(self : ContentType, name : String) -> String? {
let name_lower = ct_to_lower_case(name)
let mut idx = 0
while idx < self.parameters.length() {
let (n, v) = self.parameters[idx]
if n == name_lower {
return Some(v)
}
idx = idx + 1
}
None
}
///|
pub fn ContentType::parameters(self : ContentType) -> Array[(String, String)] {
self.parameters
}
///|
pub fn ContentType::charset(self : ContentType) -> String? {
self.parameter("charset")
}
///|
pub fn ContentType::boundary(self : ContentType) -> String? {
self.parameter("boundary")
}
///|
pub fn ContentType::is_text(self : ContentType) -> Bool {
self.media_type == "text"
}
///|
pub fn ContentType::is_json(self : ContentType) -> Bool {
self.media_type == "application" && self.subtype == "json"
}
///|
pub fn ContentType::is_multipart(self : ContentType) -> Bool {
self.media_type == "multipart"
}
///|
pub fn ContentType::is_form_data(self : ContentType) -> Bool {
self.media_type == "multipart" && self.subtype == "form-data"
}
///|
pub fn ContentType::is_form_urlencoded(self : ContentType) -> Bool {
self.media_type == "application" && self.subtype == "x-www-form-urlencoded"
}
///|
pub fn ContentType::to_string(self : ContentType) -> String {
let mut result = self.media_type + "/" + self.subtype
let mut idx = 0
while idx < self.parameters.length() {
let (name, value) = self.parameters[idx]
if ct_needs_quoting(value) {
result = result + "; " + name + "=\"" + ct_escape_quotes(value) + "\""
} else {
result = result + "; " + name + "=" + value
}
idx = idx + 1
}
result
}
///|
/// Helper functions
///
///|
fn ct_split_at_semicolon(input : String) -> (String, String) {
let semicolon_pos = ct_find_char(input, 59) // ;
match semicolon_pos {
Some(pos) =>
(
ct_trim_string(ct_string_slice(input, 0, pos)),
ct_trim_string(ct_string_slice_from(input, pos + 1)),
)
None => (ct_trim_string(input), "")
}
}
///|
fn ct_parse_media_type(
input : String,
) -> Result[(String, String), ContentTypeError] {
let s = ct_trim_string(input)
if s.length() == 0 {
return Err(InvalidMediaType)
}
let slash_pos = ct_find_char(s, 47) // /
match slash_pos {
None => return Err(InvalidMediaType)
Some(pos) => {
let mt = ct_trim_string(ct_string_slice(s, 0, pos))
let st = ct_trim_string(ct_string_slice_from(s, pos + 1))
if mt.length() == 0 || st.length() == 0 {
return Err(InvalidMediaType)
}
if !ct_is_valid_token(mt) || !ct_is_valid_token(st) {
return Err(InvalidMediaType)
}
Ok((mt, st))
}
}
}
///|
fn ct_parse_parameters(
input : String,
) -> Result[Array[(String, String)], ContentTypeError] {
let mut params : Array[(String, String)] = []
let mut rest = ct_trim_string(input)
while rest.length() > 0 {
// Skip semicolon
rest = ct_trim_string(ct_skip_leading_char(rest, 59)) // ;
if rest.length() == 0 {
break
}
// Find =
let eq_pos = ct_find_char(rest, 61) // =
match eq_pos {
None => return Err(InvalidParameter)
Some(pos) => {
let name = ct_trim_string(ct_string_slice(rest, 0, pos))
if name.length() == 0 || !ct_is_valid_token(name) {
return Err(InvalidParameter)
}
let value_part = ct_trim_string(ct_string_slice_from(rest, pos + 1))
// Parse value (quoted or token)
let parse_result = ct_parse_value(value_part)
match parse_result {
Ok((value, remaining)) => {
let new_params = []
let mut idx = 0
while idx < params.length() {
new_params.push(params[idx])
idx = idx + 1
}
new_params.push((ct_to_lower_case(name), value))
params = new_params
rest = ct_trim_string(ct_skip_leading_char(remaining, 59)) // ;
}
Err(e) => return Err(e)
}
}
}
}
Ok(params)
}
///|
fn ct_parse_value(input : String) -> Result[(String, String), ContentTypeError] {
if input.length() > 0 && ct_char_at(input, 0) == 34 { // "
let after_quote = ct_string_slice_from(input, 1)
ct_parse_quoted_string(after_quote)
} else {
Ok(ct_parse_token_value(input))
}
}
///|
fn ct_parse_quoted_string(
input : String,
) -> Result[(String, String), ContentTypeError] {
let mut result = ""
let mut escaped = false
let mut idx = 0
let len = input.length()
while idx < len {
let ch = ct_char_at(input, idx)
if escaped {
result = result + Int::unsafe_to_char(ch).to_string()
escaped = false
idx = idx + 1
} else if ch == 92 { // \
escaped = true
idx = idx + 1
} else if ch == 34 { // "
return Ok((result, ct_string_slice_from(input, idx + 1)))
} else {
result = result + Int::unsafe_to_char(ch).to_string()
idx = idx + 1
}
}
Err(UnterminatedQuote)
}
///|
fn ct_parse_token_value(input : String) -> (String, String) {
let mut end = 0
while end < input.length() {
let ch = ct_char_at(input, end)
if ch == 59 || ch == 32 || ch == 9 || ch == 10 || ch == 13 { // ; or whitespace
break
}
end = end + 1
}
(ct_string_slice(input, 0, end), ct_string_slice_from(input, end))
}
///|
fn ct_is_valid_token(s : String) -> Bool {
if s.length() == 0 {
return false
}
let mut idx = 0
while idx < s.length() {
if !ct_is_token_char(ct_char_at(s, idx)) {
return false
}
idx = idx + 1
}
true
}
///|
fn ct_is_token_char(b : Int) -> Bool {
b == 33 ||
b == 35 ||
b == 36 ||
b == 37 ||
b == 38 ||
b == 39 ||
b == 42 ||
b == 43 ||
b == 45 ||
b == 46 ||
(b >= 48 && b <= 57) || // 0-9
(b >= 65 && b <= 90) || // A-Z
b == 94 ||
b == 95 ||
b == 96 ||
(b >= 97 && b <= 122) || // a-z
b == 124 ||
b == 126
}
///|
fn ct_needs_quoting(s : String) -> Bool {
let mut idx = 0
while idx < s.length() {
if !ct_is_token_char(ct_char_at(s, idx)) {
return true
}
idx = idx + 1
}
false
}
///|
fn ct_escape_quotes(s : String) -> String {
let mut result = ""
let mut idx = 0
while idx < s.length() {
let ch = ct_char_at(s, idx)
if ch == 92 { // \
result = result + "\\\\"
} else if ch == 34 { // "
result = result + "\\\""
} else {
result = result + Int::unsafe_to_char(ch).to_string()
}
idx = idx + 1
}
result
}
///|
fn ct_to_lower_case(s : String) -> String {
str_to_lower_case(s)
}
///|
fn ct_trim_string(s : String) -> String {
str_trim_string(s)
}
///|
fn ct_skip_leading_char(s : String, target : Int) -> String {
let mut idx = 0
let len = s.length()
while idx < len && ct_char_at(s, idx) == target {
idx = idx + 1
}
ct_string_slice_from(s, idx)
}
///|
fn ct_char_at(s : String, idx : Int) -> Int {
str_char_at(s, idx)
}
///|
fn ct_find_char(s : String, target : Int) -> Int? {
let mut idx = 0
while idx < s.length() {
if ct_char_at(s, idx) == target {
return Some(idx)
}
idx = idx + 1
}
None
}
///|
fn ct_string_slice(s : String, start : Int, end : Int) -> String {
str_string_slice(s, start, end)
}
///|
fn ct_string_slice_from(s : String, start : Int) -> String {
str_string_slice_from(s, start)
}