Skip to content

Commit

Permalink
Implement multipart parser
Browse files Browse the repository at this point in the history
  • Loading branch information
andreiltd committed Feb 5, 2025
1 parent ace478d commit 4bd9ff1
Show file tree
Hide file tree
Showing 38 changed files with 1,556 additions and 14 deletions.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
cmake_minimum_required(VERSION 3.27)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if (DEFINED ENV{HOST_API})
set(HOST_API $ENV{HOST_API})
Expand Down Expand Up @@ -89,7 +90,7 @@ else()
endif()
endif()

target_link_libraries(starling-raw.wasm PRIVATE host_api extension_api builtins spidermonkey rust-url)
target_link_libraries(starling-raw.wasm PRIVATE host_api extension_api builtins spidermonkey rust-url multipart)

# build a compilation cache of ICs
if(WEVAL)
Expand Down
4 changes: 3 additions & 1 deletion builtins/web/blob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,10 @@ JSObject *Blob::data_to_owned_array_buffer(JSContext *cx, HandleObject self) {
return nullptr;
}

std::copy_n(src->begin(), size, buf.get());

auto array_buffer = JS::NewArrayBufferWithContents(
cx, size, src->begin(), JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory);
cx, size, buf.get(), JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory);
if (!array_buffer) {
JS_ReportOutOfMemory(cx);
return nullptr;
Expand Down
2 changes: 2 additions & 0 deletions builtins/web/fetch/fetch-errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ DEF_ERR(InvalidHeaderName, JSEXN_TYPEERR, "{0}: Invalid header name \"{1}\"", 2)
DEF_ERR(InvalidHeaderValue, JSEXN_TYPEERR, "{0}: Invalid header value \"{1}\"", 2)
DEF_ERR(HeadersCloningFailed, JSEXN_ERR, "Failed to clone headers", 0)
DEF_ERR(HeadersImmutable, JSEXN_TYPEERR, "{0}: Headers are immutable", 1)
DEF_ERR(InvalidFormDataHeader, JSEXN_TYPEERR, "Invalid header for FormData body type", 0)
DEF_ERR(InvalidFormData, JSEXN_TYPEERR, "FormData parsing failed", 0)
}; // namespace FetchErrors

#endif // FETCH_ERRORS_H
142 changes: 139 additions & 3 deletions builtins/web/fetch/fetch-utils.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,147 @@
#include "fetch-utils.h"

#include <string>
#include <charconv>
#include <string>
#include <ranges>
#include <string_view>
#include <fmt/format.h>

namespace builtins::web::fetch {

std::optional<std::tuple<size_t, size_t>> extract_range(std::string_view range_query, size_t full_len) {
std::string MimeType::to_string() {
std::string result = essence;

for (const auto& [key, value] : params) {
result += fmt::format(";{}={}", key, value);
}

return result;
}

std::string_view trim(std::string_view input) {
auto trimsz = input.find_first_not_of(" \t");
if (trimsz == std::string_view::npos) {
return {};
}

input.remove_prefix(trimsz);

trimsz = input.find_last_not_of(" \t");
input.remove_suffix(input.size() - trimsz - 1);

return input;
}

std::optional<MimeType> parse_mime_type(std::string_view str) {
auto input = trim(str);

if (input.empty()) {
return std::nullopt;
}

std::string essence;
std::string params;

if (auto pos = input.find(';'); pos != std::string::npos) {
essence = trim(input.substr(0, pos));
params = trim(input.substr(pos + 1));
} else {
essence = trim(input);
}

if (essence.empty() || essence.find('/') == std::string::npos) {
return std::nullopt;
}

MimeType mime;
mime.essence = essence;

if (params.empty()) {
return mime;
}

auto as_string = [](std::string_view v) -> std::string {
return {v.data(), v.size()};
};

for (const auto view : std::views::split(params, ';')) {
auto param = std::string_view(view.begin(), view.end());

if (auto pos = param.find('='); pos != std::string::npos) {
auto key = trim(param.substr(0, pos));
auto value = trim(param.substr(pos + 1));

if (!key.empty()) {
mime.params.push_back({ as_string(key), as_string(value) });
}
}
}

return mime;
}

// https://fetch.spec.whatwg.org/#concept-body-mime-type
std::optional<MimeType> extract_mime_type(std::string_view query) {
// 1. Let charset be null.
// 2. Let essence be null.
// 3. Let mimeType be null.
std::string essence;
std::string charset;
MimeType mime;

bool found = false;

// 4. Let values be the result of getting, decoding, and splitting `Content-Type` from headers.
// 5. If values is null, then return failure.
// 6. For each value of values:
for (const auto value : std::views::split(query, ',')) {
// 1. Let temporaryMimeType be the result of parsing value.
// 2. If temporaryMimeType is failure or its essence is "*/*", then continue.
auto value_str = std::string_view(value.begin(), value.end());
auto maybe_mime = parse_mime_type(value_str);
if (!maybe_mime || maybe_mime.value().essence == "*/*") {
continue;
}

// 3. Set mimeType to temporaryMimeType.
mime = maybe_mime.value();
found = true;

// 4. If mimeType's essence is not essence, then:
if (mime.essence != essence) {
// 1. Set charset to null.
charset.clear();
// 2. If mimeTypes parameters["charset"] exists, then set charset to mimeType's
// parameters["charset"].
auto it = std::find_if(mime.params.begin(), mime.params.end(),
[&](const auto &kv) { return std::get<0>(kv) == "charset"; });

if (it != mime.params.end()) {
charset = std::get<1>(*it);
}

// 3. Set essence to mimeTypes essence.
essence = mime.essence;

} else {
// 5. Otherwise, if mimeTypes parameters["charset"] does not exist, and charset is non-null,
// set mimeType's parameters["charset"] to charset.
auto it = std::find_if(mime.params.begin(), mime.params.end(),
[&](const auto &kv) { return std::get<0>(kv) == "charset"; });

if (it == mime.params.end() && !charset.empty()) {
mime.params.push_back({"charset", charset});
}
}
}

// 7. If mimeType is null, then return failure.
// 8. Return mimeType.
return found ? std::optional<MimeType>(mime) : std::nullopt;
}

std::optional<std::tuple<size_t, size_t>> extract_range(std::string_view range_query,
size_t full_len) {
if (!range_query.starts_with("bytes=")) {
return std::nullopt;
}
Expand Down Expand Up @@ -59,4 +195,4 @@ std::optional<std::tuple<size_t, size_t>> extract_range(std::string_view range_q
return std::optional<std::tuple<size_t, size_t>>{{start_range, end_range}};
}

}
} // namespace builtins::web::fetch
11 changes: 11 additions & 0 deletions builtins/web/fetch/fetch-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@

namespace builtins::web::fetch {

struct MimeType {
std::string essence;
std::vector<std::tuple<std::string, std::string>> params;

std::string to_string();
};

// https://fetch.spec.whatwg.org/#concept-body-mime-type
std::optional<MimeType> extract_mime_type(std::string_view query);


// Extracts a valid byte range from the given `Range` header query string, following
// the steps defined for "blob" schemes in the Fetch specification:
// https://fetch.spec.whatwg.org/#scheme-fetch
Expand Down
44 changes: 44 additions & 0 deletions builtins/web/fetch/request-response.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
#include "../blob.h"
#include "../form-data/form-data.h"
#include "../form-data/form-data-encoder.h"
#include "../form-data/form-data-parser.h"
#include "../streams/native-stream-source.h"
#include "../streams/transform-stream.h"
#include "../url.h"
#include "fetch-utils.h"
#include "encode.h"
#include "event_loop.h"
#include "extension-api.h"
#include "fetch_event.h"
#include "host_api.h"
#include "js/String.h"
#include "js/TypeDecls.h"
#include "picosha2.h"

#include "js/Array.h"
Expand Down Expand Up @@ -43,6 +46,7 @@ namespace builtins::web::fetch {

using blob::Blob;
using form_data::FormData;
using form_data::FormDataParser;
using form_data::MultipartFormData;

static api::Engine *ENGINE;
Expand Down Expand Up @@ -568,6 +572,7 @@ JSObject *RequestOrResponse::headers(JSContext *cx, JS::HandleObject obj) {
return headers;
}

// https://fetch.spec.whatwg.org/#body-mixin
template <RequestOrResponse::BodyReadResult result_type>
bool RequestOrResponse::parse_body(JSContext *cx, JS::HandleObject self, JS::UniqueChars buf,
size_t len) {
Expand All @@ -594,6 +599,43 @@ bool RequestOrResponse::parse_body(JSContext *cx, JS::HandleObject self, JS::Uni
}

result.setObject(*blob);

} else if constexpr (result_type == RequestOrResponse::BodyReadResult::FormData) {
RootedObject headers(cx, RequestOrResponse::maybe_headers(self));
if (!headers) {
api::throw_error(cx, FetchErrors::InvalidFormDataHeader);
return RejectPromiseWithPendingError(cx, result_promise);
}

auto content_type_str = host_api::HostString("Content-Type");
auto idx = Headers::lookup(cx, headers, content_type_str);
if (!idx) {
api::throw_error(cx, FetchErrors::InvalidFormDataHeader);
return RejectPromiseWithPendingError(cx, result_promise);
}

auto values = Headers::get_index(cx, headers, idx.value());
auto maybe_mime = extract_mime_type(std::get<1>(*values));
if (!maybe_mime) {
api::throw_error(cx, FetchErrors::InvalidFormDataHeader);
return RejectPromiseWithPendingError(cx, result_promise);
}

auto parser = FormDataParser::create(maybe_mime.value().to_string());
if (!parser) {
return JS::ResolvePromise(cx, result_promise, result);
// TODO: add parser for url/encoded data
// return RejectPromiseWithPendingError(cx, result_promise);
}

std::string_view body(buf.get(), len) ;
RootedObject form_data(cx, parser->parse(cx, body));
if (!form_data) {
api::throw_error(cx, FetchErrors::InvalidFormData);
return RejectPromiseWithPendingError(cx, result_promise);
}

result.setObject(*form_data);
} else {
JS::RootedString text(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(buf.get(), len)));
if (!text) {
Expand Down Expand Up @@ -1359,6 +1401,7 @@ const JSFunctionSpec Request::methods[] = {
JS_FN("arrayBuffer", Request::bodyAll<RequestOrResponse::BodyReadResult::ArrayBuffer>, 0,
JSPROP_ENUMERATE),
JS_FN("blob", Request::bodyAll<RequestOrResponse::BodyReadResult::Blob>, 0, JSPROP_ENUMERATE),
JS_FN("formData", Request::bodyAll<RequestOrResponse::BodyReadResult::FormData>, 0, JSPROP_ENUMERATE),
JS_FN("json", Request::bodyAll<RequestOrResponse::BodyReadResult::JSON>, 0, JSPROP_ENUMERATE),
JS_FN("text", Request::bodyAll<RequestOrResponse::BodyReadResult::Text>, 0, JSPROP_ENUMERATE),
JS_FN("clone", Request::clone, 0, JSPROP_ENUMERATE),
Expand Down Expand Up @@ -2384,6 +2427,7 @@ const JSFunctionSpec Response::methods[] = {
JS_FN("arrayBuffer", bodyAll<RequestOrResponse::BodyReadResult::ArrayBuffer>, 0,
JSPROP_ENUMERATE),
JS_FN("blob", bodyAll<RequestOrResponse::BodyReadResult::Blob>, 0, JSPROP_ENUMERATE),
JS_FN("formData", bodyAll<RequestOrResponse::BodyReadResult::FormData>, 0, JSPROP_ENUMERATE),
JS_FN("json", bodyAll<RequestOrResponse::BodyReadResult::JSON>, 0, JSPROP_ENUMERATE),
JS_FN("text", bodyAll<RequestOrResponse::BodyReadResult::Text>, 0, JSPROP_ENUMERATE),
JS_FS_END,
Expand Down
1 change: 1 addition & 0 deletions builtins/web/fetch/request-response.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class RequestOrResponse final {
enum class BodyReadResult {
ArrayBuffer,
Blob,
FormData,
JSON,
Text,
};
Expand Down
Loading

0 comments on commit 4bd9ff1

Please sign in to comment.