@sproutboat/toolchain 0.3.1 → 0.3.2

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,37 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 806c21e: Fix (baronunread/sproutboat#176): #172's bytestring UTF-8 encoding fix broke
8
+ the assets binding, which reads a file's bytes off disk into a `bytestring`
9
+ specifically so they pass through the wire untouched — a `bytestring`
10
+ carrying real text a handler built and one carrying opaque file bytes are the
11
+ same type with no way to tell them apart, so #172's blanket encoding also
12
+ re-encoded already-finished bytes, corrupting any binary asset (and any
13
+ proxied `fetch()`/service-binding response body).
14
+
15
+ Adds a second, dedicated `porf_native_fetch_read_raw_bytes` (zero-copy,
16
+ bytestring-only, no encoding, ever) alongside the untouched original —
17
+ `porf_native_fetch_read_value` is called from ~30 places across sproutboat's
18
+ own inline C, all genuine text, so its signature and behavior stay exactly as
19
+ #172 left them. `write_response_value` picks between the two based on a new
20
+ reserved `x-sb-raw-body` response header (same pattern as #163's
21
+ `x-sb-remote-addr`), which the assets binding, outbound `fetch()`, and
22
+ service-binding `fetch()` now set — the three places sproutboat's own
23
+ prelude constructs a Response from wire bytes rather than a string a handler
24
+ built. Verified end-to-end: a 256-byte all-values fixture now round-trips
25
+ byte-for-byte through a real standalone build's assets binding, and #172's
26
+ original UTF-8 fix still holds for genuine text.
27
+
28
+ Not fixed here: `env.<R2>.get()`'s `.body` is exposed directly to handler
29
+ code, which then constructs its own `new Response(obj.body)` — no
30
+ sproutboat-owned call site to attach the marker to, so R2 binary objects
31
+ served this way carry the same corruption. Needs either a real binary body
32
+ type upstream in Porffor or a different API shape; tracked as a known gap on
33
+ #176, not resolved by this fix.
34
+
3
35
  ## 0.3.1
4
36
 
5
37
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -19,15 +19,19 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
19
19
  "compiler/render.js":
20
20
  "void porf_native_fetch_runtime_init(void) {\n signal(SIGPIPE, SIG_IGN);\n porf_init(0, NULL);\n}\n" +
21
21
  "f64 porf_native_fetch_get_port(void) {\nfixture:compiler/render.js\n" +
22
+ "int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned) {\n" +
22
23
  " if (value.type == ${TYPES.bytestring}) {\n" +
23
24
  " const u32 ptr = (u32)value.val;\n" +
24
25
  " *out_buf = (const char*)(MEM + ptr + 4);\n" +
25
26
  " *out_len = (size_t)*(u32*)(MEM + ptr);\n" +
26
27
  " return 0;\n" +
27
- " }\n",
28
+ " }\n" +
29
+ " return -1;\n" +
30
+ "}\n",
28
31
  "compiler/index.js": " '-xc', '-', '-c',\n uSocketsArchive,\n '-lm'\n",
29
32
  "compiler/uwebsockets.js":
30
33
  "static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;\n" +
34
+ "int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
31
35
  "static std::string_view lookup_status_line(i32 status) {\n" +
32
36
  ' switch (status) {\n case 302: return "302 Found";\n default: return {};\n }\n}\n' +
33
37
  "static i32 collect_headers(uWS::HttpRequest* req) {\n" +
@@ -36,7 +40,60 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
36
40
  " i32 slot = 0;\n for (auto [key, value] : *req) {\n slot++;\n }\n" +
37
41
  " *((i32*)(porf_mem + headers_ptr)) = slot;\n\n return headers_ptr;\n}\n" +
38
42
  "static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n" +
39
- " const i32 headers_ptr = collect_headers(req);\n}\n",
43
+ " const i32 headers_ptr = collect_headers(req);\n}\n" +
44
+ "static bool is_forbidden_response_header(std::string_view key) {\n" +
45
+ ' return key == "connection" ||\n' +
46
+ ' key == "content-length" ||\n' +
47
+ ' key == "transfer-encoding";\n' +
48
+ "}\n" +
49
+ "static void write_response_value(uWS::HttpResponse<false>* res, struct jsval response, bool* aborted) {\n" +
50
+ " struct NativeFetchResponseParts response_parts;\n" +
51
+ " porf_native_fetch_finalize_response(response.val, response.type, &response_parts);\n" +
52
+ " const i32 status = response_parts.status;\n" +
53
+ " const struct jsval body_value = response_parts.body;\n" +
54
+ " const i32 headers_entries_ptr = (i32)response_parts.headers.val;\n" +
55
+ "\n" +
56
+ " const char* body_buf = nullptr;\n" +
57
+ " size_t body_len = 0;\n" +
58
+ " char* body_owned = nullptr;\n" +
59
+ " porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);\n" +
60
+ "\n" +
61
+ " if (!aborted || !*aborted) {\n" +
62
+ " res->cork([res, status, headers_entries_ptr, body_buf, body_len]() {\n" +
63
+ " if (status != 200) res->writeStatus(lookup_status_line(status));\n" +
64
+ "\n" +
65
+ " const i32 headers_len = *((i32*)(porf_mem + headers_entries_ptr)) / 2;\n" +
66
+ " const i32 headers_entries = *((i32*)(porf_mem + headers_entries_ptr + 4));\n" +
67
+ " for (i32 i = 0; i < headers_len; i++) {\n" +
68
+ " const i32 name_base = headers_entries + i * 16;\n" +
69
+ " const i32 value_base = name_base + 8;\n" +
70
+ " const struct jsval name_value = unpack_jsval(*((u64*)(porf_mem + name_base)));\n" +
71
+ " const struct jsval value_value = unpack_jsval(*((u64*)(porf_mem + value_base)));\n" +
72
+ "\n" +
73
+ " const char* name_buf = nullptr;\n" +
74
+ " size_t name_len = 0;\n" +
75
+ " char* name_owned = nullptr;\n" +
76
+ " const char* value_buf = nullptr;\n" +
77
+ " size_t value_len = 0;\n" +
78
+ " char* value_owned = nullptr;\n" +
79
+ " porf_native_fetch_read_value(name_value, &name_buf, &name_len, &name_owned);\n" +
80
+ " porf_native_fetch_read_value(value_value, &value_buf, &value_len, &value_owned);\n" +
81
+ "\n" +
82
+ " const std::string_view key(name_buf, name_len);\n" +
83
+ " if (!is_forbidden_response_header(key)) {\n" +
84
+ " res->writeHeader(key, std::string_view(value_buf, value_len));\n" +
85
+ " }\n" +
86
+ "\n" +
87
+ " if (name_owned) free(name_owned);\n" +
88
+ " if (value_owned) free(value_owned);\n" +
89
+ " }\n" +
90
+ "\n" +
91
+ " res->end(std::string_view(body_buf, body_len));\n" +
92
+ " });\n" +
93
+ " }\n" +
94
+ "\n" +
95
+ " if (body_owned) free(body_owned);\n" +
96
+ "}\n",
40
97
  };
41
98
  for (const [file, contents] of Object.entries(files)) {
42
99
  const path = join(source, file);
package/src/patch.test.ts CHANGED
@@ -37,9 +37,13 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
37
37
  `;
38
38
 
39
39
  // Porffor's compiler/uwebsockets.js, trimmed to the parts the patch touches:
40
- // body limit (#56), the #156 status-line switch, and #163's collect_headers.
40
+ // body limit (#56), the #156 status-line switch, #163's collect_headers, and
41
+ // #176's read_value forward decl / is_forbidden_response_header /
42
+ // write_response_value.
41
43
  const SHIM = `static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
42
44
 
45
+ int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);
46
+
43
47
  static std::string_view lookup_status_line(i32 status) {
44
48
  switch (status) {
45
49
  case 200: return "200 OK";
@@ -74,6 +78,61 @@ static i32 collect_headers(uWS::HttpRequest* req) {
74
78
  static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {
75
79
  const i32 headers_ptr = collect_headers(req);
76
80
  }
81
+
82
+ static bool is_forbidden_response_header(std::string_view key) {
83
+ return key == "connection" ||
84
+ key == "content-length" ||
85
+ key == "transfer-encoding";
86
+ }
87
+
88
+ static void write_response_value(uWS::HttpResponse<false>* res, struct jsval response, bool* aborted) {
89
+ struct NativeFetchResponseParts response_parts;
90
+ porf_native_fetch_finalize_response(response.val, response.type, &response_parts);
91
+ const i32 status = response_parts.status;
92
+ const struct jsval body_value = response_parts.body;
93
+ const i32 headers_entries_ptr = (i32)response_parts.headers.val;
94
+
95
+ const char* body_buf = nullptr;
96
+ size_t body_len = 0;
97
+ char* body_owned = nullptr;
98
+ porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);
99
+
100
+ if (!aborted || !*aborted) {
101
+ res->cork([res, status, headers_entries_ptr, body_buf, body_len]() {
102
+ if (status != 200) res->writeStatus(lookup_status_line(status));
103
+
104
+ const i32 headers_len = *((i32*)(porf_mem + headers_entries_ptr)) / 2;
105
+ const i32 headers_entries = *((i32*)(porf_mem + headers_entries_ptr + 4));
106
+ for (i32 i = 0; i < headers_len; i++) {
107
+ const i32 name_base = headers_entries + i * 16;
108
+ const i32 value_base = name_base + 8;
109
+ const struct jsval name_value = unpack_jsval(*((u64*)(porf_mem + name_base)));
110
+ const struct jsval value_value = unpack_jsval(*((u64*)(porf_mem + value_base)));
111
+
112
+ const char* name_buf = nullptr;
113
+ size_t name_len = 0;
114
+ char* name_owned = nullptr;
115
+ const char* value_buf = nullptr;
116
+ size_t value_len = 0;
117
+ char* value_owned = nullptr;
118
+ porf_native_fetch_read_value(name_value, &name_buf, &name_len, &name_owned);
119
+ porf_native_fetch_read_value(value_value, &value_buf, &value_len, &value_owned);
120
+
121
+ const std::string_view key(name_buf, name_len);
122
+ if (!is_forbidden_response_header(key)) {
123
+ res->writeHeader(key, std::string_view(value_buf, value_len));
124
+ }
125
+
126
+ if (name_owned) free(name_owned);
127
+ if (value_owned) free(value_owned);
128
+ }
129
+
130
+ res->end(std::string_view(body_buf, body_len));
131
+ });
132
+ }
133
+
134
+ if (body_owned) free(body_owned);
135
+ }
77
136
  `;
78
137
 
79
138
  async function shimDir(): Promise<string> {
@@ -110,6 +169,23 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
110
169
  expect(once).toContain('porf_native_fetch_alloc_bytestring("x-sb-remote-addr", 16)');
111
170
  expect(once).toContain("header_capacity += 2;");
112
171
 
172
+ // #176: a dedicated porf_native_fetch_read_raw_bytes, declared alongside
173
+ // the untouched original; write_response_value scans for a reserved
174
+ // x-sb-raw-body header and calls the new function instead of the old one
175
+ // only for the body, only when it's present -- header reads are
176
+ // unchanged, and the marker is dropped from what reaches the client.
177
+ expect(once).toContain(
178
+ "int porf_native_fetch_read_raw_bytes(struct jsval value, const char** out_buf, size_t* out_len);",
179
+ );
180
+ expect(once).toContain('key == "x-sb-raw-body"');
181
+ 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);");
185
+ // 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);");
188
+
113
189
  await patchUwebsockets(root);
114
190
  expect(await readFile(join(root, "compiler/uwebsockets.js"), "utf8")).toBe(once);
115
191
  } finally {
@@ -146,7 +222,17 @@ test("#165: render.js routes console output to stderr, unbuffered, idempotently"
146
222
  // #172: the bytestring branch encodes instead of copying raw bytes.
147
223
  expect(once).toContain("sproutboat #172");
148
224
  expect(once).toContain("(char)(0xc0 | (c >> 6));");
149
- expect(once).not.toContain("*out_buf = (const char*)(MEM + ptr + 4);");
225
+ // #176: a separate, dedicated function does the original zero-copy
226
+ // passthrough for already-finished bytes (an asset) -- porf_native_fetch_
227
+ // read_value's own signature and behavior are untouched, so its ~30 other
228
+ // callers across sproutboat's inline C need no changes at all.
229
+ expect(once).toContain("sproutboat #176");
230
+ expect(once).toContain(
231
+ "int porf_native_fetch_read_raw_bytes(jsval value, const char** out_buf, size_t* out_len) {",
232
+ );
233
+ expect(once).toContain(
234
+ "int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned) {",
235
+ );
150
236
 
151
237
  await patchRenderJs(root);
152
238
  expect(await readFile(join(root, "compiler/render.js"), "utf8")).toBe(once);
package/src/patch.ts CHANGED
@@ -87,6 +87,55 @@ const BYTESTRING_INJECT =
87
87
  " }";
88
88
  const BYTESTRING_MARKER = "sproutboat #172";
89
89
 
90
+ /**
91
+ * baronunread/sproutboat#176 — #172 broke the assets binding, which reads a
92
+ * file's bytes off disk into a `bytestring` specifically so they can pass
93
+ * through the wire untouched (already-finished bytes: a UTF-8 text file's
94
+ * bytes are already UTF-8, a binary file's bytes are just bytes, neither
95
+ * should ever be "encoded" again). A `bytestring` carrying real text a
96
+ * handler built and one carrying opaque file bytes are the same type with no
97
+ * way to tell them apart at this point — so the caller has to say which one
98
+ * this is.
99
+ *
100
+ * `porf_native_fetch_read_value` is called from ~30 places across
101
+ * sproutboat's own inline C (crypto, SQL params, R2/D1 paths, the HTTP
102
+ * client), every one of them genuine text, every one needing #172's
103
+ * encoding unchanged — widening its signature would mean touching all of
104
+ * them for zero behavior change, for one caller that needs something
105
+ * different. Add a second, dedicated function instead:
106
+ * `porf_native_fetch_read_raw_bytes`, zero-copy, bytestring-only, no
107
+ * encoding, ever. Only `write_response_value`'s body read (uwebsockets.js,
108
+ * below) calls it, gated on the reserved `x-sb-raw-body` response header the
109
+ * assets binding sets. Every existing call site is untouched.
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) {";
112
+ const READ_RAW_INJECT =
113
+ "// sproutboat #176: zero-copy passthrough for a bytestring that is already-\n" +
114
+ "// finished bytes (an asset read off disk), never a string to UTF-8 encode.\n" +
115
+ "// Deliberately not folded into porf_native_fetch_read_value above: that one\n" +
116
+ "// is called from ~30 places across sproutboat's own inline C, all genuine\n" +
117
+ "// text, all needing its encoding unchanged.\n" +
118
+ "int porf_native_fetch_read_raw_bytes(jsval value, const char** out_buf, size_t* out_len) {\n" +
119
+ " if (value.type != ${TYPES.bytestring}) return -1;\n" +
120
+ " const u32 ptr = (u32)value.val;\n" +
121
+ " *out_buf = (const char*)(MEM + ptr + 4);\n" +
122
+ " *out_len = (size_t)*(u32*)(MEM + ptr);\n" +
123
+ " return 0;\n" +
124
+ "}\n" +
125
+ "\n" +
126
+ READ_RAW_ANCHOR;
127
+ const READ_RAW_MARKER = "porf_native_fetch_read_raw_bytes(jsval value";
128
+
129
+ /**
130
+ * render.js full-block replacements: `[marker, anchor, inject, what]`, each a
131
+ * complete swap of the matched region (unlike `RENDER_EDITS`, which only ever
132
+ * inserts after its anchor).
133
+ */
134
+ const RENDER_REPLACEMENTS = [
135
+ [BYTESTRING_MARKER, BYTESTRING_ANCHOR, BYTESTRING_INJECT, "bytestring UTF-8 encoding (#172)"],
136
+ [READ_RAW_MARKER, READ_RAW_ANCHOR, READ_RAW_INJECT, "raw bytestring passthrough function (#176)"],
137
+ ] as const;
138
+
90
139
  // #15 — no `--port` flag: Porffor's native-fetch entry point calls
91
140
  // `porf_init(0, NULL)` (see porf_native_fetch_runtime_init in render.js), so a
92
141
  // native-fetch binary never sees argv at all. A standalone binary takes its
@@ -207,6 +256,160 @@ const HDR_APPEND_INJECT =
207
256
  " *((i32*)(porf_mem + headers_ptr)) = slot;\n\n return headers_ptr;";
208
257
  const HDR_APPEND_MARKER = "__sb_peer";
209
258
 
259
+ /**
260
+ * baronunread/sproutboat#176 — the uwebsockets.js half of the raw-bytestring
261
+ * fix: a forward declaration for render.js's new
262
+ * `porf_native_fetch_read_raw_bytes`, `write_response_value` scanning for a
263
+ * reserved `x-sb-raw-body` response header before the body read and calling
264
+ * that function instead of `porf_native_fetch_read_value` when it's present
265
+ * (header reads are untouched — a header name or value is always real
266
+ * text), and `is_forbidden_response_header` dropping that header from what
267
+ * actually reaches the client — same reserved-header pattern as #163's
268
+ * `x-sb-remote-addr`, just response-side and sproutboat-to-C instead of
269
+ * C-to-sproutboat.
270
+ */
271
+ const READ_RAW_DECL_ANCHOR =
272
+ "int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);";
273
+ const READ_RAW_DECL_INJECT =
274
+ "int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
275
+ "int porf_native_fetch_read_raw_bytes(struct jsval value, const char** out_buf, size_t* out_len);";
276
+ const READ_RAW_DECL_MARKER = "porf_native_fetch_read_raw_bytes(struct jsval value";
277
+
278
+ const FORBIDDEN_HDR_ANCHOR =
279
+ "static bool is_forbidden_response_header(std::string_view key) {\n" +
280
+ ' return key == "connection" ||\n' +
281
+ ' key == "content-length" ||\n' +
282
+ ' key == "transfer-encoding";\n' +
283
+ "}";
284
+ const FORBIDDEN_HDR_INJECT =
285
+ "static bool is_forbidden_response_header(std::string_view key) {\n" +
286
+ ' return key == "connection" ||\n' +
287
+ ' key == "content-length" ||\n' +
288
+ ' key == "transfer-encoding" ||\n' +
289
+ ' key == "x-sb-raw-body"; // sproutboat #176: internal signal, never sent\n' +
290
+ "}";
291
+ const FORBIDDEN_HDR_MARKER = 'key == "x-sb-raw-body"';
292
+
293
+ const WRITE_RESPONSE_ANCHOR =
294
+ "static void write_response_value(uWS::HttpResponse<false>* res, struct jsval response, bool* aborted) {\n" +
295
+ " struct NativeFetchResponseParts response_parts;\n" +
296
+ " porf_native_fetch_finalize_response(response.val, response.type, &response_parts);\n" +
297
+ " const i32 status = response_parts.status;\n" +
298
+ " const struct jsval body_value = response_parts.body;\n" +
299
+ " const i32 headers_entries_ptr = (i32)response_parts.headers.val;\n" +
300
+ "\n" +
301
+ " const char* body_buf = nullptr;\n" +
302
+ " size_t body_len = 0;\n" +
303
+ " char* body_owned = nullptr;\n" +
304
+ " porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);\n" +
305
+ "\n" +
306
+ " if (!aborted || !*aborted) {\n" +
307
+ " res->cork([res, status, headers_entries_ptr, body_buf, body_len]() {\n" +
308
+ " if (status != 200) res->writeStatus(lookup_status_line(status));\n" +
309
+ "\n" +
310
+ " const i32 headers_len = *((i32*)(porf_mem + headers_entries_ptr)) / 2;\n" +
311
+ " const i32 headers_entries = *((i32*)(porf_mem + headers_entries_ptr + 4));\n" +
312
+ " for (i32 i = 0; i < headers_len; i++) {\n" +
313
+ " const i32 name_base = headers_entries + i * 16;\n" +
314
+ " const i32 value_base = name_base + 8;\n" +
315
+ " const struct jsval name_value = unpack_jsval(*((u64*)(porf_mem + name_base)));\n" +
316
+ " const struct jsval value_value = unpack_jsval(*((u64*)(porf_mem + value_base)));\n" +
317
+ "\n" +
318
+ " const char* name_buf = nullptr;\n" +
319
+ " size_t name_len = 0;\n" +
320
+ " char* name_owned = nullptr;\n" +
321
+ " const char* value_buf = nullptr;\n" +
322
+ " size_t value_len = 0;\n" +
323
+ " char* value_owned = nullptr;\n" +
324
+ " porf_native_fetch_read_value(name_value, &name_buf, &name_len, &name_owned);\n" +
325
+ " porf_native_fetch_read_value(value_value, &value_buf, &value_len, &value_owned);\n" +
326
+ "\n" +
327
+ " const std::string_view key(name_buf, name_len);\n" +
328
+ " if (!is_forbidden_response_header(key)) {\n" +
329
+ " res->writeHeader(key, std::string_view(value_buf, value_len));\n" +
330
+ " }\n" +
331
+ "\n" +
332
+ " if (name_owned) free(name_owned);\n" +
333
+ " if (value_owned) free(value_owned);\n" +
334
+ " }\n" +
335
+ "\n" +
336
+ " res->end(std::string_view(body_buf, body_len));\n" +
337
+ " });\n" +
338
+ " }\n" +
339
+ "\n" +
340
+ " if (body_owned) free(body_owned);\n" +
341
+ "}";
342
+ const WRITE_RESPONSE_INJECT =
343
+ "static void write_response_value(uWS::HttpResponse<false>* res, struct jsval response, bool* aborted) {\n" +
344
+ " struct NativeFetchResponseParts response_parts;\n" +
345
+ " porf_native_fetch_finalize_response(response.val, response.type, &response_parts);\n" +
346
+ " const i32 status = response_parts.status;\n" +
347
+ " const struct jsval body_value = response_parts.body;\n" +
348
+ " const i32 headers_entries_ptr = (i32)response_parts.headers.val;\n" +
349
+ "\n" +
350
+ " // sproutboat #176: a reserved x-sb-raw-body header (set by the prelude's\n" +
351
+ " // assets binding) means the body is already-finished bytes, not a string\n" +
352
+ " // to UTF-8 encode -- scanned before the body read so it can steer that one\n" +
353
+ " // call onto porf_native_fetch_read_raw_bytes instead.\n" +
354
+ " bool raw_body = false;\n" +
355
+ " {\n" +
356
+ " const i32 scan_len = *((i32*)(porf_mem + headers_entries_ptr)) / 2;\n" +
357
+ " const i32 scan_entries = *((i32*)(porf_mem + headers_entries_ptr + 4));\n" +
358
+ " for (i32 i = 0; i < scan_len && !raw_body; i++) {\n" +
359
+ " const struct jsval scan_name = unpack_jsval(*((u64*)(porf_mem + scan_entries + i * 16)));\n" +
360
+ " const char* scan_buf = nullptr;\n" +
361
+ " size_t scan_bytes = 0;\n" +
362
+ " char* scan_owned = nullptr;\n" +
363
+ " 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" +
365
+ " if (scan_owned) free(scan_owned);\n" +
366
+ " }\n" +
367
+ " }\n" +
368
+ "\n" +
369
+ " const char* body_buf = nullptr;\n" +
370
+ " size_t body_len = 0;\n" +
371
+ " char* body_owned = nullptr;\n" +
372
+ " if (raw_body) porf_native_fetch_read_raw_bytes(body_value, &body_buf, &body_len);\n" +
373
+ " else porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);\n" +
374
+ "\n" +
375
+ " if (!aborted || !*aborted) {\n" +
376
+ " res->cork([res, status, headers_entries_ptr, body_buf, body_len]() {\n" +
377
+ " if (status != 200) res->writeStatus(lookup_status_line(status));\n" +
378
+ "\n" +
379
+ " const i32 headers_len = *((i32*)(porf_mem + headers_entries_ptr)) / 2;\n" +
380
+ " const i32 headers_entries = *((i32*)(porf_mem + headers_entries_ptr + 4));\n" +
381
+ " for (i32 i = 0; i < headers_len; i++) {\n" +
382
+ " const i32 name_base = headers_entries + i * 16;\n" +
383
+ " const i32 value_base = name_base + 8;\n" +
384
+ " const struct jsval name_value = unpack_jsval(*((u64*)(porf_mem + name_base)));\n" +
385
+ " const struct jsval value_value = unpack_jsval(*((u64*)(porf_mem + value_base)));\n" +
386
+ "\n" +
387
+ " const char* name_buf = nullptr;\n" +
388
+ " size_t name_len = 0;\n" +
389
+ " char* name_owned = nullptr;\n" +
390
+ " const char* value_buf = nullptr;\n" +
391
+ " size_t value_len = 0;\n" +
392
+ " char* value_owned = nullptr;\n" +
393
+ " porf_native_fetch_read_value(name_value, &name_buf, &name_len, &name_owned);\n" +
394
+ " porf_native_fetch_read_value(value_value, &value_buf, &value_len, &value_owned);\n" +
395
+ "\n" +
396
+ " const std::string_view key(name_buf, name_len);\n" +
397
+ " if (!is_forbidden_response_header(key)) {\n" +
398
+ " res->writeHeader(key, std::string_view(value_buf, value_len));\n" +
399
+ " }\n" +
400
+ "\n" +
401
+ " if (name_owned) free(name_owned);\n" +
402
+ " if (value_owned) free(value_owned);\n" +
403
+ " }\n" +
404
+ "\n" +
405
+ " res->end(std::string_view(body_buf, body_len));\n" +
406
+ " });\n" +
407
+ " }\n" +
408
+ "\n" +
409
+ " if (body_owned) free(body_owned);\n" +
410
+ "}";
411
+ const WRITE_RESPONSE_MARKER = "porf_native_fetch_read_raw_bytes(body_value";
412
+
210
413
  const done = new Set<string>();
211
414
 
212
415
  /** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
@@ -220,6 +423,9 @@ const UWS_EDITS = [
220
423
  [HDR_CAP_MARKER, HDR_CAP_ANCHOR, HDR_CAP_INJECT, "remote-addr header capacity (#163)"],
221
424
  [HDR_SKIP_MARKER, HDR_SKIP_ANCHOR, HDR_SKIP_INJECT, "drop client-sent x-sb-remote-addr (#163)"],
222
425
  [HDR_APPEND_MARKER, HDR_APPEND_ANCHOR, HDR_APPEND_INJECT, "append x-sb-remote-addr (#163)"],
426
+ [READ_RAW_DECL_MARKER, READ_RAW_DECL_ANCHOR, READ_RAW_DECL_INJECT, "read_raw_bytes decl (#176)"],
427
+ [FORBIDDEN_HDR_MARKER, FORBIDDEN_HDR_ANCHOR, FORBIDDEN_HDR_INJECT, "drop x-sb-raw-body from the wire (#176)"],
428
+ [WRITE_RESPONSE_MARKER, WRITE_RESPONSE_ANCHOR, WRITE_RESPONSE_INJECT, "detect x-sb-raw-body, steer the body read (#176)"],
223
429
  ] as const;
224
430
 
225
431
  /** The uWebSockets shim source: body limit + the #156 status-line fix. Exported for tests. */
@@ -286,14 +492,17 @@ export async function patchRenderJs(root: string): Promise<void> {
286
492
  changed = true;
287
493
  }
288
494
 
289
- if (!src.includes(BYTESTRING_MARKER)) {
290
- if (!src.includes(BYTESTRING_ANCHOR)) {
495
+ // Full-block replacements, not append-after-anchor: each needs the whole
496
+ // matched region gone, not just something added alongside it.
497
+ for (const [marker, anchor, inject, what] of RENDER_REPLACEMENTS) {
498
+ if (src.includes(marker)) continue;
499
+ if (!src.includes(anchor)) {
291
500
  throw new Error(
292
- "could not patch Porffor for bytestring UTF-8 encoding (#172): anchor not found in " +
293
- `${file}. Porffor's native-fetch renderer changed — check patches/UPSTREAM.md.`,
501
+ `could not patch Porffor for ${what}: anchor not found in ${file}. ` +
502
+ "Porffor's native-fetch renderer changed — check patches/UPSTREAM.md.",
294
503
  );
295
504
  }
296
- src = src.replace(BYTESTRING_ANCHOR, BYTESTRING_INJECT);
505
+ src = src.replace(anchor, inject);
297
506
  changed = true;
298
507
  }
299
508