@sproutboat/toolchain 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +4 -0
- package/src/patch.test.ts +10 -0
- package/src/patch.ts +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.4.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0b0a666: Add a native standalone R2 direct-upload bridge that streams transfer-ticket
|
|
8
|
+
request chunks to a temporary blob file, validates the digest, and atomically
|
|
9
|
+
publishes the object without buffering the upload in the Porffor request body.
|
|
10
|
+
|
|
3
11
|
## 0.4.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -40,6 +40,10 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
40
40
|
" i32 slot = 0;\n for (auto [key, value] : *req) {\n slot++;\n }\n" +
|
|
41
41
|
" *((i32*)(porf_mem + headers_ptr)) = slot;\n\n return headers_ptr;\n}\n" +
|
|
42
42
|
"static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n" +
|
|
43
|
+
" const std::string_view method = req->getCaseSensitiveMethod();\n" +
|
|
44
|
+
" const i32 method_ptr = get_method_ptr(method);\n" +
|
|
45
|
+
" if (method_ptr == 0) return;\n" +
|
|
46
|
+
" __porffor_js_enter();\n const i32 url_ptr = alloc_request_url(req);\n" +
|
|
43
47
|
" const i32 headers_ptr = collect_headers(req);\n}\n" +
|
|
44
48
|
"static bool is_forbidden_response_header(std::string_view key) {\n" +
|
|
45
49
|
' return key == "connection" ||\n' +
|
package/src/patch.test.ts
CHANGED
|
@@ -76,6 +76,11 @@ static i32 collect_headers(uWS::HttpRequest* req) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {
|
|
79
|
+
const std::string_view method = req->getCaseSensitiveMethod();
|
|
80
|
+
const i32 method_ptr = get_method_ptr(method);
|
|
81
|
+
if (method_ptr == 0) return;
|
|
82
|
+
__porffor_js_enter();
|
|
83
|
+
const i32 url_ptr = alloc_request_url(req);
|
|
79
84
|
const i32 headers_ptr = collect_headers(req);
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -159,6 +164,11 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
|
|
|
159
164
|
expect(once.match(/case 302: return "302 Found";/g)).toHaveLength(1);
|
|
160
165
|
// Body-limit edit still rides along.
|
|
161
166
|
expect(once).toContain("sb_request_body_max");
|
|
167
|
+
// #195: a valid transfer ticket is intercepted before JS request roots and
|
|
168
|
+
// the ordinary PendingRequest body allocation path.
|
|
169
|
+
expect(once).toContain("sb_r2_transfer_open");
|
|
170
|
+
expect(once).toContain("sb_r2_transfer_write");
|
|
171
|
+
expect(once).toContain("try_handle_r2_transfer(res, req, method)");
|
|
162
172
|
|
|
163
173
|
// #163: collect_headers takes res, drops a client-sent x-sb-remote-addr, and
|
|
164
174
|
// appends the real peer; the call site passes res through.
|
package/src/patch.ts
CHANGED
|
@@ -413,6 +413,69 @@ const WRITE_RESPONSE_INJECT =
|
|
|
413
413
|
"}";
|
|
414
414
|
const WRITE_RESPONSE_MARKER = "porf_native_fetch_read_raw_bytes(body_value";
|
|
415
415
|
|
|
416
|
+
// #195: reserve a direct-transfer ticket before native-fetch allocates the
|
|
417
|
+
// ordinary in-memory PendingRequest. uWS then gives each wire chunk directly
|
|
418
|
+
// to the runtime C ABI, which owns the temporary object file.
|
|
419
|
+
const R2_TRANSFER_ANCHOR = "static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n";
|
|
420
|
+
const R2_TRANSFER_INJECT = `extern "C" {
|
|
421
|
+
struct sb_r2_transfer_ctx;
|
|
422
|
+
int sb_r2_transfer_open(const char*, const char*, size_t, sb_r2_transfer_ctx**);
|
|
423
|
+
int sb_r2_transfer_write(sb_r2_transfer_ctx*, const char*, size_t);
|
|
424
|
+
int sb_r2_transfer_finish(sb_r2_transfer_ctx*);
|
|
425
|
+
void sb_r2_transfer_abort(sb_r2_transfer_ctx*);
|
|
426
|
+
}
|
|
427
|
+
static bool sb_r2_transfer_path(std::string_view path, std::string* bucket, std::string* token) {
|
|
428
|
+
static constexpr std::string_view prefix = "/__sb/r2/transfer/";
|
|
429
|
+
if (path.size() < prefix.size() || path.substr(0, prefix.size()) != prefix) return false;
|
|
430
|
+
const std::string_view rest = path.substr(prefix.size());
|
|
431
|
+
const size_t slash = rest.find('/');
|
|
432
|
+
if (slash == std::string_view::npos || slash == 0 || slash > 255 || rest.find('/', slash + 1) != std::string_view::npos) return false;
|
|
433
|
+
const std::string_view b = rest.substr(0, slash), t = rest.substr(slash + 1);
|
|
434
|
+
if (t.size() != 24) return false;
|
|
435
|
+
for (char c : b) if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) return false;
|
|
436
|
+
for (char c : t) if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return false;
|
|
437
|
+
*bucket = std::string(b); *token = std::string(t); return true;
|
|
438
|
+
}
|
|
439
|
+
static std::string sb_r2_transfer_status(int status) {
|
|
440
|
+
switch (status) {
|
|
441
|
+
case 201: return "201 Created"; case 404: return "404 Not Found"; case 410: return "410 Gone";
|
|
442
|
+
case 413: return "413 Payload Too Large"; case 422: return "422 Unprocessable Content";
|
|
443
|
+
case 503: return "503 Service Unavailable"; case 507: return "507 Insufficient Storage";
|
|
444
|
+
default: return "500 Internal Server Error";
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
static bool try_handle_r2_transfer(uWS::HttpResponse<false>* res, uWS::HttpRequest* req, std::string_view method) {
|
|
448
|
+
std::string bucket, token;
|
|
449
|
+
if (!sb_r2_transfer_path(req->getUrl(), &bucket, &token)) return false;
|
|
450
|
+
if (method != "PUT") { respond_with_error(res, "405 Method Not Allowed", "R2 transfer requires PUT", true); return true; }
|
|
451
|
+
size_t declared = 0;
|
|
452
|
+
const std::string_view content_length = req->getHeader("content-length");
|
|
453
|
+
if (!content_length.empty() && !parse_content_length(content_length, &declared)) { respond_with_error(res, "400 Bad Request", "invalid content-length", true); return true; }
|
|
454
|
+
sb_r2_transfer_ctx* opened = nullptr;
|
|
455
|
+
const int open_status = sb_r2_transfer_open(bucket.c_str(), token.c_str(), declared, &opened);
|
|
456
|
+
if (open_status != 0 || !opened) { respond_with_error(res, sb_r2_transfer_status(open_status), "R2 transfer unavailable", true); return true; }
|
|
457
|
+
auto ctx = std::make_shared<sb_r2_transfer_ctx*>(opened);
|
|
458
|
+
res->onAborted([ctx]() { if (*ctx) { sb_r2_transfer_abort(*ctx); *ctx = nullptr; } });
|
|
459
|
+
res->onData([res, ctx](std::string_view chunk, bool is_last) {
|
|
460
|
+
if (!*ctx) return;
|
|
461
|
+
const int write_status = sb_r2_transfer_write(*ctx, chunk.data(), chunk.size());
|
|
462
|
+
if (write_status != 0) { sb_r2_transfer_abort(*ctx); *ctx = nullptr; respond_with_error(res, sb_r2_transfer_status(write_status), "R2 transfer failed", true); return; }
|
|
463
|
+
if (is_last) {
|
|
464
|
+
const int finish_status = sb_r2_transfer_finish(*ctx); *ctx = nullptr;
|
|
465
|
+
if (finish_status == 201) { res->writeStatus("201 Created"); res->end(); }
|
|
466
|
+
else respond_with_error(res, sb_r2_transfer_status(finish_status), "R2 transfer failed", true);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {
|
|
472
|
+
`;
|
|
473
|
+
const R2_TRANSFER_MARKER = "sb_r2_transfer_path(std::string_view path";
|
|
474
|
+
const R2_TRANSFER_CALL_ANCHOR = " __porffor_js_enter();\n const i32 url_ptr = alloc_request_url(req);";
|
|
475
|
+
const R2_TRANSFER_CALL_INJECT =
|
|
476
|
+
" if (try_handle_r2_transfer(res, req, method)) return;\n" + R2_TRANSFER_CALL_ANCHOR;
|
|
477
|
+
const R2_TRANSFER_CALL_MARKER = "try_handle_r2_transfer(res, req, method)";
|
|
478
|
+
|
|
416
479
|
const done = new Set<string>();
|
|
417
480
|
|
|
418
481
|
/** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
|
|
@@ -444,6 +507,8 @@ const UWS_EDITS = [
|
|
|
444
507
|
WRITE_RESPONSE_INJECT,
|
|
445
508
|
"detect x-sb-raw-body, steer the body read (#176)",
|
|
446
509
|
],
|
|
510
|
+
[R2_TRANSFER_MARKER, R2_TRANSFER_ANCHOR, R2_TRANSFER_INJECT, "native direct R2 transfer ingress (#195)"],
|
|
511
|
+
[R2_TRANSFER_CALL_MARKER, R2_TRANSFER_CALL_ANCHOR, R2_TRANSFER_CALL_INJECT, "direct R2 transfer dispatch (#195)"],
|
|
447
512
|
] as const;
|
|
448
513
|
|
|
449
514
|
/** The uWebSockets shim source: body limit + the #156 status-line fix. Exported for tests. */
|