///|
async fn resolve_resource_request(
document : ResourceDocument?,
asset_root : String?,
url : String,
) -> ResourceResponse {
match document {
Some(document) if document.url == url =>
return resource_response(200, "text/html", document.content)
_ => ()
}
guard url.has_prefix(asset_base_url()) else {
return resource_text_response(404, "Not Found")
}
guard asset_root is Some(root) else {
return resource_text_response(404, "Not Found")
}
guard resource_url_path(url) is Some(relative_path) else {
return resource_text_response(400, "Invalid resource path")
}
let native_relative = if @mbpath.sep == '\\' {
relative_path.replace_all(old="/", new="\\")
} else {
relative_path
}
let candidate = @mbpath.Path(root).join(native_relative).to_string()
let canonical = @async_fs.realpath(candidate) catch {
_ => return resource_text_response(404, "Not Found")
}
guard resource_path_is_under_root(canonical, root) else {
return resource_text_response(404, "Not Found")
}
let data = @async_fs.read_file(canonical) catch {
_ => return resource_text_response(404, "Not Found")
}
resource_response(200, resource_mime_type(canonical), data.binary())
}
///|
fn resource_document_url_is_supported(url : String) -> Bool {
url.has_prefix("proton://") || url.has_prefix(asset_base_url())
}
///|
fn resource_response(
status : Int,
mime_type : String,
body : Bytes,
) -> ResourceResponse {
{ status, mime_type, body }
}
///|
fn resource_text_response(status : Int, body : String) -> ResourceResponse {
resource_response(status, "text/plain", @utf8.encode(body))
}
///|
fn resource_url_path(url : String) -> String? {
guard url.has_prefix(asset_base_url()) else { return None }
let suffix = @utf8.encode(url[asset_base_url().length():])
let mut end = suffix.length()
for index = 0; index < suffix.length(); index = index + 1 {
if suffix[index] == b'?' || suffix[index] == b'#' {
end = index
break
}
}
guard end > 0 else { return None }
resource_percent_decode_path(suffix[:end])
}
///|
fn resource_percent_decode_path(encoded : BytesView) -> String? {
let decoded : Array[Byte] = []
let mut index = 0
while index < encoded.length() {
let byte = encoded[index]
if byte == b'%' {
guard index + 2 < encoded.length() else { return None }
guard resource_hex_value(encoded[index + 1]) is Some(high) else {
return None
}
guard resource_hex_value(encoded[index + 2]) is Some(low) else {
return None
}
let value = (high * 16 + low).to_byte()
guard value != b'\x00' else { return None }
decoded.push(value)
index = index + 3
} else {
guard byte != b'\x00' else { return None }
decoded.push(byte)
index = index + 1
}
}
guard decoded.length() > 0 else { return None }
let path = @utf8.decode(Bytes::from_array(decoded[:])[:]) catch {
_ => return None
}
guard path[0] != '/' && path[0] != '\\' else { return None }
if @mbpath.sep == '\\' && path.contains(":") {
return None
}
guard !resource_path_has_parent_segment(path) else { return None }
Some(path.replace_all(old="\\", new="/"))
}
///|
fn resource_hex_value(byte : Byte) -> Int? {
if byte >= b'0' && byte <= b'9' {
Some(byte.to_int() - b'0'.to_int())
} else if byte >= b'a' && byte <= b'f' {
Some(byte.to_int() - b'a'.to_int() + 10)
} else if byte >= b'A' && byte <= b'F' {
Some(byte.to_int() - b'A'.to_int() + 10)
} else {
None
}
}
///|
fn resource_path_has_parent_segment(path : String) -> Bool {
let mut segment = StringBuilder::new()
for character in path {
if character == '/' || character == '\\' {
if segment.to_string() == ".." {
return true
}
segment = StringBuilder::new()
} else {
segment.write_char(character)
}
}
segment.to_string() == ".."
}
///|
fn resource_path_is_under_root(path : String, root : String) -> Bool {
let separator = @mbpath.sep
let separator_unit = separator.to_int().to_uint16()
let mut normalized_root = root
while normalized_root.length() > 1 &&
normalized_root[normalized_root.length() - 1] == separator_unit {
normalized_root = normalized_root[:normalized_root.length() - 1].to_owned()
}
let compared_path = if separator == '\\' { path.to_lower() } else { path }
let compared_root = if separator == '\\' {
normalized_root.to_lower()
} else {
normalized_root
}
if compared_root == separator.to_string() {
return compared_path.has_prefix(compared_root)
}
compared_path == compared_root ||
(
compared_path.has_prefix(compared_root) &&
compared_path.length() > compared_root.length() &&
compared_path[compared_root.length()] == separator_unit
)
}
///|
fn resource_mime_type(path : String) -> String {
let lower = path.to_lower()
if lower.has_suffix(".html") || lower.has_suffix(".htm") {
"text/html"
} else if lower.has_suffix(".css") {
"text/css"
} else if lower.has_suffix(".js") || lower.has_suffix(".mjs") {
"text/javascript"
} else if lower.has_suffix(".json") {
"application/json"
} else if lower.has_suffix(".svg") {
"image/svg+xml"
} else if lower.has_suffix(".png") {
"image/png"
} else if lower.has_suffix(".jpg") || lower.has_suffix(".jpeg") {
"image/jpeg"
} else if lower.has_suffix(".gif") {
"image/gif"
} else if lower.has_suffix(".webp") {
"image/webp"
} else if lower.has_suffix(".ico") {
"image/x-icon"
} else if lower.has_suffix(".txt") {
"text/plain"
} else if lower.has_suffix(".woff") {
"font/woff"
} else if lower.has_suffix(".woff2") {
"font/woff2"
} else if lower.has_suffix(".ttf") {
"font/ttf"
} else if lower.has_suffix(".otf") {
"font/otf"
} else {
"application/octet-stream"
}
}