@sproutboat/toolchain 0.3.0 → 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 +44 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +65 -2
- package/src/patch.test.ts +109 -1
- package/src/patch.ts +262 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
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
|
+
|
|
35
|
+
## 0.3.1
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- afd054c: Fix (baronunread/sproutboat#172): a standalone/native-fetch response body,
|
|
40
|
+
header name, or header value whose code points all fit one byte (any Latin-1-
|
|
41
|
+
range string — most non-astral JS strings) reached the wire as raw Latin-1
|
|
42
|
+
bytes instead of UTF-8, corrupting any non-ASCII text regardless of the
|
|
43
|
+
declared `charset`. `patchRenderJs` now encodes Porffor's `bytestring`
|
|
44
|
+
representation the same way its `string` (UTF-16) branch already did. Plain
|
|
45
|
+
ASCII output is unaffected (identical in both encodings).
|
|
46
|
+
|
|
3
47
|
## 0.3.0
|
|
4
48
|
|
|
5
49
|
### Minor Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -18,10 +18,20 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
18
18
|
"runtime/index.js": "fixture:runtime/index.js\n",
|
|
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
|
-
"f64 porf_native_fetch_get_port(void) {\nfixture:compiler/render.js\n"
|
|
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" +
|
|
23
|
+
" if (value.type == ${TYPES.bytestring}) {\n" +
|
|
24
|
+
" const u32 ptr = (u32)value.val;\n" +
|
|
25
|
+
" *out_buf = (const char*)(MEM + ptr + 4);\n" +
|
|
26
|
+
" *out_len = (size_t)*(u32*)(MEM + ptr);\n" +
|
|
27
|
+
" return 0;\n" +
|
|
28
|
+
" }\n" +
|
|
29
|
+
" return -1;\n" +
|
|
30
|
+
"}\n",
|
|
22
31
|
"compiler/index.js": " '-xc', '-', '-c',\n uSocketsArchive,\n '-lm'\n",
|
|
23
32
|
"compiler/uwebsockets.js":
|
|
24
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" +
|
|
25
35
|
"static std::string_view lookup_status_line(i32 status) {\n" +
|
|
26
36
|
' switch (status) {\n case 302: return "302 Found";\n default: return {};\n }\n}\n' +
|
|
27
37
|
"static i32 collect_headers(uWS::HttpRequest* req) {\n" +
|
|
@@ -30,7 +40,60 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
30
40
|
" i32 slot = 0;\n for (auto [key, value] : *req) {\n slot++;\n }\n" +
|
|
31
41
|
" *((i32*)(porf_mem + headers_ptr)) = slot;\n\n return headers_ptr;\n}\n" +
|
|
32
42
|
"static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n" +
|
|
33
|
-
" 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",
|
|
34
97
|
};
|
|
35
98
|
for (const [file, contents] of Object.entries(files)) {
|
|
36
99
|
const path = join(source, file);
|
package/src/patch.test.ts
CHANGED
|
@@ -17,12 +17,33 @@ const RENDER = `void porf_native_fetch_runtime_init(void) {
|
|
|
17
17
|
f64 porf_native_fetch_get_port(void) {
|
|
18
18
|
return __porffor_native_fetch_port.val;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned) {
|
|
22
|
+
if (!out_buf || !out_len || !out_owned) return -1;
|
|
23
|
+
|
|
24
|
+
*out_buf = NULL;
|
|
25
|
+
*out_len = 0;
|
|
26
|
+
*out_owned = NULL;
|
|
27
|
+
|
|
28
|
+
if (value.type == \${TYPES.bytestring}) {
|
|
29
|
+
const u32 ptr = (u32)value.val;
|
|
30
|
+
*out_buf = (const char*)(MEM + ptr + 4);
|
|
31
|
+
*out_len = (size_t)*(u32*)(MEM + ptr);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
20
37
|
`;
|
|
21
38
|
|
|
22
39
|
// Porffor's compiler/uwebsockets.js, trimmed to the parts the patch touches:
|
|
23
|
-
// body limit (#56), the #156 status-line switch,
|
|
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.
|
|
24
43
|
const SHIM = `static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
|
|
25
44
|
|
|
45
|
+
int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);
|
|
46
|
+
|
|
26
47
|
static std::string_view lookup_status_line(i32 status) {
|
|
27
48
|
switch (status) {
|
|
28
49
|
case 200: return "200 OK";
|
|
@@ -57,6 +78,61 @@ static i32 collect_headers(uWS::HttpRequest* req) {
|
|
|
57
78
|
static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {
|
|
58
79
|
const i32 headers_ptr = collect_headers(req);
|
|
59
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
|
+
}
|
|
60
136
|
`;
|
|
61
137
|
|
|
62
138
|
async function shimDir(): Promise<string> {
|
|
@@ -93,6 +169,23 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
|
|
|
93
169
|
expect(once).toContain('porf_native_fetch_alloc_bytestring("x-sb-remote-addr", 16)');
|
|
94
170
|
expect(once).toContain("header_capacity += 2;");
|
|
95
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
|
+
|
|
96
189
|
await patchUwebsockets(root);
|
|
97
190
|
expect(await readFile(join(root, "compiler/uwebsockets.js"), "utf8")).toBe(once);
|
|
98
191
|
} finally {
|
|
@@ -126,6 +219,21 @@ test("#165: render.js routes console output to stderr, unbuffered, idempotently"
|
|
|
126
219
|
// The $PORT edit still lands too.
|
|
127
220
|
expect(once).toContain('getenv("PORT")');
|
|
128
221
|
|
|
222
|
+
// #172: the bytestring branch encodes instead of copying raw bytes.
|
|
223
|
+
expect(once).toContain("sproutboat #172");
|
|
224
|
+
expect(once).toContain("(char)(0xc0 | (c >> 6));");
|
|
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
|
+
);
|
|
236
|
+
|
|
129
237
|
await patchRenderJs(root);
|
|
130
238
|
expect(await readFile(join(root, "compiler/render.js"), "utf8")).toBe(once);
|
|
131
239
|
} finally {
|
package/src/patch.ts
CHANGED
|
@@ -46,6 +46,96 @@ const RENDER_EDITS = [
|
|
|
46
46
|
[CONSOLE_MARKER, CONSOLE_ANCHOR, CONSOLE_INJECT, "console output sink (#165)"],
|
|
47
47
|
] as const;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* baronunread/sproutboat#172 — `porf_native_fetch_read_value`'s `bytestring`
|
|
51
|
+
* branch (Porffor's Latin-1-range string representation) copies the raw code
|
|
52
|
+
* units straight onto the wire, as if they were already UTF-8 bytes. Any unit
|
|
53
|
+
* >= 0x80 — any non-ASCII Latin-1 character — reaches the client corrupt
|
|
54
|
+
* instead of UTF-8 encoded. Response bodies and header values both go through
|
|
55
|
+
* this function, so both are affected; the sibling `string` (UTF-16) branch a
|
|
56
|
+
* few lines down already encodes correctly, this makes the `bytestring` one
|
|
57
|
+
* do the same, one byte in instead of two.
|
|
58
|
+
*/
|
|
59
|
+
const BYTESTRING_ANCHOR =
|
|
60
|
+
" if (value.type == ${TYPES.bytestring}) {\n" +
|
|
61
|
+
" const u32 ptr = (u32)value.val;\n" +
|
|
62
|
+
" *out_buf = (const char*)(MEM + ptr + 4);\n" +
|
|
63
|
+
" *out_len = (size_t)*(u32*)(MEM + ptr);\n" +
|
|
64
|
+
" return 0;\n" +
|
|
65
|
+
" }";
|
|
66
|
+
const BYTESTRING_INJECT =
|
|
67
|
+
" if (value.type == ${TYPES.bytestring}) {\n" +
|
|
68
|
+
" // sproutboat #172: units are Latin-1 code points, not UTF-8 bytes -- encode them\n" +
|
|
69
|
+
" const u32 ptr = (u32)value.val;\n" +
|
|
70
|
+
" const size_t len = (size_t)*(u32*)(MEM + ptr);\n" +
|
|
71
|
+
" const unsigned char* units = (const unsigned char*)(MEM + ptr + 4);\n" +
|
|
72
|
+
" char* utf8 = (char*)malloc(len * 2);\n" +
|
|
73
|
+
" if (!utf8 && len > 0) return -1;\n" +
|
|
74
|
+
" size_t out_len_local = 0;\n" +
|
|
75
|
+
" for (size_t i = 0; i < len; i++) {\n" +
|
|
76
|
+
" unsigned char c = units[i];\n" +
|
|
77
|
+
" if (c < 0x80) utf8[out_len_local++] = (char)c;\n" +
|
|
78
|
+
" else {\n" +
|
|
79
|
+
" utf8[out_len_local++] = (char)(0xc0 | (c >> 6));\n" +
|
|
80
|
+
" utf8[out_len_local++] = (char)(0x80 | (c & 0x3f));\n" +
|
|
81
|
+
" }\n" +
|
|
82
|
+
" }\n" +
|
|
83
|
+
" *out_buf = utf8;\n" +
|
|
84
|
+
" *out_len = out_len_local;\n" +
|
|
85
|
+
" *out_owned = utf8;\n" +
|
|
86
|
+
" return 0;\n" +
|
|
87
|
+
" }";
|
|
88
|
+
const BYTESTRING_MARKER = "sproutboat #172";
|
|
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
|
+
|
|
49
139
|
// #15 — no `--port` flag: Porffor's native-fetch entry point calls
|
|
50
140
|
// `porf_init(0, NULL)` (see porf_native_fetch_runtime_init in render.js), so a
|
|
51
141
|
// native-fetch binary never sees argv at all. A standalone binary takes its
|
|
@@ -166,6 +256,160 @@ const HDR_APPEND_INJECT =
|
|
|
166
256
|
" *((i32*)(porf_mem + headers_ptr)) = slot;\n\n return headers_ptr;";
|
|
167
257
|
const HDR_APPEND_MARKER = "__sb_peer";
|
|
168
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
|
+
|
|
169
413
|
const done = new Set<string>();
|
|
170
414
|
|
|
171
415
|
/** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
|
|
@@ -179,6 +423,9 @@ const UWS_EDITS = [
|
|
|
179
423
|
[HDR_CAP_MARKER, HDR_CAP_ANCHOR, HDR_CAP_INJECT, "remote-addr header capacity (#163)"],
|
|
180
424
|
[HDR_SKIP_MARKER, HDR_SKIP_ANCHOR, HDR_SKIP_INJECT, "drop client-sent x-sb-remote-addr (#163)"],
|
|
181
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)"],
|
|
182
429
|
] as const;
|
|
183
430
|
|
|
184
431
|
/** The uWebSockets shim source: body limit + the #156 status-line fix. Exported for tests. */
|
|
@@ -244,6 +491,21 @@ export async function patchRenderJs(root: string): Promise<void> {
|
|
|
244
491
|
src = src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length);
|
|
245
492
|
changed = true;
|
|
246
493
|
}
|
|
494
|
+
|
|
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)) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`could not patch Porffor for ${what}: anchor not found in ${file}. ` +
|
|
502
|
+
"Porffor's native-fetch renderer changed — check patches/UPSTREAM.md.",
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
src = src.replace(anchor, inject);
|
|
506
|
+
changed = true;
|
|
507
|
+
}
|
|
508
|
+
|
|
247
509
|
if (changed) await writeFile(file, src);
|
|
248
510
|
}
|
|
249
511
|
|