///|
/// Parse Upload-Metadata using the grammar from the tus Creation extension.
/// Entry order is preserved because applications may use it for display.
pub fn parse_upload_metadata(
value : String,
limits : TusLimits,
) -> Result[Array[MetadataEntry], TusError] {
if value.length() > limits.max_metadata_bytes {
return Err(metadata_size_error(limits.max_metadata_bytes, value.length()))
}
if value.length() == 0 {
return Ok([])
}
let entries : Array[MetadataEntry] = []
let mut decoded_bytes = 0
for raw_entry in value.split(",") {
if entries.length() >= limits.max_metadata_entries {
return Err(
tus_error(
TooManyMetadataEntries,
"TUS_METADATA_ENTRY_LIMIT",
"Upload-Metadata contains more entries than configured",
status=413,
header_name=Some("upload-metadata"),
expected=Some(limits.max_metadata_entries.to_string()),
actual=Some((entries.length() + 1).to_string()),
),
)
}
match
parse_metadata_entry(raw_entry, limits.max_metadata_bytes - decoded_bytes) {
Err(error) => return Err(error)
Ok(entry) => {
if metadata_contains_key(entries, entry.key) {
return Err(
tus_error(
DuplicateMetadataKey,
"TUS_METADATA_KEY_REPEATED",
"Upload-Metadata repeats a key",
header_name=Some("upload-metadata"),
actual=Some(entry.key),
),
)
}
decoded_bytes = decoded_bytes + entry.value.length()
entries.push(entry)
}
}
}
Ok(entries)
}
///|
fn parse_metadata_entry(
raw_entry : StringView,
remaining_budget : Int,
) -> Result[MetadataEntry, TusError] {
let entry = raw_entry.to_owned()
if entry.length() == 0 {
return Err(invalid_metadata("Upload-Metadata contains an empty entry"))
}
let mut separator : Int? = None
for index, character in entry {
if character == ' ' {
if separator is Some(_) {
return Err(
invalid_metadata("metadata entry contains more than one separator"),
)
}
separator = Some(index)
}
}
let key = match separator {
None => entry
Some(index) => entry[:index].to_owned()
}
if !is_metadata_key(key) {
return Err(
invalid_metadata(
"metadata key must be visible ASCII without comma or space",
),
)
}
let encoded = match separator {
None => ""
Some(index) => entry[index + 1:].to_owned()
}
match decode_base64(encoded, remaining_budget) {
Err(error) => Err(error)
Ok(decoded) => Ok({ key, value: decoded, encoded_value: encoded, })
}
}
///|
pub fn serialize_upload_metadata(entries : Array[MetadataEntry]) -> String {
let output = StringBuilder()
for index, entry in entries {
if index > 0 {
output.write_char(',')
}
output.write_string(entry.key)
if entry.value.length() > 0 {
output.write_char(' ')
output.write_string(encode_base64(entry.value))
}
}
output.to_string()
}
///|
pub fn metadata_entry(
key : String,
value : Bytes,
) -> Result[MetadataEntry, TusError] {
if !is_metadata_key(key) {
return Err(
invalid_metadata(
"metadata key must be visible ASCII without comma or space",
),
)
}
Ok({ key, value, encoded_value: encode_base64(value), })
}
///|
pub fn metadata_get(entries : Array[MetadataEntry], key : String) -> Bytes? {
for entry in entries {
if entry.key == key {
return Some(entry.value)
}
}
None
}
///|
pub fn metadata_encoded_get(
entries : Array[MetadataEntry],
key : String,
) -> String? {
for entry in entries {
if entry.key == key {
return Some(entry.encoded_value)
}
}
None
}
///|
pub fn metadata_decoded_size(entries : Array[MetadataEntry]) -> Int {
let mut total = 0
for entry in entries {
total = total + entry.value.length()
}
total
}
///|
pub fn is_metadata_key(key : String) -> Bool {
if key.length() == 0 {
return false
}
for character in key {
let code = character.to_int()
if code < 33 || code > 126 || character == ',' {
return false
}
}
true
}
///|
fn metadata_contains_key(entries : Array[MetadataEntry], key : String) -> Bool {
for entry in entries {
if entry.key == key {
return true
}
}
false
}
///|
fn metadata_size_error(limit : Int, actual : Int) -> TusError {
tus_error(
MetadataTooLarge,
"TUS_METADATA_TOO_LARGE",
"Upload-Metadata exceeds the configured byte budget",
status=413,
header_name=Some("upload-metadata"),
expected=Some(limit.to_string()),
actual=Some(actual.to_string()),
)
}