@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 +14 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +4 -0
- package/src/acquire.ts +40 -11
- package/src/patch.test.ts +29 -64
- package/src/patch.ts +87 -74
- package/src/pin.ts +5 -4
- package/vendor/porffor-038f415.tar.gz +0 -0
- package/vendor/porffor-1f4ae4a.tar.gz +0 -0
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
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/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-
|
|
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 = [
|
|
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)
|
|
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])
|
|
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(
|
|
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(
|
|
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 ??
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
183
|
-
|
|
184
|
-
|
|
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(
|
|
187
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
|
|
424
|
-
*
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
[
|
|
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
|
-
[
|
|
497
|
-
|
|
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-
|
|
14
|
-
export const PORFFOR_COMMIT_FULL = "
|
|
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 =
|
|
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-
|
|
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
|