@sproutboat/toolchain 0.4.1 → 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 CHANGED
@@ -1,5 +1,19 @@
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
+
11
+ ## 0.4.2
12
+
13
+ ### Patch Changes
14
+
15
+ - fa09d90: Update the pinned Porffor compiler to alpha 6 and remove the superseded local promise-resolution workaround.
16
+
3
17
  ## 0.4.1
4
18
 
5
19
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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/acquire.ts CHANGED
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
5
5
  import { resolve } from "node:path";
6
6
  // @ts-expect-error Bun's file loader supplies a path; see the pinned-commit
7
7
  // check below for why this is safe to trust unconditionally.
8
- import vendoredPorfforArchive from "../vendor/porffor-1f4ae4a.tar.gz" with { type: "file" };
8
+ import vendoredPorfforArchive from "../vendor/porffor-038f415.tar.gz" with { type: "file" };
9
9
  import { ensurePorfforPatched } from "./patch";
10
10
  import { PORFFOR_ARCHIVE_SHA256, PORFFOR_ARCHIVE_URL, PORFFOR_COMMIT_FULL } from "./pin";
11
11
 
@@ -26,7 +26,12 @@ type AcquireOptions = {
26
26
  timeoutMs?: number;
27
27
  };
28
28
 
29
- const required = ["runtime/index.js", "compiler/render.js", "compiler/index.js", "compiler/uwebsockets.js"];
29
+ const required = [
30
+ "runtime/index.js",
31
+ "compiler/render.js",
32
+ "compiler/index.js",
33
+ "compiler/uwebsockets.js",
34
+ ];
30
35
  async function digest(path: string): Promise<string> {
31
36
  return createHash("sha256")
32
37
  .update(await readFile(path))
@@ -42,9 +47,11 @@ async function complete(dir: string, expectedArchive = PORFFOR_ARCHIVE_SHA256):
42
47
  archiveSha256?: string;
43
48
  files?: Record<string, string>;
44
49
  };
45
- if (manifest.commit !== PORFFOR_COMMIT_FULL || manifest.archiveSha256 !== expectedArchive) return false;
50
+ if (manifest.commit !== PORFFOR_COMMIT_FULL || manifest.archiveSha256 !== expectedArchive)
51
+ return false;
46
52
  for (const file of required)
47
- if (!manifest.files?.[file] || (await digest(resolve(dir, file))) !== manifest.files[file]) return false;
53
+ if (!manifest.files?.[file] || (await digest(resolve(dir, file))) !== manifest.files[file])
54
+ return false;
48
55
  return true;
49
56
  } catch {
50
57
  return false;
@@ -81,10 +88,16 @@ async function extract(archive: string, stage: string): Promise<void> {
81
88
  });
82
89
  const [code, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
83
90
  if (code !== 0)
84
- throw new PorfforToolchainError("archive", `could not extract pinned Porffor archive: ${stderr.trim()}`);
91
+ throw new PorfforToolchainError(
92
+ "archive",
93
+ `could not extract pinned Porffor archive: ${stderr.trim()}`,
94
+ );
85
95
  for (const file of required)
86
96
  if (!existsSync(resolve(stage, file)))
87
- throw new PorfforToolchainError("archive", `Porffor archive is missing required file ${file}`);
97
+ throw new PorfforToolchainError(
98
+ "archive",
99
+ `Porffor archive is missing required file ${file}`,
100
+ );
88
101
  }
89
102
 
90
103
  async function waitForPublisher(dir: string, lock: string): Promise<string | null> {
@@ -113,7 +126,9 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
113
126
  return dir;
114
127
  }
115
128
  const root = resolve(
116
- options.cacheRoot ?? process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? resolve(homedir(), ".cache/sproutboat"),
129
+ options.cacheRoot ??
130
+ process.env.SPROUTBOAT_TOOLCHAIN_CACHE ??
131
+ resolve(homedir(), ".cache/sproutboat"),
117
132
  );
118
133
  const dir = resolve(root, `porffor-${PORFFOR_COMMIT_FULL}`);
119
134
  const expected = options.expectedSha256 ?? PORFFOR_ARCHIVE_SHA256;
@@ -127,7 +142,10 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
127
142
  if (published) return published;
128
143
  return ensurePorffor(options);
129
144
  }
130
- const stage = resolve(root, `.porffor-${PORFFOR_COMMIT_FULL}-${process.pid}-${crypto.randomUUID()}`);
145
+ const stage = resolve(
146
+ root,
147
+ `.porffor-${PORFFOR_COMMIT_FULL}-${process.pid}-${crypto.randomUUID()}`,
148
+ );
131
149
  try {
132
150
  if (await complete(dir, expected)) return dir;
133
151
  await rm(dir, { recursive: true, force: true });
@@ -139,9 +157,18 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
139
157
  // left stale by a pin bump (checksum below still catches a wrong file
140
158
  // rather than silently accepting it).
141
159
  const noOverride = options.url === undefined && options.expectedSha256 === undefined;
142
- const vendored = noOverride && existsSync(vendoredPorfforArchive) && (await digest(vendoredPorfforArchive)) === expected;
160
+ const vendored =
161
+ noOverride &&
162
+ existsSync(vendoredPorfforArchive) &&
163
+ (await digest(vendoredPorfforArchive)) === expected;
143
164
  if (vendored) await writeFile(archive, await readFile(vendoredPorfforArchive));
144
- else await download(options.url ?? PORFFOR_ARCHIVE_URL, archive, options.fetcher ?? fetch, options.timeoutMs ?? 30_000);
165
+ else
166
+ await download(
167
+ options.url ?? PORFFOR_ARCHIVE_URL,
168
+ archive,
169
+ options.fetcher ?? fetch,
170
+ options.timeoutMs ?? 30_000,
171
+ );
145
172
  const actual = await digest(archive);
146
173
  if (actual !== expected)
147
174
  throw new PorfforToolchainError(
@@ -152,7 +179,9 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
152
179
  await rm(archive, { force: true });
153
180
  await ensurePorfforPatched(stage);
154
181
  const files = Object.fromEntries(
155
- await Promise.all(required.map(async (file) => [file, await digest(resolve(stage, file))] as const)),
182
+ await Promise.all(
183
+ required.map(async (file) => [file, await digest(resolve(stage, file))] as const),
184
+ ),
156
185
  );
157
186
  await writeFile(
158
187
  resolve(stage, ".sproutboat-complete"),
package/src/patch.test.ts CHANGED
@@ -2,7 +2,7 @@ import { expect, test } from "bun:test";
2
2
  import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { patchPromiseTs, patchRenderJs, patchUwebsockets } from "./patch";
5
+ import { patchRenderJs, patchUwebsockets } from "./patch";
6
6
 
7
7
  // The parts of Porffor's compiler/render.js the patch anchors to.
8
8
  const RENDER = `void porf_native_fetch_runtime_init(void) {
@@ -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,10 +164,17 @@ 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.
165
- expect(once).toContain("static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {");
175
+ expect(once).toContain(
176
+ "static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {",
177
+ );
166
178
  expect(once).toContain("const i32 headers_ptr = collect_headers(req, res);");
167
179
  expect(once).toContain('if (key == "x-sb-remote-addr") continue;');
168
180
  expect(once).toContain("res->getRemoteAddressAsText()");
@@ -179,12 +191,22 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
179
191
  );
180
192
  expect(once).toContain('key == "x-sb-raw-body"');
181
193
  expect(once).toContain("bool raw_body = false;");
182
- expect(once).toContain('if (std::string_view(scan_buf, scan_bytes) == "x-sb-raw-body") raw_body = true;');
183
- expect(once).toContain("if (raw_body) porf_native_fetch_read_raw_bytes(body_value, &body_buf, &body_len);");
184
- expect(once).toContain("else porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);");
194
+ expect(once).toContain(
195
+ 'if (std::string_view(scan_buf, scan_bytes) == "x-sb-raw-body") raw_body = true;',
196
+ );
197
+ expect(once).toContain(
198
+ "if (raw_body) porf_native_fetch_read_raw_bytes(body_value, &body_buf, &body_len);",
199
+ );
200
+ expect(once).toContain(
201
+ "else porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);",
202
+ );
185
203
  // Header reads are the original 4-arg call, unchanged.
186
- expect(once).toContain("porf_native_fetch_read_value(name_value, &name_buf, &name_len, &name_owned);");
187
- expect(once).toContain("porf_native_fetch_read_value(value_value, &value_buf, &value_len, &value_owned);");
204
+ expect(once).toContain(
205
+ "porf_native_fetch_read_value(name_value, &name_buf, &name_len, &name_owned);",
206
+ );
207
+ expect(once).toContain(
208
+ "porf_native_fetch_read_value(value_value, &value_buf, &value_len, &value_owned);",
209
+ );
188
210
 
189
211
  await patchUwebsockets(root);
190
212
  expect(await readFile(join(root, "compiler/uwebsockets.js"), "utf8")).toBe(once);
@@ -240,60 +262,3 @@ test("#165: render.js routes console output to stderr, unbuffered, idempotently"
240
262
  await rm(root, { recursive: true, force: true });
241
263
  }
242
264
  });
243
-
244
- // The part of Porffor's compiler/builtins/promise.ts the #168 patch anchors to.
245
- const PROMISE_TS = `export const __Porffor_promise_resolve = (value: any, promise: any): void => {
246
- if (Porffor.type(value) == Porffor.TYPES.object) {
247
- // cheap prototype-chain probe for 'then' before the expensive Get below, does not invoke getters
248
- const thenHash: i32 = __Porffor_object_hash('then');
249
- let probe: any = value;
250
- while (Porffor.type(probe) == Porffor.TYPES.object) {
251
- if (Porffor.object.lookup(probe, 'then', thenHash) != 0) break;
252
- probe = __Porffor_object_getPrototype(probe);
253
- }
254
- if (Porffor.type(probe) != Porffor.TYPES.object) {
255
- __ecma262_FulfillPromise(promise, value);
256
- return;
257
- }
258
- }
259
- };
260
- `;
261
-
262
- test("#168: promise-resolve's then-probe gets a fixed-point guard, idempotently", async () => {
263
- const root = await mkdtemp(join(tmpdir(), "sb-patch-promise-"));
264
- try {
265
- await mkdir(join(root, "compiler/builtins"), { recursive: true });
266
- await writeFile(join(root, "compiler/builtins/promise.ts"), PROMISE_TS);
267
- await patchPromiseTs(root);
268
- const once = await readFile(join(root, "compiler/builtins/promise.ts"), "utf8");
269
-
270
- expect(once).toContain("sproutboat #168");
271
- expect(once).toContain("let probeFound: boolean = false;");
272
- expect(once).toContain("if (Porffor.object.lookup(probe, 'then', thenHash) != 0) { probeFound = true; break; }");
273
- // The fixed-point guard mirrors _internal_object.ts's lastProto idiom.
274
- expect(once).toContain("Porffor.fastOr(probe == null, Porffor.IR.ptr(probe) == Porffor.IR.ptr(lastProto))");
275
- expect(once).toContain("if (!probeFound) {");
276
- // Belt-and-suspenders: a hard iteration cap terminates the loop even if
277
- // some other corruption shape (a drifting pointer, a longer cycle) defeats
278
- // the fixed-point check above.
279
- expect(once).toContain("if (probeSteps > 64) break;");
280
- // The old type-check-only exit condition is gone.
281
- expect(once).not.toContain("if (Porffor.type(probe) != Porffor.TYPES.object) {");
282
-
283
- await patchPromiseTs(root);
284
- expect(await readFile(join(root, "compiler/builtins/promise.ts"), "utf8")).toBe(once);
285
- } finally {
286
- await rm(root, { recursive: true, force: true });
287
- }
288
- });
289
-
290
- test("#168: a drifted promise builtin fails loudly instead of silently no-op'ing", async () => {
291
- const root = await mkdtemp(join(tmpdir(), "sb-patch-promise-drift-"));
292
- try {
293
- await mkdir(join(root, "compiler/builtins"), { recursive: true });
294
- await writeFile(join(root, "compiler/builtins/promise.ts"), "// nothing the patch recognizes\n");
295
- await expect(patchPromiseTs(root)).rejects.toThrow(/anchor not found/);
296
- } finally {
297
- await rm(root, { recursive: true, force: true });
298
- }
299
- });
package/src/patch.ts CHANGED
@@ -108,7 +108,8 @@ const BYTESTRING_MARKER = "sproutboat #172";
108
108
  * below) calls it, gated on the reserved `x-sb-raw-body` response header the
109
109
  * assets binding sets. Every existing call site is untouched.
110
110
  */
111
- const READ_RAW_ANCHOR = "int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned) {";
111
+ const READ_RAW_ANCHOR =
112
+ "int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned) {";
112
113
  const READ_RAW_INJECT =
113
114
  "// sproutboat #176: zero-copy passthrough for a bytestring that is already-\n" +
114
115
  "// finished bytes (an asset read off disk), never a string to UTF-8 encode.\n" +
@@ -208,7 +209,8 @@ const STATUS_SIG_MARKER = "static std::string lookup_status_line(i32 status) {";
208
209
  // 303 is the one this was reported for (POST-redirect-GET); give it the real
209
210
  // reason phrase. Everything else unlisted rides the synthesized fallback below.
210
211
  const STATUS_303_ANCHOR = ' case 302: return "302 Found";\n';
211
- const STATUS_303_INJECT = ' case 302: return "302 Found";\n case 303: return "303 See Other";\n';
212
+ const STATUS_303_INJECT =
213
+ ' case 302: return "302 Found";\n case 303: return "303 See Other";\n';
212
214
  const STATUS_303_MARKER = 'case 303: return "303 See Other";';
213
215
  const STATUS_DEFAULT_ANCHOR = " default: return {};";
214
216
  const STATUS_DEFAULT_INJECT = ' default: return std::to_string(status) + " Status";';
@@ -225,7 +227,8 @@ const STATUS_DEFAULT_MARKER = 'std::to_string(status) + " Status"';
225
227
  * `request.cf.clientIp` and resolves it against `SB_TRUSTED_PROXIES` if set.
226
228
  */
227
229
  const HDR_SIG_ANCHOR = "static i32 collect_headers(uWS::HttpRequest* req) {\n";
228
- const HDR_SIG_INJECT = "static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {\n";
230
+ const HDR_SIG_INJECT =
231
+ "static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {\n";
229
232
  const HDR_SIG_MARKER = "collect_headers(uWS::HttpRequest* req, uWS::HttpResponse";
230
233
 
231
234
  const HDR_CALL_ANCHOR = "const i32 headers_ptr = collect_headers(req);";
@@ -361,7 +364,7 @@ const WRITE_RESPONSE_INJECT =
361
364
  " size_t scan_bytes = 0;\n" +
362
365
  " char* scan_owned = nullptr;\n" +
363
366
  " porf_native_fetch_read_value(scan_name, &scan_buf, &scan_bytes, &scan_owned);\n" +
364
- " if (std::string_view(scan_buf, scan_bytes) == \"x-sb-raw-body\") raw_body = true;\n" +
367
+ ' if (std::string_view(scan_buf, scan_bytes) == "x-sb-raw-body") raw_body = true;\n' +
365
368
  " if (scan_owned) free(scan_owned);\n" +
366
369
  " }\n" +
367
370
  " }\n" +
@@ -410,74 +413,68 @@ const WRITE_RESPONSE_INJECT =
410
413
  "}";
411
414
  const WRITE_RESPONSE_MARKER = "porf_native_fetch_read_raw_bytes(body_value";
412
415
 
413
- /**
414
- * baronunread/sproutboat#168 — `__Porffor_promise_resolve`'s duck-typing probe
415
- * for `.then` walks a value's prototype chain with no termination guard. Under
416
- * certain allocator conditions `__Porffor_object_getPrototype` returns a stale
417
- * "object" jsval instead of the `null`/`undefined` that should end the chain,
418
- * so the walk never bottoms out — a true infinite spin (100% CPU, no error, no
419
- * log line) that wedges the whole process, since native-fetch serves one
420
- * request at a time on a single event loop. Hits any handler whose `fetch`
421
- * resolves a promise with a plain object — the common shape for a JSON
422
- * response — so this is not a rare pattern; onset is nondeterministic
423
- * (allocator/pool reuse), typically after a handful of requests.
424
- *
425
- * `_internal_object.ts`'s own prototype-chain walks already guard against
426
- * exactly this failure mode: track the previous prototype and stop once the
427
- * "next" pointer stops advancing (a fixed point reads the same as reaching the
428
- * end). This probe has no such guard. Mirror the same idiom here: an explicit
429
- * `probeFound` flag (so a fixed-point break reads as "no `.then` found",
430
- * matching how a legitimate null-terminated chain is read) plus the
431
- * `lastProto` pointer comparison used everywhere else in the runtime.
432
- *
433
- * A hard iteration cap rides alongside the fixed-point check, not instead of
434
- * it: the lldb trace showed the *same* stale pointer on every sample, which
435
- * the fixed-point check alone catches on its very next iteration, but that's
436
- * one observed corruption shape, not a guarantee about every allocator state
437
- * that can produce this bug. A cap makes termination unconditional regardless
438
- * of whether some other corrupted state instead drifts (a different bad
439
- * pointer each time) or cycles across more than one node — real prototype
440
- * chains are a handful of links deep, so 64 iterations is generous headroom
441
- * with no risk of cutting off a legitimate lookup.
442
- */
443
- const THEN_PROBE_ANCHOR =
444
- " const thenHash: i32 = __Porffor_object_hash('then');\n" +
445
- " let probe: any = value;\n" +
446
- " while (Porffor.type(probe) == Porffor.TYPES.object) {\n" +
447
- " if (Porffor.object.lookup(probe, 'then', thenHash) != 0) break;\n" +
448
- " probe = __Porffor_object_getPrototype(probe);\n" +
449
- " }\n" +
450
- " if (Porffor.type(probe) != Porffor.TYPES.object) {\n";
451
- const THEN_PROBE_INJECT =
452
- " const thenHash: i32 = __Porffor_object_hash('then');\n" +
453
- " let probe: any = value;\n" +
454
- " let lastProto: any = probe;\n" +
455
- " let probeFound: boolean = false; // sproutboat #168: fixed-point chain reads as \"not found\", not a spin\n" +
456
- " let probeSteps: i32 = 0; // sproutboat #168: hard cap, belt-and-suspenders alongside the fixed-point check\n" +
457
- " while (Porffor.type(probe) == Porffor.TYPES.object) {\n" +
458
- " if (Porffor.object.lookup(probe, 'then', thenHash) != 0) { probeFound = true; break; }\n" +
459
- " probe = __Porffor_object_getPrototype(probe);\n" +
460
- " if (Porffor.fastOr(probe == null, Porffor.IR.ptr(probe) == Porffor.IR.ptr(lastProto))) break;\n" +
461
- " lastProto = probe;\n" +
462
- " probeSteps++;\n" +
463
- " if (probeSteps > 64) break;\n" +
464
- " }\n" +
465
- " if (!probeFound) {\n";
466
- const THEN_PROBE_MARKER = "sproutboat #168";
467
-
468
- /** The promise builtin: the #168 `.then`-probe livelock fix. Exported for tests. */
469
- export async function patchPromiseTs(root: string): Promise<void> {
470
- const file = resolve(root, "compiler/builtins/promise.ts");
471
- let src = await readFile(file, "utf8");
472
- if (src.includes(THEN_PROBE_MARKER)) return;
473
- if (!src.includes(THEN_PROBE_ANCHOR)) {
474
- throw new Error(
475
- "could not patch Porffor's promise-resolve then-probe (#168): anchor not found in " +
476
- `${file}. Porffor's promise builtin changed — check patches/UPSTREAM.md.`,
477
- );
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";
478
445
  }
479
- await writeFile(file, src.replace(THEN_PROBE_ANCHOR, THEN_PROBE_INJECT));
480
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)";
481
478
 
482
479
  const done = new Set<string>();
483
480
 
@@ -486,15 +483,32 @@ const UWS_EDITS = [
486
483
  [BODY_MARKER, BODY_ANCHOR, BODY_INJECT, "request body limit"],
487
484
  [STATUS_SIG_MARKER, STATUS_SIG_ANCHOR, STATUS_SIG_INJECT, "status-line return type (#156)"],
488
485
  [STATUS_303_MARKER, STATUS_303_ANCHOR, STATUS_303_INJECT, "status-line 303 case (#156)"],
489
- [STATUS_DEFAULT_MARKER, STATUS_DEFAULT_ANCHOR, STATUS_DEFAULT_INJECT, "status-line fallback (#156)"],
486
+ [
487
+ STATUS_DEFAULT_MARKER,
488
+ STATUS_DEFAULT_ANCHOR,
489
+ STATUS_DEFAULT_INJECT,
490
+ "status-line fallback (#156)",
491
+ ],
490
492
  [HDR_SIG_MARKER, HDR_SIG_ANCHOR, HDR_SIG_INJECT, "collect_headers res arg (#163)"],
491
493
  [HDR_CALL_MARKER, HDR_CALL_ANCHOR, HDR_CALL_INJECT, "collect_headers call site (#163)"],
492
494
  [HDR_CAP_MARKER, HDR_CAP_ANCHOR, HDR_CAP_INJECT, "remote-addr header capacity (#163)"],
493
495
  [HDR_SKIP_MARKER, HDR_SKIP_ANCHOR, HDR_SKIP_INJECT, "drop client-sent x-sb-remote-addr (#163)"],
494
496
  [HDR_APPEND_MARKER, HDR_APPEND_ANCHOR, HDR_APPEND_INJECT, "append x-sb-remote-addr (#163)"],
495
497
  [READ_RAW_DECL_MARKER, READ_RAW_DECL_ANCHOR, READ_RAW_DECL_INJECT, "read_raw_bytes decl (#176)"],
496
- [FORBIDDEN_HDR_MARKER, FORBIDDEN_HDR_ANCHOR, FORBIDDEN_HDR_INJECT, "drop x-sb-raw-body from the wire (#176)"],
497
- [WRITE_RESPONSE_MARKER, WRITE_RESPONSE_ANCHOR, WRITE_RESPONSE_INJECT, "detect x-sb-raw-body, steer the body read (#176)"],
498
+ [
499
+ FORBIDDEN_HDR_MARKER,
500
+ FORBIDDEN_HDR_ANCHOR,
501
+ FORBIDDEN_HDR_INJECT,
502
+ "drop x-sb-raw-body from the wire (#176)",
503
+ ],
504
+ [
505
+ WRITE_RESPONSE_MARKER,
506
+ WRITE_RESPONSE_ANCHOR,
507
+ WRITE_RESPONSE_INJECT,
508
+ "detect x-sb-raw-body, steer the body read (#176)",
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)"],
498
512
  ] as const;
499
513
 
500
514
  /** The uWebSockets shim source: body limit + the #156 status-line fix. Exported for tests. */
@@ -589,6 +603,5 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
589
603
  await patchCompilerArgs(root);
590
604
  await patchUwebsockets(root);
591
605
  await patchRenderJs(root);
592
- await patchPromiseTs(root);
593
606
  done.add(root);
594
607
  }
package/src/pin.ts CHANGED
@@ -10,13 +10,14 @@
10
10
  * uWebSockets archive in `sproutboat-cli/vendor/` — then run both repos'
11
11
  * test + conformance suites. See sproutboat-cli/MIGRATION.md.
12
12
  */
13
- export const PORFFOR_CHANNEL = "alpha-5";
14
- export const PORFFOR_COMMIT_FULL = "1f4ae4ae3e0a5f0a93b3bc084359e1a3a23391fd";
13
+ export const PORFFOR_CHANNEL = "alpha-6";
14
+ export const PORFFOR_COMMIT_FULL = "038f415e08efc5f87a6bfcb05a18824caa3a14f6";
15
15
  export const PORFFOR_COMMIT = PORFFOR_COMMIT_FULL.slice(0, 7);
16
- export const PORFFOR_ARCHIVE_SHA256 = "a49a0e857574e93cb09c574dcf53b7cc049d3cb65c5b944b169755cad2a51d5d";
16
+ export const PORFFOR_ARCHIVE_SHA256 =
17
+ "61f65d8fac5f94a7910f0a3f046e8929a852addb0033a032b9f7e08d0b07df5c";
17
18
  export const PORFFOR_ARCHIVE_URL = `https://codeload.github.com/CanadaHonk/porffor/tar.gz/${PORFFOR_COMMIT_FULL}`;
18
19
 
19
- /** A compact identity string for a manifest / report (`alpha-5 (1f4ae4a)`). */
20
+ /** A compact identity string for a manifest / report (`alpha-6 (038f415)`). */
20
21
  export function porfforVersion(): string {
21
22
  return process.env.PORFFOR_VERSION || `${PORFFOR_CHANNEL} (${PORFFOR_COMMIT})`;
22
23
  }
Binary file
Binary file