From 76545140afb7b1355c5b2630ec4c38956001659b Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 2 Oct 2025 14:55:28 -0700 Subject: [PATCH 001/191] fix(node:http) fix closing socket after upgraded to websocket (#23150) ### What does this PR do? handle socket upgrade in NodeHTTP.cpp ### How did you verify your code works? Run the test added with asan it should catch the bug --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- packages/bun-uws/src/App.h | 4 ++ packages/bun-uws/src/HttpContextData.h | 6 ++- packages/bun-uws/src/HttpResponse.h | 19 +++++--- packages/bun-uws/src/WebSocket.h | 4 +- packages/bun-uws/src/WebSocketContext.h | 3 ++ packages/bun-uws/src/WebSocketContextData.h | 1 - packages/bun-uws/src/WebSocketData.h | 10 ++++- src/bun.js/bindings/NodeHTTP.cpp | 22 ++++++--- src/js/node/_http_server.ts | 1 - test/js/node/http/node-http-with-ws.test.ts | 50 +++++++++++++++++++++ 10 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 test/js/node/http/node-http-with-ws.test.ts diff --git a/packages/bun-uws/src/App.h b/packages/bun-uws/src/App.h index 6840c23fed..9c73e64dba 100644 --- a/packages/bun-uws/src/App.h +++ b/packages/bun-uws/src/App.h @@ -641,6 +641,10 @@ public: httpContext->getSocketContextData()->onClientError = std::move(onClientError); } + void setOnSocketUpgraded(HttpContextData::OnSocketUpgradedCallback onUpgraded) { + httpContext->getSocketContextData()->onSocketUpgraded = onUpgraded; + } + TemplatedApp &&run() { uWS::run(); return std::move(*this); diff --git a/packages/bun-uws/src/HttpContextData.h b/packages/bun-uws/src/HttpContextData.h index a595927d56..538537c92c 100644 --- a/packages/bun-uws/src/HttpContextData.h +++ b/packages/bun-uws/src/HttpContextData.h @@ -43,11 +43,11 @@ struct alignas(16) HttpContextData { template friend struct TemplatedApp; private: std::vector *, int)>> filterHandlers; - using OnSocketClosedCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket); using OnSocketDataCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket, const char *data, int length, bool last); using OnSocketDrainCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket); + using OnSocketUpgradedCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket); using OnClientErrorCallback = MoveOnlyFunction; - + using OnSocketClosedCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket); MoveOnlyFunction missingServerNameHandler; @@ -66,6 +66,7 @@ private: OnSocketClosedCallback onSocketClosed = nullptr; OnSocketDrainCallback onSocketDrain = nullptr; OnSocketDataCallback onSocketData = nullptr; + OnSocketUpgradedCallback onSocketUpgraded = nullptr; OnClientErrorCallback onClientError = nullptr; uint64_t maxHeaderSize = 0; // 0 means no limit @@ -78,6 +79,7 @@ private: } public: + HttpFlags flags; }; diff --git a/packages/bun-uws/src/HttpResponse.h b/packages/bun-uws/src/HttpResponse.h index 209e0e79df..974a4a95f6 100644 --- a/packages/bun-uws/src/HttpResponse.h +++ b/packages/bun-uws/src/HttpResponse.h @@ -316,14 +316,20 @@ public: HttpContext *httpContext = (HttpContext *) us_socket_context(SSL, (struct us_socket_t *) this); /* Move any backpressure out of HttpResponse */ - BackPressure backpressure(std::move(((AsyncSocketData *) getHttpResponseData())->buffer)); - + auto* responseData = getHttpResponseData(); + BackPressure backpressure(std::move(((AsyncSocketData *) responseData)->buffer)); + + auto* socketData = responseData->socketData; + HttpContextData *httpContextData = httpContext->getSocketContextData(); + /* Destroy HttpResponseData */ - getHttpResponseData()->~HttpResponseData(); + responseData->~HttpResponseData(); /* Before we adopt and potentially change socket, check if we are corked */ bool wasCorked = Super::isCorked(); + + /* Adopting a socket invalidates it, do not rely on it directly to carry any data */ us_socket_t *usSocket = us_socket_context_adopt_socket(SSL, (us_socket_context_t *) webSocketContext, (us_socket_t *) this, sizeof(WebSocketData) + sizeof(UserData)); WebSocket *webSocket = (WebSocket *) usSocket; @@ -334,10 +340,12 @@ public: } /* Initialize websocket with any moved backpressure intact */ - webSocket->init(perMessageDeflate, compressOptions, std::move(backpressure)); + webSocket->init(perMessageDeflate, compressOptions, std::move(backpressure), socketData, httpContextData->onSocketClosed); + if (httpContextData->onSocketUpgraded) { + httpContextData->onSocketUpgraded(socketData, SSL, usSocket); + } /* We should only mark this if inside the parser; if upgrading "async" we cannot set this */ - HttpContextData *httpContextData = httpContext->getSocketContextData(); if (httpContextData->flags.isParsingHttp) { /* We need to tell the Http parser that we changed socket */ httpContextData->upgradedWebSocket = webSocket; @@ -351,7 +359,6 @@ public: /* Move construct the UserData right before calling open handler */ new (webSocket->getUserData()) UserData(std::forward(userData)); - /* Emit open event and start the timeout */ if (webSocketContextData->openHandler) { diff --git a/packages/bun-uws/src/WebSocket.h b/packages/bun-uws/src/WebSocket.h index 6b5efc81f7..5871cacb61 100644 --- a/packages/bun-uws/src/WebSocket.h +++ b/packages/bun-uws/src/WebSocket.h @@ -34,8 +34,8 @@ struct WebSocket : AsyncSocket { private: typedef AsyncSocket Super; - void *init(bool perMessageDeflate, CompressOptions compressOptions, BackPressure &&backpressure) { - new (us_socket_ext(SSL, (us_socket_t *) this)) WebSocketData(perMessageDeflate, compressOptions, std::move(backpressure)); + void *init(bool perMessageDeflate, CompressOptions compressOptions, BackPressure &&backpressure, void *socketData, WebSocketData::OnSocketClosedCallback onSocketClosed) { + new (us_socket_ext(SSL, (us_socket_t *) this)) WebSocketData(perMessageDeflate, compressOptions, std::move(backpressure), socketData, onSocketClosed); return this; } public: diff --git a/packages/bun-uws/src/WebSocketContext.h b/packages/bun-uws/src/WebSocketContext.h index 16d8092fb0..1c31050010 100644 --- a/packages/bun-uws/src/WebSocketContext.h +++ b/packages/bun-uws/src/WebSocketContext.h @@ -256,6 +256,9 @@ private: /* For whatever reason, if we already have emitted close event, do not emit it again */ WebSocketData *webSocketData = (WebSocketData *) (us_socket_ext(SSL, s)); + if (webSocketData->socketData && webSocketData->onSocketClosed) { + webSocketData->onSocketClosed(webSocketData->socketData, SSL, (us_socket_t *) s); + } if (!webSocketData->isShuttingDown) { /* Emit close event */ auto *webSocketContextData = (WebSocketContextData *) us_socket_context_ext(SSL, us_socket_context(SSL, (us_socket_t *) s)); diff --git a/packages/bun-uws/src/WebSocketContextData.h b/packages/bun-uws/src/WebSocketContextData.h index c016be49c4..b675f65dc9 100644 --- a/packages/bun-uws/src/WebSocketContextData.h +++ b/packages/bun-uws/src/WebSocketContextData.h @@ -52,7 +52,6 @@ struct WebSocketContextData { private: public: - /* This one points to the App's shared topicTree */ TopicTree *topicTree; diff --git a/packages/bun-uws/src/WebSocketData.h b/packages/bun-uws/src/WebSocketData.h index 21e96a72d9..f9139341d1 100644 --- a/packages/bun-uws/src/WebSocketData.h +++ b/packages/bun-uws/src/WebSocketData.h @@ -38,6 +38,7 @@ private: unsigned int controlTipLength = 0; bool isShuttingDown = 0; bool hasTimedOut = false; + enum CompressionStatus : char { DISABLED, ENABLED, @@ -52,7 +53,12 @@ private: /* We could be a subscriber */ Subscriber *subscriber = nullptr; public: - WebSocketData(bool perMessageDeflate, CompressOptions compressOptions, BackPressure &&backpressure) : AsyncSocketData(std::move(backpressure)), WebSocketState() { + using OnSocketClosedCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket); + void *socketData = nullptr; + /* node http compatibility callbacks */ + OnSocketClosedCallback onSocketClosed = nullptr; + + WebSocketData(bool perMessageDeflate, CompressOptions compressOptions, BackPressure &&backpressure, void *socketData, OnSocketClosedCallback onSocketClosed) : AsyncSocketData(std::move(backpressure)), WebSocketState() { compressionStatus = perMessageDeflate ? ENABLED : DISABLED; /* Initialize the dedicated sliding window(s) */ @@ -64,6 +70,8 @@ public: inflationStream = new InflationStream(compressOptions); } } + this->socketData = socketData; + this->onSocketClosed = onSocketClosed; } ~WebSocketData() { diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index daf35b9078..3b939e5929 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -139,6 +139,7 @@ public: us_socket_t* socket = nullptr; unsigned is_ssl : 1 = 0; unsigned ended : 1 = 0; + unsigned upgraded : 1 = 0; JSC::Strong strongThis = {}; static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) @@ -160,10 +161,15 @@ public: } template - static void clearSocketData(us_socket_t* socket) + static void clearSocketData(bool upgraded, us_socket_t* socket) { - auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); - httpResponseData->socketData = nullptr; + if (upgraded) { + auto* webSocket = (uWS::WebSocketData*)us_socket_ext(SSL, socket); + webSocket->socketData = nullptr; + } else { + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + httpResponseData->socketData = nullptr; + } } void close() @@ -195,9 +201,9 @@ public: { if (socket) { if (is_ssl) { - clearSocketData(socket); + clearSocketData(this->upgraded, socket); } else { - clearSocketData(socket); + clearSocketData(this->upgraded, socket); } } us_socket_free_stream_buffer(&streamBuffer); @@ -1143,6 +1149,12 @@ static void assignOnNodeJSCompat(uWS::TemplatedApp* app) ASSERT(rawSocket == socket->socket || socket->socket == nullptr); socket->onData(data, length, last); }); + app->setOnSocketUpgraded([](void* socketData, int is_ssl, struct us_socket_t* rawSocket) -> void { + auto* socket = reinterpret_cast(socketData); + // the socket is adopted and might not be the same as the rawSocket + socket->socket = rawSocket; + socket->upgraded = true; + }); } extern "C" void NodeHTTP_assignOnNodeJSCompat(bool is_ssl, void* uws_app) diff --git a/src/js/node/_http_server.ts b/src/js/node/_http_server.ts index 2981ae75ca..61cc987bc5 100644 --- a/src/js/node/_http_server.ts +++ b/src/js/node/_http_server.ts @@ -901,7 +901,6 @@ const NodeHTTPServerSocket = class Socket extends Duplex { req.destroy(); } } - this.emit("close"); } #onCloseForDestroy(closeCallback) { this.#onClose(); diff --git a/test/js/node/http/node-http-with-ws.test.ts b/test/js/node/http/node-http-with-ws.test.ts new file mode 100644 index 0000000000..f4cc63d242 --- /dev/null +++ b/test/js/node/http/node-http-with-ws.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from "bun:test"; +import { tls as options } from "harness"; +import https from "https"; +import type { AddressInfo } from "node:net"; +import tls from "tls"; +import { WebSocketServer } from "ws"; +test("should not crash when closing sockets after upgrade", async () => { + const { promise, resolve } = Promise.withResolvers(); + let http_sockets: tls.TLSSocket[] = []; + + const server = https.createServer(options, (req, res) => { + http_sockets.push(res.socket as tls.TLSSocket); + res.writeHead(200, { "Content-Type": "text/plain", "Connection": "Keep-Alive" }); + res.end("okay"); + res.detachSocket(res.socket!); + }); + + server.listen(0, "127.0.0.1", () => { + const wsServer = new WebSocketServer({ server }); + wsServer.on("connection", socket => {}); + + const port = (server.address() as AddressInfo).port; + const socket = tls.connect({ port, ca: options.cert }, () => { + // normal request keep the socket alive + socket.write(`GET / HTTP/1.1\r\nHost: localhost:${port}\r\nConnection: Keep-Alive\r\nContent-Length: 0\r\n\r\n`); + socket.write(`GET / HTTP/1.1\r\nHost: localhost:${port}\r\nConnection: Keep-Alive\r\nContent-Length: 0\r\n\r\n`); + socket.write(`GET / HTTP/1.1\r\nHost: localhost:${port}\r\nConnection: Keep-Alive\r\nContent-Length: 0\r\n\r\n`); + // upgrade to websocket + socket.write( + `GET / HTTP/1.1\r\nHost: localhost:${port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n`, + ); + }); + socket.on("data", data => { + const isWebSocket = data?.toString().includes("Upgrade: websocket"); + if (isWebSocket) { + socket.destroy(); + setTimeout(() => { + http_sockets.forEach(http_socket => { + http_socket?.destroy(); + }); + server.closeAllConnections(); + resolve(); + }, 10); + } + }); + }); + + await promise; + expect().pass(); +}); From 6ab3d931c9e5dfb99480f266cb1bb4cbb0fcf16c Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 2 Oct 2025 15:50:58 -0700 Subject: [PATCH 002/191] opsie --- test/js/valkey/valkey.connecting.fixture.ts | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 test/js/valkey/valkey.connecting.fixture.ts diff --git a/test/js/valkey/valkey.connecting.fixture.ts b/test/js/valkey/valkey.connecting.fixture.ts new file mode 100644 index 0000000000..9a167758d6 --- /dev/null +++ b/test/js/valkey/valkey.connecting.fixture.ts @@ -0,0 +1,28 @@ +import { RedisClient } from "bun"; + +function getOptions() { + if (process.env.BUN_VALKEY_TLS) { + const paths = JSON.parse(process.env.BUN_VALKEY_TLS); + return { + tls: { + key: Bun.file(paths.key), + cert: Bun.file(paths.cert), + ca: Bun.file(paths.ca), + }, + }; + } + return {}; +} + +{ + const client = new RedisClient(process.env.BUN_VALKEY_URL, getOptions()); + client + .connect() + .then(redis => { + console.log("connected"); + client.close(); + }) + .catch(err => { + console.error(err); + }); +} From 4a86d070cfc31f476d9f989bcd50215e06066834 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 2 Oct 2025 15:53:19 -0700 Subject: [PATCH 003/191] revert 6ab3d93 --- test/js/valkey/valkey.connecting.fixture.ts | 28 --------------------- 1 file changed, 28 deletions(-) delete mode 100644 test/js/valkey/valkey.connecting.fixture.ts diff --git a/test/js/valkey/valkey.connecting.fixture.ts b/test/js/valkey/valkey.connecting.fixture.ts deleted file mode 100644 index 9a167758d6..0000000000 --- a/test/js/valkey/valkey.connecting.fixture.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { RedisClient } from "bun"; - -function getOptions() { - if (process.env.BUN_VALKEY_TLS) { - const paths = JSON.parse(process.env.BUN_VALKEY_TLS); - return { - tls: { - key: Bun.file(paths.key), - cert: Bun.file(paths.cert), - ca: Bun.file(paths.ca), - }, - }; - } - return {}; -} - -{ - const client = new RedisClient(process.env.BUN_VALKEY_URL, getOptions()); - client - .connect() - .then(redis => { - console.log("connected"); - client.close(); - }) - .catch(err => { - console.error(err); - }); -} From 86924f36e8bb4d092850e2e6ffa6d3ac1cc9773d Mon Sep 17 00:00:00 2001 From: robobun Date: Thu, 2 Oct 2025 18:43:10 -0700 Subject: [PATCH 004/191] Add 'bun why' to help menu (#23197) Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alistair Smith --- src/bun.js/bindings/napi.cpp | 8 ++++---- src/cli.zig | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 75c49b966a..b06b4a2b6f 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -640,11 +640,11 @@ extern "C" napi_status napi_is_typedarray(napi_env env, napi_value value, bool* // it doesn't copy the string // but it's only safe to use if we are not setting a property // because we can't guarantee the lifetime of it -#define PROPERTY_NAME_FROM_UTF8(identifierName) \ - size_t utf8Len = strlen(utf8Name); \ +#define PROPERTY_NAME_FROM_UTF8(identifierName) \ + size_t utf8Len = strlen(utf8Name); \ WTF::String&& nameString = WTF::charactersAreAllASCII(std::span { reinterpret_cast(utf8Name), utf8Len }) \ - ? WTF::String(WTF::StringImpl::createWithoutCopying({ utf8Name, utf8Len })) \ - : WTF::String::fromUTF8(utf8Name); \ + ? WTF::String(WTF::StringImpl::createWithoutCopying({ utf8Name, utf8Len })) \ + : WTF::String::fromUTF8(utf8Name); \ const JSC::PropertyName identifierName = JSC::Identifier::fromString(vm, nameString); extern "C" napi_status napi_has_named_property(napi_env env, napi_value object, diff --git a/src/cli.zig b/src/cli.zig index 0fada12099..6cf201e69f 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -181,6 +181,7 @@ pub const HelpCommand = struct { \\ patch \ Prepare a package for patching \\ pm \ Additional package management utilities \\ info {s:<16} Display package metadata from the registry + \\ why {s:<16} Explain why a package is installed \\ \\ build ./a.ts ./b.jsx Bundle TypeScript & JavaScript into a single file \\ @@ -214,6 +215,7 @@ pub const HelpCommand = struct { packages_to_remove_filler[package_remove_i], packages_to_add_filler[(package_add_i + 1) % packages_to_add_filler.len], packages_to_add_filler[(package_add_i + 2) % packages_to_add_filler.len], + packages_to_add_filler[(package_add_i + 3) % packages_to_add_filler.len], packages_to_create_filler[package_create_i], }; From 84f94ca6ddf76283993aab1e748918cb7770169d Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 2 Oct 2025 18:50:05 -0700 Subject: [PATCH 005/191] fix(Bun.RedisClient) keep it alive when connecting (#23195) ### What does this PR do? Fixes https://github.com/oven-sh/bun/issues/23178 Fixes https://github.com/oven-sh/bun/issues/23187 Fixes https://github.com/oven-sh/bun/issues/23198 ### How did you verify your code works? Test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/valkey/js_valkey.zig | 4 +-- test/js/valkey/valkey.connecting.fixture.ts | 28 +++++++++++++++++++++ test/js/valkey/valkey.test.ts | 11 +++++++- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 test/js/valkey/valkey.connecting.fixture.ts diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index fc5a415db8..69c8d88b00 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1194,8 +1194,8 @@ pub const JSValkeyClient = struct { const has_activity = has_pending_commands or !subs_deletable or this.client.flags.is_reconnecting; // There's a couple cases to handle here: - if (has_activity) { - // If we currently have pending activity, we need to keep the event + if (has_activity or this.client.status == .connecting) { + // If we currently have pending activity or we are connecting, we need to keep the event // loop alive. this.poll_ref.ref(this.client.vm); } else { diff --git a/test/js/valkey/valkey.connecting.fixture.ts b/test/js/valkey/valkey.connecting.fixture.ts new file mode 100644 index 0000000000..9a167758d6 --- /dev/null +++ b/test/js/valkey/valkey.connecting.fixture.ts @@ -0,0 +1,28 @@ +import { RedisClient } from "bun"; + +function getOptions() { + if (process.env.BUN_VALKEY_TLS) { + const paths = JSON.parse(process.env.BUN_VALKEY_TLS); + return { + tls: { + key: Bun.file(paths.key), + cert: Bun.file(paths.cert), + ca: Bun.file(paths.ca), + }, + }; + } + return {}; +} + +{ + const client = new RedisClient(process.env.BUN_VALKEY_URL, getOptions()); + client + .connect() + .then(redis => { + console.log("connected"); + client.close(); + }) + .catch(err => { + console.error(err); + }); +} diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index d69ac7d6d6..2c50270127 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -1,6 +1,7 @@ import { randomUUIDv7, RedisClient, spawn } from "bun"; import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; -import { bunExe } from "harness"; +import { bunExe, bunRun } from "harness"; +import { join } from "node:path"; import { ctx as _ctx, awaitableCounter, @@ -36,6 +37,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); describe("Basic Operations", () => { + test("should keep process alive when connecting", async () => { + const result = bunRun(join(import.meta.dir, "valkey.connecting.fixture.ts"), { + "BUN_VALKEY_URL": connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL, + "BUN_VALKEY_TLS": connectionType === ConnectionType.TLS ? JSON.stringify(TLS_REDIS_OPTIONS.tlsPaths) : "", + }); + expect(result.stdout).toContain(`connected`); + }); + test("should set and get strings", async () => { const redis = ctx.redis; const testKey = "greeting"; From 55f8e8add37f0c826bf5327001c91768e1ac5281 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 2 Oct 2025 19:00:14 -0700 Subject: [PATCH 006/191] fix(Bun.SQL) time should be represented as a string and date as a time (#23193) ### What does this PR do? Time should be represented as HH:MM:SS or HHH:MM:SS string ### How did you verify your code works? Test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/sql/mysql/protocol/DecodeBinaryValue.zig | 24 ++++++++++-- src/sql/mysql/protocol/ResultSet.zig | 8 +++- test/js/sql/sql-mysql.test.ts | 41 ++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/sql/mysql/protocol/DecodeBinaryValue.zig b/src/sql/mysql/protocol/DecodeBinaryValue.zig index e557383ed5..5406d48060 100644 --- a/src/sql/mysql/protocol/DecodeBinaryValue.zig +++ b/src/sql/mysql/protocol/DecodeBinaryValue.zig @@ -92,18 +92,36 @@ pub fn decodeBinaryValue(globalObject: *jsc.JSGlobalObject, field_type: types.Fi }, .MYSQL_TYPE_TIME => { return switch (try reader.byte()) { - 0 => SQLDataCell{ .tag = .null, .value = .{ .null = 0 } }, + 0 => { + const slice = "00:00:00"; + return SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 }; + }, 8, 12 => |l| { var data = try reader.read(l); defer data.deinit(); const time = try Time.fromData(&data); - return SQLDataCell{ .tag = .date, .value = .{ .date = time.toJSTimestamp() } }; + + const total_hours = time.hours + time.days * 24; + // -838:59:59 to 838:59:59 is valid (it only store seconds) + // it should be represented as HH:MM:SS or HHH:MM:SS if total_hours > 99 + var buffer: [32]u8 = undefined; + const sign = if (time.negative) "-" else ""; + const slice = brk: { + if (total_hours > 99) { + break :brk std.fmt.bufPrint(&buffer, "{s}{d:0>3}:{d:0>2}:{d:0>2}", .{ sign, total_hours, time.minutes, time.seconds }) catch return error.InvalidBinaryValue; + } else { + break :brk std.fmt.bufPrint(&buffer, "{s}{d:0>2}:{d:0>2}:{d:0>2}", .{ sign, total_hours, time.minutes, time.seconds }) catch return error.InvalidBinaryValue; + } + }; + return SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 }; }, else => return error.InvalidBinaryValue, }; }, .MYSQL_TYPE_DATE, .MYSQL_TYPE_TIMESTAMP, .MYSQL_TYPE_DATETIME => switch (try reader.byte()) { - 0 => SQLDataCell{ .tag = .null, .value = .{ .null = 0 } }, + 0 => { + return SQLDataCell{ .tag = .date, .value = .{ .date = 0 } }; + }, 11, 7, 4 => |l| { var data = try reader.read(l); defer data.deinit(); diff --git a/src/sql/mysql/protocol/ResultSet.zig b/src/sql/mysql/protocol/ResultSet.zig index 1d49650869..d13a71ac8f 100644 --- a/src/sql/mysql/protocol/ResultSet.zig +++ b/src/sql/mysql/protocol/ResultSet.zig @@ -113,7 +113,13 @@ pub const Row = struct { cell.* = SQLDataCell{ .tag = .json, .value = .{ .json = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 }; }, - .MYSQL_TYPE_DATE, .MYSQL_TYPE_TIME, .MYSQL_TYPE_DATETIME, .MYSQL_TYPE_TIMESTAMP => { + .MYSQL_TYPE_TIME => { + // lets handle TIME special case as string + // -838:59:50 to 838:59:59 is valid + const slice = value.slice(); + cell.* = SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 }; + }, + .MYSQL_TYPE_DATE, .MYSQL_TYPE_DATETIME, .MYSQL_TYPE_TIMESTAMP => { var str = bun.String.init(value.slice()); defer str.deref(); const date = brk: { diff --git a/test/js/sql/sql-mysql.test.ts b/test/js/sql/sql-mysql.test.ts index ca60698759..fde64b5115 100644 --- a/test/js/sql/sql-mysql.test.ts +++ b/test/js/sql/sql-mysql.test.ts @@ -413,6 +413,47 @@ if (isDockerEnabled()) { expect(result.getTime()).toBe(-251); } }); + test("time", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a TIME)`; + const times = [ + { a: "00:00:00" }, + { a: "01:01:01" }, + { a: "10:10:10" }, + { a: "12:12:59" }, + { a: "-838:59:59" }, + { a: "838:59:59" }, + { a: null }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(times)}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result).toEqual(times); + const result2 = await sql`SELECT * FROM ${sql(random_name)}`.simple(); + expect(result2).toEqual(times); + }); + + test("date", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a DATE)`; + const dates = [{ a: "2024-01-01" }, { a: "2024-01-02" }, { a: "2024-01-03" }, { a: null }]; + await sql`INSERT INTO ${sql(random_name)} ${sql(dates)}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result).toEqual([ + { a: new Date("2024-01-01") }, + { a: new Date("2024-01-02") }, + { a: new Date("2024-01-03") }, + { a: null }, + ]); + const result2 = await sql`SELECT * FROM ${sql(random_name)}`.simple(); + expect(result2).toEqual([ + { a: new Date("2024-01-01") }, + { a: new Date("2024-01-02") }, + { a: new Date("2024-01-03") }, + { a: null }, + ]); + }); test("JSON", async () => { await using sql = new SQL({ ...getOptions(), max: 1 }); From d99d622472e68b55ae9ef9fe06685a21310b61ba Mon Sep 17 00:00:00 2001 From: pfg Date: Thu, 2 Oct 2025 19:12:45 -0700 Subject: [PATCH 007/191] Rereun-each fix (#23168) ### What does this PR do? Fix --rerun-each. Fixes #21409 ### How did you verify your code works? Test case --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/cli/test_command.zig | 17 ++-- test/cli/test/rerun-each.test.ts | 132 +++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 test/cli/test/rerun-each.test.ts diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 9626a14b65..27152e6cdc 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1828,6 +1828,13 @@ pub const TestCommand = struct { vm.onUnhandledRejection = jest.on_unhandled_rejection.onUnhandledRejection; while (repeat_index < repeat_count) : (repeat_index += 1) { + // Clear the module cache before re-running (except for the first run) + if (repeat_index > 0) { + try vm.clearEntryPoint(); + var entry = jsc.ZigString.init(file_path); + try vm.global.deleteModuleRegistryEntry(&entry); + } + var bun_test_root = &jest.Jest.runner.?.bun_test_root; // Determine if this file should run tests concurrently based on glob pattern const should_run_concurrent = reporter.jest.shouldFileRunConcurrently(file_id); @@ -1838,7 +1845,10 @@ pub const TestCommand = struct { bun.jsc.Jest.bun_test.debug.group.log("loadEntryPointForTestRunner(\"{}\")", .{std.zig.fmtEscapes(file_path)}); var promise = try vm.loadEntryPointForTestRunner(file_path); - reporter.summary().files += 1; + // Only count the file once, not once per repeat + if (repeat_index == 0) { + reporter.summary().files += 1; + } switch (promise.status(vm.global.vm())) { .rejected => { @@ -1905,11 +1915,6 @@ pub const TestCommand = struct { } vm.global.handleRejectedPromises(); - if (repeat_index > 0) { - try vm.clearEntryPoint(); - var entry = jsc.ZigString.init(file_path); - try vm.global.deleteModuleRegistryEntry(&entry); - } if (Output.is_github_action) { Output.prettyErrorln("\n::endgroup::\n", .{}); diff --git a/test/cli/test/rerun-each.test.ts b/test/cli/test/rerun-each.test.ts new file mode 100644 index 0000000000..a8166b3285 --- /dev/null +++ b/test/cli/test/rerun-each.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("--rerun-each should run tests exactly N times", async () => { + using dir = tempDir("test-rerun-each", { + "counter.test.ts": ` + import { test, expect } from "bun:test"; + + // Use a global counter that persists across module reloads + if (!globalThis.testRunCounter) { + globalThis.testRunCounter = 0; + } + + test("should increment counter", () => { + globalThis.testRunCounter++; + console.log(\`Run #\${globalThis.testRunCounter}\`); + expect(true).toBe(true); + }); + `, + }); + + // Test with --rerun-each=3 + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "counter.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + + // Should see "Run #1", "Run #2", "Run #3" in the output + expect(stdout).toContain("Run #1"); + expect(stdout).toContain("Run #2"); + expect(stdout).toContain("Run #3"); + + // Should NOT see "Run #4" + expect(stdout).not.toContain("Run #4"); + + // Should run exactly 3 tests - check stderr for test summary + const combined = stdout + stderr; + expect(combined).toMatch(/3 pass/); + + // Test with --rerun-each=1 (should run once) + await using proc2 = Bun.spawn({ + cmd: [bunExe(), "test", "counter.test.ts", "--rerun-each=1"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]); + + expect(exitCode2).toBe(0); + const combined2 = stdout2 + stderr2; + expect(combined2).toMatch(/1 pass/); +}); + +test("--rerun-each should report correct file count", async () => { + using dir = tempDir("test-rerun-each-file-count", { + "test1.test.ts": ` + import { test, expect } from "bun:test"; + test("test in file 1", () => { + expect(true).toBe(true); + }); + `, + }); + + // Run with --rerun-each=3 + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "test1.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + + // Should report "Ran 3 tests across 1 file" not "across 3 files" + const combined = stdout + stderr; + expect(combined).toContain("Ran 3 tests across 1 file"); + expect(combined).not.toContain("across 3 files"); +}); + +test("--rerun-each should handle test failures correctly", async () => { + using dir = tempDir("test-rerun-each-fail", { + "fail.test.ts": ` + import { test, expect } from "bun:test"; + + if (!globalThis.failCounter) { + globalThis.failCounter = 0; + } + + test("fails on second run", () => { + globalThis.failCounter++; + console.log(\`Attempt #\${globalThis.failCounter}\`); + // Fail on the second run + expect(globalThis.failCounter).not.toBe(2); + }); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "fail.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should have non-zero exit code due to failure + expect(exitCode).not.toBe(0); + + // Should see all three attempts + expect(stdout).toContain("Attempt #1"); + expect(stdout).toContain("Attempt #2"); + expect(stdout).toContain("Attempt #3"); + + // Should report 2 passes and 1 failure - check both stdout and stderr + const combined = stdout + stderr; + expect(combined).toMatch(/2 pass/); + expect(combined).toMatch(/1 fail/); +}); From 79e0aa9bcf5b64d7b53920459504a0b6db573a22 Mon Sep 17 00:00:00 2001 From: pfg Date: Thu, 2 Oct 2025 20:12:59 -0700 Subject: [PATCH 008/191] bun:test performance regression fix (#23199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What does this PR do? Fixes #23120 bun:test changes introduced an added 16-100ms sleep between test files. For a test suite with many fast-running test files, this caused significant impact. Elysia's test suite was running 2x slower (1.8s → 3.9s). image ### How did you verify your code works? Running elysia test suite & minimized reproduction case
Minimzed reproduction case ```ts // full2.test.ts import { it } from 'bun:test' it("timeout", () => { setTimeout(() => {}, 295000); }, 0); // bench.ts import {$} from "bun"; await $`rm -rf tests`; await $`mkdir -p tests`; for (let i = 0; i < 128; i += 1) { await Bun.write(`tests/${i}.test.ts`, ` for (let i = 0; i < 1000; i ++) { it("test${i}", () => {}, 0); } `); } Bun.spawnSync({ cmd: ["hyperfine", ...["bun-1.2.22", "bun-1.2.23+wakeup", "bun-1.2.23"].map(v => `${v} test ./full2.test.ts tests`)], stdio: ["inherit", "inherit", "inherit"], }); ```
--- src/bun.js/test/bun_test.zig | 10 +++++++++- src/cli/test_command.zig | 18 ++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index 3cb212ec7d..a66516086a 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -187,6 +187,7 @@ pub const BunTest = struct { default_concurrent: bool, first_last: BunTestRoot.FirstLast, extra_execution_entries: std.ArrayList(*ExecutionEntry), + wants_wakeup: bool = false, phase: enum { collection, @@ -465,7 +466,14 @@ pub const BunTest = struct { const done_callback_test = bun.new(RunTestsTask, .{ .weak = weak.clone(), .globalThis = globalThis, .phase = phase }); errdefer bun.destroy(done_callback_test); const task = jsc.ManagedTask.New(RunTestsTask, RunTestsTask.call).init(done_callback_test); - jsc.VirtualMachine.get().enqueueTask(task); + const vm = globalThis.bunVM(); + var strong = weak.clone().upgrade() orelse { + if (bun.Environment.ci_assert) bun.assert(false); // shouldn't be calling runNextTick after moving on to the next file + return; // but just in case + }; + defer strong.deinit(); + strong.get().wants_wakeup = true; // we need to wake up the event loop so autoTick() doesn't wait for 16-100ms because we just enqueued a task + vm.enqueueTask(task); } pub const RunTestsTask = struct { weak: BunTestPtr.Weak, diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 27152e6cdc..0749181606 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1844,6 +1844,9 @@ pub const TestCommand = struct { reporter.jest.current_file.set(file_title, file_prefix, repeat_count, repeat_index); bun.jsc.Jest.bun_test.debug.group.log("loadEntryPointForTestRunner(\"{}\")", .{std.zig.fmtEscapes(file_path)}); + + // need to wake up so autoTick() doesn't wait for 16-100ms after loading the entrypoint + vm.wakeup(); var promise = try vm.loadEntryPointForTestRunner(file_path); // Only count the file once, not once per repeat if (repeat_index == 0) { @@ -1869,16 +1872,7 @@ pub const TestCommand = struct { else => {}, } - { - vm.drainMicrotasks(); - var count = vm.unhandled_error_counter; - vm.global.handleRejectedPromises(); - while (vm.unhandled_error_counter > count) { - count = vm.unhandled_error_counter; - vm.drainMicrotasks(); - vm.global.handleRejectedPromises(); - } - } + vm.eventLoop().tick(); blk: { @@ -1901,6 +1895,10 @@ pub const TestCommand = struct { var prev_unhandled_count = vm.unhandled_error_counter; while (buntest.phase != .done) { + if (buntest.wants_wakeup) { + buntest.wants_wakeup = false; + vm.wakeup(); + } vm.eventLoop().autoTick(); if (buntest.phase == .done) break; vm.eventLoop().tick(); From 693e7995bb3093c42195d52c1773bf686b33c209 Mon Sep 17 00:00:00 2001 From: robobun Date: Thu, 2 Oct 2025 21:42:47 -0700 Subject: [PATCH 009/191] Use cached structure in JSBunRequest::clone (#23202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replace `createJSBunRequestStructure()` call with direct access to the cached structure in `JSBunRequest::clone()` method for better performance. ## Changes - Updated `JSBunRequest::clone()` to use `m_JSBunRequestStructure.getInitializedOnMainThread()` instead of calling `createJSBunRequestStructure()` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/bindings/JSBunRequest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/bindings/JSBunRequest.cpp b/src/bun.js/bindings/JSBunRequest.cpp index 9bcb685006..1dcdf1e47a 100644 --- a/src/bun.js/bindings/JSBunRequest.cpp +++ b/src/bun.js/bindings/JSBunRequest.cpp @@ -102,7 +102,7 @@ JSBunRequest* JSBunRequest::clone(JSC::VM& vm, JSGlobalObject* globalObject) { auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* structure = createJSBunRequestStructure(vm, defaultGlobalObject(globalObject)); + auto* structure = defaultGlobalObject(globalObject)->m_JSBunRequestStructure.getInitializedOnMainThread(globalObject); auto* raw = Request__clone(this->wrapped(), globalObject); EXCEPTION_ASSERT(!!raw == !throwScope.exception()); RETURN_IF_EXCEPTION(throwScope, nullptr); From 666180d7fcfd4acdd4bebcc0fd3d9ae3df5465a3 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 3 Oct 2025 02:38:55 -0700 Subject: [PATCH 010/191] fix(install): isolated install with `file` dependency resolving to root package (#23204) ### What does this PR do? Fixes `file:.` in root package.json or `file:../..` in workspace package.json (if '../..' points to the root of the project) ### How did you verify your code works? Added a test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/install/PackageManager.zig | 8 ++ .../PackageManager/CommandLineArguments.zig | 5 +- src/install/isolated_install.zig | 48 +++++++--- src/install/isolated_install/FileCopier.zig | 36 +++++-- src/install/isolated_install/Hardlinker.zig | 39 +++++--- src/install/isolated_install/Installer.zig | 95 +++++++++++++------ src/install/isolated_install/Store.zig | 9 ++ src/install/lockfile/bun.lock.zig | 12 ++- src/walker_skippable.zig | 2 +- test/cli/install/isolated-install.test.ts | 42 ++++++++ 10 files changed, 228 insertions(+), 68 deletions(-) diff --git a/src/install/PackageManager.zig b/src/install/PackageManager.zig index ea1243d845..28f8eaf6ae 100644 --- a/src/install/PackageManager.zig +++ b/src/install/PackageManager.zig @@ -875,6 +875,14 @@ pub fn init( }; manager.event_loop.loop().internal_loop_data.setParentEventLoop(bun.jsc.EventLoopHandle.init(&manager.event_loop)); manager.lockfile = try ctx.allocator.create(Lockfile); + + { + // make sure folder packages can find the root package without creating a new one + var normalized: bun.AbsPath(.{ .sep = .posix }) = .from(root_package_json_path); + defer normalized.deinit(); + try manager.folders.put(manager.allocator, FolderResolution.hash(normalized.slice()), .{ .package_id = 0 }); + } + jsc.MiniEventLoop.global = &manager.event_loop.mini; if (!manager.options.enable.cache) { manager.options.enable.manifest_cache = false; diff --git a/src/install/PackageManager/CommandLineArguments.zig b/src/install/PackageManager/CommandLineArguments.zig index 6ce9dbde57..d28d22994c 100644 --- a/src/install/PackageManager/CommandLineArguments.zig +++ b/src/install/PackageManager/CommandLineArguments.zig @@ -808,7 +808,10 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com cli.lockfile_only = args.flag("--lockfile-only"); if (args.option("--linker")) |linker| { - cli.node_linker = .fromStr(linker); + cli.node_linker = Options.NodeLinker.fromStr(linker) orelse { + Output.errGeneric("Expected --linker to be one of 'isolated' or 'hoisted'", .{}); + Global.exit(1); + }; } if (args.option("--cache-dir")) |cache_dir| { diff --git a/src/install/isolated_install.zig b/src/install/isolated_install.zig index 9020d50503..8576956ef2 100644 --- a/src/install/isolated_install.zig +++ b/src/install/isolated_install.zig @@ -59,7 +59,7 @@ pub fn installIsolatedPackages( // First pass: create full dependency tree with resolved peers next_node: while (node_queue.readItem()) |entry| { - { + check_cycle: { // check for cycles const nodes_slice = nodes.slice(); const node_pkg_ids = nodes_slice.items(.pkg_id); @@ -74,11 +74,17 @@ pub fn installIsolatedPackages( // 'node_modules/.bun/parent@version/node_modules'. const dep_id = node_dep_ids[curr_id.get()]; - if (dep_id == invalid_dependency_id or entry.dep_id == invalid_dependency_id) { + if (dep_id == invalid_dependency_id and entry.dep_id == invalid_dependency_id) { node_nodes[entry.parent_id.get()].appendAssumeCapacity(curr_id); continue :next_node; } + if (dep_id == invalid_dependency_id or entry.dep_id == invalid_dependency_id) { + // one is the root package, one is a dependency on the root package (it has a valid dep_id) + // create a new node for it. + break :check_cycle; + } + // ensure the dependency name is the same before skipping the cycle. if they aren't // we lose dependency name information for the symlinks if (dependencies[dep_id].name_hash == dependencies[entry.dep_id].name_hash) { @@ -93,7 +99,11 @@ pub fn installIsolatedPackages( const node_id: Store.Node.Id = .from(@intCast(nodes.len)); const pkg_deps = pkg_dependency_slices[entry.pkg_id]; - var skip_dependencies_of_workspace_node = false; + // for skipping dependnecies of workspace packages and the root package. the dependencies + // of these packages should only be pulled in once, but we might need to create more than + // one entry if there's multiple dependencies on the workspace or root package. + var skip_dependencies = entry.pkg_id == 0 and entry.dep_id != invalid_dependency_id; + if (entry.dep_id != invalid_dependency_id) { const entry_dep = dependencies[entry.dep_id]; if (pkg_deps.len == 0 or entry_dep.version.tag == .workspace) dont_dedupe: { @@ -106,6 +116,9 @@ pub fn installIsolatedPackages( const node_dep_ids = nodes_slice.items(.dep_id); const dedupe_dep_id = node_dep_ids[dedupe_node_id.get()]; + if (dedupe_dep_id == invalid_dependency_id) { + break :dont_dedupe; + } const dedupe_dep = dependencies[dedupe_dep_id]; if (dedupe_dep.name_hash != entry_dep.name_hash) { @@ -115,7 +128,7 @@ pub fn installIsolatedPackages( if (dedupe_dep.version.tag == .workspace and entry_dep.version.tag == .workspace) { if (dedupe_dep.behavior.isWorkspace() != entry_dep.behavior.isWorkspace()) { // only attach the dependencies to one of the workspaces - skip_dependencies_of_workspace_node = true; + skip_dependencies = true; break :dont_dedupe; } } @@ -132,8 +145,8 @@ pub fn installIsolatedPackages( .pkg_id = entry.pkg_id, .dep_id = entry.dep_id, .parent_id = entry.parent_id, - .nodes = if (skip_dependencies_of_workspace_node) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), - .dependencies = if (skip_dependencies_of_workspace_node) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), + .nodes = if (skip_dependencies) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), + .dependencies = if (skip_dependencies) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), }); const nodes_slice = nodes.slice(); @@ -146,7 +159,7 @@ pub fn installIsolatedPackages( node_nodes[parent_id].appendAssumeCapacity(node_id); } - if (skip_dependencies_of_workspace_node) { + if (skip_dependencies) { continue; } @@ -411,6 +424,11 @@ pub fn installIsolatedPackages( const curr_dep_id = node_dep_ids[entry.node_id.get()]; for (dedupe_entry.value_ptr.items) |info| { + if (info.dep_id == invalid_dependency_id or curr_dep_id == invalid_dependency_id) { + if (info.dep_id != curr_dep_id) { + continue; + } + } if (info.dep_id != invalid_dependency_id and curr_dep_id != invalid_dependency_id) { const curr_dep = dependencies[curr_dep_id]; const existing_dep = dependencies[info.dep_id]; @@ -685,6 +703,7 @@ pub fn installIsolatedPackages( const node_id = entry_node_ids[entry_id.get()]; const pkg_id = node_pkg_ids[node_id.get()]; + const dep_id = node_dep_ids[node_id.get()]; const pkg_name = pkg_names[pkg_id]; const pkg_name_hash = pkg_name_hashes[pkg_id]; @@ -700,15 +719,15 @@ pub fn installIsolatedPackages( continue; }, .root => { - // .monotonic is okay in this block because the task isn't running on another - // thread. - if (entry_id == .root) { + if (dep_id == invalid_dependency_id) { + // .monotonic is okay in this block because the task isn't running on another + // thread. entry_steps[entry_id.get()].store(.symlink_dependencies, .monotonic); - installer.startTask(entry_id); - continue; + } else { + // dep_id is valid meaning this was a dependency that resolved to the root + // package. it gets an entry in the store. } - entry_steps[entry_id.get()].store(.done, .monotonic); - installer.onTaskComplete(entry_id, .skipped); + installer.startTask(entry_id); continue; }, .workspace => { @@ -830,7 +849,6 @@ pub fn installIsolatedPackages( .isolated_package_install_context = entry_id, }; - const dep_id = node_dep_ids[node_id.get()]; const dep = lockfile.buffers.dependencies.items[dep_id]; switch (pkg_res_tag) { diff --git a/src/install/isolated_install/FileCopier.zig b/src/install/isolated_install/FileCopier.zig index 2200f6ee43..07d6dd313a 100644 --- a/src/install/isolated_install/FileCopier.zig +++ b/src/install/isolated_install/FileCopier.zig @@ -3,8 +3,32 @@ pub const FileCopier = struct { src_path: bun.AbsPath(.{ .sep = .auto, .unit = .os }), dest_subpath: bun.RelPath(.{ .sep = .auto, .unit = .os }), + walker: Walker, - pub fn copy(this: *FileCopier, skip_dirnames: []const bun.OSPathSlice) OOM!sys.Maybe(void) { + pub fn init( + src_dir: FD, + src_path: bun.AbsPath(.{ .sep = .auto, .unit = .os }), + dest_subpath: bun.RelPath(.{ .sep = .auto, .unit = .os }), + skip_dirnames: []const bun.OSPathSlice, + ) OOM!FileCopier { + return .{ + .src_dir = src_dir, + .src_path = src_path, + .dest_subpath = dest_subpath, + .walker = try .walk( + src_dir, + bun.default_allocator, + &.{}, + skip_dirnames, + ), + }; + } + + pub fn deinit(this: *const FileCopier) void { + this.walker.deinit(); + } + + pub fn copy(this: *FileCopier) OOM!sys.Maybe(void) { var dest_dir = bun.MakePath.makeOpenPath(FD.cwd().stdDir(), this.dest_subpath.sliceZ(), .{}) catch |err| { // TODO: remove the need for this and implement openDir makePath makeOpenPath in bun var errno: bun.sys.E = switch (@as(anyerror, err)) { @@ -46,15 +70,7 @@ pub const FileCopier = struct { var copy_file_state: bun.CopyFileState = .{}; - var walker: Walker = try .walk( - this.src_dir, - bun.default_allocator, - &.{}, - skip_dirnames, - ); - defer walker.deinit(); - - while (switch (walker.next()) { + while (switch (this.walker.next()) { .result => |res| res, .err => |err| return .initErr(err), }) |entry| { diff --git a/src/install/isolated_install/Hardlinker.zig b/src/install/isolated_install/Hardlinker.zig index 28a78f6212..c0d4629ae6 100644 --- a/src/install/isolated_install/Hardlinker.zig +++ b/src/install/isolated_install/Hardlinker.zig @@ -3,8 +3,32 @@ const Hardlinker = @This(); src_dir: FD, src: bun.AbsPath(.{ .sep = .auto, .unit = .os }), dest: bun.RelPath(.{ .sep = .auto, .unit = .os }), +walker: Walker, -pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.Maybe(void) { +pub fn init( + folder_dir: FD, + src: bun.AbsPath(.{ .sep = .auto, .unit = .os }), + dest: bun.RelPath(.{ .sep = .auto, .unit = .os }), + skip_dirnames: []const bun.OSPathSlice, +) OOM!Hardlinker { + return .{ + .src_dir = folder_dir, + .src = src, + .dest = dest, + .walker = try .walk( + folder_dir, + bun.default_allocator, + &.{}, + skip_dirnames, + ), + }; +} + +pub fn deinit(this: *Hardlinker) void { + this.walker.deinit(); +} + +pub fn link(this: *Hardlinker) OOM!sys.Maybe(void) { if (bun.install.PackageManager.verbose_install) { bun.Output.prettyErrorln( \\Hardlinking {} to {} @@ -14,16 +38,9 @@ pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.M bun.fmt.fmtOSPath(this.dest.slice(), .{ .path_sep = .auto }), }, ); + bun.Output.flush(); } - var walker: Walker = try .walk( - this.src_dir, - bun.default_allocator, - &.{}, - skip_dirnames, - ); - defer walker.deinit(); - if (comptime Environment.isWindows) { const cwd_buf = bun.w_path_buffer_pool.get(); defer bun.w_path_buffer_pool.put(cwd_buf); @@ -31,7 +48,7 @@ pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.M return .initErr(bun.sys.Error.fromCode(bun.sys.E.ACCES, .link)); }; - while (switch (walker.next()) { + while (switch (this.walker.next()) { .result => |res| res, .err => |err| return .initErr(err), }) |entry| { @@ -125,7 +142,7 @@ pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.M return .success; } - while (switch (walker.next()) { + while (switch (this.walker.next()) { .result => |res| res, .err => |err| return .initErr(err), }) |entry| { diff --git a/src/install/isolated_install/Installer.zig b/src/install/isolated_install/Installer.zig index 258adec2fd..42c1ccabef 100644 --- a/src/install/isolated_install/Installer.zig +++ b/src/install/isolated_install/Installer.zig @@ -421,9 +421,14 @@ pub const Installer = struct { }; }, - .folder => { + .folder, .root => { + const path = switch (pkg_res.tag) { + .folder => pkg_res.value.folder.slice(string_buf), + .root => ".", + else => unreachable, + }; // the folder does not exist in the cache. xdev is per folder dependency - const folder_dir = switch (bun.openDirForIteration(FD.cwd(), pkg_res.value.folder.slice(string_buf))) { + const folder_dir = switch (bun.openDirForIteration(FD.cwd(), path)) { .result => |fd| fd, .err => |err| return .failure(.{ .link_package = err }), }; @@ -440,13 +445,15 @@ pub const Installer = struct { installer.appendStorePath(&dest, this.entry_id); - var hardlinker: Hardlinker = .{ - .src_dir = folder_dir, - .src = src, - .dest = dest, - }; + var hardlinker: Hardlinker = try .init( + folder_dir, + src, + dest, + &.{comptime bun.OSPathLiteral("node_modules")}, + ); + defer hardlinker.deinit(); - switch (try hardlinker.link(&.{comptime bun.OSPathLiteral("node_modules")})) { + switch (try hardlinker.link()) { .result => {}, .err => |err| { if (err.getErrno() == .XDEV) { @@ -501,13 +508,15 @@ pub const Installer = struct { defer dest.deinit(); installer.appendStorePath(&dest, this.entry_id); - var file_copier: FileCopier = .{ - .src_dir = folder_dir, - .src_path = src_path, - .dest_subpath = dest, - }; + var file_copier: FileCopier = try .init( + folder_dir, + src_path, + dest, + &.{comptime bun.OSPathLiteral("node_modules")}, + ); + defer file_copier.deinit(); - switch (try file_copier.copy(&.{})) { + switch (try file_copier.copy()) { .result => {}, .err => |err| { if (PackageManager.verbose_install) { @@ -559,6 +568,18 @@ pub const Installer = struct { continue :backend .hardlink; } + if (installer.manager.options.log_level.isVerbose()) { + bun.Output.prettyErrorln( + \\Cloning {} to {} + , + .{ + bun.fmt.fmtOSPath(pkg_cache_dir_subpath.sliceZ(), .{ .path_sep = .auto }), + bun.fmt.fmtOSPath(dest_subpath.sliceZ(), .{ .path_sep = .auto }), + }, + ); + bun.Output.flush(); + } + switch (sys.clonefileat(cache_dir, pkg_cache_dir_subpath.sliceZ(), FD.cwd(), dest_subpath.sliceZ())) { .result => {}, .err => |clonefile_err1| { @@ -613,13 +634,15 @@ pub const Installer = struct { defer src.deinit(); src.appendJoin(pkg_cache_dir_subpath.slice()); - var hardlinker: Hardlinker = .{ - .src_dir = cached_package_dir.?, - .src = src, - .dest = dest_subpath, - }; + var hardlinker: Hardlinker = try .init( + cached_package_dir.?, + src, + dest_subpath, + &.{}, + ); + defer hardlinker.deinit(); - switch (try hardlinker.link(&.{})) { + switch (try hardlinker.link()) { .result => {}, .err => |err| { if (err.getErrno() == .XDEV) { @@ -678,13 +701,15 @@ pub const Installer = struct { defer src_path.deinit(); src_path.append(pkg_cache_dir_subpath.slice()); - var file_copier: FileCopier = .{ - .src_dir = cached_package_dir.?, - .src_path = src_path, - .dest_subpath = dest_subpath, - }; + var file_copier: FileCopier = try .init( + cached_package_dir.?, + src_path, + dest_subpath, + &.{}, + ); + defer file_copier.deinit(); - switch (try file_copier.copy(&.{})) { + switch (try file_copier.copy()) { .result => {}, .err => |err| { if (PackageManager.verbose_install) { @@ -1231,6 +1256,7 @@ pub const Installer = struct { const nodes = this.store.nodes.slice(); const node_pkg_ids = nodes.items(.pkg_id); + const node_dep_ids = nodes.items(.dep_id); // const node_peers = nodes.items(.peers); const pkgs = this.lockfile.packages.slice(); @@ -1240,10 +1266,25 @@ pub const Installer = struct { const node_id = entry_node_ids[entry_id.get()]; // const peers = node_peers[node_id.get()]; const pkg_id = node_pkg_ids[node_id.get()]; + const dep_id = node_dep_ids[node_id.get()]; const pkg_res = pkg_resolutions[pkg_id]; switch (pkg_res.tag) { - .root => {}, + .root => { + if (dep_id != invalid_dependency_id) { + const pkg_name = pkg_names[pkg_id]; + buf.append("node_modules/" ++ Store.modules_dir_name); + buf.appendFmt("{}", .{ + Store.Entry.fmtStorePath(entry_id, this.store, this.lockfile), + }); + buf.append("node_modules"); + if (pkg_name.isEmpty()) { + buf.append(std.fs.path.basename(bun.fs.FileSystem.instance.top_level_dir)); + } else { + buf.append(pkg_name.slice(string_buf)); + } + } + }, .workspace => { buf.append(pkg_res.value.workspace.slice(string_buf)); }, diff --git a/src/install/isolated_install/Store.zig b/src/install/isolated_install/Store.zig index cded0df949..14cf02cca6 100644 --- a/src/install/isolated_install/Store.zig +++ b/src/install/isolated_install/Store.zig @@ -145,6 +145,15 @@ pub const Store = struct { const pkg_res = pkg_resolutions[pkg_id]; switch (pkg_res.tag) { + .root => { + if (pkg_name.isEmpty()) { + try writer.writeAll(std.fs.path.basename(bun.fs.FileSystem.instance.top_level_dir)); + } else { + try writer.print("{}@root", .{ + pkg_name.fmtStorePath(string_buf), + }); + } + }, .folder => { try writer.print("{}@file+{}", .{ pkg_name.fmtStorePath(string_buf), diff --git a/src/install/lockfile/bun.lock.zig b/src/install/lockfile/bun.lock.zig index 8c393ea965..a4d00dc91c 100644 --- a/src/install/lockfile/bun.lock.zig +++ b/src/install/lockfile/bun.lock.zig @@ -1652,9 +1652,15 @@ pub fn parseIntoBinaryLockfile( return error.InvalidPackageResolution; }; - const name_str, const res_str = Dependency.splitNameAndVersion(res_info_str) catch { - try log.addError(source, res_info.loc, "Invalid package resolution"); - return error.InvalidPackageResolution; + const name_str, const res_str = name_and_res: { + if (strings.hasPrefixComptime(res_info_str, "@root:")) { + break :name_and_res .{ "", res_info_str[1..] }; + } + + break :name_and_res Dependency.splitNameAndVersion(res_info_str) catch { + try log.addError(source, res_info.loc, "Invalid package resolution"); + return error.InvalidPackageResolution; + }; }; const name_hash = String.Builder.stringHash(name_str); diff --git a/src/walker_skippable.zig b/src/walker_skippable.zig index 079cf90c98..bab654ff4b 100644 --- a/src/walker_skippable.zig +++ b/src/walker_skippable.zig @@ -109,7 +109,7 @@ pub fn next(self: *Walker) bun.sys.Maybe(?WalkerEntry) { return .initResult(null); } -pub fn deinit(self: *Walker) void { +pub fn deinit(self: *const Walker) void { if (self.stack.items.len > 0) { for (self.stack.items[1..]) |*item| { if (self.stack.items.len != 0) { diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index 99ca776545..3766f0c60c 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -268,6 +268,48 @@ test("can install folder dependencies", async () => { ).toBe("module.exports = 'hello from pkg-1';"); }); +test("can install folder dependencies on root package", async () => { + const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { isolated: true } }); + + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root-file-dep", + workspaces: ["packages/*"], + dependencies: { + self: "file:.", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + root: "file:../..", + }, + }), + ), + ]); + + console.log({ packageDir }); + + await runBunInstall(bunEnv, packageDir); + + expect( + await Promise.all([ + readlink(join(packageDir, "node_modules", "self")), + readlink(join(packageDir, "packages", "pkg1", "node_modules", "root")), + file(join(packageDir, "node_modules", "self", "package.json")).json(), + ]), + ).toEqual([ + join(".bun", "root-file-dep@root", "node_modules", "root-file-dep"), + join("..", "..", "..", "node_modules", ".bun", "root-file-dep@root", "node_modules", "root-file-dep"), + await file(packageJson).json(), + ]); +}); + describe("isolated workspaces", () => { test("basic", async () => { const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { isolated: true } }); From c8cb7713fc7e42df73502c04a681901ea74523b6 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 3 Oct 2025 02:54:23 -0700 Subject: [PATCH 011/191] Fix Windows crash in process.title when console title is empty (#23184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes a segmentation fault on Windows 11 when accessing `process.title` in certain scenarios (e.g., when fetching system information or making Discord webhook requests). ## Root Cause The crash occurred in libuv's `uv_get_process_title()` at `util.c:413` in the `strlen()` call. The issue is that `uv__get_process_title()` could return success (0) but leave `process_title` as NULL in edge cases where: 1. `GetConsoleTitleW()` returns an empty string 2. `uv__convert_utf16_to_utf8()` succeeds but doesn't allocate memory for the empty string 3. The subsequent `assert(process_title)` doesn't catch this in release builds 4. `strlen(process_title)` crashes with a null pointer dereference ## Changes Added defensive checks in `BunProcess.cpp`: 1. Initialize the title buffer to an empty string before calling `uv_get_process_title()` 2. Check if the buffer is empty after the call returns 3. Fall back to "bun" if the title is empty or the call fails ## Testing Added regression test in `test/regression/issue/23183.test.ts` that verifies: - `process.title` doesn't crash when accessed - Returns a valid string (either the console title or "bun") Fixes #23183 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/bindings/BunProcess.cpp | 3 +- test/regression/issue/23183.test.ts | 50 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/regression/issue/23183.test.ts diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index fff95e88d5..b243763dcd 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -3622,7 +3622,8 @@ JSC_DEFINE_CUSTOM_GETTER(processTitle, (JSC::JSGlobalObject * globalObject, JSC: #else auto& vm = JSC::getVM(globalObject); char title[1024]; - if (uv_get_process_title(title, sizeof(title)) != 0) { + title[0] = '\0'; // Initialize buffer to empty string + if (uv_get_process_title(title, sizeof(title)) != 0 || title[0] == '\0') { return JSValue::encode(jsString(vm, String("bun"_s))); } diff --git a/test/regression/issue/23183.test.ts b/test/regression/issue/23183.test.ts new file mode 100644 index 0000000000..a2a76cea4f --- /dev/null +++ b/test/regression/issue/23183.test.ts @@ -0,0 +1,50 @@ +// https://github.com/oven-sh/bun/issues/23183 +// Test that accessing process.title doesn't crash on Windows +import { test, expect } from "bun:test"; +import { bunExe, bunEnv, isWindows } from "harness"; +import { join } from "path"; + +test("process.title should not crash on Windows", async () => { + const proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log(typeof process.title)"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + Bun.readableStreamToText(proc.stdout), + Bun.readableStreamToText(proc.stderr), + proc.exited, + ]); + + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("string"); +}); + +test("process.title should return a non-empty string or fallback to 'bun'", async () => { + const proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log(process.title)"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + Bun.readableStreamToText(proc.stdout), + Bun.readableStreamToText(proc.stderr), + proc.exited, + ]); + + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + const title = stdout.trim(); + expect(title.length).toBeGreaterThan(0); + if (isWindows) { + // On Windows, we should get either a valid console title or "bun" + expect(typeof title).toBe("string"); + } else { + expect(title).toBe("bun"); + } +}); From a9b383bac5e15ac591cc76104634f80d26847938 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 3 Oct 2025 15:55:57 -0700 Subject: [PATCH 012/191] fix(crypto): hkdf callback should pass null (not undefined) on success (#23216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fixed crypto.hkdf callback to pass `null` instead of `undefined` for the error parameter on success - Added regression test to verify the fix ## Details Fixes #23211 Node.js convention requires crypto callbacks to receive `null` as the error parameter on success, but Bun was passing `undefined`. This caused compatibility issues with code that relies on strict null checks (e.g., [matter.js](https://github.com/matter-js/matter.js/blob/fdbec2cf88b3c810037d1df845b0244e566df1e2/packages/general/src/crypto/NodeJsStyleCrypto.ts#L169)). ### Changes - Updated `CryptoHkdf.cpp` to pass `jsNull()` instead of `jsUndefined()` for the error parameter in the success callback - Added regression test in `test/regression/issue/23211.test.ts` ## Test plan - [x] Added regression test that verifies callback receives `null` on success - [x] Test passes with the fix - [x] Ran existing crypto tests (no failures) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway --- .../bindings/node/crypto/CryptoHkdf.cpp | 2 +- .../js/node/crypto/hkdf-callback-null.test.ts | 23 +++++++++++++++++++ test/regression/issue/23183.test.ts | 5 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 test/js/node/crypto/hkdf-callback-null.test.ts diff --git a/src/bun.js/bindings/node/crypto/CryptoHkdf.cpp b/src/bun.js/bindings/node/crypto/CryptoHkdf.cpp index 267eee4a17..653e14ff0c 100644 --- a/src/bun.js/bindings/node/crypto/CryptoHkdf.cpp +++ b/src/bun.js/bindings/node/crypto/CryptoHkdf.cpp @@ -97,7 +97,7 @@ void HkdfJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback Bun__EventLoop__runCallback2(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), - JSValue::encode(jsUndefined()), + JSValue::encode(jsNull()), JSValue::encode(JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(), buf.releaseNonNull()))); } diff --git a/test/js/node/crypto/hkdf-callback-null.test.ts b/test/js/node/crypto/hkdf-callback-null.test.ts new file mode 100644 index 0000000000..f7d234e3ff --- /dev/null +++ b/test/js/node/crypto/hkdf-callback-null.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from "bun:test"; +import crypto from "node:crypto"; + +// Test that callback receives null (not undefined) for error on success +// https://github.com/oven-sh/bun/issues/23211 +test("crypto.hkdf callback should pass null (not undefined) on success", async () => { + const secret = new Uint8Array([7, 158, 216, 197, 25, 77, 201, 5, 73, 119]); + const salt = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]); + const info = new Uint8Array([67, 111, 109, 112, 114, 101, 115, 115, 101, 100]); + const length = 8; + + const promise = new Promise((resolve, reject) => { + crypto.hkdf("sha256", secret, salt, info, length, (error, key) => { + // Node.js passes null for error on success, not undefined + expect(error).toBeNull(); + expect(error).not.toBeUndefined(); + expect(key).toBeInstanceOf(ArrayBuffer); + resolve(true); + }); + }); + + await promise; +}); diff --git a/test/regression/issue/23183.test.ts b/test/regression/issue/23183.test.ts index a2a76cea4f..b5f280613d 100644 --- a/test/regression/issue/23183.test.ts +++ b/test/regression/issue/23183.test.ts @@ -1,8 +1,7 @@ // https://github.com/oven-sh/bun/issues/23183 // Test that accessing process.title doesn't crash on Windows -import { test, expect } from "bun:test"; -import { bunExe, bunEnv, isWindows } from "harness"; -import { join } from "path"; +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows } from "harness"; test("process.title should not crash on Windows", async () => { const proc = Bun.spawn({ From ddfc3f7fbc0dac8121ac1755a20b6260cb6ec9d5 Mon Sep 17 00:00:00 2001 From: pfg Date: Fri, 3 Oct 2025 16:13:06 -0700 Subject: [PATCH 013/191] Add `.cloneUpgrade()` to fix clone-upgrade (#23201) ### What does this PR do? Replaces '.upgrade()' with '.cloneUpgrade()'. '.upgrade()' is confusing and `.clone().upgrade()` was causing a leak. Caught by https://github.com/oven-sh/bun/pull/23199#discussion_r2400667320 ### How did you verify your code works? --- src/bun.js/test/bun_test.zig | 14 ++++++-------- src/bun.js/test/expect.zig | 18 +++++++++++++----- src/bun.js/test/expect/toMatchSnapshot.zig | 3 ++- .../expect/toThrowErrorMatchingSnapshot.zig | 4 ++-- src/bun.js/test/snapshot.zig | 4 +++- src/ptr/shared.zig | 6 ++---- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index a66516086a..a5ba01a625 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -311,10 +311,8 @@ pub const BunTest = struct { bun.destroy(this); buntest_weak.deinit(); } - pub fn bunTest(this: *RefData) ?*BunTest { - var buntest_strong = this.buntest_weak.clone().upgrade() orelse return null; - defer buntest_strong.deinit(); - return buntest_strong.get(); + pub fn bunTest(this: *RefData) ?BunTestPtr { + return this.buntest_weak.upgrade() orelse return null; } }; pub fn getCurrentStateData(this: *BunTest) RefDataValue { @@ -380,7 +378,7 @@ pub const BunTest = struct { const refdata: *RefData = this_ptr.asPromisePtr(RefData); defer refdata.deref(); const has_one_ref = refdata.ref_count.hasOneRef(); - var this_strong = refdata.buntest_weak.clone().upgrade() orelse return group.log("bunTestThenOrCatch -> the BunTest is no longer active", .{}); + var this_strong = refdata.buntest_weak.upgrade() orelse return group.log("bunTestThenOrCatch -> the BunTest is no longer active", .{}); defer this_strong.deinit(); const this = this_strong.get(); @@ -434,7 +432,7 @@ pub const BunTest = struct { if (!should_run) return .js_undefined; - var strong = ref_in.buntest_weak.clone().upgrade() orelse return .js_undefined; + var strong = ref_in.buntest_weak.upgrade() orelse return .js_undefined; defer strong.deinit(); const buntest = strong.get(); buntest.addResult(ref_in.phase); @@ -467,7 +465,7 @@ pub const BunTest = struct { errdefer bun.destroy(done_callback_test); const task = jsc.ManagedTask.New(RunTestsTask, RunTestsTask.call).init(done_callback_test); const vm = globalThis.bunVM(); - var strong = weak.clone().upgrade() orelse { + var strong = weak.upgrade() orelse { if (bun.Environment.ci_assert) bun.assert(false); // shouldn't be calling runNextTick after moving on to the next file return; // but just in case }; @@ -483,7 +481,7 @@ pub const BunTest = struct { pub fn call(this: *RunTestsTask) void { defer bun.destroy(this); defer this.weak.deinit(); - var strong = this.weak.clone().upgrade() orelse return; + var strong = this.weak.upgrade() orelse return; defer strong.deinit(); BunTest.run(strong, this.globalThis) catch |e| { strong.get().onUncaughtException(this.globalThis, this.globalThis.takeException(e), false, this.phase); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 700c6413aa..5a132d23c9 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -32,7 +32,9 @@ pub const Expect = struct { pub fn incrementExpectCallCounter(this: *Expect) void { const parent = this.parent orelse return; // not in bun:test - const buntest = parent.bunTest() orelse return; // the test file this expect() call was for is no longer + var buntest_strong = parent.bunTest() orelse return; // the test file this expect() call was for is no longer + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); if (parent.phase.sequence(buntest)) |sequence| { // found active sequence sequence.expect_call_count +|= 1; @@ -44,7 +46,7 @@ pub const Expect = struct { } } - pub fn bunTest(this: *Expect) ?*bun.jsc.Jest.bun_test.BunTest { + pub fn bunTest(this: *Expect) ?bun.jsc.Jest.bun_test.BunTestPtr { const parent = this.parent orelse return null; return parent.bunTest(); } @@ -275,7 +277,9 @@ pub const Expect = struct { pub fn getSnapshotName(this: *Expect, allocator: std.mem.Allocator, hint: string) ![]const u8 { const parent = this.parent orelse return error.NoTest; - const buntest = parent.bunTest() orelse return error.TestNotActive; + var buntest_strong = parent.bunTest() orelse return error.TestNotActive; + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); const execution_entry = parent.phase.entry(buntest) orelse return error.SnapshotInConcurrentGroup; const test_name = execution_entry.base.name orelse "(unnamed)"; @@ -748,10 +752,12 @@ pub const Expect = struct { } } } - const buntest = this.bunTest() orelse { + var buntest_strong = this.bunTest() orelse { const signature = comptime getSignature(fn_name, "", false); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); }; + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); // 1. find the src loc of the snapshot const srcloc = callFrame.getCallerSrcLoc(globalThis); @@ -825,7 +831,9 @@ pub const Expect = struct { const existing_value = Jest.runner.?.snapshots.getOrPut(this, pretty_value.slice(), hint) catch |err| { var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis }; defer formatter.deinit(); - const buntest = this.bunTest() orelse return globalThis.throw("Snapshot matchers cannot be used outside of a test", .{}); + var buntest_strong = this.bunTest() orelse return globalThis.throw("Snapshot matchers cannot be used outside of a test", .{}); + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); const test_file_path = Jest.runner.?.files.get(buntest.file_id).source.path.text; return switch (err) { error.FailedToOpenSnapshotFile => globalThis.throw("Failed to open snapshot file for test file: {s}", .{test_file_path}), diff --git a/src/bun.js/test/expect/toMatchSnapshot.zig b/src/bun.js/test/expect/toMatchSnapshot.zig index 3cf63e77af..30de93411d 100644 --- a/src/bun.js/test/expect/toMatchSnapshot.zig +++ b/src/bun.js/test/expect/toMatchSnapshot.zig @@ -12,10 +12,11 @@ pub fn toMatchSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); } - _ = this.bunTest() orelse { + var buntest_strong = this.bunTest() orelse { const signature = comptime getSignature("toMatchSnapshot", "", true); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); }; + defer buntest_strong.deinit(); var hint_string: ZigString = ZigString.Empty; var property_matchers: ?JSValue = null; diff --git a/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig b/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig index 31757b6f6e..5162a14557 100644 --- a/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig +++ b/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig @@ -12,11 +12,11 @@ pub fn toThrowErrorMatchingSnapshot(this: *Expect, globalThis: *JSGlobalObject, return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); } - const bunTest = this.bunTest() orelse { + var bunTest_strong = this.bunTest() orelse { const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", true); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); }; - _ = bunTest; // ? + defer bunTest_strong.deinit(); var hint_string: ZigString = ZigString.Empty; switch (arguments.len) { diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig index 7562cad792..07300f6915 100644 --- a/src/bun.js/test/snapshot.zig +++ b/src/bun.js/test/snapshot.zig @@ -53,7 +53,9 @@ pub const Snapshots = struct { return .{ count_entry.key_ptr.*, count_entry.value_ptr.* }; } pub fn getOrPut(this: *Snapshots, expect: *Expect, target_value: []const u8, hint: string) !?string { - const bunTest = expect.bunTest() orelse return error.SnapshotFailed; + var buntest_strong = expect.bunTest() orelse return error.SnapshotFailed; + defer buntest_strong.deinit(); + const bunTest = buntest_strong.get(); switch (try this.getSnapshotFile(bunTest.file_id)) { .result => {}, .err => |err| { diff --git a/src/ptr/shared.zig b/src/ptr/shared.zig index f9ff6b6951..dfdd096a99 100644 --- a/src/ptr/shared.zig +++ b/src/ptr/shared.zig @@ -285,16 +285,13 @@ fn Weak(comptime Pointer: type, comptime options: Options) type { const SharedNonOptional = WithOptions(NonOptionalPointer, options); - /// Upgrades this weak pointer into a normal shared pointer. - /// - /// This method invalidates `self`. + /// Clones this weak pointer and upgrades it to a shared pointer. You still need to `deinit` the weak pointer. pub fn upgrade(self: Self) ?SharedNonOptional { const data = if (comptime info.isOptional()) self.getData() orelse return null else self.getData(); if (!data.tryIncrementStrong()) return null; - data.decrementWeak(); return .{ .#pointer = &data.value }; } @@ -455,6 +452,7 @@ fn FullData(comptime Child: type, comptime options: Options) type { // .acq_rel because we need to make sure other threads are done using the object before // we free it. if ((comptime !options.allow_weak) or self.weak_count.decrement() == 0) { + if (bun.Environment.ci_assert) bun.assert(self.strong_count.get(.monotonic) == 0); self.destroy(); } } From f14f3b03bb97703d2c81e0a377fe84d990aaf14e Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 3 Oct 2025 17:10:28 -0700 Subject: [PATCH 014/191] Add new bindings generator; port `SSLConfig` (#23169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new generator for JS → Zig bindings. The bulk of the conversion is done in C++, after which the data is transformed into an FFI-safe representation, passed to Zig, and then finally transformed into idiomatic Zig types. In its current form, the new bindings generator supports: * Signed and unsigned integers * Floats (plus a “finite” variant that disallows NaN and infinities) * Strings * ArrayBuffer (accepts ArrayBuffer, TypedArray, or DataView) * Blob * Optional types * Nullable types (allows null, whereas Optional only allows undefined) * Arrays * User-defined string enumerations * User-defined unions (fields can optionally be named to provide a better experience in Zig) * Null and undefined, for use in unions (can more efficiently represent optional/nullable unions than wrapping a union in an optional) * User-defined dictionaries (arbitrary key-value pairs; expects a JS object and parses it into a struct) * Default values for dictionary members * Alternative names for dictionary members (e.g., to support both `serverName` and `servername` without taking up twice the space) * Descriptive error messages * Automatic `fromJS` functions in Zig for dictionaries * Automatic `deinit` functions for the generated Zig types Although this bindings generator has many features not present in `bindgen.ts`, it does not yet implement all of `bindgen.ts`'s functionality, so for the time being, it has been named `bindgenv2`, and its configuration is specified in `.bindv2.ts` files. Once all `bindgen.ts`'s functionality has been incorporated, it will be renamed. This PR ports `SSLConfig` to use the new bindings generator; see `SSLConfig.bindv2.ts`. (For internal tracking: fixes STAB-1319, STAB-1322, STAB-1323, STAB-1324) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alistair Smith --- .gitignore | 1 + .prettierrc | 6 + build.zig | 5 + bun.lock | 12 +- cmake/Options.cmake | 5 + cmake/Sources.json | 8 + cmake/targets/BuildBun.cmake | 55 ++ package.json | 10 +- packages/bun-types/bun.d.ts | 4 - packages/bun-usockets/src/libusockets.h | 6 +- src/allocators/mimalloc.zig | 3 +- src/bake/DevServer.bind.ts | 2 +- src/bake/tsconfig.json | 1 + src/bun.js.zig | 1 + src/bun.js/Strong.zig | 2 +- src/bun.js/api/bun/socket.zig | 74 +- src/bun.js/api/bun/socket/Handlers.zig | 25 +- src/bun.js/api/bun/socket/Listener.zig | 44 +- src/bun.js/api/bun/ssl_wrapper.zig | 2 +- src/bun.js/api/server.zig | 2 +- src/bun.js/api/server/SSLConfig.bindv2.ts | 90 ++ src/bun.js/api/server/SSLConfig.zig | 871 ++++++------------ src/bun.js/api/server/ServerConfig.zig | 2 +- src/bun.js/api/sql.classes.ts | 4 +- src/bun.js/bindgen.zig | 254 +++++ src/bun.js/bindings/Bindgen.h | 11 + src/bun.js/bindings/Bindgen/ExternTraits.h | 152 +++ src/bun.js/bindings/Bindgen/ExternUnion.h | 89 ++ .../bindings/Bindgen/ExternVectorTraits.h | 149 +++ src/bun.js/bindings/Bindgen/IDLConvert.h | 19 + src/bun.js/bindings/Bindgen/IDLConvertBase.h | 74 ++ src/bun.js/bindings/Bindgen/IDLTypes.h | 31 + src/bun.js/bindings/Bindgen/Macros.h | 36 + src/bun.js/bindings/BunIDLConvert.h | 280 ++++++ src/bun.js/bindings/BunIDLConvertBase.h | 77 ++ src/bun.js/bindings/BunIDLConvertBlob.h | 36 + src/bun.js/bindings/BunIDLConvertContext.h | 252 +++++ src/bun.js/bindings/BunIDLConvertNumbers.h | 174 ++++ src/bun.js/bindings/BunIDLHumanReadable.h | 139 +++ src/bun.js/bindings/BunIDLTypes.h | 87 ++ src/bun.js/bindings/ConcatCStrings.h | 78 ++ src/bun.js/bindings/IDLTypes.h | 56 +- src/bun.js/bindings/JSValue.zig | 21 +- src/bun.js/bindings/MimallocWTFMalloc.h | 103 +++ .../bindings/{Strong.cpp => StrongRef.cpp} | 1 + src/bun.js/bindings/StrongRef.h | 23 + src/bun.js/bindings/ZigGlobalObject.cpp | 4 +- src/bun.js/bindings/bindings.cpp | 102 +- src/bun.js/bindings/headers-handwritten.h | 3 +- src/bun.js/bindings/headers.h | 2 +- src/bun.js/bindings/webcore/JSDOMConvert.h | 1 + .../bindings/webcore/JSDOMConvertBase.h | 2 + .../bindings/webcore/JSDOMConvertDictionary.h | 12 +- .../webcore/JSDOMConvertEnumeration.h | 36 + .../bindings/webcore/JSDOMConvertNullable.h | 29 + .../bindings/webcore/JSDOMConvertOptional.h | 105 +++ .../bindings/webcore/JSDOMConvertSequences.h | 356 +++++-- src/bun.js/bindings/webcore/JSDOMURL.cpp | 0 src/bun.js/jsc.zig | 3 + src/bun.js/jsc/array_buffer.zig | 48 +- src/bun.zig | 2 +- src/codegen/bindgen-lib.ts | 5 +- src/codegen/bindgenv2/internal/any.ts | 42 + src/codegen/bindgenv2/internal/array.ts | 32 + src/codegen/bindgenv2/internal/base.ts | 134 +++ src/codegen/bindgenv2/internal/dictionary.ts | 451 +++++++++ src/codegen/bindgenv2/internal/enumeration.ts | 182 ++++ src/codegen/bindgenv2/internal/interfaces.ts | 40 + src/codegen/bindgenv2/internal/optional.ts | 84 ++ src/codegen/bindgenv2/internal/primitives.ts | 125 +++ src/codegen/bindgenv2/internal/string.ts | 21 + src/codegen/bindgenv2/internal/union.ts | 185 ++++ src/codegen/bindgenv2/lib.ts | 10 + src/codegen/bindgenv2/script.ts | 185 ++++ src/codegen/bindgenv2/tsconfig.json | 11 + src/codegen/bundle-modules.ts | 4 +- src/codegen/class-definitions.ts | 4 +- src/codegen/helpers.ts | 17 +- src/codegen/replacements.ts | 4 +- src/deps/uws/SocketContext.zig | 6 +- src/meta.zig | 2 + src/meta/tagged_union.zig | 236 +++++ src/napi/napi.zig | 14 +- src/node-fallbacks/build-fallbacks.ts | 6 +- src/string.zig | 7 +- src/string/{WTFStringImpl.zig => wtf.zig} | 0 src/tsconfig.json | 5 +- test/js/bun/http/bun-server.test.ts | 4 +- test/js/bun/http/serve.test.ts | 6 +- test/js/bun/net/tcp-server.test.ts | 4 +- tsconfig.base.json | 2 +- 91 files changed, 5015 insertions(+), 895 deletions(-) create mode 100644 src/bun.js/api/server/SSLConfig.bindv2.ts create mode 100644 src/bun.js/bindgen.zig create mode 100644 src/bun.js/bindings/Bindgen.h create mode 100644 src/bun.js/bindings/Bindgen/ExternTraits.h create mode 100644 src/bun.js/bindings/Bindgen/ExternUnion.h create mode 100644 src/bun.js/bindings/Bindgen/ExternVectorTraits.h create mode 100644 src/bun.js/bindings/Bindgen/IDLConvert.h create mode 100644 src/bun.js/bindings/Bindgen/IDLConvertBase.h create mode 100644 src/bun.js/bindings/Bindgen/IDLTypes.h create mode 100644 src/bun.js/bindings/Bindgen/Macros.h create mode 100644 src/bun.js/bindings/BunIDLConvert.h create mode 100644 src/bun.js/bindings/BunIDLConvertBase.h create mode 100644 src/bun.js/bindings/BunIDLConvertBlob.h create mode 100644 src/bun.js/bindings/BunIDLConvertContext.h create mode 100644 src/bun.js/bindings/BunIDLConvertNumbers.h create mode 100644 src/bun.js/bindings/BunIDLHumanReadable.h create mode 100644 src/bun.js/bindings/BunIDLTypes.h create mode 100644 src/bun.js/bindings/ConcatCStrings.h create mode 100644 src/bun.js/bindings/MimallocWTFMalloc.h rename src/bun.js/bindings/{Strong.cpp => StrongRef.cpp} (98%) create mode 100644 src/bun.js/bindings/StrongRef.h create mode 100644 src/bun.js/bindings/webcore/JSDOMConvertOptional.h mode change 100755 => 100644 src/bun.js/bindings/webcore/JSDOMURL.cpp create mode 100644 src/codegen/bindgenv2/internal/any.ts create mode 100644 src/codegen/bindgenv2/internal/array.ts create mode 100644 src/codegen/bindgenv2/internal/base.ts create mode 100644 src/codegen/bindgenv2/internal/dictionary.ts create mode 100644 src/codegen/bindgenv2/internal/enumeration.ts create mode 100644 src/codegen/bindgenv2/internal/interfaces.ts create mode 100644 src/codegen/bindgenv2/internal/optional.ts create mode 100644 src/codegen/bindgenv2/internal/primitives.ts create mode 100644 src/codegen/bindgenv2/internal/string.ts create mode 100644 src/codegen/bindgenv2/internal/union.ts create mode 100644 src/codegen/bindgenv2/lib.ts create mode 100755 src/codegen/bindgenv2/script.ts create mode 100644 src/codegen/bindgenv2/tsconfig.json create mode 100644 src/meta/tagged_union.zig rename src/string/{WTFStringImpl.zig => wtf.zig} (100%) diff --git a/.gitignore b/.gitignore index 3f71c2acc9..b0c2fa643d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env .envrc .eslintcache +.gdb_history .idea .next .ninja_deps diff --git a/.prettierrc b/.prettierrc index c9da1bd439..14285ca704 100644 --- a/.prettierrc +++ b/.prettierrc @@ -19,6 +19,12 @@ "options": { "printWidth": 80 } + }, + { + "files": ["src/codegen/bindgenv2/**/*.ts", "*.bindv2.ts"], + "options": { + "printWidth": 100 + } } ] } diff --git a/build.zig b/build.zig index fa76a2138c..0f45dce6e1 100644 --- a/build.zig +++ b/build.zig @@ -49,6 +49,7 @@ const BunBuildOptions = struct { enable_logs: bool = false, enable_asan: bool, enable_valgrind: bool, + use_mimalloc: bool, tracy_callstack_depth: u16, reported_nodejs_version: Version, /// To make iterating on some '@embedFile's faster, we load them at runtime @@ -97,6 +98,7 @@ const BunBuildOptions = struct { opts.addOption(bool, "enable_logs", this.enable_logs); opts.addOption(bool, "enable_asan", this.enable_asan); opts.addOption(bool, "enable_valgrind", this.enable_valgrind); + opts.addOption(bool, "use_mimalloc", this.use_mimalloc); opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{}", .{this.reported_nodejs_version})); opts.addOption(bool, "zig_self_hosted_backend", this.no_llvm); opts.addOption(bool, "override_no_export_cpp_apis", this.override_no_export_cpp_apis); @@ -270,6 +272,7 @@ pub fn build(b: *Build) !void { .enable_logs = b.option(bool, "enable_logs", "Enable logs in release") orelse false, .enable_asan = b.option(bool, "enable_asan", "Enable asan") orelse false, .enable_valgrind = b.option(bool, "enable_valgrind", "Enable valgrind") orelse false, + .use_mimalloc = b.option(bool, "use_mimalloc", "Use mimalloc as default allocator") orelse false, .llvm_codegen_threads = b.option(u32, "llvm_codegen_threads", "Number of threads to use for LLVM codegen") orelse 1, }; @@ -500,6 +503,7 @@ fn addMultiCheck( .no_llvm = root_build_options.no_llvm, .enable_asan = root_build_options.enable_asan, .enable_valgrind = root_build_options.enable_valgrind, + .use_mimalloc = root_build_options.use_mimalloc, .override_no_export_cpp_apis = root_build_options.override_no_export_cpp_apis, }; @@ -720,6 +724,7 @@ fn addInternalImports(b: *Build, mod: *Module, opts: *BunBuildOptions) void { // Generated code exposed as individual modules. inline for (.{ .{ .file = "ZigGeneratedClasses.zig", .import = "ZigGeneratedClasses" }, + .{ .file = "bindgen_generated.zig", .import = "bindgen_generated" }, .{ .file = "ResolvedSourceTag.zig", .import = "ResolvedSourceTag" }, .{ .file = "ErrorCode.zig", .import = "ErrorCode" }, .{ .file = "runtime.out.js", .enable = opts.shouldEmbedCode() }, diff --git a/bun.lock b/bun.lock index be4ab107ae..fedf606d75 100644 --- a/bun.lock +++ b/bun.lock @@ -8,14 +8,14 @@ "@lezer/cpp": "^1.1.3", "@types/bun": "workspace:*", "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8", - "esbuild": "^0.21.4", - "mitata": "^0.1.11", + "esbuild": "^0.21.5", + "mitata": "^0.1.14", "peechy": "0.4.34", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.0.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "source-map-js": "^1.2.0", + "source-map-js": "^1.2.1", "typescript": "5.9.2", }, }, @@ -284,7 +284,7 @@ "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], - "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.2.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg=="], + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.3.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw=="], "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 1e9b664321..93a3698563 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -202,4 +202,9 @@ optionx(USE_WEBKIT_ICU BOOL "Use the ICU libraries from WebKit" DEFAULT ${DEFAUL optionx(ERROR_LIMIT STRING "Maximum number of errors to show when compiling C++ code" DEFAULT "100") +# This is not an `option` because setting this variable to OFF is experimental +# and unsupported. This replaces the `use_mimalloc` variable previously in +# bun.zig, and enables C++ code to also be aware of the option. +set(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR ON) + list(APPEND CMAKE_ARGS -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) diff --git a/cmake/Sources.json b/cmake/Sources.json index cd86d86989..5ae4930693 100644 --- a/cmake/Sources.json +++ b/cmake/Sources.json @@ -31,6 +31,14 @@ "output": "BindgenSources.txt", "paths": ["src/**/*.bind.ts"] }, + { + "output": "BindgenV2Sources.txt", + "paths": ["src/**/*.bindv2.ts"] + }, + { + "output": "BindgenV2InternalSources.txt", + "paths": ["src/codegen/bindgenv2/**/*.ts"] + }, { "output": "ZigSources.txt", "paths": ["src/**/*.zig"] diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 31b007050c..945f8074fb 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -395,6 +395,54 @@ register_command( ${BUN_BAKE_RUNTIME_OUTPUTS} ) +set(BUN_BINDGENV2_SCRIPT ${CWD}/src/codegen/bindgenv2/script.ts) + +absolute_sources(BUN_BINDGENV2_SOURCES ${CWD}/cmake/sources/BindgenV2Sources.txt) +# These sources include the script itself. +absolute_sources(BUN_BINDGENV2_INTERNAL_SOURCES + ${CWD}/cmake/sources/BindgenV2InternalSources.txt) +string(REPLACE ";" "," BUN_BINDGENV2_SOURCES_COMMA_SEPARATED + "${BUN_BINDGENV2_SOURCES}") + +execute_process( + COMMAND ${BUN_EXECUTABLE} run ${BUN_BINDGENV2_SCRIPT} + --command=list-outputs + --sources=${BUN_BINDGENV2_SOURCES_COMMA_SEPARATED} + --codegen-path=${CODEGEN_PATH} + RESULT_VARIABLE bindgen_result + OUTPUT_VARIABLE bindgen_outputs +) +if(${bindgen_result}) + message(FATAL_ERROR "bindgenv2/script.ts exited with non-zero status") +endif() +foreach(output IN LISTS bindgen_outputs) + if(output MATCHES "\.cpp$") + list(APPEND BUN_BINDGENV2_CPP_OUTPUTS ${output}) + elseif(output MATCHES "\.zig$") + list(APPEND BUN_BINDGENV2_ZIG_OUTPUTS ${output}) + else() + message(FATAL_ERROR "unexpected bindgen output: [${output}]") + endif() +endforeach() + +register_command( + TARGET + bun-bindgen-v2 + COMMENT + "Generating bindings (v2)" + COMMAND + ${BUN_EXECUTABLE} run ${BUN_BINDGENV2_SCRIPT} + --command=generate + --codegen-path=${CODEGEN_PATH} + --sources=${BUN_BINDGENV2_SOURCES_COMMA_SEPARATED} + SOURCES + ${BUN_BINDGENV2_SOURCES} + ${BUN_BINDGENV2_INTERNAL_SOURCES} + OUTPUTS + ${BUN_BINDGENV2_CPP_OUTPUTS} + ${BUN_BINDGENV2_ZIG_OUTPUTS} +) + set(BUN_BINDGEN_SCRIPT ${CWD}/src/codegen/bindgen.ts) absolute_sources(BUN_BINDGEN_SOURCES ${CWD}/cmake/sources/BindgenSources.txt) @@ -573,6 +621,7 @@ set(BUN_ZIG_GENERATED_SOURCES ${BUN_ZIG_GENERATED_CLASSES_OUTPUTS} ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_CPP_OUTPUTS} + ${BUN_BINDGENV2_ZIG_OUTPUTS} ) # In debug builds, these are not embedded, but rather referenced at runtime. @@ -636,6 +685,7 @@ register_command( -Denable_logs=$,true,false> -Denable_asan=$,true,false> -Denable_valgrind=$,true,false> + -Duse_mimalloc=$,true,false> -Dllvm_codegen_threads=${LLVM_ZIG_CODEGEN_THREADS} -Dversion=${VERSION} -Dreported_nodejs_version=${NODEJS_VERSION} @@ -712,6 +762,7 @@ list(APPEND BUN_CPP_SOURCES ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_OBJECT_LUT_OUTPUTS} ${BUN_BINDGEN_CPP_OUTPUTS} + ${BUN_BINDGENV2_CPP_OUTPUTS} ) if(WIN32) @@ -849,6 +900,10 @@ if(WIN32) ) endif() +if(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR) + target_compile_definitions(${bun} PRIVATE USE_MIMALLOC=1) +endif() + target_compile_definitions(${bun} PRIVATE _HAS_EXCEPTIONS=0 LIBUS_USE_OPENSSL=1 diff --git a/package.json b/package.json index 737d6c33f4..372a9d596e 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,14 @@ "@lezer/cpp": "^1.1.3", "@types/bun": "workspace:*", "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8", - "esbuild": "^0.21.4", - "mitata": "^0.1.11", + "esbuild": "^0.21.5", + "mitata": "^0.1.14", "peechy": "0.4.34", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.0.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "source-map-js": "^1.2.0", + "source-map-js": "^1.2.1", "typescript": "5.9.2" }, "resolutions": { diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 406e3d8466..3425900c86 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3707,10 +3707,6 @@ declare module "bun" { */ secureOptions?: number | undefined; // Value is a numeric bitmask of the `SSL_OP_*` options - keyFile?: string; - - certFile?: string; - ALPNProtocols?: string | BufferSource; ciphers?: string; diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index a5156f700c..0e746a0388 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -226,11 +226,11 @@ struct us_bun_socket_context_options_t { const char *ca_file_name; const char *ssl_ciphers; int ssl_prefer_low_memory_usage; /* Todo: rename to prefer_low_memory_usage and apply for TCP as well */ - const char **key; + const char * const *key; unsigned int key_count; - const char **cert; + const char * const *cert; unsigned int cert_count; - const char **ca; + const char * const *ca; unsigned int ca_count; unsigned int secure_options; int reject_unauthorized; diff --git a/src/allocators/mimalloc.zig b/src/allocators/mimalloc.zig index a486fe2abf..3d9e9b2e07 100644 --- a/src/allocators/mimalloc.zig +++ b/src/allocators/mimalloc.zig @@ -216,8 +216,7 @@ pub extern fn mi_new_reallocn(p: ?*anyopaque, newcount: usize, size: usize) ?*an pub const MI_SMALL_WSIZE_MAX = @as(c_int, 128); pub const MI_SMALL_SIZE_MAX = MI_SMALL_WSIZE_MAX * @import("std").zig.c_translation.sizeof(?*anyopaque); pub const MI_ALIGNMENT_MAX = (@as(c_int, 16) * @as(c_int, 1024)) * @as(c_ulong, 1024); - -const MI_MAX_ALIGN_SIZE = 16; +pub const MI_MAX_ALIGN_SIZE = 16; pub fn mustUseAlignedAlloc(alignment: std.mem.Alignment) bool { return alignment.toByteUnits() > MI_MAX_ALIGN_SIZE; diff --git a/src/bake/DevServer.bind.ts b/src/bake/DevServer.bind.ts index d56758a4a6..df89b41feb 100644 --- a/src/bake/DevServer.bind.ts +++ b/src/bake/DevServer.bind.ts @@ -1,5 +1,5 @@ // @ts-ignore -import { fn, t } from "../codegen/bindgen-lib"; +import { fn, t } from "bindgen"; export const getDeinitCountForTesting = fn({ args: {}, ret: t.usize, diff --git a/src/bake/tsconfig.json b/src/bake/tsconfig.json index 11e0dce4dd..8ab4d5832d 100644 --- a/src/bake/tsconfig.json +++ b/src/bake/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"], + "baseUrl": ".", "paths": { "bun-framework-react/*": ["./bun-framework-react/*"], "bindgen": ["../codegen/bindgen-lib"] diff --git a/src/bun.js.zig b/src/bun.js.zig index 13ca71282d..fb59390ea0 100644 --- a/src/bun.js.zig +++ b/src/bun.js.zig @@ -1,6 +1,7 @@ pub const jsc = @import("./bun.js/jsc.zig"); pub const webcore = @import("./bun.js/webcore.zig"); pub const api = @import("./bun.js/api.zig"); +pub const bindgen = @import("./bun.js/bindgen.zig"); pub const Run = struct { ctx: Command.Context, diff --git a/src/bun.js/Strong.zig b/src/bun.js/Strong.zig index a23b68627a..5c098b88eb 100644 --- a/src/bun.js/Strong.zig +++ b/src/bun.js/Strong.zig @@ -114,7 +114,7 @@ pub const Optional = struct { } }; -const Impl = opaque { +pub const Impl = opaque { pub fn init(global: *jsc.JSGlobalObject, value: jsc.JSValue) *Impl { jsc.markBinding(@src()); return Bun__StrongRef__new(global, value); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index fb18864c97..8fa141fb59 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1412,23 +1412,12 @@ pub fn NewSocket(comptime ssl: bool) type { } var ssl_opts: ?jsc.API.ServerConfig.SSLConfig = null; - defer { - if (!success) { - if (ssl_opts) |*ssl_config| { - ssl_config.deinit(); - } - } - } if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls)) |ssl_config| { - ssl_opts = ssl_config; - } + if (!tls.isBoolean()) { + ssl_opts = try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls); + } else if (tls.toBoolean()) { + ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; } } @@ -1436,9 +1425,10 @@ pub fn NewSocket(comptime ssl: bool) type { return .zero; } - if (ssl_opts == null) { + const socket_config = &(ssl_opts orelse { return globalObject.throw("Expected \"tls\" option", .{}); - } + }); + defer socket_config.deinit(); var default_data = JSValue.zero; if (try opts.fastGet(globalObject, .data)) |default_data_value| { @@ -1449,14 +1439,7 @@ pub fn NewSocket(comptime ssl: bool) type { return .zero; } - var socket_config = ssl_opts.?; - ssl_opts = null; - defer socket_config.deinit(); const options = socket_config.asUSockets(); - - const protos = socket_config.protos; - const protos_len = socket_config.protos_len; - const ext_size = @sizeOf(WrappedSocket); var handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); @@ -1470,8 +1453,14 @@ pub fn NewSocket(comptime ssl: bool) type { .socket = TLSSocket.Socket.detached, .connection = if (this.connection) |c| c.clone() else null, .wrapped = .tls, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p[0..protos_len])) else null, - .server_name = if (socket_config.server_name) |server_name| bun.handleOom(bun.default_allocator.dupe(u8, server_name[0..bun.len(server_name)])) else null, + .protos = if (socket_config.protos) |p| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(p))) + else + null, + .server_name = if (socket_config.server_name) |sn| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(sn))) + else + null, .socket_context = null, // only set after the wrapTLS .flags = .{ .is_active = false, @@ -1955,19 +1944,15 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C var ssl_opts: ?jsc.API.ServerConfig.SSLConfig = null; if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls)) |ssl_config| { - ssl_opts = ssl_config; - } + if (!tls.isBoolean()) { + ssl_opts = try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls); + } else if (tls.toBoolean()) { + ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; } } - if (ssl_opts == null) { + const socket_config = &(ssl_opts orelse { return globalObject.throw("Expected \"tls\" option", .{}); - } + }); var default_data = JSValue.zero; if (try opts.fastGet(globalObject, .data)) |default_data_value| { @@ -1975,11 +1960,6 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C default_data.ensureStillAlive(); } - const socket_config = ssl_opts.?; - - const protos = socket_config.protos; - const protos_len = socket_config.protos_len; - const is_server = false; // A duplex socket is always handled as a client var handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); @@ -1994,8 +1974,14 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C .socket = TLSSocket.Socket.detached, .connection = null, .wrapped = .tls, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p[0..protos_len])) else null, - .server_name = if (socket_config.server_name) |server_name| bun.handleOom(bun.default_allocator.dupe(u8, server_name[0..bun.len(server_name)])) else null, + .protos = if (socket_config.protos) |p| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(p))) + else + null, + .server_name = if (socket_config.server_name) |sn| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(sn))) + else + null, .socket_context = null, // only set after the wrapTLS }); const tls_js_value = tls.getThisValue(globalObject); @@ -2006,7 +1992,7 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C .tls = tls, .vm = globalObject.bunVM(), .task = undefined, - .ssl_config = socket_config, + .ssl_config = socket_config.*, }); tls.ref(); diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index b87975827e..b874d9f4f2 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -210,7 +210,7 @@ pub const SocketConfig = struct { hostname_or_unix: jsc.ZigString.Slice, port: ?u16 = null, fd: ?bun.FileDescriptor = null, - ssl: ?jsc.API.ServerConfig.SSLConfig = null, + ssl: ?SSLConfig = null, handlers: Handlers, default_data: jsc.JSValue = .zero, exclusive: bool = false, @@ -246,26 +246,18 @@ pub const SocketConfig = struct { var reusePort = false; var ipv6Only = false; - var ssl: ?jsc.API.ServerConfig.SSLConfig = null; + var ssl: ?SSLConfig = null; var default_data = JSValue.zero; if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(vm, globalObject, tls)) |ssl_config| { - ssl = ssl_config; - } + if (!tls.isBoolean()) { + ssl = try SSLConfig.fromJS(vm, globalObject, tls); + } else if (tls.toBoolean()) { + ssl = SSLConfig.zero; } } - errdefer { - if (ssl != null) { - ssl.?.deinit(); - } - } + errdefer bun.memory.deinit(&ssl); hostname_or_unix: { if (try opts.getTruthy(globalObject, "fd")) |fd_| { @@ -382,9 +374,10 @@ const bun = @import("bun"); const Environment = bun.Environment; const strings = bun.strings; const uws = bun.uws; +const Listener = bun.api.Listener; +const SSLConfig = bun.api.ServerConfig.SSLConfig; const jsc = bun.jsc; const JSValue = jsc.JSValue; const ZigString = jsc.ZigString; const BinaryType = jsc.ArrayBuffer.BinaryType; -const Listener = jsc.API.Listener; diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 755bcb16b6..84afd1b5e8 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -132,7 +132,7 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa const connection: Listener.UnixOrHost = .{ .unix = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice() }; if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } } var socket = Listener{ @@ -156,9 +156,10 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa this.* = socket; //TODO: server_name is not supported on named pipes, I belive its , lets wait for someone to ask for it + const ssl_ptr = if (ssl) |*s| s else null; this.listener = .{ // we need to add support for the backlog parameter on listen here we use the default value of nodejs - .namedPipe = WindowsNamedPipeListeningContext.listen(globalObject, pipe_name, 511, ssl, this) catch { + .namedPipe = WindowsNamedPipeListeningContext.listen(globalObject, pipe_name, 511, ssl_ptr, this) catch { this.deinit(); return globalObject.throwInvalidArguments("Failed to listen at {s}", .{pipe_name}); }, @@ -172,8 +173,8 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa } } } - const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl != null) - jsc.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |*some_ssl| + some_ssl.asUSockets() else .{}; @@ -203,7 +204,7 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } uws.NewSocketHandler(true).configure( @@ -411,10 +412,12 @@ pub fn addServerName(this: *Listener, global: *jsc.JSGlobalObject, hostname: JSV return global.throwInvalidArguments("hostname pattern cannot be empty", .{}); } - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), global, tls)) |ssl_config| { + if (try SSLConfig.fromJS(jsc.VirtualMachine.get(), global, tls)) |ssl_config| { // to keep nodejs compatibility, we allow to replace the server name this.socket_context.?.removeServerName(true, server_name); this.socket_context.?.addServerName(true, server_name, ssl_config.asUSockets()); + var ssl_config_mut = ssl_config; + ssl_config_mut.deinit(); } return .js_undefined; @@ -569,7 +572,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock var protos: ?[]const u8 = null; var server_name: ?[]const u8 = null; const ssl_enabled = ssl != null; - defer if (ssl != null) ssl.?.deinit(); + defer if (ssl) |*some_ssl| some_ssl.deinit(); vm.eventLoop().ensureWaker(); @@ -704,8 +707,8 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock } } - const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl != null) - jsc.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |*some_ssl| + some_ssl.asUSockets() else .{}; @@ -726,7 +729,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } if (ssl.?.server_name) |s| { server_name = bun.handleOom(bun.default_allocator.dupe(u8, s[0..bun.len(s)])); @@ -907,7 +910,13 @@ pub const WindowsNamedPipeListeningContext = if (Environment.isWindows) struct { this.uvPipe.close(onPipeClosed); } - pub fn listen(globalThis: *jsc.JSGlobalObject, path: []const u8, backlog: i32, ssl_config: ?jsc.API.ServerConfig.SSLConfig, listener: *Listener) !*WindowsNamedPipeListeningContext { + pub fn listen( + globalThis: *jsc.JSGlobalObject, + path: []const u8, + backlog: i32, + ssl_config: ?*const SSLConfig, + listener: *Listener, + ) !*WindowsNamedPipeListeningContext { const this = WindowsNamedPipeListeningContext.new(.{ .globalThis = globalThis, .vm = globalThis.bunVM(), @@ -917,7 +926,7 @@ pub const WindowsNamedPipeListeningContext = if (Environment.isWindows) struct { if (ssl_config) |ssl_options| { bun.BoringSSL.load(); - const ctx_opts: uws.SocketContext.BunSocketContextOptions = jsc.API.ServerConfig.SSLConfig.asUSockets(ssl_options); + const ctx_opts: uws.SocketContext.BunSocketContextOptions = ssl_options.asUSockets(); var err: uws.create_bun_socket_error_t = .none; // Create SSL context using uSockets to match behavior of node.js const ctx = ctx_opts.createSSLContext(&err) orelse return error.InvalidOptions; // invalid options @@ -984,13 +993,18 @@ const bun = @import("bun"); const Async = bun.Async; const Environment = bun.Environment; const Output = bun.Output; -const api = bun.api; const default_allocator = bun.default_allocator; const strings = bun.strings; const uws = bun.uws; const BoringSSL = bun.BoringSSL.c; const uv = bun.windows.libuv; +const api = bun.api; +const Handlers = bun.api.SocketHandlers; +const TCPSocket = bun.api.TCPSocket; +const TLSSocket = bun.api.TLSSocket; +const SSLConfig = bun.api.ServerConfig.SSLConfig; + const NewSocket = api.socket.NewSocket; const SocketConfig = api.socket.SocketConfig; const WindowsNamedPipeContext = api.socket.WindowsNamedPipeContext; @@ -1000,7 +1014,3 @@ const JSGlobalObject = jsc.JSGlobalObject; const JSValue = jsc.JSValue; const ZigString = jsc.ZigString; const NodePath = jsc.Node.path; - -const Handlers = jsc.API.SocketHandlers; -const TCPSocket = jsc.API.TCPSocket; -const TLSSocket = jsc.API.TLSSocket; diff --git a/src/bun.js/api/bun/ssl_wrapper.zig b/src/bun.js/api/bun/ssl_wrapper.zig index 829c4dea43..819107fa9e 100644 --- a/src/bun.js/api/bun/ssl_wrapper.zig +++ b/src/bun.js/api/bun/ssl_wrapper.zig @@ -93,7 +93,7 @@ pub fn SSLWrapper(comptime T: type) type { pub fn init(ssl_options: jsc.API.ServerConfig.SSLConfig, is_client: bool, handlers: Handlers) !This { bun.BoringSSL.load(); - const ctx_opts: uws.SocketContext.BunSocketContextOptions = jsc.API.ServerConfig.SSLConfig.asUSockets(ssl_options); + const ctx_opts: uws.SocketContext.BunSocketContextOptions = ssl_options.asUSockets(); var err: uws.create_bun_socket_error_t = .none; // Create SSL context using uSockets to match behavior of node.js const ctx = ctx_opts.createSSLContext(&err) orelse return error.InvalidOptions; // invalid options diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index c90540d795..1448dc2e52 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2741,7 +2741,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d // apply SNI routes if any if (this.config.sni) |*sni| { for (sni.slice()) |*sni_ssl_config| { - const sni_servername: [:0]const u8 = std.mem.span(sni_ssl_config.server_name); + const sni_servername: [:0]const u8 = std.mem.span(sni_ssl_config.server_name.?); if (sni_servername.len > 0) { app.addServerNameWithOptions(sni_servername, sni_ssl_config.asUSockets()) catch { if (!globalThis.hasException()) { diff --git a/src/bun.js/api/server/SSLConfig.bindv2.ts b/src/bun.js/api/server/SSLConfig.bindv2.ts new file mode 100644 index 0000000000..04a3f0b0f1 --- /dev/null +++ b/src/bun.js/api/server/SSLConfig.bindv2.ts @@ -0,0 +1,90 @@ +import * as b from "bindgenv2"; + +export const SSLConfigSingleFile = b.union("SSLConfigSingleFile", { + string: b.String, + buffer: b.ArrayBuffer, + file: b.Blob, +}); + +export const SSLConfigFile = b.union("SSLConfigFile", { + none: b.null, + string: b.String, + buffer: b.ArrayBuffer, + file: b.Blob, + array: b.Array(SSLConfigSingleFile), +}); + +export const ALPNProtocols = b.union("ALPNProtocols", { + none: b.null, + string: b.String, + buffer: b.ArrayBuffer, +}); + +export const SSLConfig = b.dictionary( + { + name: "SSLConfig", + userFacingName: "TLSOptions", + generateConversionFunction: true, + }, + { + passphrase: b.String.nullable, + dhParamsFile: { + type: b.String.nullable, + internalName: "dh_params_file", + }, + serverName: { + type: b.String.nullable, + internalName: "server_name", + altNames: ["servername"], + }, + lowMemoryMode: { + type: b.bool, + default: false, + internalName: "low_memory_mode", + }, + rejectUnauthorized: { + type: b.bool.nullable, + internalName: "reject_unauthorized", + }, + requestCert: { + type: b.bool, + default: false, + internalName: "request_cert", + }, + ca: SSLConfigFile, + cert: SSLConfigFile, + key: SSLConfigFile, + secureOptions: { + type: b.u32, + default: 0, + internalName: "secure_options", + }, + keyFile: { + type: b.String.nullable, + internalName: "key_file", + }, + certFile: { + type: b.String.nullable, + internalName: "cert_file", + }, + caFile: { + type: b.String.nullable, + internalName: "ca_file", + }, + ALPNProtocols: { + type: ALPNProtocols, + internalName: "alpn_protocols", + }, + ciphers: b.String.nullable, + clientRenegotiationLimit: { + type: b.u32, + default: 0, + internalName: "client_renegotiation_limit", + }, + clientRenegotiationWindow: { + type: b.u32, + default: 0, + internalName: "client_renegotiation_window", + }, + }, +); diff --git a/src/bun.js/api/server/SSLConfig.zig b/src/bun.js/api/server/SSLConfig.zig index fe309c8d4e..3d36e04b5c 100644 --- a/src/bun.js/api/server/SSLConfig.zig +++ b/src/bun.js/api/server/SSLConfig.zig @@ -1,65 +1,60 @@ const SSLConfig = @This(); -server_name: [*c]const u8 = null, +server_name: ?[*:0]const u8 = null, -key_file_name: [*c]const u8 = null, -cert_file_name: [*c]const u8 = null, +key_file_name: ?[*:0]const u8 = null, +cert_file_name: ?[*:0]const u8 = null, -ca_file_name: [*c]const u8 = null, -dh_params_file_name: [*c]const u8 = null, +ca_file_name: ?[*:0]const u8 = null, +dh_params_file_name: ?[*:0]const u8 = null, -passphrase: [*c]const u8 = null, +passphrase: ?[*:0]const u8 = null, -key: ?[][*c]const u8 = null, -key_count: u32 = 0, - -cert: ?[][*c]const u8 = null, -cert_count: u32 = 0, - -ca: ?[][*c]const u8 = null, -ca_count: u32 = 0, +key: ?[][*:0]const u8 = null, +cert: ?[][*:0]const u8 = null, +ca: ?[][*:0]const u8 = null, secure_options: u32 = 0, request_cert: i32 = 0, reject_unauthorized: i32 = 0, ssl_ciphers: ?[*:0]const u8 = null, protos: ?[*:0]const u8 = null, -protos_len: usize = 0, client_renegotiation_limit: u32 = 0, client_renegotiation_window: u32 = 0, requires_custom_request_ctx: bool = false, is_using_default_ciphers: bool = true, low_memory_mode: bool = false, -const BlobFileContentResult = struct { - data: [:0]const u8, - - fn init(comptime fieldname: []const u8, js_obj: jsc.JSValue, global: *jsc.JSGlobalObject) bun.JSError!?BlobFileContentResult { - { - const body = try jsc.WebCore.Body.Value.fromJS(global, js_obj); - if (body == .Blob and body.Blob.store != null and body.Blob.store.?.data == .file) { - var fs: jsc.Node.fs.NodeFS = .{}; - const read = fs.readFileWithOptions(.{ .path = body.Blob.store.?.data.file.pathlike }, .sync, .null_terminated); - switch (read) { - .err => { - return global.throwValue(read.err.toJS(global)); - }, - else => { - const str = read.result.null_terminated; - if (str.len > 0) { - return .{ .data = str }; - } - return global.throwInvalidArguments(std.fmt.comptimePrint("Invalid {s} file", .{fieldname}), .{}); - }, - } - } - } - - return null; - } +const ReadFromBlobError = bun.JSError || error{ + NullStore, + NotAFile, + EmptyFile, }; -pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { +fn readFromBlob( + global: *jsc.JSGlobalObject, + blob: *bun.webcore.Blob, +) ReadFromBlobError![:0]const u8 { + const store = blob.store orelse return error.NullStore; + const file = switch (store.data) { + .file => |f| f, + else => return error.NotAFile, + }; + var fs: jsc.Node.fs.NodeFS = .{}; + const maybe = fs.readFileWithOptions( + .{ .path = file.pathlike }, + .sync, + .null_terminated, + ); + const result = switch (maybe) { + .result => |result| result, + .err => |err| return global.throwValue(err.toJS(global)), + }; + if (result.null_terminated.len == 0) return error.EmptyFile; + return bun.default_allocator.dupeZ(u8, result.null_terminated); +} + +pub fn asUSockets(this: *const SSLConfig) uws.SocketContext.BunSocketContextOptions { var ctx_opts: uws.SocketContext.BunSocketContextOptions = .{}; if (this.key_file_name != null) @@ -76,15 +71,15 @@ pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { if (this.key) |key| { ctx_opts.key = key.ptr; - ctx_opts.key_count = this.key_count; + ctx_opts.key_count = @intCast(key.len); } if (this.cert) |cert| { ctx_opts.cert = cert.ptr; - ctx_opts.cert_count = this.cert_count; + ctx_opts.cert_count = @intCast(cert.len); } if (this.ca) |ca| { ctx_opts.ca = ca.ptr; - ctx_opts.ca_count = this.ca_count; + ctx_opts.ca_count = @intCast(ca.len); } if (this.ssl_ciphers != null) { @@ -96,595 +91,295 @@ pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { return ctx_opts; } -pub fn isSame(thisConfig: *const SSLConfig, otherConfig: *const SSLConfig) bool { - { //strings - const fields = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "ssl_ciphers", - "protos", - }; - - inline for (fields) |field| { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - if (lhs != null and rhs != null) { - if (!stringsEqual(lhs, rhs)) - return false; - } else if (lhs != null or rhs != null) { - return false; - } - } - } - - { - //numbers - const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode" }; - - inline for (fields) |field| { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - if (lhs != rhs) - return false; - } - } - - { - // complex fields - const fields = .{ "key", "ca", "cert" }; - inline for (fields) |field| { - const lhs_count = @field(thisConfig, field ++ "_count"); - const rhs_count = @field(otherConfig, field ++ "_count"); - if (lhs_count != rhs_count) - return false; - if (lhs_count > 0) { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - for (0..lhs_count) |i| { - if (!stringsEqual(lhs.?[i], rhs.?[i])) - return false; +pub fn isSame(this: *const SSLConfig, other: *const SSLConfig) bool { + inline for (comptime std.meta.fieldNames(SSLConfig)) |field| { + const first = @field(this, field); + const second = @field(other, field); + switch (@FieldType(SSLConfig, field)) { + ?[*:0]const u8 => { + const a = first orelse return second == null; + const b = second orelse return false; + if (!stringsEqual(a, b)) return false; + }, + ?[][*:0]const u8 => { + const slice1 = first orelse return second == null; + const slice2 = second orelse return false; + if (slice1.len != slice2.len) return false; + for (slice1, slice2) |a, b| { + if (!stringsEqual(a, b)) return false; } - } + }, + else => if (first != second) return false, } } - return true; } -fn stringsEqual(a: [*c]const u8, b: [*c]const u8) bool { +fn stringsEqual(a: [*:0]const u8, b: [*:0]const u8) bool { const lhs = bun.asByteSlice(a); const rhs = bun.asByteSlice(b); return strings.eqlLong(lhs, rhs, true); } -pub fn deinit(this: *SSLConfig) void { - const fields = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "protos", - }; - - if (!this.is_using_default_ciphers) { - if (this.ssl_ciphers) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - } - - inline for (fields) |field| { - if (@field(this, field)) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - @field(this, field) = ""; - } - } - - if (this.cert) |cert| { - for (0..this.cert_count) |i| { - const slice = std.mem.span(cert[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(cert); - this.cert = null; - } - - if (this.key) |key| { - for (0..this.key_count) |i| { - const slice = std.mem.span(key[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(key); - this.key = null; - } - - if (this.ca) |ca| { - for (0..this.ca_count) |i| { - const slice = std.mem.span(ca[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(ca); - this.ca = null; +fn freeStrings(slice: *?[][*:0]const u8) void { + const inner = slice.* orelse return; + for (inner) |string| { + bun.freeSensitive(bun.default_allocator, std.mem.span(string)); } + bun.default_allocator.free(inner); + slice.* = null; } + +fn freeString(string: *?[*:0]const u8) void { + const inner = string.* orelse return; + bun.freeSensitive(bun.default_allocator, std.mem.span(inner)); + string.* = null; +} + +pub fn deinit(this: *SSLConfig) void { + bun.meta.useAllFields(SSLConfig, .{ + .server_name = freeString(&this.server_name), + .key_file_name = freeString(&this.key_file_name), + .cert_file_name = freeString(&this.cert_file_name), + .ca_file_name = freeString(&this.ca_file_name), + .dh_params_file_name = freeString(&this.dh_params_file_name), + .passphrase = freeString(&this.passphrase), + .key = freeStrings(&this.key), + .cert = freeStrings(&this.cert), + .ca = freeStrings(&this.ca), + .secure_options = {}, + .request_cert = {}, + .reject_unauthorized = {}, + .ssl_ciphers = freeString(&this.ssl_ciphers), + .protos = freeString(&this.protos), + .client_renegotiation_limit = {}, + .client_renegotiation_window = {}, + .requires_custom_request_ctx = {}, + .is_using_default_ciphers = {}, + .low_memory_mode = {}, + }); +} + +fn cloneStrings(slice: ?[][*:0]const u8) ?[][*:0]const u8 { + const inner = slice orelse return null; + const result = bun.handleOom(bun.default_allocator.alloc([*:0]const u8, inner.len)); + for (inner, result) |string, *out| { + out.* = bun.handleOom(bun.default_allocator.dupeZ(u8, std.mem.span(string))); + } + return result; +} + +fn cloneString(string: ?[*:0]const u8) ?[*:0]const u8 { + return bun.handleOom(bun.default_allocator.dupeZ(u8, std.mem.span(string orelse return null))); +} + pub fn clone(this: *const SSLConfig) SSLConfig { - var cloned: SSLConfig = .{ + return .{ + .server_name = cloneString(this.server_name), + .key_file_name = cloneString(this.key_file_name), + .cert_file_name = cloneString(this.cert_file_name), + .ca_file_name = cloneString(this.ca_file_name), + .dh_params_file_name = cloneString(this.dh_params_file_name), + .passphrase = cloneString(this.passphrase), + .key = cloneStrings(this.key), + .cert = cloneStrings(this.cert), + .ca = cloneStrings(this.ca), .secure_options = this.secure_options, .request_cert = this.request_cert, .reject_unauthorized = this.reject_unauthorized, + .ssl_ciphers = cloneString(this.ssl_ciphers), + .protos = cloneString(this.protos), .client_renegotiation_limit = this.client_renegotiation_limit, .client_renegotiation_window = this.client_renegotiation_window, .requires_custom_request_ctx = this.requires_custom_request_ctx, .is_using_default_ciphers = this.is_using_default_ciphers, .low_memory_mode = this.low_memory_mode, - .protos_len = this.protos_len, }; - const fields_cloned_by_memcopy = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "protos", - }; - - if (!this.is_using_default_ciphers) { - if (this.ssl_ciphers) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - cloned.ssl_ciphers = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } else { - cloned.ssl_ciphers = null; - } - } - } - - inline for (fields_cloned_by_memcopy) |field| { - if (@field(this, field)) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - @field(cloned, field) = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } - } - - const array_fields_cloned_by_memcopy = .{ - "cert", - "key", - "ca", - }; - inline for (array_fields_cloned_by_memcopy) |field| { - if (@field(this, field)) |array| { - const cloned_array = bun.handleOom(bun.default_allocator.alloc([*c]const u8, @field(this, field ++ "_count"))); - @field(cloned, field) = cloned_array; - @field(cloned, field ++ "_count") = @field(this, field ++ "_count"); - for (0..@field(this, field ++ "_count")) |i| { - const slice = std.mem.span(array[i]); - if (slice.len > 0) { - cloned_array[i] = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } else { - cloned_array[i] = ""; - } - } - } - } - return cloned; } pub const zero = SSLConfig{}; -pub fn fromJS(vm: *jsc.VirtualMachine, global: *jsc.JSGlobalObject, obj: jsc.JSValue) bun.JSError!?SSLConfig { - var result = zero; +pub fn fromJS( + vm: *jsc.VirtualMachine, + global: *jsc.JSGlobalObject, + value: jsc.JSValue, +) bun.JSError!?SSLConfig { + var generated: jsc.generated.SSLConfig = try .fromJS(global, value); + defer generated.deinit(); + var result: SSLConfig = zero; errdefer result.deinit(); - - var arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - - if (!obj.isObject()) { - return global.throwInvalidArguments("tls option expects an object", .{}); - } - var any = false; - result.reject_unauthorized = @intFromBool(vm.getTLSRejectUnauthorized()); - - // Required - if (try obj.getTruthy(global, "keyFile")) |key_file_name| { - var sliced = try key_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.key_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.key_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Unable to access keyFile path", .{}); - } - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "key")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("key", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - result.requires_custom_request_ctx = true; - any = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all keys - result.key = native_array; - return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.key = native_array; - } - - result.key_count = valid_count; - } - } else if (try BlobFileContentResult.init("key", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.key = native_array; - result.key_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.key = native_array; - result.key_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.key = native_array; - return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getTruthy(global, "certFile")) |cert_file_name| { - var sliced = try cert_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.cert_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.cert_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Unable to access certFile path", .{}); - } - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "ALPNProtocols")) |protocols| { - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - result.protos = try bun.default_allocator.dupeZ(u8, sliced); - result.protos_len = sliced.len; - } - - any = true; - result.requires_custom_request_ctx = true; - } else { - return global.throwInvalidArguments("ALPNProtocols argument must be an string, Buffer or TypedArray", .{}); - } - } - - if (try obj.getTruthy(global, "cert")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("cert", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - result.requires_custom_request_ctx = true; - any = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all certs - result.cert = native_array; - return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.cert = native_array; - } - - result.cert_count = valid_count; - } - } else if (try BlobFileContentResult.init("cert", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.cert = native_array; - result.cert_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.cert = native_array; - result.cert_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.cert = native_array; - return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getBooleanStrict(global, "requestCert")) |request_cert| { - result.request_cert = if (request_cert) 1 else 0; + if (generated.passphrase.get()) |passphrase| { + result.passphrase = passphrase.toOwnedSliceZ(bun.default_allocator); any = true; } - - if (try obj.getBooleanStrict(global, "rejectUnauthorized")) |reject_unauthorized| { - result.reject_unauthorized = if (reject_unauthorized) 1 else 0; + if (generated.dh_params_file.get()) |dh_params_file| { + result.dh_params_file_name = try handlePath(global, "dhParamsFile", dh_params_file); any = true; } - - if (try obj.getTruthy(global, "ciphers")) |ssl_ciphers| { - var sliced = try ssl_ciphers.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.ssl_ciphers = try bun.default_allocator.dupeZ(u8, sliced.slice()); - result.is_using_default_ciphers = false; - any = true; - result.requires_custom_request_ctx = true; - } - } - if (result.is_using_default_ciphers) { - result.ssl_ciphers = global.bunVM().rareData().tlsDefaultCiphers() orelse null; + if (generated.server_name.get()) |server_name| { + result.server_name = server_name.toOwnedSliceZ(bun.default_allocator); + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "serverName") orelse try obj.getTruthy(global, "servername")) |server_name| { - var sliced = try server_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.server_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - any = true; - result.requires_custom_request_ctx = true; - } + result.low_memory_mode = generated.low_memory_mode; + result.reject_unauthorized = @intFromBool( + generated.reject_unauthorized orelse vm.getTLSRejectUnauthorized(), + ); + result.request_cert = @intFromBool(generated.request_cert); + result.secure_options = generated.secure_options; + any = any or + result.low_memory_mode or + generated.reject_unauthorized != null or + generated.request_cert or + result.secure_options != 0; + + result.ca = try handleFileForField(global, "ca", &generated.ca); + result.cert = try handleFileForField(global, "cert", &generated.cert); + result.key = try handleFileForField(global, "key", &generated.key); + result.requires_custom_request_ctx = result.requires_custom_request_ctx or + result.ca != null or + result.cert != null or + result.key != null; + + if (generated.key_file.get()) |key_file| { + result.key_file_name = try handlePath(global, "keyFile", key_file); + result.requires_custom_request_ctx = true; + } + if (generated.cert_file.get()) |cert_file| { + result.cert_file_name = try handlePath(global, "certFile", cert_file); + result.requires_custom_request_ctx = true; + } + if (generated.ca_file.get()) |ca_file| { + result.ca_file_name = try handlePath(global, "caFile", ca_file); + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "ca")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("ca", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all CA's - result.cert = native_array; - return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.ca = native_array; - } - - result.ca_count = valid_count; - } - } else if (try BlobFileContentResult.init("ca", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.ca = native_array; - result.ca_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.ca = native_array; - result.ca_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.ca = native_array; - return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } + const protocols = switch (generated.alpn_protocols) { + .none => null, + .string => |*ref| ref.get().toOwnedSliceZ(bun.default_allocator), + .buffer => |*ref| blk: { + const buffer: jsc.ArrayBuffer = ref.get().asArrayBuffer(); + break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice()); + }, + }; + if (protocols) |some_protocols| { + result.protos = some_protocols; + result.requires_custom_request_ctx = true; + } + if (generated.ciphers.get()) |ciphers| { + result.ssl_ciphers = ciphers.toOwnedSliceZ(bun.default_allocator); + result.is_using_default_ciphers = false; + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "caFile")) |ca_file_name| { - var sliced = try ca_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.ca_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.ca_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Invalid caFile path", .{}); - } - } + result.client_renegotiation_limit = generated.client_renegotiation_limit; + result.client_renegotiation_window = generated.client_renegotiation_window; + any = any or + result.requires_custom_request_ctx or + result.client_renegotiation_limit != 0 or + generated.client_renegotiation_window != 0; + + // We don't need to deinit `result` if `any` is false. + return if (any) result else null; +} + +fn handlePath( + global: *jsc.JSGlobalObject, + comptime field: []const u8, + string: bun.string.WTFStringImpl, +) bun.JSError![:0]const u8 { + const name = string.toOwnedSliceZ(bun.default_allocator); + errdefer bun.freeSensitive(bun.default_allocator, name); + if (std.posix.system.access(name, std.posix.F_OK) != 0) { + return global.throwInvalidArguments( + std.fmt.comptimePrint("Unable to access {s} path", .{field}), + .{}, + ); } - // Optional - if (any) { - if (try obj.getTruthy(global, "secureOptions")) |secure_options| { - if (secure_options.isNumber()) { - result.secure_options = secure_options.toU32(); - } - } + return name; +} - if (try obj.getTruthy(global, "clientRenegotiationLimit")) |client_renegotiation_limit| { - if (client_renegotiation_limit.isNumber()) { - result.client_renegotiation_limit = client_renegotiation_limit.toU32(); - } - } +fn handleFileForField( + global: *jsc.JSGlobalObject, + comptime field: []const u8, + file: *const jsc.generated.SSLConfigFile, +) bun.JSError!?[][*:0]const u8 { + return handleFile(global, file) catch |err| switch (err) { + error.JSError => return error.JSError, + error.OutOfMemory => return error.OutOfMemory, + error.EmptyFile => return global.throwInvalidArguments( + std.fmt.comptimePrint("TLSOptions.{s} is an empty file", .{field}), + .{}, + ), + error.NullStore, error.NotAFile => return global.throwInvalidArguments( + std.fmt.comptimePrint( + "TLSOptions.{s} is not a valid BunFile (non-BunFile `Blob`s are not supported)", + .{field}, + ), + .{}, + ), + }; +} - if (try obj.getTruthy(global, "clientRenegotiationWindow")) |client_renegotiation_window| { - if (client_renegotiation_window.isNumber()) { - result.client_renegotiation_window = client_renegotiation_window.toU32(); - } - } - - if (try obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| { - var sliced = try dh_params_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.dh_params_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.dh_params_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Invalid dhParamsFile path", .{}); - } - } - } - - if (try obj.getTruthy(global, "passphrase")) |passphrase| { - var sliced = try passphrase.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.passphrase = try bun.default_allocator.dupeZ(u8, sliced.slice()); - } - } - - if (try obj.get(global, "lowMemoryMode")) |low_memory_mode| { - if (low_memory_mode.isBoolean() or low_memory_mode.isUndefined()) { - result.low_memory_mode = low_memory_mode.toBoolean(); - any = true; - } else { - return global.throw("Expected lowMemoryMode to be a boolean", .{}); - } - } - } - - if (!any) - return null; +fn handleFile( + global: *jsc.JSGlobalObject, + file: *const jsc.generated.SSLConfigFile, +) ReadFromBlobError!?[][*:0]const u8 { + const single = try handleSingleFile(global, switch (file.*) { + .none => return null, + .string => |*ref| .{ .string = ref.get() }, + .buffer => |*ref| .{ .buffer = ref.get() }, + .file => |*ref| .{ .file = ref.get() }, + .array => |*list| return try handleFileArray(global, list.items()), + }); + errdefer bun.freeSensitive(bun.default_allocator, single); + const result = try bun.default_allocator.alloc([*:0]const u8, 1); + result[0] = single; return result; } +fn handleFileArray( + global: *jsc.JSGlobalObject, + elements: []const jsc.generated.SSLConfigSingleFile, +) ReadFromBlobError!?[][*:0]const u8 { + if (elements.len == 0) return null; + var result: bun.collections.ArrayListDefault([*:0]const u8) = try .initCapacity(elements.len); + errdefer { + for (result.items()) |string| { + bun.freeSensitive(bun.default_allocator, std.mem.span(string)); + } + result.deinit(); + } + for (elements) |*elem| { + result.appendAssumeCapacity(try handleSingleFile(global, switch (elem.*) { + .string => |*ref| .{ .string = ref.get() }, + .buffer => |*ref| .{ .buffer = ref.get() }, + .file => |*ref| .{ .file = ref.get() }, + })); + } + return try result.toOwnedSlice(); +} + +fn handleSingleFile( + global: *jsc.JSGlobalObject, + file: union(enum) { + string: bun.string.WTFStringImpl, + buffer: *jsc.JSCArrayBuffer, + file: *bun.webcore.Blob, + }, +) ReadFromBlobError![:0]const u8 { + return switch (file) { + .string => |string| string.toOwnedSliceZ(bun.default_allocator), + .buffer => |jsc_buffer| blk: { + const buffer: jsc.ArrayBuffer = jsc_buffer.asArrayBuffer(); + break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice()); + }, + .file => |blob| try readFromBlob(global, blob), + }; +} + const std = @import("std"); const bun = @import("bun"); diff --git a/src/bun.js/api/server/ServerConfig.zig b/src/bun.js/api/server/ServerConfig.zig index 5e347924d5..8a1caca83d 100644 --- a/src/bun.js/api/server/ServerConfig.zig +++ b/src/bun.js/api/server/ServerConfig.zig @@ -931,7 +931,7 @@ pub fn fromJS( if (args.ssl_config == null) { args.ssl_config = ssl_config; } else { - if (ssl_config.server_name == null or std.mem.span(ssl_config.server_name).len == 0) { + if ((ssl_config.server_name orelse "")[0] == 0) { defer ssl_config.deinit(); return global.throwInvalidArguments("SNI tls object must have a serverName", .{}); } diff --git a/src/bun.js/api/sql.classes.ts b/src/bun.js/api/sql.classes.ts index 3fdfe17a8d..ee1405ca47 100644 --- a/src/bun.js/api/sql.classes.ts +++ b/src/bun.js/api/sql.classes.ts @@ -1,7 +1,7 @@ -import { define } from "../../codegen/class-definitions"; +import { ClassDefinition, define } from "../../codegen/class-definitions"; const types = ["PostgresSQL", "MySQL"]; -const classes = []; +const classes: ClassDefinition[] = []; for (const type of types) { classes.push( define({ diff --git a/src/bun.js/bindgen.zig b/src/bun.js/bindgen.zig new file mode 100644 index 0000000000..be303cb6ff --- /dev/null +++ b/src/bun.js/bindgen.zig @@ -0,0 +1,254 @@ +pub fn BindgenTrivial(comptime T: type) type { + return struct { + pub const ZigType = T; + pub const ExternType = T; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return extern_value; + } + }; +} + +pub const BindgenBool = BindgenTrivial(bool); +pub const BindgenU8 = BindgenTrivial(u8); +pub const BindgenI8 = BindgenTrivial(i8); +pub const BindgenU16 = BindgenTrivial(u16); +pub const BindgenI16 = BindgenTrivial(i16); +pub const BindgenU32 = BindgenTrivial(u32); +pub const BindgenI32 = BindgenTrivial(i32); +pub const BindgenU64 = BindgenTrivial(u64); +pub const BindgenI64 = BindgenTrivial(i64); +pub const BindgenF64 = BindgenTrivial(f64); +pub const BindgenRawAny = BindgenTrivial(jsc.JSValue); + +pub const BindgenStrongAny = struct { + pub const ZigType = jsc.Strong; + pub const ExternType = ?*jsc.Strong.Impl; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .{ .impl = extern_value.? }; + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .{ .impl = extern_value }; + } +}; + +/// This represents both `IDLNull` and `IDLMonostateUndefined`. +pub const BindgenNull = struct { + pub const ZigType = void; + pub const ExternType = u8; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + _ = extern_value; + } +}; + +pub fn BindgenOptional(comptime Child: type) type { + return struct { + pub const ZigType = if (@hasDecl(Child, "OptionalZigType")) + Child.OptionalZigType + else + ?Child.ZigType; + + pub const ExternType = if (@hasDecl(Child, "OptionalExternType")) + Child.OptionalExternType + else + ExternTaggedUnion(&.{ u8, Child.ExternType }); + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + if (comptime @hasDecl(Child, "OptionalExternType")) { + return Child.convertOptionalFromExtern(extern_value); + } + if (extern_value.tag == 0) { + return null; + } + bun.assert_eql(extern_value.tag, 1); + return Child.convertFromExtern(extern_value.data.@"1"); + } + }; +} + +pub const BindgenString = struct { + pub const ZigType = bun.string.WTFString; + pub const ExternType = ?bun.string.WTFStringImpl; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .adopt(extern_value.?); + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .adopt(extern_value); + } +}; + +pub fn BindgenUnion(comptime children: []const type) type { + var tagged_field_types: [children.len]type = undefined; + var untagged_field_types: [children.len]type = undefined; + for (&tagged_field_types, &untagged_field_types, children) |*tagged, *untagged, *child| { + tagged.* = child.ZigType; + untagged.* = child.ExternType; + } + + const tagged_field_types_const = tagged_field_types; + const untagged_field_types_const = untagged_field_types; + const zig_type = bun.meta.TaggedUnion(&tagged_field_types_const); + const extern_type = ExternTaggedUnion(&untagged_field_types_const); + + return struct { + pub const ZigType = zig_type; + pub const ExternType = extern_type; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + const tag: std.meta.Tag(ZigType) = @enumFromInt(extern_value.tag); + return switch (tag) { + inline else => |t| @unionInit( + ZigType, + @tagName(t), + children[@intFromEnum(t)].convertFromExtern( + @field(extern_value.data, @tagName(t)), + ), + ), + }; + } + }; +} + +pub fn ExternTaggedUnion(comptime field_types: []const type) type { + if (comptime field_types.len > std.math.maxInt(u8)) { + @compileError("too many union fields"); + } + return extern struct { + data: ExternUnion(field_types), + tag: u8, + }; +} + +fn ExternUnion(comptime field_types: []const type) type { + var info = @typeInfo(bun.meta.TaggedUnion(field_types)); + info.@"union".tag_type = null; + info.@"union".layout = .@"extern"; + info.@"union".decls = &.{}; + return @Type(info); +} + +pub fn BindgenArray(comptime Child: type) type { + return struct { + pub const ZigType = bun.collections.ArrayListDefault(Child.ZigType); + pub const ExternType = ExternArrayList(Child.ExternType); + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + const length: usize = @intCast(extern_value.length); + const capacity: usize = @intCast(extern_value.capacity); + + const data = extern_value.data orelse return .init(); + bun.assertf( + length <= capacity, + "length ({d}) should not exceed capacity ({d})", + .{ length, capacity }, + ); + var unmanaged: std.ArrayListUnmanaged(Child.ExternType) = .{ + .items = data[0..length], + .capacity = capacity, + }; + + if (comptime !bun.use_mimalloc) { + // Don't reuse memory in this case; it would be freed by the wrong allocator. + } else if (comptime Child.ZigType == Child.ExternType) { + return .fromUnmanaged(.{}, unmanaged); + } else if (comptime @sizeOf(Child.ZigType) <= @sizeOf(Child.ExternType) and + @alignOf(Child.ZigType) <= bun.allocators.mimalloc.MI_MAX_ALIGN_SIZE) + { + // We can reuse the allocation, but we still need to convert the elements. + var storage: []u8 = @ptrCast(unmanaged.allocatedSlice()); + + // Convert the elements. + for (0..length) |i| { + // Zig doesn't have a formal aliasing model, so we should be maximally + // pessimistic. + var old_elem: Child.ExternType = undefined; + @memcpy( + std.mem.asBytes(&old_elem), + storage[i * @sizeOf(Child.ExternType) ..][0..@sizeOf(Child.ExternType)], + ); + const new_elem = Child.convertFromExtern(old_elem); + @memcpy( + storage[i * @sizeOf(Child.ZigType) ..][0..@sizeOf(Child.ZigType)], + std.mem.asBytes(&new_elem), + ); + } + + const new_size_is_multiple = + comptime @sizeOf(Child.ExternType) % @sizeOf(Child.ZigType) == 0; + const new_capacity = if (comptime new_size_is_multiple) + capacity * (@sizeOf(Child.ExternType) / @sizeOf(Child.ZigType)) + else blk: { + const new_capacity = storage.len / @sizeOf(Child.ZigType); + const new_alloc_size = new_capacity * @sizeOf(Child.ZigType); + if (new_alloc_size != storage.len) { + // Allocation isn't a multiple of `@sizeOf(Child.ZigType)`; we have to + // resize it. + storage = bun.handleOom( + bun.default_allocator.realloc(storage, new_alloc_size), + ); + } + break :blk new_capacity; + }; + + const items_ptr: [*]Child.ZigType = @ptrCast(@alignCast(storage.ptr)); + const new_unmanaged: std.ArrayListUnmanaged(Child.ZigType) = .{ + .items = items_ptr[0..length], + .capacity = new_capacity, + }; + return .fromUnmanaged(.{}, new_unmanaged); + } + + defer unmanaged.deinit( + if (bun.use_mimalloc) bun.default_allocator else std.heap.raw_c_allocator, + ); + var result = bun.handleOom(ZigType.initCapacity(length)); + for (unmanaged.items) |*item| { + result.appendAssumeCapacity(Child.convertFromExtern(item.*)); + } + return result; + } + }; +} + +fn ExternArrayList(comptime Child: type) type { + return extern struct { + data: ?[*]Child, + length: c_uint, + capacity: c_uint, + }; +} + +fn BindgenExternalShared(comptime T: type) type { + return struct { + pub const ZigType = bun.ptr.ExternalShared(T); + pub const ExternType = ?*T; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .adopt(extern_value.?); + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .adopt(extern_value); + } + }; +} + +pub const BindgenArrayBuffer = BindgenExternalShared(jsc.JSCArrayBuffer); +pub const BindgenBlob = BindgenExternalShared(webcore.Blob); + +const bun = @import("bun"); +const std = @import("std"); + +const jsc = bun.bun_js.jsc; +const webcore = bun.bun_js.webcore; diff --git a/src/bun.js/bindings/Bindgen.h b/src/bun.js/bindings/Bindgen.h new file mode 100644 index 0000000000..e1bcb166af --- /dev/null +++ b/src/bun.js/bindings/Bindgen.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "Bindgen/ExternTraits.h" +#include "Bindgen/IDLTypes.h" +#include "Bindgen/IDLConvertBase.h" diff --git a/src/bun.js/bindings/Bindgen/ExternTraits.h b/src/bun.js/bindings/Bindgen/ExternTraits.h new file mode 100644 index 0000000000..af9d63f8db --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternTraits.h @@ -0,0 +1,152 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ExternUnion.h" + +namespace Bun::Bindgen { + +template +struct ExternTraits; + +template +struct TrivialExtern { + using ExternType = T; + + static ExternType convertToExtern(T&& cppValue) + { + return std::move(cppValue); + } +}; + +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; + +enum ExternNullPtr : std::uint8_t {}; + +template<> struct ExternTraits { + using ExternType = ExternNullPtr; + + static ExternType convertToExtern(std::nullptr_t cppValue) + { + return ExternType { 0 }; + } +}; + +template<> struct ExternTraits { + using ExternType = ExternNullPtr; + + static ExternType convertToExtern(std::monostate cppValue) + { + return ExternType { 0 }; + } +}; + +template +struct ExternVariant { + ExternUnion data; + std::uint8_t tag; + + static_assert(sizeof...(Args) > 0); + static_assert(sizeof...(Args) - 1 <= std::numeric_limits::max()); + + explicit ExternVariant(std::variant&& variant) + : tag(static_cast(variant.index())) + { + data.initFromVariant(std::move(variant)); + } +}; + +template +struct ExternTraits> { + using ExternType = ExternVariant::ExternType...>; + + static ExternType convertToExtern(std::variant&& cppValue) + { + using VariantOfExtern = std::variant::ExternType...>; + return ExternType { std::visit([](auto&& arg) -> VariantOfExtern { + using ArgType = std::decay_t; + return { ExternTraits::convertToExtern(std::move(arg)) }; + }, + std::move(cppValue)) }; + } +}; + +template +struct ExternTraits> { + using ExternType = ExternVariant::ExternType>; + + static ExternType convertToExtern(std::optional&& cppValue) + { + using StdVariant = std::variant::ExternType>; + if (!cppValue) { + return ExternType { StdVariant { ExternNullPtr {} } }; + } + return ExternType { StdVariant { ExternTraits::convertToExtern(std::move(*cppValue)) } }; + } +}; + +template<> struct ExternTraits { + using ExternType = WTF::StringImpl*; + + static ExternType convertToExtern(WTF::String&& cppValue) + { + return cppValue.releaseImpl().leakRef(); + } +}; + +template<> struct ExternTraits { + using ExternType = JSC::EncodedJSValue; + + static ExternType convertToExtern(JSC::JSValue cppValue) + { + return JSC::JSValue::encode(cppValue); + } +}; + +template<> struct ExternTraits { + using ExternType = JSC::JSValue*; + + static ExternType convertToExtern(Bun::StrongRef&& cppValue) + { + return cppValue.release(); + } +}; + +template struct ExternTraits> { + using ExternType = T*; + + static ExternType convertToExtern(WTF::Ref&& cppValue) + { + return &cppValue.leakRef(); + } +}; + +template struct ExternTraits> { + using ExternType = T*; + + static ExternType convertToExtern(WTF::RefPtr&& cppValue) + { + return cppValue.leakRef(); + } +}; + +} diff --git a/src/bun.js/bindings/Bindgen/ExternUnion.h b/src/bun.js/bindings/Bindgen/ExternUnion.h new file mode 100644 index 0000000000..9a92950fdb --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternUnion.h @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include +#include +#include "Macros.h" + +#define BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, ...) \ + template \ + union ExternUnion { \ + BUN_BINDGEN_DETAIL_FOREACH( \ + BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD, \ + T0 __VA_OPT__(, ) __VA_ARGS__) \ + void initFromVariant( \ + std::variant&& variant) \ + { \ + const std::size_t index = variant.index(); \ + std::visit([this, index](auto&& arg) { \ + using Arg = std::decay_t; \ + BUN_BINDGEN_DETAIL_FOREACH( \ + BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT, \ + T0 __VA_OPT__(, ) __VA_ARGS__) \ + }, \ + std::move(variant)); \ + } \ + } + +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_TEMPLATE_PARAM(Type) , typename Type +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD(Type) \ + static_assert(std::is_trivially_copyable_v); \ + Type alternative##Type; +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT(Type) \ + if constexpr (std::is_same_v) { \ + if (index == ::Bun::Bindgen::Detail::indexOf##Type) { \ + alternative##Type = std::move(arg); \ + return; \ + } \ + } + +namespace Bun::Bindgen { +namespace Detail { +// For use in macros. +static constexpr std::size_t indexOfT0 = 0; +static constexpr std::size_t indexOfT1 = 1; +static constexpr std::size_t indexOfT2 = 2; +static constexpr std::size_t indexOfT3 = 3; +static constexpr std::size_t indexOfT4 = 4; +static constexpr std::size_t indexOfT5 = 5; +static constexpr std::size_t indexOfT6 = 6; +static constexpr std::size_t indexOfT7 = 7; +static constexpr std::size_t indexOfT8 = 8; +static constexpr std::size_t indexOfT9 = 9; +static constexpr std::size_t indexOfT10 = 10; +static constexpr std::size_t indexOfT11 = 11; +static constexpr std::size_t indexOfT12 = 12; +static constexpr std::size_t indexOfT13 = 13; +static constexpr std::size_t indexOfT14 = 14; +static constexpr std::size_t indexOfT15 = 15; +} + +template +union ExternUnion; + +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7, T8); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15); +} diff --git a/src/bun.js/bindings/Bindgen/ExternVectorTraits.h b/src/bun.js/bindings/Bindgen/ExternVectorTraits.h new file mode 100644 index 0000000000..f6f62bc8f6 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternVectorTraits.h @@ -0,0 +1,149 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#if ASAN_ENABLED && __has_include() +#include +#endif + +namespace Bun::Bindgen { + +template +struct ExternTraits; + +template +struct ExternVector { + T* data; + // WTF::Vector stores the length and capacity as `unsigned`. We can save space by using that + // instead of `std::size_t` here. + unsigned length; + unsigned capacity; +}; + +namespace Detail { +template +void asanSetBufferSizeToFullCapacity(T* buffer, std::size_t length, std::size_t capacity) +{ +#if ASAN_ENABLED + // Without this, ASan will complain if Zig touches memory in the range + // [storage + length, storage + capacity), which will always happen when freeing the + // memory in Debug mode when Zig writes 0xaa to it. + __sanitizer_annotate_contiguous_container( + buffer, // beg + buffer + capacity, // end + buffer + length, // old_mid + buffer + capacity // new_mid + ); +#endif +} +} + +template +struct ExternTraits> { +private: + using CPPType = WTF::Vector; + using ExternElement = ExternTraits::ExternType; + +public: + using ExternType = ExternVector; + + static ExternType convertToExtern(CPPType&& cppValue) + { + if constexpr (std::is_same_v) { + // We can reuse the allocation. + alignas(CPPType) std::byte cppStorage[sizeof(CPPType)]; + // This prevents the contents from being freed or destructed. + CPPType* const vec = new (cppStorage) CPPType { std::move(cppValue) }; + T* const buffer = vec->mutableSpan().data(); + const std::size_t length = vec->size(); + const std::size_t capacity = vec->capacity(); + Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); + + return ExternType { + .data = vec->mutableSpan().data(), + .length = static_cast(length), + .capacity = static_cast(capacity), + }; + } else if constexpr (sizeof(ExternElement) <= sizeof(T) + && alignof(ExternElement) <= MimallocMalloc::maxAlign) { + + // We can reuse the allocation, but we still need to convert the elements. + alignas(CPPType) std::byte cppStorage[sizeof(CPPType)]; + // Prevent the memory from being freed. + CPPType* const vec = new (cppStorage) CPPType { std::move(cppValue) }; + const std::size_t length = vec->size(); + const std::size_t capacity = vec->capacity(); + const std::size_t allocSize = capacity * sizeof(T); + + T* const buffer = vec->mutableSpan().data(); + Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); + std::byte* storage = reinterpret_cast(buffer); + + // Convert the elements. + for (std::size_t i = 0; i < length; ++i) { + T* oldPtr = std::launder(reinterpret_cast(storage + i * sizeof(T))); + ExternElement newElem { ExternTraits::convertToExtern(std::move(*oldPtr)) }; + oldPtr->~T(); + new (storage + i * sizeof(ExternElement)) ExternElement { std::move(newElem) }; + } + + std::size_t newCapacity {}; + std::size_t newAllocSize {}; + + static constexpr bool newSizeIsMultiple = sizeof(T) % sizeof(ExternElement) == 0; + if (newSizeIsMultiple) { + newCapacity = capacity * (sizeof(T) / sizeof(ExternElement)); + newAllocSize = allocSize; + } else { + newCapacity = allocSize / sizeof(ExternElement); + newAllocSize = newCapacity * sizeof(ExternElement); + if (newAllocSize != allocSize) { + static_assert(std::is_trivially_copyable_v); + storage = static_cast( + MimallocMalloc::realloc(storage, newCapacity * sizeof(ExternElement))); + } + } + +#if __cpp_lib_start_lifetime_as >= 202207L + ExternElement* data = std::start_lifetime_as_array(storage, newCapacity); +#else + // We need to start the lifetime of an object of type "array of `capacity` + // `ExternElement`" without invalidating the object representation. Without + // `std::start_lifetime_as_array`, one way to do this is to use a no-op `memmove`, + // which implicitly creates objects, plus `std::launder` to obtain a pointer to + // the created object. + std::memmove(storage, storage, newAllocSize); + ExternElement* data = std::launder(reinterpret_cast(storage)); +#endif + return ExternType { + .data = data, + .length = static_cast(length), + .capacity = static_cast(newCapacity), + }; + } + + const std::size_t length = cppValue.size(); + const std::size_t newAllocSize = sizeof(ExternElement) * length; + ExternElement* memory = reinterpret_cast( + alignof(ExternElement) > MimallocMalloc::maxAlign + ? MimallocMalloc::alignedMalloc(newAllocSize, alignof(ExternElement)) + : MimallocMalloc::malloc(newAllocSize)); + for (std::size_t i = 0; i < length; ++i) { + new (memory + i) ExternElement { + ExternTraits::convertToExtern(std::move(cppValue[i])), + }; + } + return ExternType { + .data = memory, + .length = static_cast(length), + .capacity = static_cast(length), + }; + } +}; + +} diff --git a/src/bun.js/bindings/Bindgen/IDLConvert.h b/src/bun.js/bindings/Bindgen/IDLConvert.h new file mode 100644 index 0000000000..669b74c2d8 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLConvert.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include "IDLTypes.h" +#include "IDLConvertBase.h" + +namespace Bun { +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("any"); +}; +} + +template<> struct WebCore::Converter + : WebCore::DefaultConverter { + + static Bun::StrongRef convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + return Bun::StrongRef { Bun__StrongRef__new(&globalObject, JSC::JSValue::encode(value)) }; + } +}; diff --git a/src/bun.js/bindings/Bindgen/IDLConvertBase.h b/src/bun.js/bindings/Bindgen/IDLConvertBase.h new file mode 100644 index 0000000000..e73a664c1f --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLConvertBase.h @@ -0,0 +1,74 @@ +#pragma once +#include +#include +#include + +namespace Bun::Bindgen { + +namespace Detail { + +template +struct ContextBase : Bun::IDLConversionContextBase { + template + void throwGenericTypeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + Bun::throwError( + &global, + scope, + ErrorCode::ERR_INVALID_ARG_TYPE, + std::forward(message)); + } + + template + void throwGenericRangeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + Bun::throwError(&global, scope, ErrorCode::ERR_OUT_OF_RANGE, std::forward(message)); + } +}; + +template +struct ElementOf : ContextBase> { + using ElementContext = ElementOf>; + + explicit ElementOf(Parent parent) + : m_parent(std::move(parent)) + { + } + + auto source() + { + return WTF::makeString("element of "_s, m_parent.source()); + } + +private: + Parent m_parent; +}; + +} + +// Conversion context where the name of the value being converted is specified as an +// ASCIILiteral. Calls Bun::throwError. +struct LiteralConversionContext : Detail::ContextBase { + using ElementContext = Detail::ElementOf; + + explicit consteval LiteralConversionContext(WTF::ASCIILiteral name) + : m_name(name) + { + } + + auto source() + { + return m_name; + } + +private: + const WTF::ASCIILiteral m_name; +}; + +} diff --git a/src/bun.js/bindings/Bindgen/IDLTypes.h b/src/bun.js/bindings/Bindgen/IDLTypes.h new file mode 100644 index 0000000000..d0275d9fb0 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLTypes.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include + +namespace Bun::Bindgen { + +// See also: Bun::IDLRawAny +struct IDLStrongAny : WebCore::IDLType { + using NullableType = Bun::StrongRef; + using NullableInnerParameterType = NullableType; + + static inline std::nullptr_t nullValue() { return nullptr; } + template static inline bool isNullValue(U&& value) { return !value; } + template static inline U&& extractValueFromNullable(U&& value) + { + return std::forward(value); + } +}; + +template +struct IsIDLStrongAny : std::integral_constant::value> {}; + +// Dictionaries that contain raw `JSValue`s must live on the stack. +template +struct IDLStackOnlyDictionary : WebCore::IDLType { + using SequenceStorageType = void; + using ParameterType = const T&; + using NullableParameterType = const T&; +}; + +} diff --git a/src/bun.js/bindings/Bindgen/Macros.h b/src/bun.js/bindings/Bindgen/Macros.h new file mode 100644 index 0000000000..f70d2d8120 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/Macros.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +#define BUN_BINDGEN_DETAIL_FOREACH(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH2(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH2(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH3(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH3(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH4(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH4(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH5(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH5(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH6(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH6(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH7(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH7(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH8(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH8(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH9(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH9(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH10(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH10(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH11(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH11(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH12(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH12(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH13(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH13(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH14(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH14(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH15(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH15(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH16(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH16(macro, arg, ...) macro(arg) \ + __VA_OPT__(static_assert(false, "Bindgen/Macros.h: too many items")) diff --git a/src/bun.js/bindings/BunIDLConvert.h b/src/bun.js/bindings/BunIDLConvert.h new file mode 100644 index 0000000000..4aa4963f7f --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvert.h @@ -0,0 +1,280 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include "BunIDLConvertNumbers.h" +#include "BunIDLHumanReadable.h" +#include "JSDOMConvert.h" +#include +#include +#include + +template<> struct WebCore::Converter : WebCore::DefaultConverter { + static JSC::JSValue convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + return value; + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefinedOrNull()) { + return nullptr; + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNull(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefined()) { + return std::monostate {}; + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotUndefined(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isBoolean()) { + return value.asBoolean(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBoolean(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isString()) { + return value.toWTFString(&globalObject); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotString(globalObject, scope); + } +}; + +template +struct WebCore::Converter> : Bun::DefaultTryConverter> { + template + static std::optional::ImplementationType> tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (JSC::isJSArray(value)) { + return Bun::convert::Base>(globalObject, value, ctx); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.template throwNotArray(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) { + return jsBuffer; + } + if (auto* jsView = JSC::jsDynamicCast(value)) { + return jsView->unsharedBuffer(); + } + if (auto* jsDataView = JSC::jsDynamicCast(value)) { + return jsDataView->unsharedBuffer(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBufferSource(globalObject, scope); + } +}; + +template +struct WebCore::Converter> + : Bun::DefaultTryConverter> { +private: + using Base = Bun::DefaultTryConverter>; + +public: + using typename Base::ReturnType; + + static constexpr bool conversionHasSideEffects + = (WebCore::Converter::conversionHasSideEffects || ...); + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + using Last = std::tuple_element_t>; + if constexpr (requires { + WebCore::Converter::tryConvert(globalObject, value, ctx); + }) { + return Base::convert(globalObject, value, ctx); + } else { + return convertWithInfallibleLast( + globalObject, + value, + ctx, + std::make_index_sequence {}); + } + } + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + std::optional result; + auto tryAlternative = [&]() -> bool { + auto alternativeResult = Bun::tryConvertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + if (!alternativeResult.has_value()) { + return false; + } + result = ReturnType { std::move(*alternativeResult) }; + return true; + }; + (tryAlternative.template operator()() || ...); + return result; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.template throwNoMatchInUnion(globalObject, scope); + } + +private: + template + static ReturnType convertWithInfallibleLast( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx, + std::index_sequence) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + std::optional result; + auto tryAlternative = [&]() -> bool { + using T = std::tuple_element_t>; + if constexpr (index == sizeof...(IDL) - 1) { + auto alternativeResult = Bun::convertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + result = ReturnType { std::move(alternativeResult) }; + return true; + } else { + auto alternativeResult = Bun::tryConvertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + if (!alternativeResult.has_value()) { + return false; + } + result = ReturnType { std::move(*alternativeResult) }; + return true; + } + }; + bool done = (tryAlternative.template operator()() || ...); + ASSERT(done); + if (!result.has_value()) { + // Exception + return {}; + } + return std::move(*result); + } +}; diff --git a/src/bun.js/bindings/BunIDLConvertBase.h b/src/bun.js/bindings/BunIDLConvertBase.h new file mode 100644 index 0000000000..7d53546df0 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertBase.h @@ -0,0 +1,77 @@ +#pragma once +#include "BunIDLConvertContext.h" +#include "JSDOMConvertBase.h" +#include +#include +#include +#include + +namespace Bun { + +template +typename WebCore::Converter::ReturnType convertIDL( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) +{ + if constexpr (WebCore::Converter::takesContext) { + return WebCore::Converter::convert(globalObject, value, ctx); + } else { + return WebCore::Converter::convert(globalObject, value); + } +} + +template +std::optional::ReturnType> tryConvertIDL( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) +{ + if constexpr (WebCore::Converter::takesContext) { + return WebCore::Converter::tryConvert(globalObject, value, ctx); + } else { + return WebCore::Converter::tryConvert(globalObject, value); + } +} + +template +struct DefaultContextConverter : WebCore::DefaultConverter { + using typename WebCore::DefaultConverter::ReturnType; + + static constexpr bool takesContext = true; + + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + auto ctx = DefaultConversionContext {}; + return WebCore::Converter::convert(globalObject, value, ctx); + } +}; + +template +struct DefaultTryConverter : DefaultContextConverter { + using typename DefaultContextConverter::ReturnType; + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto result = WebCore::Converter::tryConvert(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, {}); + if (result.has_value()) { + return std::move(*result); + } + WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); + return ReturnType {}; + } + + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + auto ctx = DefaultConversionContext {}; + return WebCore::Converter::tryConvert(globalObject, value, ctx); + } +}; + +} diff --git a/src/bun.js/bindings/BunIDLConvertBlob.h b/src/bun.js/bindings/BunIDLConvertBlob.h new file mode 100644 index 0000000000..2e57c6f453 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertBlob.h @@ -0,0 +1,36 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include "blob.h" +#include "ZigGeneratedClasses.h" + +namespace Bun { +struct IDLBlobRef : IDLBunInterface {}; +} + +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (auto* jsBlob = JSC::jsDynamicCast(value)) { + if (void* wrapped = jsBlob->wrapped()) { + return static_cast(wrapped); + } + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBlob(globalObject, scope); + } +}; diff --git a/src/bun.js/bindings/BunIDLConvertContext.h b/src/bun.js/bindings/BunIDLConvertContext.h new file mode 100644 index 0000000000..5dde10a730 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertContext.h @@ -0,0 +1,252 @@ +#pragma once +#include "BunIDLHumanReadable.h" +#include +#include +#include + +namespace Bun { + +namespace Detail { +struct IDLConversionContextMarker {}; +} + +template +concept IDLConversionContext = std::derived_from; + +namespace Detail { +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; + +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; + +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; +} + +template +struct IDLConversionContextBase : Detail::IDLConversionContextMarker { + void throwRequired(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeErrorWithPredicate(global, scope, "is required"_s); + } + + void throwNumberNotFinite(JSC::JSGlobalObject& global, JSC::ThrowScope& scope, double value) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString("must be finite (received "_s, value, ')')); + } + + void throwNumberNotInteger(JSC::JSGlobalObject& global, JSC::ThrowScope& scope, double value) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString("must be an integer (received "_s, value, ')')); + } + + template + void throwIntegerOutOfRange( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + Int value, + Limit min, + Limit max) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString( + "must be in the range ["_s, + min, + ", "_s, + max, + "] (received "_s, + value, + ')')); + } + + template + void throwBigIntOutOfRange( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + Limit min, + Limit max) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString( + "must be in the range ["_s, + min, + ", "_s, + max, + ']')); + } + + void throwNotNumber(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a number"_s); + } + + void throwNotString(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a string"_s); + } + + void throwNotBoolean(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a boolean"_s); + } + + void throwNotObject(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an object"_s); + } + + void throwNotNull(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "null"_s); + } + + void throwNotUndefined(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "undefined"_s); + } + + void throwNotBufferSource(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an ArrayBuffer or TypedArray"_s); + } + + void throwNotBlob(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a Blob"_s); + } + + template + void throwNotArray(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an array"_s); + } + + template + void throwNotArray(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe( + global, + scope, + WTF::makeString("an array of "_s, idlHumanReadableName())); + } + + template + void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwRangeErrorWithPredicate(global, scope, "is not a valid enumeration value"_s); + } + + template + void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, idlHumanReadableName()); + } + + template + requires(sizeof...(Alternatives) > 0) + void throwNoMatchInUnion(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + using Union = Detail::IDLUnionForDiagnostic::Type; + derived().throwTypeErrorWithPredicate( + global, + scope, + WTF::makeString("must be of type "_s, idlHumanReadableName())); + } + + template + void throwNoMatchInUnion(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeErrorWithPredicate(global, scope, "is of an unsupported type"_s); + } + + template + void throwTypeMustBe( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& expectedNounPhrase) + { + derived().throwTypeErrorWithPredicate( + global, + scope, + WTF::makeString("must be "_s, std::forward(expectedNounPhrase))); + } + + template + void throwTypeErrorWithPredicate( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& predicate) + { + derived().throwGenericTypeError( + global, + scope, + WTF::makeString(derived().source(), ' ', std::forward(predicate))); + } + + template + void throwRangeErrorWithPredicate( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& predicate) + { + derived().throwGenericRangeError( + global, + scope, + WTF::makeString(derived().source(), ' ', std::forward(predicate))); + } + + template + void throwGenericTypeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + JSC::throwTypeError(&global, scope, std::forward(message)); + } + + template + void throwGenericRangeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + JSC::throwRangeError(&global, scope, std::forward(message)); + } + + using ElementContext = Derived; + + // When converting a sequence, the result of this function will be used as the context for + // converting each element of the sequence. + auto contextForElement() + { + return typename Derived::ElementContext { derived() }; + } + +private: + Derived& derived() { return *static_cast(this); } +}; + +// Default conversion context: throws a plain TypeError or RangeError with the message +// "value must be ...". See also Bindgen::LiteralConversionContext, which uses Bun::throwError. +struct DefaultConversionContext : IDLConversionContextBase { + WTF::ASCIILiteral source() { return "value"_s; } +}; + +} diff --git a/src/bun.js/bindings/BunIDLConvertNumbers.h b/src/bun.js/bindings/BunIDLConvertNumbers.h new file mode 100644 index 0000000000..d2e5025220 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertNumbers.h @@ -0,0 +1,174 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include +#include +#include +#include +#include + +namespace Bun::Detail { +template +std::optional tryBigIntToInt(JSC::JSValue value) +{ + static constexpr std::int64_t minInt = std::numeric_limits::min(); + static constexpr std::int64_t maxInt = std::numeric_limits::max(); + using ComparisonResult = JSC::JSBigInt::ComparisonResult; + if (JSC::JSBigInt::compare(value, minInt) != ComparisonResult::LessThan + && JSC::JSBigInt::compare(value, maxInt) != ComparisonResult::GreaterThan) { + return static_cast(JSC::JSBigInt::toBigInt64(value)); + } + return std::nullopt; +} + +template +std::optional tryBigIntToInt(JSC::JSValue value) +{ + static constexpr std::uint64_t minInt = 0; + static constexpr std::uint64_t maxInt = std::numeric_limits::max(); + using ComparisonResult = JSC::JSBigInt::ComparisonResult; + if (JSC::JSBigInt::compare(value, minInt) != ComparisonResult::LessThan + && JSC::JSBigInt::compare(value, maxInt) != ComparisonResult::GreaterThan) { + return static_cast(JSC::JSBigInt::toBigUInt64(value)); + } + return std::nullopt; +} +} + +template + requires(sizeof(T) <= sizeof(std::uint64_t)) +struct WebCore::Converter> + : Bun::DefaultTryConverter> { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + static constexpr auto minInt = std::numeric_limits::min(); + static constexpr auto maxInt = std::numeric_limits::max(); + static constexpr auto maxSafeInteger = 9007199254740991LL; + + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (value.isInt32()) { + auto intValue = value.asInt32(); + if (intValue >= minInt && intValue <= maxInt) { + return intValue; + } + ctx.throwIntegerOutOfRange(globalObject, scope, intValue, minInt, maxInt); + return {}; + } + + using Largest = std::conditional_t, std::int64_t, std::uint64_t>; + if (value.isBigInt()) { + if (auto result = Bun::Detail::tryBigIntToInt(value)) { + return *result; + } + if constexpr (maxInt < std::numeric_limits::max()) { + if (auto result = Bun::Detail::tryBigIntToInt(value)) { + ctx.throwIntegerOutOfRange(globalObject, scope, *result, minInt, maxInt); + } + } + ctx.throwBigIntOutOfRange(globalObject, scope, minInt, maxInt); + return {}; + } + + if (!value.isNumber()) { + return std::nullopt; + } + + double number = value.asNumber(); + if (number > maxSafeInteger || number < -maxSafeInteger) { + ctx.throwNumberNotInteger(globalObject, scope, number); + return {}; + } + auto intVal = static_cast(number); + if (intVal != number) { + ctx.throwNumberNotInteger(globalObject, scope, number); + return {}; + } + if constexpr (maxInt >= static_cast(maxSafeInteger)) { + if (std::signed_integral || intVal >= 0) { + return static_cast(intVal); + } + } else if (intVal >= static_cast(minInt) + && intVal <= static_cast(maxInt)) { + return static_cast(intVal); + } + ctx.throwIntegerOutOfRange(globalObject, scope, intVal, minInt, maxInt); + return {}; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; + +template<> +struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isNumber()) { + return value.asNumber(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; + +template<> +struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!value.isNumber()) { + return std::nullopt; + } + double number = value.asNumber(); + if (std::isnan(number) || std::isinf(number)) { + ctx.throwNumberNotFinite(globalObject, scope, number); + return std::nullopt; + } + return number; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; diff --git a/src/bun.js/bindings/BunIDLHumanReadable.h b/src/bun.js/bindings/BunIDLHumanReadable.h new file mode 100644 index 0000000000..91e322d8d5 --- /dev/null +++ b/src/bun.js/bindings/BunIDLHumanReadable.h @@ -0,0 +1,139 @@ +#pragma once +#include "BunIDLTypes.h" +#include "ConcatCStrings.h" +#include +#include +#include + +namespace Bun { + +template +struct IDLHumanReadableName; + +template +concept HasIDLHumanReadableName = requires { IDLHumanReadableName::humanReadableName; }; + +struct BaseIDLHumanReadableName { + static constexpr bool isDisjunction = false; + static constexpr bool hasPreposition = false; +}; + +template +static constexpr WTF::ASCIILiteral idlHumanReadableName() +{ + static_assert(IDLHumanReadableName::humanReadableName.back() == '\0'); + return WTF::ASCIILiteral::fromLiteralUnsafe( + IDLHumanReadableName::humanReadableName.data()); +} + +namespace Detail { +template +static constexpr auto nestedHumanReadableName() +{ + static constexpr auto& name = IDLHumanReadableName::humanReadableName; + if constexpr (IDLHumanReadableName::isDisjunction) { + return Bun::concatCStrings("<", name, ">"); + } else { + return name; + } +} + +template +static constexpr auto separatorForHumanReadableBinaryDisjunction() +{ + if constexpr (IDLHumanReadableName::hasPreposition) { + return std::to_array(", or "); + } else { + return std::to_array(" or "); + } +} +} + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("null"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("undefined"); +}; + +template + requires std::derived_from +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("boolean"); +}; + +template + requires WebCore::IsIDLInteger::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("integer"); +}; + +template + requires WebCore::IsIDLFloatingPoint::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("number"); +}; + +template + requires WebCore::IsIDLString::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("string"); +}; + +// Will generally be overridden by each specific enumeration type. +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("enumeration (string)"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = true; + static constexpr auto humanReadableName = Bun::concatCStrings( + Detail::nestedHumanReadableName(), + Detail::separatorForHumanReadableBinaryDisjunction(), + "null"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = true; + static constexpr auto humanReadableName = Bun::concatCStrings( + Detail::nestedHumanReadableName(), + Detail::separatorForHumanReadableBinaryDisjunction(), + "undefined"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool hasPreposition = true; + static constexpr auto humanReadableName + = Bun::concatCStrings("array of ", Detail::nestedHumanReadableName()); +}; + +// Will generally be overridden by each specific dictionary type. +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("dictionary (object)"); +}; + +template +struct IDLHumanReadableName> : IDLHumanReadableName {}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = sizeof...(IDL) > 1; + static constexpr auto humanReadableName + = Bun::joinCStringsAsList(Detail::nestedHumanReadableName()...); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("ArrayBuffer"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("Blob"); +}; + +} diff --git a/src/bun.js/bindings/BunIDLTypes.h b/src/bun.js/bindings/BunIDLTypes.h new file mode 100644 index 0000000000..aab971072b --- /dev/null +++ b/src/bun.js/bindings/BunIDLTypes.h @@ -0,0 +1,87 @@ +#pragma once +#include "IDLTypes.h" +#include +#include +#include +#include +#include + +namespace WTF { +struct CrashOnOverflow; +} + +namespace Bun { + +struct MimallocMalloc; + +// Like `IDLAny`, but always stored as a raw `JSValue`. This should only be +// used in contexts where the `JSValue` will be stored on the stack. +struct IDLRawAny : WebCore::IDLType { + // Storage in a sequence is explicitly unsupported, as this would create a + // `Vector`, whose contents are invisible to the GC. + using SequenceStorageType = void; + using NullableType = JSC::JSValue; + using NullableParameterType = JSC::JSValue; + using NullableInnerParameterType = JSC::JSValue; + static NullableType nullValue() { return JSC::jsUndefined(); } + static bool isNullValue(const NullableType& value) { return value.isUndefined(); } + static ImplementationType extractValueFromNullable(const NullableType& value) { return value; } + static constexpr auto humanReadableName() { return std::to_array("any"); } +}; + +// For use in unions, to represent a nullable union. +struct IDLStrictNull : WebCore::IDLType { + static constexpr auto humanReadableName() { return std::to_array("null"); } +}; + +// For use in unions, to represent an optional union. +struct IDLStrictUndefined : WebCore::IDLType { + static constexpr auto humanReadableName() { return std::to_array("undefined"); } +}; + +template +struct IDLStrictInteger : WebCore::IDLInteger {}; +struct IDLStrictDouble : WebCore::IDLUnrestrictedDouble {}; +struct IDLFiniteDouble : WebCore::IDLDouble {}; +struct IDLStrictBoolean : WebCore::IDLBoolean {}; +struct IDLStrictString : WebCore::IDLDOMString {}; + +template +struct IDLOrderedUnion : WebCore::IDLType> {}; + +namespace Detail { +template +using IDLMimallocSequence = WebCore::IDLSequence< + IDL, + WTF::Vector< + typename IDL::SequenceStorageType, + 0, + WTF::CrashOnOverflow, + 16, + MimallocMalloc>>; +} + +template +struct IDLArray : Detail::IDLMimallocSequence { + using Base = Detail::IDLMimallocSequence; +}; + +template> +struct IDLBunInterface : WebCore::IDLType, RefDerefTraits>> { + using NullableType = WTF::RefPtr, RefDerefTraits>; + using NullableInnerParameterType = NullableType; + + static inline std::nullptr_t nullValue() { return nullptr; } + template static inline bool isNullValue(U&& value) { return !value; } + template static inline U&& extractValueFromNullable(U&& value) + { + return std::forward(value); + } +}; + +struct IDLArrayBufferRef : IDLBunInterface {}; + +// Defined in BunIDLConvertBlob.h +struct IDLBlobRef; + +} diff --git a/src/bun.js/bindings/ConcatCStrings.h b/src/bun.js/bindings/ConcatCStrings.h new file mode 100644 index 0000000000..2b0a65cd73 --- /dev/null +++ b/src/bun.js/bindings/ConcatCStrings.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include +#include + +namespace Bun { + +namespace Detail { +template +static constexpr bool isCharArray = false; + +template +static constexpr bool isCharArray = true; + +template +static constexpr bool isCharArray> = true; + +// Intentionally not defined, to force consteval to fail. +void stringIsNotNullTerminated(); +} + +template + requires(Detail::isCharArray> && ...) +consteval auto concatCStrings(T&&... nullTerminatedCharArrays) +{ + std::array) - 1) + ...) + 1> result; + auto it = result.begin(); + auto append = [&it](auto&& arg) { + if (std::end(arg)[-1] != '\0') { + // This will cause consteval to fail. + Detail::stringIsNotNullTerminated(); + } + it = std::copy(std::begin(arg), std::end(arg) - 1, it); + }; + (append(nullTerminatedCharArrays), ...); + result.back() = '\0'; + return result; +} + +namespace Detail { +template +consteval auto listSeparatorForIndex() +{ + if constexpr (length == 2) { + return std::to_array(" or "); + } else if constexpr (index == length - 1) { + return std::to_array(", or "); + } else { + return std::to_array(", "); + } +} + +template +consteval auto joinCStringsAsList(std::index_sequence, T&& first, Rest&&... rest) +{ + return concatCStrings( + first, + concatCStrings( + listSeparatorForIndex(), + std::forward(rest))...); +} +} + +template + requires(Detail::isCharArray> && ...) +consteval auto joinCStringsAsList(T&&... nullTerminatedCharArrays) +{ + if constexpr (sizeof...(T) == 0) { + return std::to_array(""); + } else { + return Detail::joinCStringsAsList( + std::make_index_sequence {}, + std::forward(nullTerminatedCharArrays)...); + } +} + +} diff --git a/src/bun.js/bindings/IDLTypes.h b/src/bun.js/bindings/IDLTypes.h index 3ebea7e596..4e9cbd03c0 100644 --- a/src/bun.js/bindings/IDLTypes.h +++ b/src/bun.js/bindings/IDLTypes.h @@ -28,6 +28,7 @@ #include "StringAdaptors.h" #include #include +#include #include #include #include @@ -76,6 +77,7 @@ struct IDLType { static NullableType nullValue() { return std::nullopt; } static bool isNullValue(const NullableType& value) { return !value; } static ImplementationType extractValueFromNullable(const NullableType& value) { return value.value(); } + static ImplementationType extractValueFromNullable(NullableType&& value) { return std::move(value.value()); } template using NullableTypeWithLessPadding = Markable; template @@ -84,6 +86,8 @@ struct IDLType { static bool isNullType(const NullableTypeWithLessPadding& value) { return !value; } template static ImplementationType extractValueFromNullable(const NullableTypeWithLessPadding& value) { return value.value(); } + template + static ImplementationType extractValueFromNullable(NullableTypeWithLessPadding&& value) { return std::move(value.value()); } }; // IDLUnsupportedType is a special type that serves as a base class for currently unsupported types. @@ -94,8 +98,12 @@ struct IDLUnsupportedType : IDLType { struct IDLNull : IDLType { }; +// See also: Bun::IDLRawAny, Bun::Bindgen::IDLStrongAny struct IDLAny : IDLType> { - using SequenceStorageType = JSC::JSValue; + // SequenceStorageType must be left as JSC::Strong; otherwise + // IDLSequence would yield a Vector, whose contents + // are invisible to the GC. + // [do not uncomment] using SequenceStorageType = JSC::JSValue; using ParameterType = JSC::JSValue; using NullableParameterType = JSC::JSValue; @@ -247,18 +255,23 @@ template struct IDLNullable : IDLType { template static inline auto extractValueFromNullable(U&& value) -> decltype(T::extractValueFromNullable(std::forward(value))) { return T::extractValueFromNullable(std::forward(value)); } }; -template struct IDLSequence : IDLType> { - using InnerType = T; - - using ParameterType = const Vector&; - using NullableParameterType = const std::optional>&; +// Like `IDLNullable`, but does not permit `null`, only `undefined`. +template struct IDLOptional : IDLNullable { }; -template struct IDLFrozenArray : IDLType> { +template> +struct IDLSequence : IDLType { using InnerType = T; - using ParameterType = const Vector&; - using NullableParameterType = const std::optional>&; + using ParameterType = const VectorType&; + using NullableParameterType = const std::optional&; +}; + +template struct IDLFrozenArray : IDLType> { + using InnerType = T; + + using ParameterType = const Vector&; + using NullableParameterType = const std::optional>&; }; template struct IDLRecord : IDLType>> { @@ -282,6 +295,31 @@ template struct IDLUnion : IDLType> { using TypeList = brigand::list; + // If `SequenceStorageType` and `ImplementationType` are different for any + // type in `Ts`, this union should not be allowed to be stored in a + // sequence. Sequence elements are stored on the heap (in a `Vector`), so + // if `SequenceStorageType` and `ImplementationType` differ for some type, + // this is an indication that the `ImplementationType` should not be stored + // on the heap (e.g., because it is or contains a raw `JSValue`). When this + // is the case, we indicate that the union itself should not be stored on + // the heap by defining its `SequenceStorageType` as void. + // + // Note that we cannot define `SequenceStorageType` as + // `std::variant`, as this would cause + // sequence conversion to fail to compile, because + // `std::variant` is not convertible to + // `std::variant`. + // + // A potential avenue for future work would be to extend the IDL type + // traits interface to allow defining custom conversions from + // `ImplementationType` to `SequenceStorageType`, and to properly propagate + // `SequenceStorageType` in other types like `IDLDictionary`; however, one + // should keep in mind that some types may still disallow heap storage + // entirely by defining `SequenceStorageType` as void. + using SequenceStorageType = std::conditional_t< + (std::is_same_v && ...), + std::variant, + void>; using ParameterType = const std::variant&; using NullableParameterType = const std::optional>&; }; diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index a8afc337f4..76b66be73b 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -1133,25 +1133,16 @@ pub const JSValue = enum(i64) { return bun.cpp.JSC__JSValue__toMatch(this, global, other); } - extern fn JSC__JSValue__asArrayBuffer_(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool; - pub fn asArrayBuffer_(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool { - return JSC__JSValue__asArrayBuffer_(this, global, out); - } + extern fn JSC__JSValue__asArrayBuffer(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool; pub fn asArrayBuffer(this: JSValue, global: *JSGlobalObject) ?ArrayBuffer { - var out: ArrayBuffer = .{ - .offset = 0, - .len = 0, - .byte_len = 0, - .shared = false, - .typed_array_type = .Uint8Array, - }; - - if (this.asArrayBuffer_(global, &out)) { - out.value = this; + var out: ArrayBuffer = undefined; + // `ptr` might not get set if the ArrayBuffer is empty, so make sure it starts out with a + // defined value. + out.ptr = &.{}; + if (JSC__JSValue__asArrayBuffer(this, global, &out)) { return out; } - return null; } extern fn JSC__JSValue__fromInt64NoTruncate(globalObject: *JSGlobalObject, i: i64) JSValue; diff --git a/src/bun.js/bindings/MimallocWTFMalloc.h b/src/bun.js/bindings/MimallocWTFMalloc.h new file mode 100644 index 0000000000..32e43713ba --- /dev/null +++ b/src/bun.js/bindings/MimallocWTFMalloc.h @@ -0,0 +1,103 @@ +#pragma once +#include +#include +#include +#include +#include +#include "mimalloc.h" +#include "mimalloc/types.h" + +namespace Bun { +// For use with WTF types like WTF::Vector. +struct MimallocMalloc { +#if USE(MIMALLOC) + static constexpr std::size_t maxAlign = MI_MAX_ALIGN_SIZE; +#else + static constexpr std::size_t maxAlign = alignof(std::max_align_t); +#endif + + static void* malloc(std::size_t size) + { + void* result = tryMalloc(size); + if (!result) CRASH(); + return result; + } + + static void* tryMalloc(std::size_t size) + { +#if USE(MIMALLOC) + return mi_malloc(size); +#else + return std::malloc(size); +#endif + } + + static void* zeroedMalloc(std::size_t size) + { + void* result = tryZeroedMalloc(size); + if (!result) CRASH(); + return result; + } + + static void* tryZeroedMalloc(std::size_t size) + { +#if USE(MIMALLOC) + return mi_zalloc(size); +#else + return std::calloc(size, 1); +#endif + } + + static void* alignedMalloc(std::size_t size, std::size_t alignment) + { + void* result = tryAlignedMalloc(size, alignment); + if (!result) CRASH(); + return result; + } + + static void* tryAlignedMalloc(std::size_t size, std::size_t alignment) + { + ASSERT(alignment > 0); + ASSERT((alignment & (alignment - 1)) == 0); // ensure power of two + ASSERT(((alignment - 1) & size) == 0); // ensure size multiple of alignment +#if USE(MIMALLOC) + return mi_malloc_aligned(size, alignment); +#elif !OS(WINDOWS) + return std::aligned_alloc(alignment, size); +#else + LOG_ERROR("cannot allocate memory with alignment %zu", alignment); + return nullptr; +#endif + } + + static void* realloc(void* p, std::size_t size) + { + void* result = tryRealloc(p, size); + if (!result) CRASH(); + return result; + } + + static void* tryRealloc(void* p, std::size_t size) + { +#if USE(MIMALLOC) + return mi_realloc(p, size); +#else + return std::realloc(p, size); +#endif + } + + static void free(void* p) + { +#if USE(MIMALLOC) + mi_free(p); +#else + std::free(p); +#endif + } + + static constexpr ALWAYS_INLINE std::size_t nextCapacity(std::size_t capacity) + { + return std::max(capacity + capacity / 2, capacity + 1); + } +}; +} diff --git a/src/bun.js/bindings/Strong.cpp b/src/bun.js/bindings/StrongRef.cpp similarity index 98% rename from src/bun.js/bindings/Strong.cpp rename to src/bun.js/bindings/StrongRef.cpp index d4ce228f4a..8466df5cfa 100644 --- a/src/bun.js/bindings/Strong.cpp +++ b/src/bun.js/bindings/StrongRef.cpp @@ -1,4 +1,5 @@ #include "root.h" +#include "StrongRef.h" #include #include #include "BunClientData.h" diff --git a/src/bun.js/bindings/StrongRef.h b/src/bun.js/bindings/StrongRef.h new file mode 100644 index 0000000000..9726d6895a --- /dev/null +++ b/src/bun.js/bindings/StrongRef.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +extern "C" void Bun__StrongRef__delete(JSC::JSValue* _Nonnull handleSlot); +extern "C" JSC::JSValue* Bun__StrongRef__new(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); +extern "C" JSC::EncodedJSValue Bun__StrongRef__get(JSC::JSValue* _Nonnull handleSlot); +extern "C" void Bun__StrongRef__set(JSC::JSValue* _Nonnull handleSlot, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); +extern "C" void Bun__StrongRef__clear(JSC::JSValue* _Nonnull handleSlot); + +namespace Bun { + +struct StrongRefDeleter { + // `std::unique_ptr` will never call this with a null pointer. + void operator()(JSC::JSValue* _Nonnull handleSlot) + { + Bun__StrongRef__delete(handleSlot); + } +}; + +using StrongRef = std::unique_ptr; + +} diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 21141e1935..76d2538f59 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -58,6 +58,7 @@ #include "AddEventListenerOptions.h" #include "AsyncContextFrame.h" #include "BunClientData.h" +#include "BunIDLConvert.h" #include "BunObject.h" #include "GeneratedBunObject.h" #include "BunPlugin.h" @@ -2080,10 +2081,9 @@ extern "C" bool ReadableStream__tee(JSC::EncodedJSValue possibleReadableStream, RETURN_IF_EXCEPTION(scope, false); if (!returnedValue) return false; - auto results = Detail::SequenceConverter::convert(*lexicalGlobalObject, *returnedValue); + auto results = convert>>(*lexicalGlobalObject, *returnedValue); RETURN_IF_EXCEPTION(scope, false); - ASSERT(results.size() == 2); *possibleReadableStream1 = JSValue::encode(results[0]); *possibleReadableStream2 = JSValue::encode(results[1]); return true; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index df1ddfe46e..e41c9d6979 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -32,6 +32,7 @@ #include "WebCoreJSBuiltins.h" #include "JavaScriptCore/AggregateError.h" +#include "JavaScriptCore/ArrayBufferView.h" #include "JavaScriptCore/BytecodeIndex.h" #include "JavaScriptCore/CodeBlock.h" #include "JavaScriptCore/Completion.h" @@ -3023,16 +3024,19 @@ JSC::EncodedJSValue JSC__JSValue__values(JSC::JSGlobalObject* globalObject, JSC: return JSValue::encode(JSC::objectValues(vm, globalObject, value)); } -bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, - Bun__ArrayBuffer* arg2) +bool JSC__JSValue__asArrayBuffer( + JSC::EncodedJSValue encodedValue, + JSC::JSGlobalObject* globalObject, + Bun__ArrayBuffer* out) { - ASSERT_NO_PENDING_EXCEPTION(arg1); - JSC::JSValue value = JSC::JSValue::decode(JSValue0); + ASSERT_NO_PENDING_EXCEPTION(globalObject); + JSC::JSValue value = JSC::JSValue::decode(encodedValue); if (!value || !value.isCell()) [[unlikely]] { return false; } auto type = value.asCell()->type(); + void* data = nullptr; switch (type) { case JSC::JSType::Uint8ArrayType: @@ -3048,60 +3052,56 @@ bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObj case JSC::JSType::Float64ArrayType: case JSC::JSType::BigInt64ArrayType: case JSC::JSType::BigUint64ArrayType: { - JSC::JSArrayBufferView* typedArray = JSC::jsCast(value); - arg2->len = typedArray->length(); - arg2->byte_len = typedArray->byteLength(); - // the offset is already set by vector() - // https://github.com/oven-sh/bun/issues/561 - arg2->offset = 0; - arg2->cell_type = type; - arg2->ptr = (char*)typedArray->vectorWithoutPACValidation(); - arg2->_value = JSValue::encode(value); - return true; + JSC::JSArrayBufferView* view = JSC::jsCast(value); + data = view->vector(); + out->len = view->length(); + out->byte_len = view->byteLength(); + out->cell_type = type; + out->shared = view->isShared(); + break; } case JSC::JSType::ArrayBufferType: { - JSC::ArrayBuffer* typedArray = JSC::jsCast(value)->impl(); - arg2->len = typedArray->byteLength(); - arg2->byte_len = typedArray->byteLength(); - arg2->offset = 0; - arg2->cell_type = JSC::JSType::ArrayBufferType; - arg2->ptr = (char*)typedArray->data(); - arg2->shared = typedArray->isShared(); - arg2->_value = JSValue::encode(value); - return true; + JSC::ArrayBuffer* buffer = JSC::jsCast(value)->impl(); + data = buffer->data(); + out->len = buffer->byteLength(); + out->byte_len = buffer->byteLength(); + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = buffer->isShared(); + break; } case JSC::JSType::ObjectType: case JSC::JSType::FinalObjectType: { if (JSC::JSArrayBufferView* view = JSC::jsDynamicCast(value)) { - arg2->len = view->length(); - arg2->byte_len = view->byteLength(); - arg2->offset = 0; - arg2->cell_type = view->type(); - arg2->ptr = (char*)view->vectorWithoutPACValidation(); - arg2->_value = JSValue::encode(value); - return true; - } - - if (JSC::JSArrayBuffer* jsBuffer = JSC::jsDynamicCast(value)) { + data = view->vector(); + out->len = view->length(); + out->byte_len = view->byteLength(); + out->cell_type = view->type(); + out->shared = view->isShared(); + } else if (JSC::JSArrayBuffer* jsBuffer = JSC::jsDynamicCast(value)) { JSC::ArrayBuffer* buffer = jsBuffer->impl(); if (!buffer) return false; - arg2->len = buffer->byteLength(); - arg2->byte_len = buffer->byteLength(); - arg2->offset = 0; - arg2->cell_type = JSC::JSType::ArrayBufferType; - arg2->ptr = (char*)buffer->data(); - arg2->_value = JSValue::encode(value); - return true; + data = buffer->data(); + out->len = buffer->byteLength(); + out->byte_len = buffer->byteLength(); + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = buffer->isShared(); + } else { + return false; } break; } default: { - break; + return false; } } - - return false; + out->_value = JSValue::encode(value); + if (data) { + // Avoid setting `ptr` to null; the corresponding Zig field is a non-optional pointer. + // The caller should have already set `ptr` to a zero-length array. + out->ptr = static_cast(data); + } + return true; } CPP_DECL JSC::EncodedJSValue JSC__JSValue__createEmptyArray(JSC::JSGlobalObject* arg0, size_t length) @@ -6885,3 +6885,19 @@ CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int Bun__CallFrame__getLineNumber(JSC: return lineColumn.line; } + +extern "C" void JSC__ArrayBuffer__ref(JSC::ArrayBuffer* self) { self->ref(); } +extern "C" void JSC__ArrayBuffer__deref(JSC::ArrayBuffer* self) { self->deref(); } +extern "C" void JSC__ArrayBuffer__asBunArrayBuffer(JSC::ArrayBuffer* self, Bun__ArrayBuffer* out) +{ + const std::size_t byteLength = self->byteLength(); + if (void* data = self->data()) { + // Avoid setting `ptr` to null; it's a non-optional pointer in Zig. + out->ptr = static_cast(data); + } + out->len = byteLength; + out->byte_len = byteLength; + out->_value = 0; + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = self->isShared(); +} diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 5020140860..b5f56ffa0a 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -322,11 +322,10 @@ BunString toStringView(WTF::StringView view); typedef struct { char* ptr; - size_t offset; size_t len; size_t byte_len; - uint8_t cell_type; int64_t _value; + uint8_t cell_type; bool shared; } Bun__ArrayBuffer; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index f02d203054..59c5a0b4a0 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -195,7 +195,7 @@ CPP_DECL uint32_t JSC__JSMap__size(JSC::JSMap* arg0, JSC::JSGlobalObject* arg1); #pragma mark - JSC::JSValue CPP_DECL void JSC__JSValue__then(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue JSValue2, SYSV_ABI JSC::EncodedJSValue(* ArgFn3)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1), SYSV_ABI JSC::EncodedJSValue(* ArgFn4)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1)); -CPP_DECL bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, Bun__ArrayBuffer* arg2); +CPP_DECL bool JSC__JSValue__asArrayBuffer(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, Bun__ArrayBuffer* arg2); CPP_DECL unsigned char JSC__JSValue__asBigIntCompare(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue JSValue2); CPP_DECL JSC::JSCell* JSC__JSValue__asCell(JSC::EncodedJSValue JSValue0); CPP_DECL JSC::JSInternalPromise* JSC__JSValue__asInternalPromise(JSC::EncodedJSValue JSValue0); diff --git a/src/bun.js/bindings/webcore/JSDOMConvert.h b/src/bun.js/bindings/webcore/JSDOMConvert.h index 24c95835e2..c8ebcbac44 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvert.h +++ b/src/bun.js/bindings/webcore/JSDOMConvert.h @@ -39,6 +39,7 @@ #include "JSDOMConvertNullable.h" #include "JSDOMConvertNumbers.h" #include "JSDOMConvertObject.h" +#include "JSDOMConvertOptional.h" #include "JSDOMConvertRecord.h" #include "JSDOMConvertSequences.h" #include "JSDOMConvertSerializedScriptValue.h" diff --git a/src/bun.js/bindings/webcore/JSDOMConvertBase.h b/src/bun.js/bindings/webcore/JSDOMConvertBase.h index cd4b872c28..05233c9cc3 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertBase.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertBase.h @@ -268,6 +268,8 @@ template struct DefaultConverter { // is something having a converter that does JSC::JSValue::toBoolean. // toBoolean() in JS can't call arbitrary functions. static constexpr bool conversionHasSideEffects = true; + + static constexpr bool takesContext = false; }; // Conversion from JSValue -> Implementation for variadic arguments diff --git a/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h b/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h index 87f1a1c468..305063eb34 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h @@ -31,11 +31,21 @@ namespace WebCore { // Specialized by generated code for IDL dictionary conversion. -template T convertDictionary(JSC::JSGlobalObject&, JSC::JSValue); +template T convertDictionary(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value); template struct Converter> : DefaultConverter> { using ReturnType = T; + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value) + { + if (value.isObject()) { + return convert(lexicalGlobalObject, value); + } + return std::nullopt; + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) { return convertDictionary(lexicalGlobalObject, value); diff --git a/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h b/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h index 71df8e0298..52ef8d2bf9 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h @@ -28,6 +28,7 @@ #include "IDLTypes.h" #include "JSDOMConvertBase.h" #include "JSDOMGlobalObject.h" +#include "BunIDLConvertBase.h" namespace WebCore { @@ -41,7 +42,42 @@ template ASCIILiteral expectedEnumerationValues(); template JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, T); template struct Converter> : DefaultConverter> { + static constexpr bool takesContext = true; + + // `tryConvert` for enumerations is strict: it returns null if the value is not a string. + template + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isString()) { + return parseEnumeration(lexicalGlobalObject, value); + } + return std::nullopt; + } + + // When converting with Context, the conversion is stricter: non-strings are disallowed. + template + static T convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + if (!value.isString()) { + ctx.throwNotString(lexicalGlobalObject, throwScope); + return {}; + } + auto result = parseEnumeration(lexicalGlobalObject, value); + RETURN_IF_EXCEPTION(throwScope, {}); + if (result.has_value()) { + return std::move(*result); + } + ctx.template throwBadEnumValue>(lexicalGlobalObject, throwScope); + return {}; + } + template + requires(!Bun::IDLConversionContext>) static T convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { auto& vm = JSC::getVM(&lexicalGlobalObject); diff --git a/src/bun.js/bindings/webcore/JSDOMConvertNullable.h b/src/bun.js/bindings/webcore/JSDOMConvertNullable.h index 549d126bb4..40821ca8d7 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertNullable.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertNullable.h @@ -30,6 +30,7 @@ #include "JSDOMConvertInterface.h" #include "JSDOMConvertNumbers.h" #include "JSDOMConvertStrings.h" +#include "BunIDLConvertBase.h" namespace WebCore { @@ -58,6 +59,10 @@ struct NullableConversionType { template struct Converter> : DefaultConverter> { using ReturnType = typename Detail::NullableConversionType::Type; + static constexpr bool conversionHasSideEffects = WebCore::Converter::conversionHasSideEffects; + + static constexpr bool takesContext = true; + // 1. If Type(V) is not Object, and the conversion to an IDL value is being performed // due to V being assigned to an attribute whose type is a nullable callback function // that is annotated with [LegacyTreatNonObjectAsNull], then return the IDL nullable @@ -68,6 +73,29 @@ template struct Converter> : DefaultConverter + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefinedOrNull()) + return T::nullValue(); + auto result = Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); + if (result.has_value()) { + return std::move(*result); + } + return std::nullopt; + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + if (value.isUndefinedOrNull()) + return T::nullValue(); + return Bun::convertIDL(lexicalGlobalObject, value, ctx); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) { if (value.isUndefinedOrNull()) @@ -87,6 +115,7 @@ template struct Converter> : DefaultConverter::convert(lexicalGlobalObject, value, globalObject); } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower) { if (value.isUndefinedOrNull()) diff --git a/src/bun.js/bindings/webcore/JSDOMConvertOptional.h b/src/bun.js/bindings/webcore/JSDOMConvertOptional.h new file mode 100644 index 0000000000..6052fd9ff6 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSDOMConvertOptional.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "IDLTypes.h" +#include "JSDOMConvertNullable.h" + +namespace WebCore { + +template struct Converter> : DefaultConverter> { + using ReturnType = typename Converter>::ReturnType; + + static constexpr bool conversionHasSideEffects = WebCore::Converter::conversionHasSideEffects; + + static constexpr bool takesContext = true; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefined()) + return T::nullValue(); + auto result = Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); + if (result.has_value()) { + return std::move(*result); + } + return std::nullopt; + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + if (value.isUndefined()) + return T::nullValue(); + return Bun::convertIDL(lexicalGlobalObject, value, ctx); + } + + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSC::JSObject& thisObject) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, thisObject); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSDOMGlobalObject& globalObject) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, globalObject); + } + template + requires(!Bun::IDLConversionContext>) + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower)); + } + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSC::JSObject& thisObject, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, thisObject, std::forward(exceptionThrower)); + } + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSDOMGlobalObject& globalObject, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, globalObject, std::forward(exceptionThrower)); + } +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSDOMConvertSequences.h b/src/bun.js/bindings/webcore/JSDOMConvertSequences.h index 36818b187a..8b93310323 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertSequences.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertSequences.h @@ -33,43 +33,187 @@ #include #include #include +#include +#include +#include +#include "BunIDLConvertBase.h" namespace WebCore { namespace Detail { -template +template +struct SequenceTraits; + +template +struct SequenceTraits< + IDLType, + Vector< + typename IDLType::SequenceStorageType, + inlineCapacity, + OverflowHandler, + minCapacity, + Malloc>> { + + using VectorType = Vector< + typename IDLType::SequenceStorageType, + inlineCapacity, + OverflowHandler, + minCapacity, + Malloc>; + + static void reserveExact( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!sequence.tryReserveCapacity(size)) { + // FIXME: Is the right exception to throw? + throwTypeError(&lexicalGlobalObject, scope); + return; + } + } + + static void reserveEstimated( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + reserveExact(lexicalGlobalObject, sequence, size); + } + + template + static void append( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t index, + T&& element) + { + ASSERT(index == sequence.size()); + if constexpr (std::is_same_v, JSC::JSValue>) { + // `JSValue` should not be stored on the heap. + sequence.append(JSC::Strong { JSC::getVM(&lexicalGlobalObject), element }); + } else { + sequence.append(std::forward(element)); + } + } +}; + +template +struct SequenceTraits> { + using VectorType = std::array; + + static void reserveExact( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (size != arraySize) { + throwTypeError(&lexicalGlobalObject, scope); + } + } + + static void reserveEstimated( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) {} + + template + static void append( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t index, + T&& element) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (index >= arraySize) { + throwTypeError(&lexicalGlobalObject, scope); + } + sequence[index] = std::forward(element); + } +}; + +template> struct GenericSequenceConverter { - using ReturnType = Vector; + using Traits = SequenceTraits; + using ReturnType = Traits::VectorType; + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, Ctx& ctx) + { + return convert(lexicalGlobalObject, object, ReturnType(), ctx); + } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object) { - return convert(lexicalGlobalObject, object, ReturnType()); + auto ctx = Bun::DefaultConversionContext {}; + return convert(lexicalGlobalObject, object, ctx); + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ReturnType&& result, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + size_t index = 0; + auto elementCtx = ctx.contextForElement(); + forEachInIterable(&lexicalGlobalObject, object, [&result, &index, &elementCtx](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { + auto scope = DECLARE_THROW_SCOPE(vm); + + // auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue); + auto convertedValue = Bun::convertIDL(*lexicalGlobalObject, nextValue, elementCtx); + RETURN_IF_EXCEPTION(scope, ); + Traits::append(*lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); + }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } + return WTFMove(result); } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ReturnType&& result) { - forEachInIterable(&lexicalGlobalObject, object, [&result](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { - auto scope = DECLARE_THROW_SCOPE(vm); - - auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue); - RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); - }); - return WTFMove(result); + auto ctx = Bun::DefaultConversionContext {}; + return convert(lexicalGlobalObject, object, WTFMove(result), ctx); } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ReturnType result; - forEachInIterable(&lexicalGlobalObject, object, [&result, &exceptionThrower](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { + size_t index = 0; + forEachInIterable(&lexicalGlobalObject, object, [&result, &index, &exceptionThrower](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { auto scope = DECLARE_THROW_SCOPE(vm); auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); + Traits::append(*lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } return WTFMove(result); } @@ -80,13 +224,24 @@ struct GenericSequenceConverter { static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, JSC::JSValue method, ReturnType&& result) { - forEachInIterable(lexicalGlobalObject, object, method, [&result](JSC::VM& vm, JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue nextValue) { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + size_t index = 0; + forEachInIterable(lexicalGlobalObject, object, method, [&result, &index](JSC::VM& vm, JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue nextValue) { auto scope = DECLARE_THROW_SCOPE(vm); auto convertedValue = Converter::convert(lexicalGlobalObject, nextValue); RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); + Traits::append(lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } return WTFMove(result); } }; @@ -95,9 +250,10 @@ struct GenericSequenceConverter { // FIXME: This is only implemented for the IDLFloatingPointTypes and IDLLong. To add // support for more numeric types, add an overload of Converter::convert that // takes a JSGlobalObject, ThrowScope and double as its arguments. -template +template> struct NumericSequenceConverter { - using GenericConverter = GenericSequenceConverter; + using Traits = SequenceTraits; + using GenericConverter = GenericSequenceConverter; using ReturnType = typename GenericConverter::ReturnType; static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSArray* array, unsigned length, JSC::IndexingType indexingType, ReturnType&& result) @@ -107,9 +263,10 @@ struct NumericSequenceConverter { auto indexValue = array->butterfly()->contiguousInt32().at(array, i).get(); ASSERT(!indexValue || indexValue.isInt32()); if (!indexValue) - result.append(0); + Traits::append(lexicalGlobalObject, result, i, 0); else - result.append(indexValue.asInt32()); + Traits::append(lexicalGlobalObject, result, i, indexValue.asInt32()); + RETURN_IF_EXCEPTION(scope, {}); } return WTFMove(result); } @@ -119,12 +276,13 @@ struct NumericSequenceConverter { for (unsigned i = 0; i < length; i++) { double doubleValue = array->butterfly()->contiguousDouble().at(array, i); if (std::isnan(doubleValue)) - result.append(0); + Traits::append(lexicalGlobalObject, result, i, 0); else { auto convertedValue = Converter::convert(lexicalGlobalObject, scope, doubleValue); RETURN_IF_EXCEPTION(scope, {}); - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, convertedValue); + RETURN_IF_EXCEPTION(scope, {}); } } return WTFMove(result); @@ -150,20 +308,23 @@ struct NumericSequenceConverter { unsigned length = array->length(); ReturnType result; + // If we're not an int32/double array, it's possible that converting a // JSValue to a number could cause the iterator protocol to change, hence, // we may need more capacity, or less. In such cases, we use the length // as a proxy for the capacity we will most likely need (it's unlikely that // a program is written with a valueOf that will augment the iterator protocol). // If we are an int32/double array, then length is precisely the capacity we need. - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } - JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; - if (indexingType != JSC::Int32Shape && indexingType != JSC::DoubleShape) + bool isLengthExact = indexingType == JSC::Int32Shape || indexingType == JSC::DoubleShape; + if (isLengthExact) { + Traits::reserveExact(lexicalGlobalObject, result, length); + } else { + Traits::reserveEstimated(lexicalGlobalObject, result, length); + } + RETURN_IF_EXCEPTION(scope, {}); + + if (!isLengthExact) RELEASE_AND_RETURN(scope, GenericConverter::convert(lexicalGlobalObject, object, WTFMove(result))); return convertArray(lexicalGlobalObject, scope, array, length, indexingType, WTFMove(result)); @@ -189,50 +350,52 @@ struct NumericSequenceConverter { // as a proxy for the capacity we will most likely need (it's unlikely that // a program is written with a valueOf that will augment the iterator protocol). // If we are an int32/double array, then length is precisely the capacity we need. - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } - JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; - if (indexingType != JSC::Int32Shape && indexingType != JSC::DoubleShape) + bool isLengthExact = indexingType == JSC::Int32Shape || indexingType == JSC::DoubleShape; + if (isLengthExact) { + Traits::reserveExact(lexicalGlobalObject, result, length); + } else { + Traits::reserveEstimated(lexicalGlobalObject, result, length); + } + RETURN_IF_EXCEPTION(scope, {}); + + if (!isLengthExact) RELEASE_AND_RETURN(scope, GenericConverter::convert(lexicalGlobalObject, object, method, WTFMove(result))); return convertArray(lexicalGlobalObject, scope, array, length, indexingType, WTFMove(result)); } }; -template +template> struct SequenceConverter { - using GenericConverter = GenericSequenceConverter; + using Traits = SequenceTraits; + using GenericConverter = GenericSequenceConverter; using ReturnType = typename GenericConverter::ReturnType; - static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array) + template + static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array, Ctx& ctx) { auto& vm = lexicalGlobalObject.vm(); auto scope = DECLARE_THROW_SCOPE(vm); unsigned length = array->length(); ReturnType result; - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } + Traits::reserveExact(lexicalGlobalObject, result, length); + RETURN_IF_EXCEPTION(scope, {}); JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; + auto elementCtx = ctx.contextForElement(); if (indexingType == JSC::ContiguousShape) { for (unsigned i = 0; i < length; i++) { auto indexValue = array->butterfly()->contiguous().at(array, i).get(); if (!indexValue) indexValue = JSC::jsUndefined(); - auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + // auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + auto convertedValue = Bun::convertIDL(lexicalGlobalObject, indexValue, elementCtx); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); } return result; } @@ -244,15 +407,22 @@ struct SequenceConverter { if (!indexValue) indexValue = JSC::jsUndefined(); - auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + // auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + auto convertedValue = Bun::convertIDL(lexicalGlobalObject, indexValue, elementCtx); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); } return result; } + static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array) + { + auto ctx = Bun::DefaultConversionContext {}; + return convertArray(lexicalGlobalObject, array, ctx); + } + template + requires(!Bun::IDLConversionContext>) static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { auto& vm = lexicalGlobalObject.vm(); @@ -260,11 +430,8 @@ struct SequenceConverter { unsigned length = array->length(); ReturnType result; - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } + Traits::reserveExact(lexicalGlobalObject, result, length); + RETURN_IF_EXCEPTION(scope, {}); JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; @@ -276,8 +443,8 @@ struct SequenceConverter { auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, {}); } return result; } @@ -291,37 +458,59 @@ struct SequenceConverter { auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, {}); } return result; } + template + static ReturnType convertObject(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (Converter::conversionHasSideEffects) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + if (!JSC::isJSArray(object)) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + JSC::JSArray* array = JSC::asArray(object); + if (!array->isIteratorProtocolFastAndNonObservable()) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + RELEASE_AND_RETURN(scope, (convertArray(lexicalGlobalObject, array, ctx))); + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (auto* object = value.getObject()) { + RELEASE_AND_RETURN(scope, (convertObject(lexicalGlobalObject, object, ctx))); + } + ctx.throwTypeMustBe(lexicalGlobalObject, scope, "a sequence"_s); + return {}; + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { auto& vm = JSC::getVM(&lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - if (!value.isObject()) { - throwSequenceTypeError(lexicalGlobalObject, scope, functionName, argumentName); - return {}; + if (auto* object = value.getObject()) { + auto ctx = Bun::DefaultConversionContext {}; + RELEASE_AND_RETURN(scope, (convertObject(lexicalGlobalObject, object, ctx))); } - - JSC::JSObject* object = JSC::asObject(value); - if (Converter::conversionHasSideEffects) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - if (!JSC::isJSArray(object)) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - JSC::JSArray* array = JSC::asArray(object); - if (!array->isIteratorProtocolFastAndNonObservable()) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - RELEASE_AND_RETURN(scope, (convertArray(lexicalGlobalObject, array))); + throwSequenceTypeError(lexicalGlobalObject, scope, functionName, argumentName); + return {}; } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower = ExceptionThrower(), @@ -442,22 +631,31 @@ struct SequenceConverter { } -template struct Converter> : DefaultConverter> { - using ReturnType = typename Detail::SequenceConverter::ReturnType; +template +struct Converter> : DefaultConverter> { + using ReturnType = typename Detail::SequenceConverter::ReturnType; + + static constexpr bool takesContext = true; + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, ctx); + } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, value, functionName, argumentName); + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, functionName, argumentName); } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, JSC::JSValue method) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, object, method); + return Detail::SequenceConverter::convert(lexicalGlobalObject, object, method); } template static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower), functionName, argumentName); + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower), functionName, argumentName); } }; diff --git a/src/bun.js/bindings/webcore/JSDOMURL.cpp b/src/bun.js/bindings/webcore/JSDOMURL.cpp old mode 100755 new mode 100644 diff --git a/src/bun.js/jsc.zig b/src/bun.js/jsc.zig index 4ad4818864..0e42512c45 100644 --- a/src/bun.js/jsc.zig +++ b/src/bun.js/jsc.zig @@ -44,6 +44,7 @@ pub const AnyPromise = @import("./bindings/AnyPromise.zig").AnyPromise; pub const array_buffer = @import("./jsc/array_buffer.zig"); pub const ArrayBuffer = array_buffer.ArrayBuffer; pub const MarkedArrayBuffer = array_buffer.MarkedArrayBuffer; +pub const JSCArrayBuffer = array_buffer.JSCArrayBuffer; pub const CachedBytecode = @import("./bindings/CachedBytecode.zig").CachedBytecode; pub const CallFrame = @import("./bindings/CallFrame.zig").CallFrame; pub const CommonAbortReason = @import("./bindings/CommonAbortReason.zig").CommonAbortReason; @@ -276,5 +277,7 @@ pub const math = struct { } }; +pub const generated = @import("bindgen_generated"); + const bun = @import("bun"); const std = @import("std"); diff --git a/src/bun.js/jsc/array_buffer.zig b/src/bun.js/jsc/array_buffer.zig index 9751b476b6..19b8cde91e 100644 --- a/src/bun.js/jsc/array_buffer.zig +++ b/src/bun.js/jsc/array_buffer.zig @@ -1,10 +1,9 @@ pub const ArrayBuffer = extern struct { ptr: [*]u8 = &[0]u8{}, - offset: usize = 0, len: usize = 0, byte_len: usize = 0, - typed_array_type: jsc.JSValue.JSType = .Cell, value: jsc.JSValue = jsc.JSValue.zero, + typed_array_type: jsc.JSValue.JSType = .Cell, shared: bool = false, // require('buffer').kMaxLength. @@ -132,7 +131,7 @@ pub const ArrayBuffer = extern struct { } }; - pub const empty = ArrayBuffer{ .offset = 0, .len = 0, .byte_len = 0, .typed_array_type = .Uint8Array, .ptr = undefined }; + pub const empty = ArrayBuffer{ .len = 0, .byte_len = 0, .typed_array_type = .Uint8Array, .ptr = &.{} }; pub const name = "Bun__ArrayBuffer"; pub const Stream = std.io.FixedBufferStream([]u8); @@ -186,11 +185,7 @@ pub const ArrayBuffer = extern struct { extern "c" fn Bun__createArrayBufferForCopy(*jsc.JSGlobalObject, ptr: ?*const anyopaque, len: usize) jsc.JSValue; pub fn fromTypedArray(ctx: *jsc.JSGlobalObject, value: jsc.JSValue) ArrayBuffer { - var out: ArrayBuffer = .{}; - const was = value.asArrayBuffer_(ctx, &out); - bun.assert(was); - out.value = value; - return out; + return value.asArrayBuffer(ctx).?; } extern "c" fn JSArrayBuffer__fromDefaultAllocator(*jsc.JSGlobalObject, ptr: [*]u8, len: usize) jsc.JSValue; @@ -207,7 +202,7 @@ pub const ArrayBuffer = extern struct { } pub fn fromBytes(bytes: []u8, typed_array_type: jsc.JSValue.JSType) ArrayBuffer { - return ArrayBuffer{ .offset = 0, .len = @as(u32, @intCast(bytes.len)), .byte_len = @as(u32, @intCast(bytes.len)), .typed_array_type = typed_array_type, .ptr = bytes.ptr }; + return ArrayBuffer{ .len = @as(u32, @intCast(bytes.len)), .byte_len = @as(u32, @intCast(bytes.len)), .typed_array_type = typed_array_type, .ptr = bytes.ptr }; } pub fn toJSUnchecked(this: ArrayBuffer, ctx: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { @@ -320,7 +315,7 @@ pub const ArrayBuffer = extern struct { /// new ArrayBuffer(view.buffer, view.byteOffset, view.byteLength) /// ``` pub inline fn byteSlice(this: *const @This()) []u8 { - return this.ptr[this.offset..][0..this.byte_len]; + return this.ptr[0..this.byte_len]; } /// The equivalent of @@ -331,15 +326,19 @@ pub const ArrayBuffer = extern struct { pub const slice = byteSlice; pub inline fn asU16(this: *const @This()) []u16 { - return std.mem.bytesAsSlice(u16, @as([*]u16, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @alignCast(this.asU16Unaligned()); } pub inline fn asU16Unaligned(this: *const @This()) []align(1) u16 { - return std.mem.bytesAsSlice(u16, @as([*]align(1) u16, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @ptrCast(this.ptr[0 .. this.byte_len / @sizeOf(u16) * @sizeOf(u16)]); } pub inline fn asU32(this: *const @This()) []u32 { - return std.mem.bytesAsSlice(u32, @as([*]u32, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @alignCast(this.asU32Unaligned()); + } + + pub inline fn asU32Unaligned(this: *const @This()) []align(1) u32 { + return @ptrCast(this.ptr[0 .. this.byte_len / @sizeOf(u32) * @sizeOf(u32)]); } pub const BinaryType = enum(u4) { @@ -652,6 +651,29 @@ pub fn makeTypedArrayWithBytesNoCopy(globalObject: *jsc.JSGlobalObject, arrayTyp return bun.jsc.fromJSHostCall(globalObject, @src(), Bun__makeTypedArrayWithBytesNoCopy, .{ globalObject, arrayType, ptr, len, deallocator, deallocatorContext }); } +/// Corresponds to `JSC::ArrayBuffer`. +pub const JSCArrayBuffer = opaque { + const Self = @This(); + + extern fn JSC__ArrayBuffer__asBunArrayBuffer(self: *Self, out: *ArrayBuffer) void; + extern fn JSC__ArrayBuffer__ref(self: *Self) void; + extern fn JSC__ArrayBuffer__deref(self: *Self) void; + + pub const Ref = bun.ptr.ExternalShared(Self); + + pub const external_shared_descriptor = struct { + pub const ref = JSC__ArrayBuffer__ref; + pub const deref = JSC__ArrayBuffer__deref; + }; + + pub fn asArrayBuffer(self: *Self) ArrayBuffer { + var out: ArrayBuffer = undefined; + out.ptr = &.{}; // `ptr` might not get set if the ArrayBuffer is empty + JSC__ArrayBuffer__asBunArrayBuffer(self, &out); + return out; + } +}; + const std = @import("std"); const bun = @import("bun"); diff --git a/src/bun.zig b/src/bun.zig index a47f334f01..7e24954e94 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -8,7 +8,7 @@ const bun = @This(); pub const Environment = @import("./env.zig"); -pub const use_mimalloc = true; +pub const use_mimalloc = @import("build_options").use_mimalloc; pub const default_allocator: std.mem.Allocator = allocators.c_allocator; /// Zero-sized type whose `allocator` method returns `default_allocator`. pub const DefaultAllocator = allocators.Default; diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index d7133b7b69..37a7d6cddb 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -33,7 +33,8 @@ export type Type< type TypeFlag = boolean | "opt-nonnull" | null; -interface BaseTypeProps { +// This needs to be exported to avoid error TS4023. +export interface BaseTypeProps { [isType]: true | [T, K]; /** * Optional means the value may be omitted from a parameter definition. @@ -334,7 +335,7 @@ interface FuncOptionsWithVariant extends FuncMetadata { variants: FuncVariant[]; } type FuncWithoutOverloads = FuncMetadata & FuncVariant; -type FuncOptions = FuncOptionsWithVariant | FuncWithoutOverloads; +export type FuncOptions = FuncOptionsWithVariant | FuncWithoutOverloads; export interface FuncMetadata { /** diff --git a/src/codegen/bindgenv2/internal/any.ts b/src/codegen/bindgenv2/internal/any.ts new file mode 100644 index 0000000000..8ca3199762 --- /dev/null +++ b/src/codegen/bindgenv2/internal/any.ts @@ -0,0 +1,42 @@ +import { CodeStyle, Type } from "./base"; + +export const RawAny: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLRawAny"; + } + get bindgenType() { + return "bindgen.BindgenRawAny"; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.JSValue"; + } + toCpp(value: any): string { + throw RangeError("`RawAny` cannot have a default value"); + } +})(); + +export const StrongAny: Type = new (class extends Type { + get idlType() { + return "::Bun::Bindgen::IDLStrongAny"; + } + get bindgenType() { + return "bindgen.BindgenStrongAny"; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.Strong"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("`StrongAny` cannot have a default value"); + } +})(); + +export function isAny(type: Type): boolean { + return type === RawAny || type === StrongAny; +} + +export function hasRawAny(type: Type): boolean { + return type === RawAny || type.dependencies.some(hasRawAny); +} diff --git a/src/codegen/bindgenv2/internal/array.ts b/src/codegen/bindgenv2/internal/array.ts new file mode 100644 index 0000000000..51444b8c6c --- /dev/null +++ b/src/codegen/bindgenv2/internal/array.ts @@ -0,0 +1,32 @@ +import { hasRawAny } from "./any"; +import { CodeStyle, Type } from "./base"; + +export abstract class ArrayType extends Type {} + +export function Array(elemType: Type): ArrayType { + if (hasRawAny(elemType)) { + throw RangeError("arrays cannot contain `RawAny` (use `StrongAny`)"); + } + return new (class extends ArrayType { + get idlType() { + return `::Bun::IDLArray<${elemType.idlType}>`; + } + get bindgenType() { + return `bindgen.BindgenArray(${elemType.bindgenType})`; + } + zigType(style?: CodeStyle) { + return `bun.collections.ArrayListDefault(${elemType.zigType(style)})`; + } + toCpp(value: any[]): string { + const args = `${value.map(elem => elemType.toCpp(elem)).join(", ")}`; + return `${this.idlType}::ImplementationType { ${args} }`; + } + get dependencies() { + return [elemType]; + } + getHeaders(result: Set): void { + result.add("Bindgen/ExternVectorTraits.h"); + elemType.getHeaders(result); + } + })(); +} diff --git a/src/codegen/bindgenv2/internal/base.ts b/src/codegen/bindgenv2/internal/base.ts new file mode 100644 index 0000000000..c696a6ebd7 --- /dev/null +++ b/src/codegen/bindgenv2/internal/base.ts @@ -0,0 +1,134 @@ +import util from "node:util"; +import type { NullableType, OptionalType } from "./optional"; + +/** Default is "compact". */ +export type CodeStyle = "compact" | "pretty"; + +export abstract class Type { + get optional(): OptionalType { + return require("./optional").optional(this); + } + + get nullable(): NullableType { + return require("./optional").nullable(this); + } + + abstract readonly idlType: string; + abstract readonly bindgenType: string; + + /** + * This can be overridden to make the generated code clearer. If overridden, it must return an + * expression that evaluates to the same type as `${this.bindgenType}.ZigType`; it should not + * actually change the type. + */ + zigType(style?: CodeStyle): string { + return this.bindgenType + ".ZigType"; + } + + /** This must be overridden if bindgen.zig defines a custom `OptionalZigType`. */ + optionalZigType(style?: CodeStyle): string { + return `?${this.zigType(style)}`; + } + + /** Converts a JS value into a C++ expression. Used for default values. */ + abstract toCpp(value: any): string; + + /** Other types that this type contains or otherwise depends on. */ + get dependencies(): readonly Type[] { + return []; + } + + /** Headers required by users of this type. */ + getHeaders(result: Set): void { + for (const type of this.dependencies) { + type.getHeaders(result); + } + } +} + +export abstract class NamedType extends Type { + abstract readonly name: string; + get cppHeader(): string | null { + return null; + } + get cppSource(): string | null { + return null; + } + get zigSource(): string | null { + return null; + } + // These getters are faster than `.cppHeader != null` etc. + get hasCppHeader(): boolean { + return false; + } + get hasCppSource(): boolean { + return false; + } + get hasZigSource(): boolean { + return false; + } + getHeaders(result: Set): void { + result.add(`Generated${this.name}.h`); + } +} + +export function validateName(name: string): void { + const reservedPrefixes = ["IDL", "Bindgen", "Extern", "Generated", "MemberType"]; + const reservedNames = ["Bun", "WTF", "JSC", "WebCore", "Self"]; + if (!/^[A-Z]/.test(name)) { + throw RangeError(`name must start with a capital letter: ${name}`); + } + if (/[^a-zA-Z0-9_]/.test(name)) { + throw RangeError(`name may only contain letters, numbers, and underscores: ${name}`); + } + if (reservedPrefixes.some(s => name.startsWith(s))) { + throw RangeError(`name starts with reserved prefix: ${name}`); + } + if (reservedNames.includes(name)) { + throw RangeError(`cannot use reserved name: ${name}`); + } +} + +export function headersForTypes(types: readonly Type[]): string[] { + const headers = new Set(); + for (const type of types) { + type.getHeaders(headers); + } + return Array.from(headers); +} + +export function dedent(text: string): string { + const commonIndent = Math.min( + ...Array.from(text.matchAll(/\n( *)[^ \n]/g) ?? []).map(m => m[1].length), + ); + text = text.trim(); + if (commonIndent > 0 && commonIndent !== Infinity) { + text = text.replaceAll("\n" + " ".repeat(commonIndent), "\n"); + } + return text.replace(/^ +$/gm, ""); +} + +/** Converts indents from 2 spaces to 4. */ +export function reindent(text: string): string { + return dedent(text).replace(/^ +/gm, "$&$&"); +} + +/** Does not indent the first line. */ +export function addIndent(amount: number, text: string): string { + return text.replaceAll("\n", "\n" + " ".repeat(amount)); +} + +export function joinIndented(amount: number, pieces: readonly string[]): string { + return addIndent(amount, pieces.map(dedent).join("\n")); +} + +export function toQuotedLiteral(value: string): string { + return `"${util.inspect(value).slice(1, -1).replaceAll('"', '\\"')}"`; +} + +export function toASCIILiteral(value: string): string { + if (value[Symbol.iterator]().some(c => c.charCodeAt(0) >= 128)) { + throw RangeError(`string must be ASCII: ${util.inspect(value)}`); + } + return `${toQuotedLiteral(value)}_s`; +} diff --git a/src/codegen/bindgenv2/internal/dictionary.ts b/src/codegen/bindgenv2/internal/dictionary.ts new file mode 100644 index 0000000000..9244646941 --- /dev/null +++ b/src/codegen/bindgenv2/internal/dictionary.ts @@ -0,0 +1,451 @@ +import { hasRawAny, isAny } from "./any"; +import { + addIndent, + CodeStyle, + dedent, + headersForTypes, + joinIndented, + NamedType, + reindent, + toASCIILiteral, + toQuotedLiteral, + Type, + validateName, +} from "./base"; +import * as optional from "./optional"; +import { isUnion } from "./union"; + +export interface DictionaryMember { + type: Type; + /** Optional default value to use when this member is missing or undefined. */ + default?: any; + /** The name used in generated Zig/C++ code. Defaults to the public JS name. */ + internalName?: string; + /** Alternative JavaScript names for this member. */ + altNames?: string[]; +} + +export interface DictionaryMembers { + readonly [name: string]: Type | DictionaryMember; +} + +export interface DictionaryInstance { + readonly [name: string]: any; +} + +export abstract class DictionaryType extends NamedType {} + +interface DictionaryOptions { + name: string; + /** Used in error messages. Defaults to `name`. */ + userFacingName?: string; + /** Whether to generate a Zig `fromJS` function. */ + generateConversionFunction?: boolean; +} + +export function dictionary( + nameOrOptions: string | DictionaryOptions, + members: DictionaryMembers, +): DictionaryType { + let name: string; + let userFacingName: string; + let generateConversionFunction = false; + if (typeof nameOrOptions === "string") { + name = nameOrOptions; + userFacingName = name; + } else { + name = nameOrOptions.name; + userFacingName = nameOrOptions.userFacingName ?? name; + generateConversionFunction = !!nameOrOptions.generateConversionFunction; + } + validateName(name); + const fullMembers = Object.entries(members).map( + ([name, value]) => new FullDictionaryMember(name, value), + ); + + return new (class extends DictionaryType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + get dependencies() { + return fullMembers.map(m => m.type); + } + + toCpp(value: DictionaryInstance): string { + for (const memberName of Object.keys(value)) { + if (!(memberName in members)) throw RangeError(`unexpected key: ${memberName}`); + } + return reindent(`${name} { + ${joinIndented( + 8, + fullMembers.map(memberInfo => { + let memberValue; + if (Object.hasOwn(value, memberInfo.name)) { + memberValue = value[memberInfo.name]; + } else if (memberInfo.hasDefault) { + memberValue = memberInfo.default; + } else if (!permitsUndefined(memberInfo.type)) { + throw RangeError(`missing key: ${memberInfo.name}`); + } + const internalName = memberInfo.internalName; + return `.${internalName} = ${memberInfo.type.toCpp(memberValue)},`; + }), + )} + }`); + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + return reindent(` + #pragma once + #include "Bindgen.h" + #include "JSDOMConvertDictionary.h" + ${headersForTypes(Object.values(fullMembers).map(m => m.type)) + .map(headerName => `#include <${headerName}>\n` + " ".repeat(8)) + .join("")} + namespace Bun { + namespace Bindgen { + namespace Generated { + struct ${name} { + ${joinIndented( + 10, + fullMembers.map((memberInfo, i) => { + return ` + using MemberType${i} = ${memberInfo.type.idlType}::ImplementationType; + MemberType${i} ${memberInfo.internalName}; + `; + }), + )} + }; + using IDL${name} = ::WebCore::IDLDictionary<${name}>; + struct Extern${name} { + ${joinIndented( + 10, + fullMembers.map((memberInfo, i) => { + return ` + using MemberType${i} = ExternTraits<${name}::MemberType${i}>::ExternType; + MemberType${i} ${memberInfo.internalName}; + `; + }), + )} + };${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = dedent(` + extern "C" bool bindgenConvertJSTo${name}( + ::JSC::JSGlobalObject* globalObject, + ::JSC::EncodedJSValue value, + Extern${name}* result); + `); + return addIndent(8, "\n" + result); + })()} + } + + template<> struct ExternTraits { + using ExternType = Generated::Extern${name}; + static ExternType convertToExtern(Generated::${name}&& cppValue) + { + return ExternType { + ${joinIndented( + 14, + fullMembers.map((memberInfo, i) => { + const cppType = `Generated::${name}::MemberType${i}`; + const cppValue = `::std::move(cppValue.${memberInfo.internalName})`; + const rhs = `ExternTraits<${cppType}>::convertToExtern(${cppValue})`; + return `.${memberInfo.internalName} = ${rhs},`; + }), + )} + }; + } + }; + } + + template<> + struct IDLHumanReadableName<::WebCore::IDLDictionary> + : BaseIDLHumanReadableName { + static constexpr auto humanReadableName + = ::std::to_array(${toQuotedLiteral(userFacingName)}); + }; + } + + template<> Bun::Bindgen::Generated::${name} + WebCore::convertDictionary( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value); + + ${(() => { + if (!hasRawAny(this)) { + return ""; + } + const code = ` + template<> struct WebCore::IDLDictionary<::Bun::Bindgen::Generated::${name}> + : ::Bun::Bindgen::IDLStackOnlyDictionary<::Bun::Bindgen::Generated::${name}> {}; + `; + return joinIndented(8, [code]); + })()} + `); + } + + get hasCppSource() { + return true; + } + get cppSource() { + return reindent(` + #include "root.h" + #include "Generated${name}.h" + #include "Bindgen/IDLConvert.h" + #include + + template<> Bun::Bindgen::Generated::${name} + WebCore::convertDictionary( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + ::JSC::VM& vm = globalObject.vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto ctx = Bun::Bindgen::LiteralConversionContext { ${toASCIILiteral(userFacingName)} }; + auto* object = value.getObject(); + if (!object) [[unlikely]] { + ctx.throwNotObject(globalObject, throwScope); + return {}; + } + ::Bun::Bindgen::Generated::${name} result; + ${joinIndented( + 10, + fullMembers.map((m, i) => memberConversion(userFacingName, m, i)), + )} + return result; + } + + ${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = ` + namespace Bun::Bindgen::Generated { + extern "C" bool bindgenConvertJSTo${name}( + ::JSC::JSGlobalObject* globalObject, + ::JSC::EncodedJSValue value, + Extern${name}* result) + { + ::JSC::VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + ${name} convertedValue = ::WebCore::convert>( + *globalObject, + JSC::JSValue::decode(value) + ); + RETURN_IF_EXCEPTION(throwScope, false); + *result = ExternTraits<${name}>::convertToExtern(::std::move(convertedValue)); + return true; + } + } + `; + return joinIndented(8, [result]); + })()} + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = struct { + const Self = @This(); + + ${joinIndented( + 10, + fullMembers.map(memberInfo => { + return `${memberInfo.internalName}: ${memberInfo.type.zigType("pretty")},`; + }), + )} + + pub fn deinit(self: *Self) void { + ${joinIndented( + 12, + fullMembers.map(memberInfo => { + return `bun.memory.deinit(&self.${memberInfo.internalName});`; + }), + )} + self.* = undefined; + }${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = dedent(` + pub fn fromJS(globalThis: *jsc.JSGlobalObject, value: jsc.JSValue) bun.JSError!Self { + var scope: jsc.ExceptionValidationScope = undefined; + scope.init(globalThis, @src()); + defer scope.deinit(); + var extern_result: Extern${name} = undefined; + const success = bindgenConvertJSTo${name}(globalThis, value, &extern_result); + scope.assertExceptionPresenceMatches(!success); + return if (success) + Bindgen${name}.convertFromExtern(extern_result) + else + error.JSError; + } + `); + return addIndent(10, "\n" + result); + })()} + }; + + pub const Bindgen${name} = struct { + const Self = @This(); + pub const ZigType = ${name}; + pub const ExternType = Extern${name}; + pub fn convertFromExtern(extern_value: Self.ExternType) Self.ZigType { + return .{ + ${joinIndented( + 14, + fullMembers.map(memberInfo => { + const internalName = memberInfo.internalName; + const bindgenType = memberInfo.type.bindgenType; + const rhs = `${bindgenType}.convertFromExtern(extern_value.${internalName})`; + return `.${internalName} = ${rhs},`; + }), + )} + }; + } + }; + + const Extern${name} = extern struct { + ${joinIndented( + 10, + fullMembers.map(memberInfo => { + return `${memberInfo.internalName}: ${memberInfo.type.bindgenType}.ExternType,`; + }), + )} + }; + + extern fn bindgenConvertJSTo${name}( + globalObject: *jsc.JSGlobalObject, + value: jsc.JSValue, + result: *Extern${name}, + ) bool; + + const bindgen_generated = @import("bindgen_generated"); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + const jsc = bun.bun_js.jsc; + `); + } + })(); +} + +class FullDictionaryMember { + names: string[]; + internalName: string; + type: Type; + hasDefault: boolean = false; + default?: any; + + constructor(name: string, member: Type | DictionaryMember) { + if (member instanceof Type) { + this.names = [name]; + this.internalName = name; + this.type = member; + } else { + this.names = [name, ...(member.altNames ?? [])]; + this.internalName = member.internalName ?? name; + this.type = member.type; + this.hasDefault = Object.hasOwn(member, "default"); + this.default = member.default; + } + } + + get name(): string { + return this.names[0]; + } +} + +function memberConversion( + userFacingDictName: string, + memberInfo: FullDictionaryMember, + memberIndex: number, +): string { + const i = memberIndex; + const internalName = memberInfo.internalName; + const idlType = memberInfo.type.idlType; + const qualifiedName = `${userFacingDictName}.${memberInfo.name}`; + + const start = ` + ::JSC::JSValue value${i}; + auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${toASCIILiteral(qualifiedName)} }; + do { + ${joinIndented( + 6, + memberInfo.names.map((memberName, altNameIndex) => { + let result = ""; + if (altNameIndex > 0) { + result = `if (!value${i}.isUndefined()) break;\n`; + } + result += dedent(` + value${i} = object->get( + &globalObject, + ::JSC::Identifier::fromString(vm, ${toASCIILiteral(memberName)})); + RETURN_IF_EXCEPTION(throwScope, {}); + `); + return result; + }), + )} + } while (false); + `; + + let end: string; + if (memberInfo.hasDefault) { + end = ` + if (value${i}.isUndefined()) { + result.${internalName} = ${memberInfo.type.toCpp(memberInfo.default)}; + } else { + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + } + `; + } else if (permitsUndefined(memberInfo.type)) { + end = ` + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + `; + } else { + end = ` + if (value${i}.isUndefined()) { + ctx${i}.throwRequired(globalObject, throwScope); + return {}; + } + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + `; + } + const body = dedent(start) + "\n" + dedent(end); + return addIndent(2, "{\n" + body) + "\n}"; +} + +function basicPermitsUndefined(type: Type): boolean { + return ( + type instanceof optional.OptionalType || + type instanceof optional.NullableType || + type === optional.undefined || + type === optional.null || + isAny(type) + ); +} + +function permitsUndefined(type: Type): boolean { + if (isUnion(type)) { + return type.dependencies.some(basicPermitsUndefined); + } + return basicPermitsUndefined(type); +} diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts new file mode 100644 index 0000000000..8da8ed147a --- /dev/null +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -0,0 +1,182 @@ +import assert from "node:assert"; +import util from "node:util"; +import { + CodeStyle, + joinIndented, + NamedType, + reindent, + toASCIILiteral, + toQuotedLiteral, +} from "./base"; + +abstract class EnumType extends NamedType {} + +export function enumeration(name: string, values: string[]): EnumType { + if (values.length === 0) { + throw RangeError("enum cannot be empty: " + name); + } + if (values.length > 1n << 32n) { + throw RangeError("too many enum values: " + name); + } + + const valueSet = new Set(); + const cppMemberSet = new Set(); + for (const value of values) { + if (valueSet.size === valueSet.add(value).size) { + throw RangeError(`duplicate enum value in ${name}: ${util.inspect(value)}`); + } + let cppName = "k"; + cppName += value + .split(/[^A-Za-z0-9]+/) + .filter(x => x) + .map(s => s[0].toUpperCase() + s.slice(1)) + .join(""); + if (cppMemberSet.size === cppMemberSet.add(cppName).size) { + let i = 2; + while (cppMemberSet.size === cppMemberSet.add(cppName + i).size) { + ++i; + } + } + } + const cppMembers = Array.from(cppMemberSet); + return new (class extends EnumType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + toCpp(value: string): string { + const index = values.indexOf(value); + if (index === -1) { + throw RangeError(`not a member of this enumeration: ${value}`); + } + return `::Bun::Bindgen::Generated::${name}::${cppMembers[index]}`; + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + const quotedValues = values.map(v => `"${v}"`); + let humanReadableName; + if (quotedValues.length == 0) { + assert(false); // unreachable + } else if (quotedValues.length == 1) { + humanReadableName = quotedValues[0]; + } else if (quotedValues.length == 2) { + humanReadableName = quotedValues[0] + " or " + quotedValues[1]; + } else { + humanReadableName = + quotedValues.slice(0, -1).join(", ") + ", or " + quotedValues[quotedValues.length - 1]; + } + + return reindent(` + #pragma once + #include "Bindgen/ExternTraits.h" + #include "JSDOMConvertEnumeration.h" + + namespace Bun { + namespace Bindgen { + namespace Generated { + enum class ${name} : ::std::uint32_t { + ${joinIndented( + 10, + cppMembers.map(memberName => `${memberName},`), + )} + }; + using IDL${name} = ::WebCore::IDLEnumeration; + } + template<> struct ExternTraits : TrivialExtern {}; + } + template<> + struct IDLHumanReadableName<::WebCore::IDLEnumeration> + : BaseIDLHumanReadableName { + static constexpr auto humanReadableName + = std::to_array(${toQuotedLiteral(humanReadableName)}); + }; + } + + template<> std::optional + WebCore::parseEnumerationFromString( + const WTF::String&); + + template<> std::optional + WebCore::parseEnumeration( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value); + `); + } + + get hasCppSource() { + return true; + } + get cppSource() { + const qualifiedName = "Bun::Bindgen::Generated::" + name; + const pairType = `::std::pair<::WTF::ComparableASCIILiteral, ::${qualifiedName}>`; + return reindent(` + #include "root.h" + #include "Generated${name}.h" + #include + + template<> std::optional<${qualifiedName}> + WebCore::parseEnumerationFromString<${qualifiedName}>(const WTF::String& stringVal) + { + static constexpr ::std::array<${pairType}, ${values.length}> mappings { + ${joinIndented( + 12, + values + .map<[string, number]>((value, i) => [value, i]) + .sort() + .map(([value, i]) => { + return `${pairType} { + ${toASCIILiteral(value)}, + ::${qualifiedName}::${cppMembers[i]}, + },`; + }), + )} + }; + static constexpr ::WTF::SortedArrayMap enumerationMapping { mappings }; + if (auto* enumerationValue = enumerationMapping.tryGet(stringVal)) [[likely]] { + return *enumerationValue; + } + return std::nullopt; + } + + template<> std::optional<${qualifiedName}> + WebCore::parseEnumeration<${qualifiedName}>( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + return parseEnumerationFromString<::${qualifiedName}>( + value.toWTFString(&globalObject) + ); + } + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = enum(u32) { + ${joinIndented( + 10, + values.map(value => `@${toQuotedLiteral(value)},`), + )} + }; + + pub const Bindgen${name} = bindgen.BindgenTrivial(${name}); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + `); + } + })(); +} diff --git a/src/codegen/bindgenv2/internal/interfaces.ts b/src/codegen/bindgenv2/internal/interfaces.ts new file mode 100644 index 0000000000..21584ba0f4 --- /dev/null +++ b/src/codegen/bindgenv2/internal/interfaces.ts @@ -0,0 +1,40 @@ +import { CodeStyle, Type } from "./base"; + +export const ArrayBuffer = new (class extends Type { + get idlType() { + return `::Bun::IDLArrayBufferRef`; + } + get bindgenType() { + return `bindgen.BindgenArrayBuffer`; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.JSCArrayBuffer.Ref"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("default values for `ArrayBuffer` are not supported"); + } +})(); + +export const Blob = new (class extends Type { + get idlType() { + return `::Bun::IDLBlobRef`; + } + get bindgenType() { + return `bindgen.BindgenBlob`; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.webcore.Blob.Ref"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("default values for `Blob` are not supported"); + } + getHeaders(result: Set): void { + result.add("BunIDLConvertBlob.h"); + } +})(); diff --git a/src/codegen/bindgenv2/internal/optional.ts b/src/codegen/bindgenv2/internal/optional.ts new file mode 100644 index 0000000000..74235b6fbe --- /dev/null +++ b/src/codegen/bindgenv2/internal/optional.ts @@ -0,0 +1,84 @@ +import { isAny } from "./any"; +import { CodeStyle, Type } from "./base"; + +export abstract class OptionalType extends Type {} + +export function optional(payload: Type): OptionalType { + if (isAny(payload)) { + throw RangeError("`Any` types are already optional"); + } + return new (class extends OptionalType { + get idlType() { + return `::WebCore::IDLOptional<${payload.idlType}>`; + } + get bindgenType() { + return `bindgen.BindgenOptional(${payload.bindgenType})`; + } + zigType(style?: CodeStyle) { + return payload.optionalZigType(style); + } + toCpp(value: any): string { + if (value === undefined) { + return `::WebCore::IDLOptional<${payload.idlType}>::nullValue()`; + } + return payload.toCpp(value); + } + })(); +} + +export abstract class NullableType extends Type {} + +export function nullable(payload: Type): NullableType { + const AsOptional = optional(payload); + return new (class extends NullableType { + get idlType() { + return `::WebCore::IDLNullable<${payload.idlType}>`; + } + get bindgenType() { + return AsOptional.bindgenType; + } + zigType(style?: CodeStyle) { + return AsOptional.zigType(style); + } + toCpp(value: any): string { + if (value == null) { + return `::WebCore::IDLNullable<${payload.idlType}>::nullValue()`; + } + return payload.toCpp(value); + } + })(); +} + +/** For use in unions, to represent an optional union. */ +const Undefined = new (class extends Type { + get idlType() { + return `::Bun::IDLStrictUndefined`; + } + get bindgenType() { + return `bindgen.BindgenNull`; + } + zigType(style?: CodeStyle) { + return "void"; + } + toCpp(value: undefined): string { + return `{}`; + } +})(); + +/** For use in unions, to represent a nullable union. */ +const Null = new (class extends Type { + get idlType() { + return `::Bun::IDLStrictNull`; + } + get bindgenType() { + return `bindgen.BindgenNull`; + } + zigType(style?: CodeStyle) { + return "void"; + } + toCpp(value: null): string { + return `nullptr`; + } +})(); + +export { Null as null, Undefined as undefined }; diff --git a/src/codegen/bindgenv2/internal/primitives.ts b/src/codegen/bindgenv2/internal/primitives.ts new file mode 100644 index 0000000000..72d24405d7 --- /dev/null +++ b/src/codegen/bindgenv2/internal/primitives.ts @@ -0,0 +1,125 @@ +import assert from "node:assert"; +import util from "node:util"; +import { CodeStyle, Type } from "./base"; + +export const bool: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLStrictBoolean"; + } + get bindgenType() { + return `bindgen.BindgenBool`; + } + zigType(style?: CodeStyle) { + return "bool"; + } + toCpp(value: boolean): string { + assert(typeof value === "boolean"); + return value ? "true" : "false"; + } +})(); + +function makeUnsignedType(width: number): Type { + assert(Number.isInteger(width) && width > 0); + return new (class extends Type { + get idlType() { + return `::Bun::IDLStrictInteger<::std::uint${width}_t>`; + } + get bindgenType() { + return `bindgen.BindgenU${width}`; + } + zigType(style?: CodeStyle) { + return `u${width}`; + } + toCpp(value: number | bigint): string { + assert(typeof value === "bigint" || Number.isSafeInteger(value)); + const intValue = BigInt(value); + if (intValue < 0) throw RangeError("unsigned int cannot be negative"); + const max = 1n << BigInt(width); + if (intValue >= max) throw RangeError("integer out of range"); + return intValue.toString(); + } + })(); +} + +function makeSignedType(width: number): Type { + assert(Number.isInteger(width) && width > 0); + return new (class extends Type { + get idlType() { + return `::Bun::IDLStrictInteger<::std::int${width}_t>`; + } + get bindgenType() { + return `bindgen.BindgenI${width}`; + } + zigType(style?: CodeStyle) { + return `i${width}`; + } + toCpp(value: number | bigint): string { + assert(typeof value === "bigint" || Number.isSafeInteger(value)); + const intValue = BigInt(value); + const max = 1n << BigInt(width - 1); + const min = -max; + if (intValue >= max || intValue < min) { + throw RangeError("integer out of range"); + } + if (width === 64 && intValue === min) { + return `(${intValue + 1n} - 1)`; + } + return intValue.toString(); + } + })(); +} + +export const u8: Type = makeUnsignedType(8); +export const u16: Type = makeUnsignedType(16); +export const u32: Type = makeUnsignedType(32); +export const u64: Type = makeUnsignedType(64); + +export const i8: Type = makeSignedType(8); +export const i16: Type = makeSignedType(16); +export const i32: Type = makeSignedType(32); +export const i64: Type = makeSignedType(64); + +export const f64: Type = new (class extends Type { + get finite() { + return finiteF64; + } + + get idlType() { + return "::Bun::IDLStrictDouble"; + } + get bindgenType() { + return `bindgen.BindgenF64`; + } + zigType(style?: CodeStyle) { + return `f64`; + } + toCpp(value: number): string { + assert(typeof value === "number"); + if (Number.isNaN(value)) { + return "::std::numeric_limits::quiet_NaN()"; + } else if (value === Infinity) { + return "::std::numeric_limits::infinity()"; + } else if (value === -Infinity) { + return "-::std::numeric_limits::infinity()"; + } else { + return util.inspect(value); + } + } +})(); + +export const finiteF64: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLFiniteDouble"; + } + get bindgenType() { + return f64.bindgenType; + } + zigType(style?: CodeStyle) { + return f64.zigType(style); + } + toCpp(value: number): string { + assert(typeof value === "number"); + if (!Number.isFinite(value)) throw RangeError("number must be finite"); + return util.inspect(value); + } +})(); diff --git a/src/codegen/bindgenv2/internal/string.ts b/src/codegen/bindgenv2/internal/string.ts new file mode 100644 index 0000000000..1363942d46 --- /dev/null +++ b/src/codegen/bindgenv2/internal/string.ts @@ -0,0 +1,21 @@ +import assert from "node:assert"; +import { CodeStyle, Type, toASCIILiteral } from "./base"; + +export const String: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLStrictString"; + } + get bindgenType() { + return "bindgen.BindgenString"; + } + zigType(style?: CodeStyle) { + return "bun.string.WTFString"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: string): string { + assert(typeof value === "string"); + return toASCIILiteral(value); + } +})(); diff --git a/src/codegen/bindgenv2/internal/union.ts b/src/codegen/bindgenv2/internal/union.ts new file mode 100644 index 0000000000..e452f8b1f0 --- /dev/null +++ b/src/codegen/bindgenv2/internal/union.ts @@ -0,0 +1,185 @@ +import assert from "node:assert"; +import { + CodeStyle, + dedent, + headersForTypes, + joinIndented, + NamedType, + reindent, + Type, + validateName, +} from "./base"; + +export interface NamedAlternatives { + readonly [name: string]: Type; +} + +export interface UnionInstance { + readonly type: Type; + readonly value: any; +} + +export abstract class AnonymousUnionType extends Type {} +export abstract class NamedUnionType extends NamedType {} + +export function isUnion(type: Type): boolean { + return type instanceof AnonymousUnionType || type instanceof NamedUnionType; +} + +export function union(alternatives: Type[]): AnonymousUnionType; +export function union(name: string, alternatives: NamedAlternatives): NamedUnionType; + +/** + * The order of types in this union is significant. Each type is tried in order, and the first one + * that successfully converts determines the active field in the corresponding Zig tagged union. + * + * This means that it is an error to specify `RawAny` or `StrongAny` as anything other than the + * last alternative, as conversion to any subsequent types would never be attempted. + */ +export function union( + alternativesOrName: Type[] | string, + maybeNamedAlternatives?: NamedAlternatives, +): AnonymousUnionType | NamedUnionType { + let alternatives: Type[]; + + function toCpp(value: UnionInstance): string { + assert(alternatives.includes(value.type)); + return `${value.type.idlType}::ImplementationType { ${value.type.toCpp(value.value)} }`; + } + + function getUnionType() { + return `::Bun::IDLOrderedUnion<${alternatives.map(a => a.idlType).join(", ")}>`; + } + + function validateAlternatives(name?: string) { + const suffix = name == null ? "" : `: ${name}`; + if (alternatives.length === 0) { + throw RangeError("union cannot be empty" + suffix); + } + } + + if (typeof alternativesOrName !== "string") { + alternatives = alternativesOrName.slice(); + validateAlternatives(); + // anonymous union (neither union nor fields are named) + return new (class extends AnonymousUnionType { + get idlType() { + return getUnionType(); + } + get bindgenType() { + return `bindgen.BindgenUnion(&.{ ${alternatives.map(a => a.bindgenType).join(", ")} })`; + } + zigType(style?: CodeStyle) { + if (style !== "pretty") { + return `bun.meta.TaggedUnion(&.{ ${alternatives.map(a => a.zigType()).join(", ")} })`; + } + return dedent(`bun.meta.TaggedUnion(&.{ + ${joinIndented( + 10, + alternatives.map(a => a.zigType("pretty") + ","), + )} + })`); + } + get dependencies() { + return Object.freeze(alternatives); + } + toCpp(value: UnionInstance): string { + return toCpp(value); + } + })(); + } + + assert(maybeNamedAlternatives !== undefined); + const namedAlternatives: NamedAlternatives = maybeNamedAlternatives; + const name: string = alternativesOrName; + validateName(name); + alternatives = Object.values(namedAlternatives); + validateAlternatives(name); + // named union (both union and fields are named) + return new (class extends NamedUnionType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + get dependencies() { + return Object.freeze(alternatives); + } + toCpp(value: UnionInstance): string { + return toCpp(value); + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + return reindent(` + #pragma once + #include "Bindgen/IDLTypes.h" + ${headersForTypes(alternatives) + .map(headerName => `#include <${headerName}>\n` + " ".repeat(8)) + .join("")} + namespace Bun::Bindgen::Generated { + using IDL${name} = ${getUnionType()}; + using ${name} = IDL${name}::ImplementationType; + } + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = union(enum) { + ${joinIndented( + 10, + Object.entries(namedAlternatives).map(([altName, altType]) => { + return `${altName}: ${altType.zigType("pretty")},`; + }), + )} + + pub fn deinit(self: *@This()) void { + switch (std.meta.activeTag(self.*)) { + inline else => |tag| bun.memory.deinit(&@field(self, @tagName(tag))), + } + self.* = undefined; + } + }; + + pub const Bindgen${name} = struct { + const Self = @This(); + pub const ZigType = ${name}; + pub const ExternType = bindgen.ExternTaggedUnion(&.{ ${alternatives + .map(a => a.bindgenType + ".ExternType") + .join(", ")} }); + pub fn convertFromExtern(extern_value: Self.ExternType) Self.ZigType { + return switch (extern_value.tag) { + ${joinIndented( + 14, + Object.entries(namedAlternatives).map(([altName, altType], i) => { + const bindgenType = altType.bindgenType; + const innerRhs = `${bindgenType}.convertFromExtern(extern_value.data.@"${i}")`; + return `${i} => .{ .${altName} = ${innerRhs} },`; + }), + )} + else => unreachable, + }; + } + }; + + const bindgen_generated = @import("bindgen_generated"); + const std = @import("std"); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + `); + } + })(); +} diff --git a/src/codegen/bindgenv2/lib.ts b/src/codegen/bindgenv2/lib.ts new file mode 100644 index 0000000000..ce92a319e9 --- /dev/null +++ b/src/codegen/bindgenv2/lib.ts @@ -0,0 +1,10 @@ +// organize-imports-ignore +export { bool, u8, u16, u32, u64, i8, i16, i32, i64, f64 } from "./internal/primitives"; +export { RawAny, StrongAny } from "./internal/any"; +export { String } from "./internal/string"; +export { optional, nullable, undefined, null } from "./internal/optional"; +export { union } from "./internal/union"; +export { dictionary } from "./internal/dictionary"; +export { enumeration } from "./internal/enumeration"; +export { Array } from "./internal/array"; +export { ArrayBuffer, Blob } from "./internal/interfaces"; diff --git a/src/codegen/bindgenv2/script.ts b/src/codegen/bindgenv2/script.ts new file mode 100755 index 0000000000..6d4e96e197 --- /dev/null +++ b/src/codegen/bindgenv2/script.ts @@ -0,0 +1,185 @@ +#!/usr/bin/env bun +import * as helpers from "../helpers"; +import { NamedType, Type } from "./internal/base"; + +const USAGE = `\ +Usage: script.ts [options] + +Options (all required): + --command= Command to run (see below) + --sources= Comma-separated list of *.bindv2.ts files + --codegen-path= Path to build/*/codegen + +Commands: + list-outputs List files that will be generated, separated by semicolons (for CMake) + generate Generate all files +`; + +let codegenPath: string; +let sources: string[]; + +function getNamedExports(): NamedType[] { + return sources.flatMap(path => { + const exports = import.meta.require(path); + return Object.values(exports).filter(v => v instanceof NamedType); + }); +} + +function getNamedDependencies(type: Type, result: Set): void { + for (const dependency of type.dependencies) { + if (dependency instanceof NamedType) { + result.add(dependency); + } + getNamedDependencies(dependency, result); + } +} + +function cppHeaderPath(type: NamedType): string { + return `${codegenPath}/Generated${type.name}.h`; +} + +function cppSourcePath(type: NamedType): string { + return `${codegenPath}/Generated${type.name}.cpp`; +} + +function zigSourcePath(typeOrNamespace: NamedType | string): string { + let ns: string; + if (typeof typeOrNamespace === "string") { + ns = typeOrNamespace; + } else { + ns = toZigNamespace(typeOrNamespace.name); + } + return `${codegenPath}/bindgen_generated/${ns}.zig`; +} + +function toZigNamespace(name: string): string { + const result = name + .replace(/([^A-Z_])([A-Z])/g, "$1_$2") + .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2") + .toLowerCase(); + if (result === name) { + return result + "_namespace"; + } + return result; +} + +function listOutputs(): void { + const outputs: string[] = [`${codegenPath}/bindgen_generated.zig`]; + for (const type of getNamedExports()) { + if (type.hasCppSource) outputs.push(cppSourcePath(type)); + if (type.hasZigSource) outputs.push(zigSourcePath(type)); + } + process.stdout.write(outputs.join(";")); +} + +function generate(): void { + const names = new Set(); + const zigRoot: string[] = []; + const zigRootInternal: string[] = []; + + const namedExports = getNamedExports(); + { + const namedDependencies = new Set(); + for (const type of namedExports) { + getNamedDependencies(type, namedDependencies); + } + const namedExportsSet = new Set(namedExports); + for (const type of namedDependencies) { + if (!namedExportsSet.has(type)) { + console.error(`error: named type must be exported: ${type.name}`); + process.exit(1); + } + } + const namedTypeNames = new Set(); + for (const type of namedExports) { + if (namedTypeNames.size == namedTypeNames.add(type.name).size) { + console.error(`error: multiple types with same name: ${type.name}`); + process.exit(1); + } + } + } + + for (const type of namedExports) { + const zigNamespace = toZigNamespace(type.name); + const size = names.size; + names.add(type.name); + names.add(zigNamespace); + if (names.size !== size + 2) { + console.error(`error: duplicate name: ${type.name}`); + process.exit(1); + } + + const cppHeader = type.cppHeader; + const cppSource = type.cppSource; + const zigSource = type.zigSource; + if (cppHeader) { + helpers.writeIfNotChanged(cppHeaderPath(type), cppHeader); + } + if (cppSource) { + helpers.writeIfNotChanged(cppSourcePath(type), cppSource); + } + if (zigSource) { + zigRoot.push( + `pub const ${zigNamespace} = @import("./bindgen_generated/${zigNamespace}.zig");`, + `pub const ${type.name} = ${zigNamespace}.${type.name};`, + "", + ); + zigRootInternal.push(`pub const ${type.name} = ${zigNamespace}.Bindgen${type.name};`); + helpers.writeIfNotChanged(zigSourcePath(zigNamespace), zigSource); + } + } + + helpers.writeIfNotChanged( + `${codegenPath}/bindgen_generated.zig`, + [ + ...zigRoot, + `pub const internal = struct {`, + ...zigRootInternal.map(s => " " + s), + `};`, + "", + ].join("\n"), + ); +} + +function main(): void { + const args = helpers.argParse(["command", "codegen-path", "sources", "help"]); + if (Object.keys(args).length === 0) { + process.stderr.write(USAGE); + process.exit(1); + } + const { command, "codegen-path": codegenPathArg, sources: sourcesArg, help } = args; + if (help != null) { + process.stdout.write(USAGE); + process.exit(0); + } + + if (typeof codegenPathArg !== "string") { + console.error("error: missing --codegen-path"); + process.exit(1); + } + codegenPath = codegenPathArg; + + if (typeof sourcesArg !== "string") { + console.error("error: missing --sources"); + process.exit(1); + } + sources = sourcesArg.split(",").filter(x => x); + + switch (command) { + case "list-outputs": + listOutputs(); + break; + case "generate": + generate(); + break; + default: + if (typeof command === "string") { + console.error("error: unknown command: " + command); + } else { + console.error("error: missing --command"); + } + process.exit(1); + } +} + +main(); diff --git a/src/codegen/bindgenv2/tsconfig.json b/src/codegen/bindgenv2/tsconfig.json new file mode 100644 index 0000000000..2f087e4473 --- /dev/null +++ b/src/codegen/bindgenv2/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "noUnusedLocals": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitAny": true, + "noImplicitThis": true, + "exactOptionalPropertyTypes": true + }, + "include": ["**/*.ts", "../helpers.ts"] +} diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 18a5941d02..a89b703fb5 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -114,7 +114,7 @@ for (let i = 0; i < nativeStartIndex; i++) { `Cannot use ESM import statement within builtin modules. Use require("${imp.path}") instead. See src/js/README.md (from ${moduleList[i]})`, ); err.name = "BunError"; - err.fileName = moduleList[i]; + err["fileName"] = moduleList[i]; throw err; } } @@ -125,7 +125,7 @@ for (let i = 0; i < nativeStartIndex; i++) { `Using \`export default\` AND named exports together in builtin modules is unsupported. See src/js/README.md (from ${moduleList[i]})`, ); err.name = "BunError"; - err.fileName = moduleList[i]; + err["fileName"] = moduleList[i]; throw err; } let importStatements: string[] = []; diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 985cc01053..a1792fdb38 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -286,7 +286,7 @@ export function define( Object.entries(klass) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => { - v.DOMJIT = undefined; + v["DOMJIT"] = undefined; return [k, v]; }), ), @@ -294,7 +294,7 @@ export function define( Object.entries(proto) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => { - v.DOMJIT = undefined; + v["DOMJIT"] = undefined; return [k, v]; }), ), diff --git a/src/codegen/helpers.ts b/src/codegen/helpers.ts index e6868ccd61..4a9665b4b4 100644 --- a/src/codegen/helpers.ts +++ b/src/codegen/helpers.ts @@ -132,15 +132,20 @@ export function pascalCase(string: string) { } export function argParse(keys: string[]): any { - const options = {}; + const options: { [key: string]: boolean | string } = {}; for (const arg of process.argv.slice(2)) { if (!arg.startsWith("--")) { - console.error("Unknown argument " + arg); + console.error("error: unknown argument: " + arg); process.exit(1); } - const split = arg.split("="); - const value = split[1] || "true"; - options[split[0].slice(2)] = value; + const splitPos = arg.indexOf("="); + let name = arg; + let value: boolean | string = true; + if (splitPos !== -1) { + name = arg.slice(0, splitPos); + value = arg.slice(splitPos + 1); + } + options[name.slice(2)] = value; } const unknown = new Set(Object.keys(options)); @@ -148,7 +153,7 @@ export function argParse(keys: string[]): any { unknown.delete(key); } for (const key of unknown) { - console.error("Unknown argument: --" + key); + console.error("error: unknown argument: --" + key); } if (unknown.size > 0) process.exit(1); return options; diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index fd1f3438ad..a0b43be968 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -253,7 +253,7 @@ export function applyReplacements(src: string, length: number) { } } - const id = registerNativeCall(kind, args[0], args[1], is_create_fn ? args[2] : undefined); + const id = registerNativeCall(kind, args[0], args[1], is_create_fn ? args[2] : null); return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; } else if (name === "isPromiseFulfilled") { @@ -305,7 +305,7 @@ export function applyReplacements(src: string, length: number) { throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`); } - const id = registerNativeCall("bind", args[0], args[1], undefined); + const id = registerNativeCall("bind", args[0], args[1], null); return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; } else { diff --git a/src/deps/uws/SocketContext.zig b/src/deps/uws/SocketContext.zig index d2737f270f..2672402e7e 100644 --- a/src/deps/uws/SocketContext.zig +++ b/src/deps/uws/SocketContext.zig @@ -229,11 +229,11 @@ pub const SocketContext = opaque { ca_file_name: [*c]const u8 = null, ssl_ciphers: [*c]const u8 = null, ssl_prefer_low_memory_usage: i32 = 0, - key: ?[*]?[*:0]const u8 = null, + key: ?[*]const ?[*:0]const u8 = null, key_count: u32 = 0, - cert: ?[*]?[*:0]const u8 = null, + cert: ?[*]const ?[*:0]const u8 = null, cert_count: u32 = 0, - ca: ?[*]?[*:0]const u8 = null, + ca: ?[*]const ?[*:0]const u8 = null, ca_count: u32 = 0, secure_options: u32 = 0, reject_unauthorized: i32 = 0, diff --git a/src/meta.zig b/src/meta.zig index 964235a26f..89658ea945 100644 --- a/src/meta.zig +++ b/src/meta.zig @@ -195,6 +195,8 @@ fn CreateUniqueTuple(comptime N: comptime_int, comptime types: [N]type) type { }); } +pub const TaggedUnion = @import("./meta/tagged_union.zig").TaggedUnion; + pub fn hasStableMemoryLayout(comptime T: type) bool { const tyinfo = @typeInfo(T); return switch (tyinfo) { diff --git a/src/meta/tagged_union.zig b/src/meta/tagged_union.zig new file mode 100644 index 0000000000..0d32507926 --- /dev/null +++ b/src/meta/tagged_union.zig @@ -0,0 +1,236 @@ +fn deinitImpl(comptime Union: type, value: *Union) void { + switch (std.meta.activeTag(value.*)) { + inline else => |tag| bun.memory.deinit(&@field(value, @tagName(tag))), + } + value.* = undefined; +} + +/// Creates a tagged union with fields corresponding to `field_types`. The fields are named +/// @"0", @"1", @"2", etc. +pub fn TaggedUnion(comptime field_types: []const type) type { + // Types created with @Type can't contain decls, so in order to have a `deinit` method, we + // have to do it this way... + return switch (comptime field_types.len) { + 0 => @compileError("cannot create an empty tagged union"), + 1 => union(enum) { + @"0": field_types[0], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 2 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 3 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 4 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 5 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 6 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 7 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 8 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 9 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 10 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 11 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 12 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 13 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 14 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 15 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + @"14": field_types[14], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 16 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + @"14": field_types[14], + @"15": field_types[15], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + else => @compileError("too many union fields"), + }; +} + +const bun = @import("bun"); +const std = @import("std"); diff --git a/src/napi/napi.zig b/src/napi/napi.zig index d961c52a8f..779e0acaf3 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -832,7 +832,7 @@ pub export fn napi_get_typedarray_info( maybe_length: ?*usize, maybe_data: ?*[*]u8, maybe_arraybuffer: ?*napi_value, - maybe_byte_offset: ?*usize, + maybe_byte_offset: ?*usize, // note: this is always 0 ) napi_status { log("napi_get_typedarray_info", .{}); const env = env_ orelse { @@ -859,7 +859,10 @@ pub export fn napi_get_typedarray_info( arraybuffer.set(env, JSValue.c(jsc.C.JSObjectGetTypedArrayBuffer(env.toJS().ref(), typedarray.asObjectRef(), null))); if (maybe_byte_offset) |byte_offset| - byte_offset.* = array_buffer.offset; + // `jsc.ArrayBuffer` used to have an `offset` field, but it was always 0 because `ptr` + // already had the offset applied. See . + //byte_offset.* = array_buffer.offset; + byte_offset.* = 0; return env.ok(); } pub extern fn napi_create_dataview(env: napi_env, length: usize, arraybuffer: napi_value, byte_offset: usize, result: *napi_value) napi_status; @@ -881,7 +884,7 @@ pub export fn napi_get_dataview_info( maybe_bytelength: ?*usize, maybe_data: ?*[*]u8, maybe_arraybuffer: ?*napi_value, - maybe_byte_offset: ?*usize, + maybe_byte_offset: ?*usize, // note: this is always 0 ) napi_status { log("napi_get_dataview_info", .{}); const env = env_ orelse { @@ -900,7 +903,10 @@ pub export fn napi_get_dataview_info( arraybuffer.set(env, JSValue.c(jsc.C.JSObjectGetTypedArrayBuffer(env.toJS().ref(), dataview.asObjectRef(), null))); if (maybe_byte_offset) |byte_offset| - byte_offset.* = array_buffer.offset; + // `jsc.ArrayBuffer` used to have an `offset` field, but it was always 0 because `ptr` + // already had the offset applied. See . + //byte_offset.* = array_buffer.offset; + byte_offset.* = 0; return env.ok(); } diff --git a/src/node-fallbacks/build-fallbacks.ts b/src/node-fallbacks/build-fallbacks.ts index bb5d23b6ee..8e06d0d548 100644 --- a/src/node-fallbacks/build-fallbacks.ts +++ b/src/node-fallbacks/build-fallbacks.ts @@ -5,13 +5,13 @@ import { basename, extname } from "path"; const allFiles = fs.readdirSync(".").filter(f => f.endsWith(".js")); const outdir = process.argv[2]; const builtins = Module.builtinModules; -let commands = []; +let commands: Promise[] = []; -let moduleFiles = []; +let moduleFiles: string[] = []; for (const name of allFiles) { const mod = basename(name, extname(name)).replaceAll(".", "/"); const file = allFiles.find(f => f.startsWith(mod)); - moduleFiles.push(file); + moduleFiles.push(file as string); } for (let fileIndex = 0; fileIndex < allFiles.length; fileIndex++) { diff --git a/src/string.zig b/src/string.zig index b7524f0792..17d70e05e5 100644 --- a/src/string.zig +++ b/src/string.zig @@ -6,8 +6,9 @@ pub const PathString = @import("./string/PathString.zig").PathString; pub const SmolStr = @import("./string/SmolStr.zig").SmolStr; pub const StringBuilder = @import("./string/StringBuilder.zig"); pub const StringJoiner = @import("./string/StringJoiner.zig"); -pub const WTFStringImpl = @import("./string/WTFStringImpl.zig").WTFStringImpl; -pub const WTFStringImplStruct = @import("./string/WTFStringImpl.zig").WTFStringImplStruct; +pub const WTFString = @import("./string/wtf.zig").WTFString; +pub const WTFStringImpl = @import("./string/wtf.zig").WTFStringImpl; +pub const WTFStringImplStruct = @import("./string/wtf.zig").WTFStringImplStruct; pub const Tag = enum(u8) { /// String is not valid. Observed on some failed operations. @@ -47,7 +48,7 @@ pub const String = extern struct { pub const empty = String{ .tag = .Empty, .value = .{ .ZigString = .Empty } }; pub const dead = String{ .tag = .Dead, .value = .{ .Dead = {} } }; - pub const StringImplAllocator = @import("./string/WTFStringImpl.zig").StringImplAllocator; + pub const StringImplAllocator = @import("./string/wtf.zig").StringImplAllocator; pub fn toInt32(this: *const String) ?i32 { const val = bun.cpp.BunString__toInt32(this); diff --git a/src/string/WTFStringImpl.zig b/src/string/wtf.zig similarity index 100% rename from src/string/WTFStringImpl.zig rename to src/string/wtf.zig diff --git a/src/tsconfig.json b/src/tsconfig.json index 3f63af9f7a..9fd93876cd 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -4,10 +4,11 @@ // Path remapping "baseUrl": ".", "paths": { - "bindgen": ["./codegen/bindgen-lib.ts"] + "bindgen": ["./codegen/bindgen-lib.ts"], + "bindgenv2": ["./codegen/bindgenv2/lib.ts"] } }, "include": ["**/*.ts", "**/*.tsx"], // separate projects have extra settings that only apply in those scopes - "exclude": ["js", "bake"] + "exclude": ["js", "bake", "init", "create", "bun.js/bindings/libuv"] } diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index c4b6c70273..68922965e7 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -110,7 +110,7 @@ describe.concurrent("Server", () => { }, port: 0, }); - }).toThrow("tls option expects an object"); + }).toThrow("TLSOptions must be an object"); }); }); @@ -125,7 +125,7 @@ describe.concurrent("Server", () => { }, port: 0, }); - }).not.toThrow("tls option expects an object"); + }).not.toThrow("TLSOptions must be an object"); }); }); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index d52b25ba71..b25fa22e19 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -1611,7 +1611,7 @@ describe.concurrent("should error with invalid options", async () => { requestCert: "invalid", }, }); - }).toThrow('The "requestCert" property must be of type boolean, got string'); + }).toThrow("TLSOptions.requestCert must be a boolean"); }); it("rejectUnauthorized", () => { expect(() => { @@ -1624,7 +1624,7 @@ describe.concurrent("should error with invalid options", async () => { rejectUnauthorized: "invalid", }, }); - }).toThrow('The "rejectUnauthorized" property must be of type boolean, got string'); + }).toThrow("TLSOptions.rejectUnauthorized must be a boolean"); }); it("lowMemoryMode", () => { expect(() => { @@ -1638,7 +1638,7 @@ describe.concurrent("should error with invalid options", async () => { lowMemoryMode: "invalid", }, }); - }).toThrow("Expected lowMemoryMode to be a boolean"); + }).toThrow("TLSOptions.lowMemoryMode must be a boolean"); }); it("multiple missing server name", () => { expect(() => { diff --git a/test/js/bun/net/tcp-server.test.ts b/test/js/bun/net/tcp-server.test.ts index 6cd76e1ca5..b9e74a8267 100644 --- a/test/js/bun/net/tcp-server.test.ts +++ b/test/js/bun/net/tcp-server.test.ts @@ -71,7 +71,7 @@ it("should not allow invalid tls option", () => { hostname: "localhost", tls: value, }); - }).toThrow("tls option expects an object"); + }).toThrow("TLSOptions must be an object"); }); }); @@ -89,7 +89,7 @@ it("should allow using false, null or undefined tls option", () => { hostname: "localhost", tls: value, }); - }).not.toThrow("tls option expects an object"); + }).not.toThrow("TLSOptions must be an object"); }); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index a28d20e3fa..1da8c6b919 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,7 +24,7 @@ "noFallthroughCasesInSwitch": true, "isolatedModules": true, - // Stricter type-checking + // Less strict type-checking "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, From 2aa373ab63023d2eafbcacf98a8c74d78ee1ffbf Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 3 Oct 2025 17:13:06 -0700 Subject: [PATCH 015/191] Refactor: Split JSNodeHTTPServerSocket into separate files (#23203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Split `JSNodeHTTPServerSocket` and `JSNodeHTTPServerSocketPrototype` from `NodeHTTP.cpp` into dedicated files, following the same pattern as `JSDiffieHellman` in the crypto module. ## Changes - **Created 4 new files:** - `JSNodeHTTPServerSocket.h` - Class declaration - `JSNodeHTTPServerSocket.cpp` - Class implementation and methods - `JSNodeHTTPServerSocketPrototype.h` - Prototype declaration - `JSNodeHTTPServerSocketPrototype.cpp` - Prototype methods and property table - **Moved from NodeHTTP.cpp:** - All custom getters/setters (onclose, ondrain, ondata, etc.) - All host functions (close, write, end) - Event handlers (onClose, onDrain, onData) - Helper functions and templates - **Preserved:** - All extern C bindings for Zig interop - All existing functionality - Proper namespace and include structure - **Merged changes from main:** - Added `upgraded` flag for websocket support (from #23150) - Updated `clearSocketData` to handle WebSocketData - Added `onSocketUpgraded` callback handler ## Impact - Reduced `NodeHTTP.cpp` from ~1766 lines to 1010 lines (43% reduction) - Better code organization and maintainability - No functional changes ## Test plan - [x] Build compiles successfully - [x] `test/js/node/http/node-http.test.ts` passes (72/74 tests pass, same as before) - [x] `test/js/node/http/node-http-with-ws.test.ts` passes (websocket upgrade test) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/bindings/NodeHTTP.cpp | 765 +----------------- .../bindings/node/JSNodeHTTPServerSocket.cpp | 375 +++++++++ .../bindings/node/JSNodeHTTPServerSocket.h | 108 +++ .../node/JSNodeHTTPServerSocketPrototype.cpp | 330 ++++++++ .../node/JSNodeHTTPServerSocketPrototype.h | 45 ++ 5 files changed, 860 insertions(+), 763 deletions(-) create mode 100644 src/bun.js/bindings/node/JSNodeHTTPServerSocket.cpp create mode 100644 src/bun.js/bindings/node/JSNodeHTTPServerSocket.h create mode 100644 src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp create mode 100644 src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.h diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index 3b939e5929..116183e7f7 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -21,770 +21,14 @@ #include #include #include "JSSocketAddressDTO.h" - -extern "C" { -struct us_socket_stream_buffer_t { - char* list_ptr = nullptr; - size_t list_cap = 0; - size_t listLen = 0; - size_t total_bytes_written = 0; - size_t cursor = 0; - - size_t bufferedSize() const - { - return listLen - cursor; - } - size_t totalBytesWritten() const - { - return total_bytes_written; - } -}; -} - -extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6); -extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6); - -extern "C" void Bun__NodeHTTPResponse_setClosed(void* zigResponse); -extern "C" void Bun__NodeHTTPResponse_onClose(void* zigResponse, JSC::EncodedJSValue jsValue); -extern "C" EncodedJSValue us_socket_buffered_js_write(void* socket, bool is_ssl, bool ended, us_socket_stream_buffer_t* streamBuffer, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue data, JSC::EncodedJSValue encoding); -extern "C" void us_socket_free_stream_buffer(us_socket_stream_buffer_t* streamBuffer); +#include "node/JSNodeHTTPServerSocket.h" +#include "node/JSNodeHTTPServerSocketPrototype.h" namespace Bun { using namespace JSC; using namespace WebCore; -JSC_DEFINE_CUSTOM_SETTER(noOpSetter, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, PropertyName propertyName)) -{ - return false; -} - -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnDrain); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed); -JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose); -JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnDrain); -JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnData); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnData); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten); -JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose); -JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite); -JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress); - BUN_DECLARE_HOST_FUNCTION(Bun__drainMicrotasksFromJS); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex); -JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished); -// Create a static hash table of values containing an onclose DOMAttributeGetterSetter and a close function -static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = { - { "onclose"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnClose, jsNodeHttpServerSocketSetterOnClose } }, - { "ondrain"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnDrain, jsNodeHttpServerSocketSetterOnDrain } }, - { "ondata"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnData, jsNodeHttpServerSocketSetterOnData } }, - { "bytesWritten"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterBytesWritten, noOpSetter } }, - { "closed"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterClosed, noOpSetter } }, - { "response"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterResponse, noOpSetter } }, - { "duplex"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterDuplex, jsNodeHttpServerSocketSetterDuplex } }, - { "remoteAddress"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterRemoteAddress, noOpSetter } }, - { "localAddress"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterLocalAddress, noOpSetter } }, - { "close"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } }, - { "write"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketWrite, 2 } }, - { "end"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketEnd, 0 } }, - { "secureEstablished"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterIsSecureEstablished, noOpSetter } }, -}; - -class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - - static JSNodeHTTPServerSocketPrototype* create(VM& vm, Structure* structure) - { - JSNodeHTTPServerSocketPrototype* prototype = new (NotNull, allocateCell(vm)) JSNodeHTTPServerSocketPrototype(vm, structure); - prototype->finishCreation(vm); - return prototype; - } - - DECLARE_INFO; - - static constexpr bool needsDestruction = false; - static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable; - - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSNodeHTTPServerSocketPrototype, Base); - return &vm.plainObjectSpace(); - } - -private: - JSNodeHTTPServerSocketPrototype(VM& vm, Structure* structure) - : Base(vm, structure) - { - } - - void finishCreation(VM& vm) - { - Base::finishCreation(vm); - ASSERT(inherits(info())); - reifyStaticProperties(vm, info(), JSNodeHTTPServerSocketPrototypeTableValues, *this); - this->structure()->setMayBePrototype(true); - } -}; - -class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { -public: - using Base = JSC::JSDestructibleObject; - us_socket_stream_buffer_t streamBuffer = {}; - us_socket_t* socket = nullptr; - unsigned is_ssl : 1 = 0; - unsigned ended : 1 = 0; - unsigned upgraded : 1 = 0; - JSC::Strong strongThis = {}; - - static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) - { - auto* object = new (JSC::allocateCell(vm)) JSNodeHTTPServerSocket(vm, structure, socket, is_ssl, response); - object->finishCreation(vm); - return object; - } - - static JSNodeHTTPServerSocket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) - { - auto* structure = globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject); - return create(vm, structure, socket, is_ssl, response); - } - - static void destroy(JSC::JSCell* cell) - { - static_cast(cell)->JSNodeHTTPServerSocket::~JSNodeHTTPServerSocket(); - } - - template - static void clearSocketData(bool upgraded, us_socket_t* socket) - { - if (upgraded) { - auto* webSocket = (uWS::WebSocketData*)us_socket_ext(SSL, socket); - webSocket->socketData = nullptr; - } else { - auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); - httpResponseData->socketData = nullptr; - } - } - - void close() - { - if (socket) { - us_socket_close(is_ssl, socket, 0, nullptr); - } - } - - bool isClosed() const - { - return !socket || us_socket_is_closed(is_ssl, socket); - } - // This means: - // - [x] TLS - // - [x] Handshake has completed - // - [x] Handshake marked the connection as authorized - bool isAuthorized() const - { - // is secure means that tls was established successfully - if (!is_ssl || !socket) return false; - auto* context = us_socket_context(is_ssl, socket); - if (!context) return false; - auto* data = (uWS::HttpContextData*)us_socket_context_ext(is_ssl, context); - if (!data) return false; - return data->flags.isAuthorized; - } - ~JSNodeHTTPServerSocket() - { - if (socket) { - if (is_ssl) { - clearSocketData(this->upgraded, socket); - } else { - clearSocketData(this->upgraded, socket); - } - } - us_socket_free_stream_buffer(&streamBuffer); - } - - JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) - : JSC::JSDestructibleObject(vm, structure) - , socket(socket) - , is_ssl(is_ssl) - { - currentResponseObject.setEarlyValue(vm, this, response); - } - - mutable WriteBarrier functionToCallOnClose; - mutable WriteBarrier functionToCallOnDrain; - mutable WriteBarrier functionToCallOnData; - mutable WriteBarrier currentResponseObject; - mutable WriteBarrier m_remoteAddress; - mutable WriteBarrier m_localAddress; - mutable WriteBarrier m_duplex; - - DECLARE_INFO; - DECLARE_VISIT_CHILDREN; - - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForJSNodeHTTPServerSocket.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSNodeHTTPServerSocket = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForJSNodeHTTPServerSocket.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForJSNodeHTTPServerSocket = std::forward(space); }); - } - - void detach() - { - this->m_duplex.clear(); - this->currentResponseObject.clear(); - this->strongThis.clear(); - } - - void onClose() - { - - this->socket = nullptr; - if (auto* res = this->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { - Bun__NodeHTTPResponse_setClosed(res->m_ctx); - } - - // This function can be called during GC! - Zig::GlobalObject* globalObject = static_cast(this->globalObject()); - if (!functionToCallOnClose) { - if (auto* res = this->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { - Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); - } - this->detach(); - return; - } - - WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); - - if (scriptExecutionContext) { - scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { - WTF::NakedPtr exception; - auto* globalObject = defaultGlobalObject(context.globalObject()); - auto* thisObject = self; - auto* callbackObject = thisObject->functionToCallOnClose.get(); - if (!callbackObject) { - if (auto* res = thisObject->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { - Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); - } - thisObject->detach(); - return; - } - auto callData = JSC::getCallData(callbackObject); - MarkedArgumentBuffer args; - EnsureStillAliveScope ensureStillAlive(self); - - if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { - if (auto* res = thisObject->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { - Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); - } - - profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); - - if (auto* ptr = exception.get()) { - exception.clear(); - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); - } - } - thisObject->detach(); - }); - } - } - - void onDrain() - { - // This function can be called during GC! - Zig::GlobalObject* globalObject = static_cast(this->globalObject()); - if (!functionToCallOnDrain) { - return; - } - - auto bufferedSize = this->streamBuffer.bufferedSize(); - if (bufferedSize > 0) { - - auto* globalObject = defaultGlobalObject(this->globalObject()); - auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); - us_socket_buffered_js_write(this->socket, this->is_ssl, this->ended, &this->streamBuffer, globalObject, JSValue::encode(JSC::jsUndefined()), JSValue::encode(JSC::jsUndefined())); - if (scope.exception()) { - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); - return; - } - bufferedSize = this->streamBuffer.bufferedSize(); - - if (bufferedSize > 0) { - // need to drain more - return; - } - } - WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); - - if (scriptExecutionContext) { - scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { - WTF::NakedPtr exception; - auto* globalObject = defaultGlobalObject(context.globalObject()); - auto* thisObject = self; - auto* callbackObject = thisObject->functionToCallOnDrain.get(); - if (!callbackObject) { - return; - } - auto callData = JSC::getCallData(callbackObject); - MarkedArgumentBuffer args; - EnsureStillAliveScope ensureStillAlive(self); - - if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { - profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); - - if (auto* ptr = exception.get()) { - exception.clear(); - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); - } - } - }); - } - } - - void - onData(const char* data, int length, bool last) - { - // This function can be called during GC! - Zig::GlobalObject* globalObject = static_cast(this->globalObject()); - if (!functionToCallOnData) { - return; - } - - WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); - - if (scriptExecutionContext) { - auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); - JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, std::span(reinterpret_cast(data), length)); - auto chunk = JSC::JSValue(buffer); - if (scope.exception()) { - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); - return; - } - gcProtect(chunk); - scriptExecutionContext->postTask([self = this, chunk = chunk, last = last](ScriptExecutionContext& context) { - WTF::NakedPtr exception; - auto* globalObject = defaultGlobalObject(context.globalObject()); - auto* thisObject = self; - auto* callbackObject = thisObject->functionToCallOnData.get(); - EnsureStillAliveScope ensureChunkStillAlive(chunk); - gcUnprotect(chunk); - if (!callbackObject) { - return; - } - - auto callData = JSC::getCallData(callbackObject); - MarkedArgumentBuffer args; - args.append(chunk); - args.append(JSC::jsBoolean(last)); - EnsureStillAliveScope ensureStillAlive(self); - - if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { - profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); - - if (auto* ptr = exception.get()) { - exception.clear(); - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); - } - } - }); - } - } - - static Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) - { - auto* structure = JSC::Structure::create(vm, globalObject, globalObject->objectPrototype(), JSC::TypeInfo(JSC::ObjectType, StructureFlags), JSNodeHTTPServerSocketPrototype::info()); - auto* prototype = JSNodeHTTPServerSocketPrototype::create(vm, structure); - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - - void finishCreation(JSC::VM& vm) - { - Base::finishCreation(vm); - } -}; - -JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto* thisObject = jsDynamicCast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - return JSValue::encode(JSC::jsUndefined()); - } - if (thisObject->isClosed()) { - return JSValue::encode(JSC::jsUndefined()); - } - thisObject->close(); - - return JSValue::encode(JSC::jsUndefined()); -} - -JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto* thisObject = jsDynamicCast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - return JSValue::encode(JSC::jsNumber(0)); - } - if (thisObject->isClosed() || thisObject->ended) { - return JSValue::encode(JSC::jsNumber(0)); - } - - return us_socket_buffered_js_write(thisObject->socket, thisObject->is_ssl, thisObject->ended, &thisObject->streamBuffer, globalObject, JSValue::encode(callFrame->argument(0)), JSValue::encode(callFrame->argument(1))); -} - -JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto* thisObject = jsDynamicCast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - return JSValue::encode(JSC::jsUndefined()); - } - if (thisObject->isClosed()) { - return JSValue::encode(JSC::jsUndefined()); - } - - thisObject->ended = true; - auto bufferedSize = thisObject->streamBuffer.bufferedSize(); - if (bufferedSize == 0) { - return us_socket_buffered_js_write(thisObject->socket, thisObject->is_ssl, thisObject->ended, &thisObject->streamBuffer, globalObject, JSValue::encode(JSC::jsUndefined()), JSValue::encode(JSC::jsUndefined())); - } - return JSValue::encode(JSC::jsUndefined()); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - return JSValue::encode(JSC::jsBoolean(thisObject->isAuthorized())); -} -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - if (thisObject->m_duplex) { - return JSValue::encode(thisObject->m_duplex.get()); - } - return JSValue::encode(JSC::jsNull()); -} - -JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) -{ - auto& vm = globalObject->vm(); - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - JSValue value = JSC::JSValue::decode(encodedValue); - if (auto* object = value.getObject()) { - thisObject->m_duplex.set(vm, thisObject, object); - - } else { - thisObject->m_duplex.clear(); - } - - return true; -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto& vm = globalObject->vm(); - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - if (thisObject->m_remoteAddress) { - return JSValue::encode(thisObject->m_remoteAddress.get()); - } - - us_socket_t* socket = thisObject->socket; - if (!socket) { - return JSValue::encode(JSC::jsNull()); - } - - const char* address = nullptr; - int port = 0; - bool is_ipv6 = false; - - uws_res_get_remote_address_info(socket, &address, &port, &is_ipv6); - - if (address == nullptr) { - return JSValue::encode(JSC::jsNull()); - } - - auto addressString = WTF::String::fromUTF8(address); - if (addressString.isEmpty()) { - return JSValue::encode(JSC::jsNull()); - } - - auto* object = JSSocketAddressDTO::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); - thisObject->m_remoteAddress.set(vm, thisObject, object); - return JSValue::encode(object); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto& vm = globalObject->vm(); - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - if (thisObject->m_localAddress) { - return JSValue::encode(thisObject->m_localAddress.get()); - } - - us_socket_t* socket = thisObject->socket; - if (!socket) { - return JSValue::encode(JSC::jsNull()); - } - - const char* address = nullptr; - int port = 0; - bool is_ipv6 = false; - - uws_res_get_local_address_info(socket, &address, &port, &is_ipv6); - - if (address == nullptr) { - return JSValue::encode(JSC::jsNull()); - } - - auto addressString = WTF::String::fromUTF8(address); - if (addressString.isEmpty()) { - return JSValue::encode(JSC::jsNull()); - } - - auto* object = JSSocketAddressDTO::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); - thisObject->m_localAddress.set(vm, thisObject, object); - return JSValue::encode(object); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - - if (thisObject->functionToCallOnClose) { - return JSValue::encode(thisObject->functionToCallOnClose.get()); - } - - return JSValue::encode(JSC::jsUndefined()); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnDrain, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - - if (thisObject->functionToCallOnDrain) { - return JSValue::encode(thisObject->functionToCallOnDrain.get()); - } - - return JSValue::encode(JSC::jsUndefined()); -} -JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnDrain, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - JSValue value = JSC::JSValue::decode(encodedValue); - - if (value.isUndefined() || value.isNull()) { - thisObject->functionToCallOnDrain.clear(); - return true; - } - - if (!value.isCallable()) { - return false; - } - - thisObject->functionToCallOnDrain.set(vm, thisObject, value.getObject()); - return true; -} -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnData, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - - if (thisObject->functionToCallOnData) { - return JSValue::encode(thisObject->functionToCallOnData.get()); - } - - return JSValue::encode(JSC::jsUndefined()); -} -JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnData, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - JSValue value = JSC::JSValue::decode(encodedValue); - - if (value.isUndefined() || value.isNull()) { - thisObject->functionToCallOnData.clear(); - return true; - } - - if (!value.isCallable()) { - return false; - } - - thisObject->functionToCallOnData.set(vm, thisObject, value.getObject()); - return true; -} -JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - JSValue value = JSC::JSValue::decode(encodedValue); - - if (value.isUndefined() || value.isNull()) { - thisObject->functionToCallOnClose.clear(); - return true; - } - - if (!value.isCallable()) { - return false; - } - - thisObject->functionToCallOnClose.set(vm, thisObject, value.getObject()); - return true; -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - return JSValue::encode(JSC::jsBoolean(thisObject->isClosed())); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - return JSValue::encode(JSC::jsNumber(thisObject->streamBuffer.totalBytesWritten())); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - if (!thisObject->currentResponseObject) { - return JSValue::encode(JSC::jsNull()); - } - - return JSValue::encode(thisObject->currentResponseObject.get()); -} - -template -void JSNodeHTTPServerSocket::visitChildrenImpl(JSCell* cell, Visitor& visitor) -{ - JSNodeHTTPServerSocket* fn = jsCast(cell); - ASSERT_GC_OBJECT_INHERITS(fn, info()); - Base::visitChildren(fn, visitor); - - visitor.append(fn->currentResponseObject); - visitor.append(fn->functionToCallOnClose); - visitor.append(fn->functionToCallOnDrain); - visitor.append(fn->functionToCallOnData); - visitor.append(fn->m_remoteAddress); - visitor.append(fn->m_localAddress); - visitor.append(fn->m_duplex); -} - -DEFINE_VISIT_CHILDREN(JSNodeHTTPServerSocket); - -template -static JSNodeHTTPServerSocket* getNodeHTTPServerSocket(us_socket_t* socket) -{ - auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); - return reinterpret_cast(httpResponseData->socketData); -} - -template -static WebCore::JSNodeHTTPResponse* getNodeHTTPResponse(us_socket_t* socket) -{ - auto* serverSocket = getNodeHTTPServerSocket(socket); - if (!serverSocket) { - return nullptr; - } - return serverSocket->currentResponseObject.get(); -} - -const JSC::ClassInfo JSNodeHTTPServerSocket::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, - CREATE_METHOD_TABLE(JSNodeHTTPServerSocket) }; - -const JSC::ClassInfo JSNodeHTTPServerSocketPrototype::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, - CREATE_METHOD_TABLE(JSNodeHTTPServerSocketPrototype) }; - -template -static void* getNodeHTTPResponsePtr(us_socket_t* socket) -{ - WebCore::JSNodeHTTPResponse* responseObject = getNodeHTTPResponse(socket); - if (!responseObject) { - return nullptr; - } - return responseObject->wrapped(); -} - -extern "C" EncodedJSValue Bun__getNodeHTTPResponseThisValue(bool is_ssl, us_socket_t* socket) -{ - if (is_ssl) { - return JSValue::encode(getNodeHTTPResponse(socket)); - } - return JSValue::encode(getNodeHTTPResponse(socket)); -} - -extern "C" EncodedJSValue Bun__getNodeHTTPServerSocketThisValue(bool is_ssl, us_socket_t* socket) -{ - if (is_ssl) { - return JSValue::encode(getNodeHTTPServerSocket(socket)); - } - return JSValue::encode(getNodeHTTPServerSocket(socket)); -} - -extern "C" void Bun__setNodeHTTPServerSocketUsSocketValue(EncodedJSValue thisValue, us_socket_t* socket) -{ - auto* response = jsCast(JSValue::decode(thisValue)); - response->socket = socket; -} - -extern "C" JSC::EncodedJSValue Bun__createNodeHTTPServerSocketForClientError(bool isSSL, us_socket_t* us_socket, Zig::GlobalObject* globalObject) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - RETURN_IF_EXCEPTION(scope, {}); - - if (isSSL) { - uWS::HttpResponse* response = reinterpret_cast*>(us_socket); - auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); - if (currentSocketDataPtr) { - return JSValue::encode(currentSocketDataPtr); - } - } else { - uWS::HttpResponse* response = reinterpret_cast*>(us_socket); - auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); - if (currentSocketDataPtr) { - return JSValue::encode(currentSocketDataPtr); - } - } - // socket without response because is not valid http - JSNodeHTTPServerSocket* socket = JSNodeHTTPServerSocket::create( - vm, - globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject), - us_socket, - isSSL, nullptr); - if (isSSL) { - uWS::HttpResponse* response = reinterpret_cast*>(us_socket); - response->getHttpResponseData()->socketData = socket; - } else { - uWS::HttpResponse* response = reinterpret_cast*>(us_socket); - response->getHttpResponseData()->socketData = socket; - } - RETURN_IF_EXCEPTION(scope, {}); - if (socket) { - socket->strongThis.set(vm, socket); - return JSValue::encode(socket); - } - - return JSValue::encode(JSC::jsNull()); -} - BUN_DECLARE_HOST_FUNCTION(jsFunctionRequestOrResponseHasBodyValue); BUN_DECLARE_HOST_FUNCTION(jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer); extern "C" uWS::HttpRequest* Request__getUWSRequest(void*); @@ -1769,9 +1013,4 @@ extern "C" void WebCore__FetchHeaders__toUWSResponse(WebCore::FetchHeaders* arg0 } } -JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) -{ - return JSNodeHTTPServerSocket::createStructure(vm, globalObject); -} - } // namespace Bun diff --git a/src/bun.js/bindings/node/JSNodeHTTPServerSocket.cpp b/src/bun.js/bindings/node/JSNodeHTTPServerSocket.cpp new file mode 100644 index 0000000000..9006ddd1c0 --- /dev/null +++ b/src/bun.js/bindings/node/JSNodeHTTPServerSocket.cpp @@ -0,0 +1,375 @@ +#include "JSNodeHTTPServerSocket.h" +#include "JSNodeHTTPServerSocketPrototype.h" +#include "ZigGlobalObject.h" +#include "ZigGeneratedClasses.h" +#include "DOMIsoSubspaces.h" +#include "ScriptExecutionContext.h" +#include "helpers.h" +#include "JSSocketAddressDTO.h" +#include +#include +#include +#include + +extern "C" void Bun__NodeHTTPResponse_setClosed(void* zigResponse); +extern "C" void Bun__NodeHTTPResponse_onClose(void* zigResponse, JSC::EncodedJSValue jsValue); +extern "C" void us_socket_free_stream_buffer(us_socket_stream_buffer_t* streamBuffer); +extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6); +extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6); +extern "C" EncodedJSValue us_socket_buffered_js_write(void* socket, bool is_ssl, bool ended, us_socket_stream_buffer_t* streamBuffer, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue data, JSC::EncodedJSValue encoding); + +namespace Bun { + +using namespace JSC; +using namespace WebCore; + +const JSC::ClassInfo JSNodeHTTPServerSocket::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, + CREATE_METHOD_TABLE(JSNodeHTTPServerSocket) }; + +JSNodeHTTPServerSocket* JSNodeHTTPServerSocket::create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) +{ + auto* object = new (JSC::allocateCell(vm)) JSNodeHTTPServerSocket(vm, structure, socket, is_ssl, response); + object->finishCreation(vm); + return object; +} + +JSNodeHTTPServerSocket* JSNodeHTTPServerSocket::create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) +{ + auto* structure = globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject); + return create(vm, structure, socket, is_ssl, response); +} + +template +void JSNodeHTTPServerSocket::clearSocketData(bool upgraded, us_socket_t* socket) +{ + if (upgraded) { + auto* webSocket = (uWS::WebSocketData*)us_socket_ext(SSL, socket); + webSocket->socketData = nullptr; + } else { + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + httpResponseData->socketData = nullptr; + } +} + +void JSNodeHTTPServerSocket::close() +{ + if (socket) { + us_socket_close(is_ssl, socket, 0, nullptr); + } +} + +bool JSNodeHTTPServerSocket::isClosed() const +{ + return !socket || us_socket_is_closed(is_ssl, socket); +} + +bool JSNodeHTTPServerSocket::isAuthorized() const +{ + // is secure means that tls was established successfully + if (!is_ssl || !socket) + return false; + auto* context = us_socket_context(is_ssl, socket); + if (!context) + return false; + auto* data = (uWS::HttpContextData*)us_socket_context_ext(is_ssl, context); + if (!data) + return false; + return data->flags.isAuthorized; +} + +JSNodeHTTPServerSocket::~JSNodeHTTPServerSocket() +{ + if (socket) { + if (is_ssl) { + clearSocketData(this->upgraded, socket); + } else { + clearSocketData(this->upgraded, socket); + } + } + us_socket_free_stream_buffer(&streamBuffer); +} + +JSNodeHTTPServerSocket::JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) + : JSC::JSDestructibleObject(vm, structure) + , socket(socket) + , is_ssl(is_ssl) +{ + currentResponseObject.setEarlyValue(vm, this, response); +} + +void JSNodeHTTPServerSocket::detach() +{ + this->m_duplex.clear(); + this->currentResponseObject.clear(); + this->strongThis.clear(); +} + +void JSNodeHTTPServerSocket::onClose() +{ + this->socket = nullptr; + if (auto* res = this->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { + Bun__NodeHTTPResponse_setClosed(res->m_ctx); + } + + // This function can be called during GC! + Zig::GlobalObject* globalObject = static_cast(this->globalObject()); + if (!functionToCallOnClose) { + if (auto* res = this->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { + Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); + } + this->detach(); + return; + } + + WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); + + if (scriptExecutionContext) { + scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { + WTF::NakedPtr exception; + auto* globalObject = defaultGlobalObject(context.globalObject()); + auto* thisObject = self; + auto* callbackObject = thisObject->functionToCallOnClose.get(); + if (!callbackObject) { + if (auto* res = thisObject->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { + Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); + } + thisObject->detach(); + return; + } + auto callData = JSC::getCallData(callbackObject); + MarkedArgumentBuffer args; + EnsureStillAliveScope ensureStillAlive(self); + + if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { + if (auto* res = thisObject->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { + Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); + } + + profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); + + if (auto* ptr = exception.get()) { + exception.clear(); + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); + } + } + thisObject->detach(); + }); + } +} + +void JSNodeHTTPServerSocket::onDrain() +{ + // This function can be called during GC! + Zig::GlobalObject* globalObject = static_cast(this->globalObject()); + if (!functionToCallOnDrain) { + return; + } + + auto bufferedSize = this->streamBuffer.bufferedSize(); + if (bufferedSize > 0) { + auto* globalObject = defaultGlobalObject(this->globalObject()); + auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + us_socket_buffered_js_write(this->socket, this->is_ssl, this->ended, &this->streamBuffer, globalObject, JSValue::encode(JSC::jsUndefined()), JSValue::encode(JSC::jsUndefined())); + if (scope.exception()) { + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); + return; + } + bufferedSize = this->streamBuffer.bufferedSize(); + + if (bufferedSize > 0) { + // need to drain more + return; + } + } + WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); + + if (scriptExecutionContext) { + scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { + WTF::NakedPtr exception; + auto* globalObject = defaultGlobalObject(context.globalObject()); + auto* thisObject = self; + auto* callbackObject = thisObject->functionToCallOnDrain.get(); + if (!callbackObject) { + return; + } + auto callData = JSC::getCallData(callbackObject); + MarkedArgumentBuffer args; + EnsureStillAliveScope ensureStillAlive(self); + + if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { + profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); + + if (auto* ptr = exception.get()) { + exception.clear(); + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); + } + } + }); + } +} + +void JSNodeHTTPServerSocket::onData(const char* data, int length, bool last) +{ + // This function can be called during GC! + Zig::GlobalObject* globalObject = static_cast(this->globalObject()); + if (!functionToCallOnData) { + return; + } + + WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); + + if (scriptExecutionContext) { + auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, std::span(reinterpret_cast(data), length)); + auto chunk = JSC::JSValue(buffer); + if (scope.exception()) { + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); + return; + } + gcProtect(chunk); + scriptExecutionContext->postTask([self = this, chunk = chunk, last = last](ScriptExecutionContext& context) { + WTF::NakedPtr exception; + auto* globalObject = defaultGlobalObject(context.globalObject()); + auto* thisObject = self; + auto* callbackObject = thisObject->functionToCallOnData.get(); + EnsureStillAliveScope ensureChunkStillAlive(chunk); + gcUnprotect(chunk); + if (!callbackObject) { + return; + } + + auto callData = JSC::getCallData(callbackObject); + MarkedArgumentBuffer args; + args.append(chunk); + args.append(JSC::jsBoolean(last)); + EnsureStillAliveScope ensureStillAlive(self); + + if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { + profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); + + if (auto* ptr = exception.get()) { + exception.clear(); + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); + } + } + }); + } +} + +JSC::Structure* JSNodeHTTPServerSocket::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + auto* structure = JSC::Structure::create(vm, globalObject, globalObject->objectPrototype(), JSC::TypeInfo(JSC::ObjectType, StructureFlags), JSNodeHTTPServerSocketPrototype::info()); + auto* prototype = JSNodeHTTPServerSocketPrototype::create(vm, structure); + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); +} + +void JSNodeHTTPServerSocket::finishCreation(JSC::VM& vm) +{ + Base::finishCreation(vm); +} + +template +void JSNodeHTTPServerSocket::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSNodeHTTPServerSocket* fn = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(fn, info()); + Base::visitChildren(fn, visitor); + + visitor.append(fn->currentResponseObject); + visitor.append(fn->functionToCallOnClose); + visitor.append(fn->functionToCallOnDrain); + visitor.append(fn->functionToCallOnData); + visitor.append(fn->m_remoteAddress); + visitor.append(fn->m_localAddress); + visitor.append(fn->m_duplex); +} + +DEFINE_VISIT_CHILDREN(JSNodeHTTPServerSocket); + +template +static JSNodeHTTPServerSocket* getNodeHTTPServerSocket(us_socket_t* socket) +{ + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + return reinterpret_cast(httpResponseData->socketData); +} + +template +static WebCore::JSNodeHTTPResponse* getNodeHTTPResponse(us_socket_t* socket) +{ + auto* serverSocket = getNodeHTTPServerSocket(socket); + if (!serverSocket) { + return nullptr; + } + return serverSocket->currentResponseObject.get(); +} + +extern "C" JSC::EncodedJSValue Bun__getNodeHTTPResponseThisValue(bool is_ssl, us_socket_t* socket) +{ + if (is_ssl) { + return JSValue::encode(getNodeHTTPResponse(socket)); + } + return JSValue::encode(getNodeHTTPResponse(socket)); +} + +extern "C" JSC::EncodedJSValue Bun__getNodeHTTPServerSocketThisValue(bool is_ssl, us_socket_t* socket) +{ + if (is_ssl) { + return JSValue::encode(getNodeHTTPServerSocket(socket)); + } + return JSValue::encode(getNodeHTTPServerSocket(socket)); +} + +extern "C" void Bun__setNodeHTTPServerSocketUsSocketValue(JSC::EncodedJSValue thisValue, us_socket_t* socket) +{ + auto* response = jsCast(JSValue::decode(thisValue)); + response->socket = socket; +} + +extern "C" JSC::EncodedJSValue Bun__createNodeHTTPServerSocketForClientError(bool isSSL, us_socket_t* us_socket, Zig::GlobalObject* globalObject) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + RETURN_IF_EXCEPTION(scope, {}); + + if (isSSL) { + uWS::HttpResponse* response = reinterpret_cast*>(us_socket); + auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); + if (currentSocketDataPtr) { + return JSValue::encode(currentSocketDataPtr); + } + } else { + uWS::HttpResponse* response = reinterpret_cast*>(us_socket); + auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); + if (currentSocketDataPtr) { + return JSValue::encode(currentSocketDataPtr); + } + } + // socket without response because is not valid http + JSNodeHTTPServerSocket* socket = JSNodeHTTPServerSocket::create( + vm, + globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject), + us_socket, + isSSL, nullptr); + if (isSSL) { + uWS::HttpResponse* response = reinterpret_cast*>(us_socket); + response->getHttpResponseData()->socketData = socket; + } else { + uWS::HttpResponse* response = reinterpret_cast*>(us_socket); + response->getHttpResponseData()->socketData = socket; + } + RETURN_IF_EXCEPTION(scope, {}); + if (socket) { + socket->strongThis.set(vm, socket); + return JSValue::encode(socket); + } + + return JSValue::encode(JSC::jsNull()); +} + +JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + return JSNodeHTTPServerSocket::createStructure(vm, globalObject); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/node/JSNodeHTTPServerSocket.h b/src/bun.js/bindings/node/JSNodeHTTPServerSocket.h new file mode 100644 index 0000000000..111a27ac1d --- /dev/null +++ b/src/bun.js/bindings/node/JSNodeHTTPServerSocket.h @@ -0,0 +1,108 @@ +#pragma once + +#include "root.h" +#include +#include +#include "BunClientData.h" + +extern "C" { +struct us_socket_stream_buffer_t { + char* list_ptr = nullptr; + size_t list_cap = 0; + size_t listLen = 0; + size_t total_bytes_written = 0; + size_t cursor = 0; + + size_t bufferedSize() const + { + return listLen - cursor; + } + size_t totalBytesWritten() const + { + return total_bytes_written; + } +}; + +struct us_socket_t; +} + +namespace uWS { +template +struct HttpResponseData; +struct WebSocketData; +} + +namespace WebCore { +class JSNodeHTTPResponse; +} + +namespace Bun { + +class JSNodeHTTPServerSocketPrototype; + +class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + us_socket_stream_buffer_t streamBuffer = {}; + us_socket_t* socket = nullptr; + unsigned is_ssl : 1 = 0; + unsigned ended : 1 = 0; + unsigned upgraded : 1 = 0; + JSC::Strong strongThis = {}; + + static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response); + static JSNodeHTTPServerSocket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response); + + static void destroy(JSC::JSCell* cell) + { + static_cast(cell)->JSNodeHTTPServerSocket::~JSNodeHTTPServerSocket(); + } + + template + static void clearSocketData(bool upgraded, us_socket_t* socket); + + void close(); + bool isClosed() const; + bool isAuthorized() const; + + ~JSNodeHTTPServerSocket(); + + JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response); + + mutable JSC::WriteBarrier functionToCallOnClose; + mutable JSC::WriteBarrier functionToCallOnDrain; + mutable JSC::WriteBarrier functionToCallOnData; + mutable JSC::WriteBarrier currentResponseObject; + mutable JSC::WriteBarrier m_remoteAddress; + mutable JSC::WriteBarrier m_localAddress; + mutable JSC::WriteBarrier m_duplex; + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSNodeHTTPServerSocket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSNodeHTTPServerSocket = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSNodeHTTPServerSocket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSNodeHTTPServerSocket = std::forward(space); }); + } + + void detach(); + void onClose(); + void onDrain(); + void onData(const char* data, int length, bool last); + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); + void finishCreation(JSC::VM& vm); +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp b/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp new file mode 100644 index 0000000000..4695bb909c --- /dev/null +++ b/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp @@ -0,0 +1,330 @@ +#include "JSNodeHTTPServerSocketPrototype.h" +#include "JSNodeHTTPServerSocket.h" +#include "JSSocketAddressDTO.h" +#include "ZigGlobalObject.h" +#include "ZigGeneratedClasses.h" +#include "helpers.h" +#include +#include + +extern "C" EncodedJSValue us_socket_buffered_js_write(void* socket, bool is_ssl, bool ended, us_socket_stream_buffer_t* streamBuffer, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue data, JSC::EncodedJSValue encoding); +extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6); +extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6); + +namespace Bun { + +using namespace JSC; +using namespace WebCore; + +// Declare custom getters/setters and host functions +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnDrain); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnDrain); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnData); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnData); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten); +JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose); +JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite); +JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished); + +JSC_DEFINE_CUSTOM_SETTER(noOpSetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName propertyName)) +{ + return false; +} + +const JSC::ClassInfo JSNodeHTTPServerSocketPrototype::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, + CREATE_METHOD_TABLE(JSNodeHTTPServerSocketPrototype) }; + +static const JSC::HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = { + { "onclose"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnClose, jsNodeHttpServerSocketSetterOnClose } }, + { "ondrain"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnDrain, jsNodeHttpServerSocketSetterOnDrain } }, + { "ondata"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnData, jsNodeHttpServerSocketSetterOnData } }, + { "bytesWritten"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterBytesWritten, noOpSetter } }, + { "closed"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterClosed, noOpSetter } }, + { "response"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterResponse, noOpSetter } }, + { "duplex"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterDuplex, jsNodeHttpServerSocketSetterDuplex } }, + { "remoteAddress"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterRemoteAddress, noOpSetter } }, + { "localAddress"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterLocalAddress, noOpSetter } }, + { "close"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } }, + { "write"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketWrite, 2 } }, + { "end"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketEnd, 0 } }, + { "secureEstablished"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterIsSecureEstablished, noOpSetter } }, +}; + +void JSNodeHTTPServerSocketPrototype::finishCreation(JSC::VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + reifyStaticProperties(vm, info(), JSNodeHTTPServerSocketPrototypeTableValues, *this); + this->structure()->setMayBePrototype(true); +} + +// Implementation of host functions +JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + return JSValue::encode(JSC::jsUndefined()); + } + if (thisObject->isClosed()) { + return JSValue::encode(JSC::jsUndefined()); + } + thisObject->close(); + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + return JSValue::encode(JSC::jsNumber(0)); + } + if (thisObject->isClosed() || thisObject->ended) { + return JSValue::encode(JSC::jsNumber(0)); + } + + return us_socket_buffered_js_write(thisObject->socket, thisObject->is_ssl, thisObject->ended, &thisObject->streamBuffer, globalObject, JSValue::encode(callFrame->argument(0)), JSValue::encode(callFrame->argument(1))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + return JSValue::encode(JSC::jsUndefined()); + } + if (thisObject->isClosed()) { + return JSValue::encode(JSC::jsUndefined()); + } + + thisObject->ended = true; + auto bufferedSize = thisObject->streamBuffer.bufferedSize(); + if (bufferedSize == 0) { + return us_socket_buffered_js_write(thisObject->socket, thisObject->is_ssl, thisObject->ended, &thisObject->streamBuffer, globalObject, JSValue::encode(JSC::jsUndefined()), JSValue::encode(JSC::jsUndefined())); + } + return JSValue::encode(JSC::jsUndefined()); +} + +// Implementation of custom getters +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsBoolean(thisObject->isAuthorized())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_duplex) { + return JSValue::encode(thisObject->m_duplex.get()); + } + return JSValue::encode(JSC::jsNull()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + if (auto* object = value.getObject()) { + thisObject->m_duplex.set(vm, thisObject, object); + } else { + thisObject->m_duplex.clear(); + } + + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_remoteAddress) { + return JSValue::encode(thisObject->m_remoteAddress.get()); + } + + us_socket_t* socket = thisObject->socket; + if (!socket) { + return JSValue::encode(JSC::jsNull()); + } + + const char* address = nullptr; + int port = 0; + bool is_ipv6 = false; + + uws_res_get_remote_address_info(socket, &address, &port, &is_ipv6); + + if (address == nullptr) { + return JSValue::encode(JSC::jsNull()); + } + + auto addressString = WTF::String::fromUTF8(address); + if (addressString.isEmpty()) { + return JSValue::encode(JSC::jsNull()); + } + + auto* object = JSSocketAddressDTO::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); + thisObject->m_remoteAddress.set(vm, thisObject, object); + return JSValue::encode(object); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_localAddress) { + return JSValue::encode(thisObject->m_localAddress.get()); + } + + us_socket_t* socket = thisObject->socket; + if (!socket) { + return JSValue::encode(JSC::jsNull()); + } + + const char* address = nullptr; + int port = 0; + bool is_ipv6 = false; + + uws_res_get_local_address_info(socket, &address, &port, &is_ipv6); + + if (address == nullptr) { + return JSValue::encode(JSC::jsNull()); + } + + auto addressString = WTF::String::fromUTF8(address); + if (addressString.isEmpty()) { + return JSValue::encode(JSC::jsNull()); + } + + auto* object = JSSocketAddressDTO::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); + thisObject->m_localAddress.set(vm, thisObject, object); + return JSValue::encode(object); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + + if (thisObject->functionToCallOnClose) { + return JSValue::encode(thisObject->functionToCallOnClose.get()); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnDrain, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + + if (thisObject->functionToCallOnDrain) { + return JSValue::encode(thisObject->functionToCallOnDrain.get()); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnDrain, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + + if (value.isUndefined() || value.isNull()) { + thisObject->functionToCallOnDrain.clear(); + return true; + } + + if (!value.isCallable()) { + return false; + } + + thisObject->functionToCallOnDrain.set(vm, thisObject, value.getObject()); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnData, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + + if (thisObject->functionToCallOnData) { + return JSValue::encode(thisObject->functionToCallOnData.get()); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnData, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + + if (value.isUndefined() || value.isNull()) { + thisObject->functionToCallOnData.clear(); + return true; + } + + if (!value.isCallable()) { + return false; + } + + thisObject->functionToCallOnData.set(vm, thisObject, value.getObject()); + return true; +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + + if (value.isUndefined() || value.isNull()) { + thisObject->functionToCallOnClose.clear(); + return true; + } + + if (!value.isCallable()) { + return false; + } + + thisObject->functionToCallOnClose.set(vm, thisObject, value.getObject()); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsBoolean(thisObject->isClosed())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsNumber(thisObject->streamBuffer.totalBytesWritten())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (!thisObject->currentResponseObject) { + return JSValue::encode(JSC::jsNull()); + } + + return JSValue::encode(thisObject->currentResponseObject.get()); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.h b/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.h new file mode 100644 index 0000000000..8aecf5f467 --- /dev/null +++ b/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.h @@ -0,0 +1,45 @@ +#pragma once + +#include "root.h" +#include +#include + +namespace Bun { + +class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags | JSC::HasStaticPropertyTable; + + static JSNodeHTTPServerSocketPrototype* create(JSC::VM& vm, JSC::Structure* structure) + { + JSNodeHTTPServerSocketPrototype* prototype = new (NotNull, JSC::allocateCell(vm)) JSNodeHTTPServerSocketPrototype(vm, structure); + prototype->finishCreation(vm); + return prototype; + } + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; + } + +private: + JSNodeHTTPServerSocketPrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; + +} // namespace Bun From f1204ea2fd3b76379b8c990d85f00dd5945ea284 Mon Sep 17 00:00:00 2001 From: pfg Date: Fri, 3 Oct 2025 17:13:22 -0700 Subject: [PATCH 016/191] bun test dots reporter (#22919) Adds a simple dots reporter for bun test image --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/bun.js/ConsoleObject.zig | 4 + src/bun.js/test/Execution.zig | 2 - src/bun.js/test/bun_test.zig | 30 +++- src/bun.js/test/jest.zig | 26 ++- src/bunfig.zig | 6 +- src/cli.zig | 5 +- src/cli/Arguments.zig | 14 +- src/cli/test_command.zig | 169 +++++++++++------- src/output.zig | 18 ++ test/js/bun/test/dots.fixture.ts | 7 + test/js/bun/test/dots.test.ts | 103 +++++++++++ .../bun/test/printing/dots/dots1.fixture.ts | 11 ++ .../bun/test/printing/dots/dots2.fixture.ts | 8 + .../bun/test/printing/dots/dots3.fixture.ts | 11 ++ 14 files changed, 330 insertions(+), 84 deletions(-) create mode 100644 test/js/bun/test/dots.fixture.ts create mode 100644 test/js/bun/test/dots.test.ts create mode 100644 test/js/bun/test/printing/dots/dots1.fixture.ts create mode 100644 test/js/bun/test/printing/dots/dots2.fixture.ts create mode 100644 test/js/bun/test/printing/dots/dots3.fixture.ts diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index e8a3e1c926..6b27a3adc7 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -153,6 +153,10 @@ fn messageWithTypeAndLevel_( var writer = buffered_writer.writer(); const Writer = @TypeOf(writer); + if (bun.jsc.Jest.Jest.runner) |runner| { + runner.bun_test_root.onBeforePrint(); + } + var print_length = len; // Get console depth from CLI options or bunfig, fallback to default const cli_context = CLI.get(); diff --git a/src/bun.js/test/Execution.zig b/src/bun.js/test/Execution.zig index 08f43ab059..2d439e26e9 100644 --- a/src/bun.js/test/Execution.zig +++ b/src/bun.js/test/Execution.zig @@ -593,8 +593,6 @@ pub fn handleUncaughtException(this: *Execution, user_data: bun_test.BunTest.Ref groupLog.begin(@src()); defer groupLog.end(); - if (bun.jsc.Jest.Jest.runner) |runner| runner.current_file.printIfNeeded(); - const sequence, const group = this.getCurrentAndValidExecutionSequence(user_data) orelse return .show_unhandled_error_between_tests; _ = group; diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index a5ba01a625..2ce02522ba 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -169,10 +169,25 @@ pub const BunTestRoot = struct { first: bool, last: bool, }; + + pub fn onBeforePrint(this: *BunTestRoot) void { + if (this.active_file.get()) |active_file| { + if (active_file.reporter) |reporter| { + if (reporter.last_printed_dot and reporter.reporters.dots) { + bun.Output.prettyError("\n", .{}); + bun.Output.flush(); + reporter.last_printed_dot = false; + } + if (bun.jsc.Jest.Jest.runner) |runner| { + runner.current_file.printIfNeeded(); + } + } + } + } }; pub const BunTest = struct { - buntest: *BunTestRoot, + bun_test_root: *BunTestRoot, in_run_loop: bool, allocation_scope: bun.AllocationScope, gpa: std.mem.Allocator, @@ -207,7 +222,7 @@ pub const BunTest = struct { this.arena = this.arena_allocator.allocator(); this.* = .{ - .buntest = bunTest, + .bun_test_root = bunTest, .in_run_loop = false, .allocation_scope = this.allocation_scope, .gpa = this.gpa, @@ -569,10 +584,10 @@ pub const BunTest = struct { }); defer order.deinit(); - const beforeall_order: Order.AllOrderResult = if (this.first_last.first) try order.generateAllOrder(this.buntest.hook_scope.beforeAll.items) else .empty; + const beforeall_order: Order.AllOrderResult = if (this.first_last.first) try order.generateAllOrder(this.bun_test_root.hook_scope.beforeAll.items) else .empty; try order.generateOrderDescribe(this.collection.root_scope); beforeall_order.setFailureSkipTo(&order); - const afterall_order: Order.AllOrderResult = if (this.first_last.last) try order.generateAllOrder(this.buntest.hook_scope.afterAll.items) else .empty; + const afterall_order: Order.AllOrderResult = if (this.first_last.last) try order.generateAllOrder(this.bun_test_root.hook_scope.afterAll.items) else .empty; afterall_order.setFailureSkipTo(&order); try this.execution.loadFromOrder(&order); @@ -703,6 +718,7 @@ pub const BunTest = struct { if (handle_status == .hide_error) return; // do not print error, it was already consumed if (exception == null) return; // the exception should not be visible (eg m_terminationException) + this.bun_test_root.onBeforePrint(); if (handle_status == .show_unhandled_error_between_tests or handle_status == .show_unhandled_error_in_describe) { this.reporter.?.jest.unhandled_errors_between_tests += 1; bun.Output.prettyErrorln( @@ -713,12 +729,14 @@ pub const BunTest = struct { , .{}); bun.Output.flush(); } + globalThis.bunVM().runErrorHandler(exception.?, null); - bun.Output.flush(); + if (handle_status == .show_unhandled_error_between_tests or handle_status == .show_unhandled_error_in_describe) { bun.Output.prettyError("-------------------------------\n\n", .{}); - bun.Output.flush(); } + + bun.Output.flush(); } }; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 521c3d064d..d34e0c22bb 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -7,8 +7,15 @@ const CurrentFile = struct { } = .{}, has_printed_filename: bool = false, - pub fn set(this: *CurrentFile, title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { - if (Output.isAIAgent()) { + pub fn set( + this: *CurrentFile, + title: string, + prefix: string, + repeat_count: u32, + repeat_index: u32, + reporter: *CommandLineReporter, + ) void { + if (Output.isAIAgent() or reporter.reporters.dots) { this.freeAndClear(); this.title = bun.handleOom(bun.default_allocator.dupe(u8, title)); this.prefix = bun.handleOom(bun.default_allocator.dupe(u8, prefix)); @@ -28,14 +35,19 @@ const CurrentFile = struct { } fn print(title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { + const enable_buffering = Output.enableBufferingScope(); + defer enable_buffering.deinit(); + + Output.prettyError("\n", .{}); + if (repeat_count > 0) { if (repeat_count > 1) { - Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ prefix, title, repeat_index + 1 }); + Output.prettyErrorln("{s}{s}: (run #{d})\n", .{ prefix, title, repeat_index + 1 }); } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); + Output.prettyErrorln("{s}{s}:\n", .{ prefix, title }); } } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); + Output.prettyErrorln("{s}{s}:\n", .{ prefix, title }); } Output.flush(); @@ -44,6 +56,7 @@ const CurrentFile = struct { pub fn printIfNeeded(this: *CurrentFile) void { if (this.has_printed_filename) return; this.has_printed_filename = true; + print(this.title, this.prefix, this.repeat_info.count, this.repeat_info.index); } }; @@ -456,7 +469,7 @@ pub fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: [] pub fn captureTestLineNumber(callframe: *jsc.CallFrame, globalThis: *JSGlobalObject) u32 { if (Jest.runner) |runner| { - if (runner.test_options.file_reporter == .junit) { + if (runner.test_options.reporters.junit) { return bun.cpp.Bun__CallFrame__getLineNumber(callframe, globalThis); } } @@ -475,6 +488,7 @@ const string = []const u8; pub const bun_test = @import("./bun_test.zig"); const std = @import("std"); +const CommandLineReporter = @import("../../cli/test_command.zig").CommandLineReporter; const Snapshots = @import("./snapshot.zig").Snapshots; const expect = @import("./expect.zig"); diff --git a/src/bunfig.zig b/src/bunfig.zig index 39cf0d3b7e..b551af317b 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -244,10 +244,14 @@ pub const Bunfig = struct { if (expr.get("junit")) |junit_expr| { try this.expectString(junit_expr); if (junit_expr.data.e_string.len() > 0) { - this.ctx.test_options.file_reporter = .junit; + this.ctx.test_options.reporters.junit = true; this.ctx.test_options.reporter_outfile = try junit_expr.data.e_string.string(allocator); } } + if (expr.get("dots") orelse expr.get("dot")) |dots_expr| { + try this.expect(dots_expr, .e_boolean); + this.ctx.test_options.reporters.dots = dots_expr.data.e_boolean.value; + } } if (test_.get("coverageReporter")) |expr| brk: { diff --git a/src/cli.zig b/src/cli.zig index 6cf201e69f..bccc5c29f1 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -352,7 +352,10 @@ pub const Command = struct { test_filter_regex: ?*RegularExpression = null, max_concurrency: u32 = 20, - file_reporter: ?TestCommand.FileReporter = null, + reporters: struct { + dots: bool = false, + junit: bool = false, + } = .{}, reporter_outfile: ?[]const u8 = null, }; diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 0a3ac55964..4b55ba7446 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -204,8 +204,9 @@ pub const test_only_params = [_]ParamType{ clap.parseParam("--coverage-dir Directory for coverage files. Defaults to 'coverage'.") catch unreachable, clap.parseParam("--bail ? Exit the test suite after failures. If you do not specify a number, it defaults to 1.") catch unreachable, clap.parseParam("-t, --test-name-pattern Run only tests with a name that matches the given regex.") catch unreachable, - clap.parseParam("--reporter Test output reporter format. Available: 'junit' (requires --reporter-outfile). Default: console output.") catch unreachable, + clap.parseParam("--reporter Test output reporter format. Available: 'junit' (requires --reporter-outfile), 'dots'. Default: console output.") catch unreachable, clap.parseParam("--reporter-outfile Output file path for the reporter format (required with --reporter).") catch unreachable, + clap.parseParam("--dots Enable dots reporter. Shorthand for --reporter=dots.") catch unreachable, clap.parseParam("--max-concurrency Maximum number of concurrent tests to execute at once. Default is 20.") catch unreachable, }; pub const test_params = test_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; @@ -455,13 +456,20 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("--reporter=junit requires --reporter-outfile [file] to specify where to save the XML report", .{}); Global.crash(); } - ctx.test_options.file_reporter = .junit; + ctx.test_options.reporters.junit = true; + } else if (strings.eqlComptime(reporter, "dots") or strings.eqlComptime(reporter, "dot")) { + ctx.test_options.reporters.dots = true; } else { - Output.errGeneric("unsupported reporter format '{s}'. Available options: 'junit' (for XML test results)", .{reporter}); + Output.errGeneric("unsupported reporter format '{s}'. Available options: 'junit' (for XML test results), 'dots'", .{reporter}); Global.crash(); } } + // Handle --dots flag as shorthand for --reporter=dots + if (args.flag("--dots")) { + ctx.test_options.reporters.dots = true; + } + if (args.option("--coverage-dir")) |dir| { ctx.test_options.coverage.reports_directory = dir; } diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 0749181606..c74e88db93 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -62,11 +62,6 @@ fn fmtStatusTextLine(status: bun_test.Execution.Result, emoji_or_color: bool) [] } pub fn writeTestStatusLine(comptime status: bun_test.Execution.Result, writer: anytype) void { - // When using AI agents, only print failures - if (Output.isAIAgent() and status != .fail) { - return; - } - switch (Output.enable_ansi_colors_stderr) { inline else => |enable_ansi_colors_stderr| writer.print(comptime fmtStatusTextLine(status, enable_ansi_colors_stderr), .{}) catch unreachable, } @@ -576,16 +571,16 @@ pub const CommandLineReporter = struct { last_dot: u32 = 0, prev_file: u64 = 0, repeat_count: u32 = 1, + last_printed_dot: bool = false, failures_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{}, skips_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{}, todos_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{}, - file_reporter: ?FileReporter = null, - - pub const FileReporter = union(enum) { - junit: *JunitReporter, - }; + reporters: struct { + dots: bool = false, + junit: ?*JunitReporter = null, + } = .{}, const DotColorMap = std.EnumMap(TestRunner.Test.Status, string); const dots: DotColorMap = brk: { @@ -602,7 +597,6 @@ pub const CommandLineReporter = struct { fn printTestLine( comptime status: bun_test.Execution.Result, - buntest: *bun_test.BunTest, sequence: *bun_test.Execution.ExecutionSequence, test_entry: *bun_test.ExecutionEntry, elapsed_ns: u64, @@ -611,10 +605,6 @@ pub const CommandLineReporter = struct { ) void { var scopes_stack = bun.BoundedArray(*bun_test.DescribeScope, 64).init(0) catch unreachable; var parent_: ?*bun_test.DescribeScope = test_entry.base.parent; - const assertions = sequence.expect_call_count; - const line_number = test_entry.base.line_no; - - const file: []const u8 = if (bun.jsc.Jest.Jest.runner) |runner| runner.files.get(buntest.file_id).source.path.text else ""; while (parent_) |scope| { scopes_stack.append(scope) catch break; @@ -711,10 +701,33 @@ pub const CommandLineReporter = struct { }, } } + } - if (buntest.reporter) |cmd_reporter| if (cmd_reporter.file_reporter) |reporter| { - switch (reporter) { - .junit => |junit| { + fn maybePrintJunitLine( + comptime status: bun_test.Execution.Result, + buntest: *bun_test.BunTest, + sequence: *bun_test.Execution.ExecutionSequence, + test_entry: *bun_test.ExecutionEntry, + elapsed_ns: u64, + ) void { + if (buntest.reporter) |cmd_reporter| { + if (cmd_reporter.reporters.junit) |junit| { + var scopes_stack = bun.BoundedArray(*bun_test.DescribeScope, 64).init(0) catch unreachable; + var parent_: ?*bun_test.DescribeScope = test_entry.base.parent; + const assertions = sequence.expect_call_count; + const line_number = test_entry.base.line_no; + + const file: []const u8 = if (bun.jsc.Jest.Jest.runner) |runner| runner.files.get(buntest.file_id).source.path.text else ""; + + while (parent_) |scope| { + scopes_stack.append(scope) catch break; + parent_ = scope.base.parent; + } + + const scopes: []*bun_test.DescribeScope = scopes_stack.slice(); + const display_label = test_entry.base.name orelse "(unnamed)"; + + { const filename = brk: { if (strings.hasPrefix(file, bun.fs.FileSystem.instance.top_level_dir)) { break :brk strings.withoutLeadingPathSeparator(file[bun.fs.FileSystem.instance.top_level_dir.len..]); @@ -827,9 +840,9 @@ pub const CommandLineReporter = struct { } bun.handleOom(junit.writeTestCase(status, filename, display_label, concatenated_describe_scopes.items, assertions, elapsed_ns, line_number)); - }, + } } - }; + } } pub inline fn summary(this: *CommandLineReporter) *TestRunner.Summary { @@ -846,17 +859,39 @@ pub const CommandLineReporter = struct { switch (sequence.result) { inline else => |result| { - if (result != .skipped_because_label or buntest.reporter != null and buntest.reporter.?.file_reporter != null) { - writeTestStatusLine(result, &writer); - const dim = switch (comptime result.basicResult()) { - .todo => if (bun.jsc.Jest.Jest.runner) |runner| !runner.run_todo else true, - .skip, .pending => true, - .pass, .fail => false, - }; - switch (dim) { - inline else => |dim_comptime| printTestLine(result, buntest, sequence, test_entry, elapsed_ns, &writer, dim_comptime), + if (result != .skipped_because_label) { + if (buntest.reporter != null and buntest.reporter.?.reporters.dots and (comptime switch (result.basicResult()) { + .pass, .skip, .todo, .pending => true, + .fail => false, + })) { + switch (Output.enable_ansi_colors_stderr) { + inline else => |enable_ansi_colors_stderr| switch (comptime result.basicResult()) { + .pass => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + .skip => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + .todo => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + .pending => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + .fail => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + }, + } + buntest.reporter.?.last_printed_dot = true; + } else if (Output.isAIAgent() and (comptime result.basicResult()) != .fail) { + // when using AI agents, only print failures + } else { + buntest.bun_test_root.onBeforePrint(); + + writeTestStatusLine(result, &writer); + const dim = switch (comptime result.basicResult()) { + .todo => if (bun.jsc.Jest.Jest.runner) |runner| !runner.run_todo else true, + .skip, .pending => true, + .pass, .fail => false, + }; + switch (dim) { + inline else => |dim_comptime| printTestLine(result, sequence, test_entry, elapsed_ns, &writer, dim_comptime), + } } } + // always print junit if needed + maybePrintJunitLine(result, buntest, sequence, test_entry, elapsed_ns); }, } @@ -865,12 +900,12 @@ pub const CommandLineReporter = struct { var this: *CommandLineReporter = buntest.reporter orelse return; // command line reporter is missing! uh oh! - switch (sequence.result.basicResult()) { + if (!this.reporters.dots) switch (sequence.result.basicResult()) { .skip => bun.handleOom(this.skips_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), .todo => bun.handleOom(this.todos_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), .fail => bun.handleOom(this.failures_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), .pass, .pending => {}, - } + }; switch (sequence.result) { .pending => {}, @@ -1258,10 +1293,6 @@ pub const TestCommand = struct { lcov: bool, }; - pub const FileReporter = enum { - junit, - }; - pub fn exec(ctx: Command.Context) !void { Output.is_github_action = Output.isGithubAction(); @@ -1293,12 +1324,8 @@ pub const TestCommand = struct { var reporter = try ctx.allocator.create(CommandLineReporter); defer { - if (reporter.file_reporter) |*file_reporter| { - switch (file_reporter.*) { - .junit => |junit_reporter| { - junit_reporter.deinit(); - }, - } + if (reporter.reporters.junit) |file_reporter| { + file_reporter.deinit(); } } reporter.* = CommandLineReporter{ @@ -1328,10 +1355,11 @@ pub const TestCommand = struct { jest.Jest.runner = &reporter.jest; reporter.jest.test_options = &ctx.test_options; - if (ctx.test_options.file_reporter) |file_reporter| { - reporter.file_reporter = switch (file_reporter) { - .junit => .{ .junit = JunitReporter.init() }, - }; + if (ctx.test_options.reporters.junit) { + reporter.reporters.junit = JunitReporter.init(); + } + if (ctx.test_options.reporters.dots) { + reporter.reporters.dots = true; } js_ast.Expr.Data.Store.create(); @@ -1500,7 +1528,7 @@ pub const TestCommand = struct { const write_snapshots_success = try jest.Jest.runner.?.snapshots.writeInlineSnapshots(); try jest.Jest.runner.?.snapshots.writeSnapshotFile(); var coverage_options = ctx.test_options.coverage; - if (reporter.summary().pass > 20 and !Output.isAIAgent()) { + if (reporter.summary().pass > 20 and !Output.isAIAgent() and !reporter.reporters.dots) { if (reporter.summary().skip > 0) { Output.prettyError("\n{d} tests skipped:\n", .{reporter.summary().skip}); Output.flush(); @@ -1618,25 +1646,40 @@ pub const TestCommand = struct { const did_label_filter_out_all_tests = summary.didLabelFilterOutAllTests() and reporter.jest.unhandled_errors_between_tests == 0; if (!did_label_filter_out_all_tests) { + const DotIndenter = struct { + indent: bool = false, + + pub fn format(this: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { + if (this.indent) { + try writer.writeAll(" "); + } + } + }; + + const indenter = DotIndenter{ .indent = !ctx.test_options.reporters.dots }; + if (!indenter.indent) { + Output.prettyError("\n", .{}); + } + // Display the random seed if tests were randomized if (random != null) { - Output.prettyError(" --seed={d}\n", .{seed}); + Output.prettyError("{}--seed={d}\n", .{ indenter, seed }); } if (summary.pass > 0) { Output.prettyError("", .{}); } - Output.prettyError(" {d:5>} pass\n", .{summary.pass}); + Output.prettyError("{}{d:5>} pass\n", .{ indenter, summary.pass }); if (summary.skip > 0) { - Output.prettyError(" {d:5>} skip\n", .{summary.skip}); + Output.prettyError("{}{d:5>} skip\n", .{ indenter, summary.skip }); } else if (summary.skipped_because_label > 0) { - Output.prettyError(" {d:5>} filtered out\n", .{summary.skipped_because_label}); + Output.prettyError("{}{d:5>} filtered out\n", .{ indenter, summary.skipped_because_label }); } if (summary.todo > 0) { - Output.prettyError(" {d:5>} todo\n", .{summary.todo}); + Output.prettyError("{}{d:5>} todo\n", .{ indenter, summary.todo }); } if (summary.fail > 0) { @@ -1645,9 +1688,9 @@ pub const TestCommand = struct { Output.prettyError("", .{}); } - Output.prettyError(" {d:5>} fail\n", .{summary.fail}); + Output.prettyError("{}{d:5>} fail\n", .{ indenter, summary.fail }); if (reporter.jest.unhandled_errors_between_tests > 0) { - Output.prettyError(" {d:5>} error{s}\n", .{ reporter.jest.unhandled_errors_between_tests, if (reporter.jest.unhandled_errors_between_tests > 1) "s" else "" }); + Output.prettyError("{}{d:5>} error{s}\n", .{ indenter, reporter.jest.unhandled_errors_between_tests, if (reporter.jest.unhandled_errors_between_tests > 1) "s" else "" }); } var print_expect_calls = reporter.summary().expectations > 0; @@ -1659,9 +1702,9 @@ pub const TestCommand = struct { var first = true; if (print_expect_calls and added == 0 and failed == 0) { print_expect_calls = false; - Output.prettyError(" {d:5>} snapshots, {d:5>} expect() calls", .{ reporter.jest.snapshots.total, reporter.summary().expectations }); + Output.prettyError("{}{d:5>} snapshots, {d:5>} expect() calls", .{ indenter, reporter.jest.snapshots.total, reporter.summary().expectations }); } else { - Output.prettyError(" snapshots: ", .{}); + Output.prettyError("snapshots: ", .{}); if (passed > 0) { Output.prettyError("{d} passed", .{passed}); @@ -1691,7 +1734,7 @@ pub const TestCommand = struct { } if (print_expect_calls) { - Output.prettyError(" {d:5>} expect() calls\n", .{reporter.summary().expectations}); + Output.prettyError("{}{d:5>} expect() calls\n", .{ indenter, reporter.summary().expectations }); } reporter.printSummary(); @@ -1710,15 +1753,11 @@ pub const TestCommand = struct { Output.prettyError("\n", .{}); Output.flush(); - if (reporter.file_reporter) |file_reporter| { - switch (file_reporter) { - .junit => |junit| { - if (junit.current_file.len > 0) { - junit.endTestSuite() catch {}; - } - junit.writeToFile(ctx.test_options.reporter_outfile.?) catch {}; - }, + if (reporter.reporters.junit) |junit| { + if (junit.current_file.len > 0) { + junit.endTestSuite() catch {}; } + junit.writeToFile(ctx.test_options.reporter_outfile.?) catch {}; } if (vm.hot_reload == .watch) { @@ -1841,7 +1880,7 @@ pub const TestCommand = struct { bun_test_root.enterFile(file_id, reporter, should_run_concurrent, first_last); defer bun_test_root.exitFile(); - reporter.jest.current_file.set(file_title, file_prefix, repeat_count, repeat_index); + reporter.jest.current_file.set(file_title, file_prefix, repeat_count, repeat_index, reporter); bun.jsc.Jest.bun_test.debug.group.log("loadEntryPointForTestRunner(\"{}\")", .{std.zig.fmtEscapes(file_path)}); diff --git a/src/output.zig b/src/output.zig index 7b5a2d33f7..6d1f79d90a 100644 --- a/src/output.zig +++ b/src/output.zig @@ -521,6 +521,24 @@ pub fn enableBuffering() void { if (comptime Environment.isNative) enable_buffering = true; } +const EnableBufferingScope = struct { + prev_buffering: bool, + pub fn init() EnableBufferingScope { + const prev_buffering = enable_buffering; + enable_buffering = true; + return .{ .prev_buffering = prev_buffering }; + } + + /// Does not call Output.flush(). + pub fn deinit(self: EnableBufferingScope) void { + enable_buffering = self.prev_buffering; + } +}; + +pub fn enableBufferingScope() EnableBufferingScope { + return EnableBufferingScope.init(); +} + pub fn disableBuffering() void { flush(); if (comptime Environment.isNative) enable_buffering = false; diff --git a/test/js/bun/test/dots.fixture.ts b/test/js/bun/test/dots.fixture.ts new file mode 100644 index 0000000000..5f36924ca4 --- /dev/null +++ b/test/js/bun/test/dots.fixture.ts @@ -0,0 +1,7 @@ +test.each(Array.from({ length: 10 }, () => 0))("passing filterin", () => {}); +test.skip.each(Array.from({ length: 10 }, () => 0))("skipped filterin", () => {}); +test.failing("failing filterin", () => {}); +test("passing filterout", () => {}); +test.failing("failing filterin", () => {}); +test.failing("failing filterin", () => {}); +test.todo.each(Array.from({ length: 10 }, () => 0))("todo filterin", () => {}); diff --git a/test/js/bun/test/dots.test.ts b/test/js/bun/test/dots.test.ts new file mode 100644 index 0000000000..3cb4027199 --- /dev/null +++ b/test/js/bun/test/dots.test.ts @@ -0,0 +1,103 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("dots 1", async () => { + const result = await Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/dots.fixture.ts", "--dots", "-t", "filterin"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + expect({ + exitCode, + stdout: normalizeBunSnapshot(stdout), + stderr: normalizeBunSnapshot(stderr), + }).toMatchInlineSnapshot(` + { + "exitCode": 1, + "stderr": + ".................... + + test/js/bun/test/dots.fixture.ts: + (fail) failing filterin + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + (fail) failing filterin + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + (fail) failing filterin + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + .......... + + 10 pass + 10 skip + 10 todo + 3 fail + Ran 33 tests across 1 file." + , + "stdout": "bun test ()", + } + `); +}); + +test("dots 2", async () => { + const result = await Bun.spawn({ + cmd: [ + bunExe(), + "test", + import.meta.dir + "/printing/dots/dots1.fixture.ts", + import.meta.dir + "/printing/dots/dots2.fixture.ts", + import.meta.dir + "/printing/dots/dots3.fixture.ts", + "--dots", + ], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + expect({ + exitCode, + stderr: normalizeBunSnapshot(stderr), + }).toMatchInlineSnapshot(` + { + "exitCode": 1, + "stderr": + ".......... + + test/js/bun/test/printing/dots/dots1.fixture.ts: + Hello, world! + ........... + Hello, world! + . + + test/js/bun/test/printing/dots/dots2.fixture.ts: + Hello, world! + ........... + (fail) failing test + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + .................... + + test/js/bun/test/printing/dots/dots3.fixture.ts: + 3 | // unhandled failure. it should print the filename + 4 | test("failure", async () => { + 5 | const { resolve, reject, promise } = Promise.withResolvers(); + 6 | setTimeout(() => { + 7 | resolve(); + 8 | throw new Error("unhandled error"); + ^ + error: unhandled error + at (file:NN:NN) + (fail) failure + + + 43 pass + 10 skip + 2 fail + Ran 55 tests across 3 files." + , + } + `); +}); diff --git a/test/js/bun/test/printing/dots/dots1.fixture.ts b/test/js/bun/test/printing/dots/dots1.fixture.ts new file mode 100644 index 0000000000..603e253327 --- /dev/null +++ b/test/js/bun/test/printing/dots/dots1.fixture.ts @@ -0,0 +1,11 @@ +test.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); +// now, console.log. it should show the filename +test("console.log", () => { + console.warn("Hello, world!"); +}); +// more tests +test.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); +// console.log again. it should add a newline but not show the filename again. +test("console.log again", () => { + console.warn("Hello, world!"); +}); diff --git a/test/js/bun/test/printing/dots/dots2.fixture.ts b/test/js/bun/test/printing/dots/dots2.fixture.ts new file mode 100644 index 0000000000..557d6c61ba --- /dev/null +++ b/test/js/bun/test/printing/dots/dots2.fixture.ts @@ -0,0 +1,8 @@ +test("console.log first. it should not add a newline but should show the filename", () => { + console.warn("Hello, world!"); +}); +// more dots +test.skip.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); +// failing test. it should add a newline but not show the filename again. +test.failing("failing test", () => {}); +test.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); diff --git a/test/js/bun/test/printing/dots/dots3.fixture.ts b/test/js/bun/test/printing/dots/dots3.fixture.ts new file mode 100644 index 0000000000..e3b566696c --- /dev/null +++ b/test/js/bun/test/printing/dots/dots3.fixture.ts @@ -0,0 +1,11 @@ +test.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); + +// unhandled failure. it should print the filename +test("failure", async () => { + const { resolve, reject, promise } = Promise.withResolvers(); + setTimeout(() => { + resolve(); + throw new Error("unhandled error"); + }, 0); + await promise; +}); From e3bd03628a0b709d3e53a1ed71f5737d943ebab2 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Fri, 3 Oct 2025 17:50:47 -0700 Subject: [PATCH 017/191] fix(Bun.SQL) fix command detection on sqlite (#23221) ### What does this PR do? Returning clause should work with insert now ### How did you verify your code works? Tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/js/internal/sql/sqlite.ts | 385 ++++++++++++--------------------- test/js/sql/sqlite-sql.test.ts | 31 ++- 2 files changed, 174 insertions(+), 242 deletions(-) diff --git a/src/js/internal/sql/sqlite.ts b/src/js/internal/sql/sqlite.ts index 0a150cb79a..9899948908 100644 --- a/src/js/internal/sql/sqlite.ts +++ b/src/js/internal/sql/sqlite.ts @@ -29,11 +29,11 @@ const enum SQLCommand { interface SQLParsedInfo { command: SQLCommand; - firstKeyword: string; // SELECT, INSERT, UPDATE, etc. - hasReturning: boolean; + lastToken?: string; + canReturnRows: boolean; } -function commandToString(command: SQLCommand): string { +function commandToString(command: SQLCommand, lastToken?: string): string { switch (command) { case SQLCommand.insert: return "INSERT"; @@ -42,258 +42,168 @@ function commandToString(command: SQLCommand): string { return "UPDATE"; case SQLCommand.in: case SQLCommand.where: + if (lastToken) return lastToken; return "WHERE"; default: + if (lastToken) return lastToken; return ""; } } -function matchAsciiIgnoreCase(str: string, start: number, end: number, target: string): boolean { - if (end - start !== target.length) return false; - for (let i = 0; i < target.length; i++) { - const c = str.charCodeAt(start + i); - const t = target.charCodeAt(i); - - if (c !== t) { - if (c >= 65 && c <= 90) { - if (c + 32 !== t) return false; - } else if (c >= 97 && c <= 122) { - if (c - 32 !== t) return false; - } else { - return false; - } - } - } - - return true; -} - -// Check if character is whitespace or delimiter (anything that's not a letter/digit/underscore) -function isTokenDelimiter(code: number): boolean { - // Quick check for common ASCII whitespace - if (code <= 32) return true; - // Letters A-Z, a-z - if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122)) return false; - // Digits 0-9 - if (code >= 48 && code <= 57) return false; - // Underscore (allowed in SQL identifiers) - if (code === 95) return false; - // Everything else is a delimiter (including Unicode whitespace, punctuation, etc.) - return true; -} - -function parseSQLQuery(query: string): SQLParsedInfo { - const text_len = query.length; - - // Skip leading whitespace/delimiters - let i = 0; - while (i < text_len && isTokenDelimiter(query.charCodeAt(i))) { - i++; - } +/** + * Parse the SQL query and return the command and the last token + * @param query - The SQL query to parse + * @param partial - Whether to stop on the first command we find + * @returns The command, the last token, and whether it can return rows + */ +function parseSQLQuery(query: string, partial: boolean = false): SQLParsedInfo { + const text = query.toUpperCase().trim(); + const text_len = text.length; + let token = ""; let command = SQLCommand.none; - let firstKeyword = ""; - let hasReturning = false; - let quotedDouble = false; - let tokenStart = i; - - while (i < text_len) { - const char = query[i]; - const charCode = query.charCodeAt(i); - - // Handle quotes BEFORE checking delimiters, since quotes are also delimiters - // Handle single quotes - skip entire string literal - if (!quotedDouble && char === "'") { - // Process any pending token before the quote - if (i > tokenStart) { - // We have a token to process before the quote - // Check what token it is - // Track the first keyword for the command string - if (!firstKeyword) { - if (matchAsciiIgnoreCase(query, tokenStart, i, "select")) { - firstKeyword = "SELECT"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "insert")) { - firstKeyword = "INSERT"; - command = SQLCommand.insert; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "update")) { - firstKeyword = "UPDATE"; - command = SQLCommand.update; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "delete")) { - firstKeyword = "DELETE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "create")) { - firstKeyword = "CREATE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "drop")) { - firstKeyword = "DROP"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "alter")) { - firstKeyword = "ALTER"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "pragma")) { - firstKeyword = "PRAGMA"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "explain")) { - firstKeyword = "EXPLAIN"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { - firstKeyword = "WITH"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - firstKeyword = "IN"; - command = SQLCommand.in; - } - } else { - // After we have the first keyword, look for other keywords - if (matchAsciiIgnoreCase(query, tokenStart, i, "where")) { - command = SQLCommand.where; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "set")) { - if (command === SQLCommand.update) { - command = SQLCommand.updateSet; + let lastToken = ""; + let canReturnRows = false; + let quoted: false | "'" | '"' = false; + // we need to reverse search so we find the closest command to the parameter + for (let i = text_len - 1; i >= 0; i--) { + const char = text[i]; + switch (char) { + case " ": + case "\n": + case "\t": + case "\r": + case "\f": + case "\v": { + switch (token) { + case "INSERT": { + if (command === SQLCommand.none) { + command = SQLCommand.insert; + } + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.insert, lastToken, canReturnRows }; } - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - command = SQLCommand.in; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { - hasReturning = true; - } - } - } - - // Now skip the entire string literal - i++; - while (i < text_len) { - if (query[i] === "'") { - // Check for escaped quote - if (i + 1 < text_len && query[i + 1] === "'") { - i += 2; // Skip escaped quote continue; } - i++; - break; - } - i++; - } - // After string, skip any whitespace and reset token start - while (i < text_len && isTokenDelimiter(query.charCodeAt(i))) { - i++; - } - tokenStart = i; - continue; - } - - if (char === '"') { - quotedDouble = !quotedDouble; - i++; - continue; - } - - if (quotedDouble) { - i++; - continue; - } - - if (isTokenDelimiter(charCode)) { - if (i > tokenStart) { - // Track the first keyword for the command string - if (!firstKeyword) { - if (matchAsciiIgnoreCase(query, tokenStart, i, "select")) { - firstKeyword = "SELECT"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "insert")) { - firstKeyword = "INSERT"; - command = SQLCommand.insert; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "update")) { - firstKeyword = "UPDATE"; - command = SQLCommand.update; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "delete")) { - firstKeyword = "DELETE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "create")) { - firstKeyword = "CREATE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "drop")) { - firstKeyword = "DROP"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "alter")) { - firstKeyword = "ALTER"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "pragma")) { - firstKeyword = "PRAGMA"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "explain")) { - firstKeyword = "EXPLAIN"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { - firstKeyword = "WITH"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - firstKeyword = "IN"; - command = SQLCommand.in; + case "UPDATE": { + if (command === SQLCommand.none) { + command = SQLCommand.update; + } + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.update, lastToken, canReturnRows }; + } + continue; } - } else { - // After we have the first keyword, look for other keywords - if (matchAsciiIgnoreCase(query, tokenStart, i, "where")) { - command = SQLCommand.where; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "set")) { - if (command === SQLCommand.update) { + case "WHERE": { + if (command === SQLCommand.none) { + command = SQLCommand.where; + } + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.where, lastToken, canReturnRows }; + } + continue; + } + case "SET": { + if (command === SQLCommand.none) { command = SQLCommand.updateSet; } - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - command = SQLCommand.in; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { - hasReturning = true; + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.updateSet, lastToken, canReturnRows }; + } + continue; + } + case "IN": { + if (command === SQLCommand.none) { + command = SQLCommand.in; + } + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.in, lastToken, canReturnRows }; + } + continue; + } + case "SELECT": + case "PRAGMA": + case "WITH": + case "EXPLAIN": + case "RETURNING": { + lastToken = token; + canReturnRows = true; + token = ""; + continue; + } + default: { + lastToken = token; + token = ""; + continue; } } } - - // Skip delimiters but stop at quotes (they need special handling) - while (++i < text_len) { - const nextChar = query[i]; - if (nextChar === "'" || nextChar === '"') { - break; // Stop at quotes, they'll be handled in next iteration + default: { + // skip quoted commands + if (char === '"' || char === "'") { + if (quoted === char) { + quoted = false; + } else { + quoted = char; + } + continue; } - if (!isTokenDelimiter(query.charCodeAt(i))) { - break; // Stop at non-delimiter + if (!quoted) { + token = char + token; } } - tokenStart = i; - continue; } - i++; } - - // Handle last token if we reached end of string - if (i >= text_len && i > tokenStart && !quotedDouble) { - // Track the first keyword for the command string - if (!firstKeyword) { - if (matchAsciiIgnoreCase(query, tokenStart, i, "select")) { - firstKeyword = "SELECT"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "insert")) { - firstKeyword = "INSERT"; - command = SQLCommand.insert; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "update")) { - firstKeyword = "UPDATE"; - command = SQLCommand.update; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "delete")) { - firstKeyword = "DELETE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "create")) { - firstKeyword = "CREATE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "drop")) { - firstKeyword = "DROP"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "alter")) { - firstKeyword = "ALTER"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "pragma")) { - firstKeyword = "PRAGMA"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "explain")) { - firstKeyword = "EXPLAIN"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { - firstKeyword = "WITH"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - firstKeyword = "IN"; - command = SQLCommand.in; - } - } else { - // After we have the first keyword, look for other keywords - if (matchAsciiIgnoreCase(query, tokenStart, i, "where")) { - command = SQLCommand.where; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "set")) { - if (command === SQLCommand.update) { + if (token) { + lastToken = token; + switch (token) { + case "INSERT": + if (command === SQLCommand.none) { + command = SQLCommand.insert; + } + break; + case "UPDATE": + if (command === SQLCommand.none) command = SQLCommand.update; + break; + case "WHERE": + if (command === SQLCommand.none) { + command = SQLCommand.where; + } + break; + case "SET": + if (command === SQLCommand.none) { command = SQLCommand.updateSet; } - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - command = SQLCommand.in; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { - hasReturning = true; + break; + case "IN": + if (command === SQLCommand.none) { + command = SQLCommand.in; + } + break; + case "SELECT": + case "PRAGMA": + case "WITH": + case "EXPLAIN": + case "RETURNING": { + canReturnRows = true; + break; } + default: + command = SQLCommand.none; + break; } } - - return { command, firstKeyword, hasReturning }; + return { command, lastToken, canReturnRows }; } class SQLiteQueryHandle implements BaseQueryHandle { @@ -323,19 +233,11 @@ class SQLiteQueryHandle implements BaseQueryHandle { } const { sql, values, mode, parsedInfo } = this; - try { - const command = parsedInfo.firstKeyword; - + const command = parsedInfo.command; // For SELECT queries, we need to use a prepared statement // For other queries, we can check if there are multiple statements and use db.run() if so - if ( - command === "SELECT" || - command === "PRAGMA" || - command === "WITH" || - command === "EXPLAIN" || - parsedInfo.hasReturning - ) { + if (parsedInfo.canReturnRows) { // SELECT queries must use prepared statements for results const stmt = db.prepare(sql); let result: unknown[] | undefined; @@ -350,7 +252,7 @@ class SQLiteQueryHandle implements BaseQueryHandle { const sqlResult = $isArray(result) ? new SQLResultArray(result) : new SQLResultArray([result]); - sqlResult.command = command; + sqlResult.command = commandToString(command, parsedInfo.lastToken); sqlResult.count = $isArray(result) ? result.length : 1; stmt.finalize(); @@ -360,7 +262,7 @@ class SQLiteQueryHandle implements BaseQueryHandle { const changes = db.run.$apply(db, [sql].concat(values)); const sqlResult = new SQLResultArray(); - sqlResult.command = command; + sqlResult.command = commandToString(command, parsedInfo.lastToken); sqlResult.count = changes.changes; sqlResult.lastInsertRowid = changes.lastInsertRowid; @@ -512,7 +414,8 @@ class SQLiteAdapter implements DatabaseAdapter { await sql.close(); }); }); - describe("Transactions", () => { let sql: SQL; @@ -1185,6 +1184,36 @@ describe("SQLite-specific features", () => { expect(results[0].id).toBe(1); expect(results[1].id).toBe(3); }); + test("returning clause on insert statements", async () => { + await using sql = new SQL("sqlite://:memory:"); + await sql` + create table users ( + id integer primary key, + name text not null, + verified integer not null default 0, + created_at integer not null default (strftime('%s', 'now')) + )`; + + const result = + await sql`insert into "users" ("id", "name", "verified", "created_at") values (null, ${"John"}, ${0}, strftime('%s', 'now')), (null, ${"Bruce"}, ${0}, strftime('%s', 'now')), (null, ${"Jane"}, ${0}, strftime('%s', 'now')), (null, ${"Austin"}, ${0}, strftime('%s', 'now')) returning "id", "name", "verified"`; + + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].verified).toBe(0); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Bruce"); + expect(result[1].verified).toBe(0); + expect(result[2].id).toBe(3); + expect(result[2].name).toBe("Jane"); + expect(result[2].verified).toBe(0); + expect(result[3].id).toBe(4); + expect(result[3].name).toBe("Austin"); + expect(result[3].verified).toBe(0); + + const [{ 'upper("name")': upperName }] = + await sql`insert into "users" ("id", "name", "verified", "created_at") values (null, ${"John"}, ${0}, strftime('%s', 'now')) returning upper("name")`; + expect(upperName).toBe("JOHN"); + }); test("last_insert_rowid()", async () => { await sql`CREATE TABLE rowid_test (id INTEGER PRIMARY KEY, value TEXT)`; From d8350c2c59de1f6f2d6c3154222cbb4345662447 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 3 Oct 2025 22:05:29 -0700 Subject: [PATCH 018/191] Add `jsc.DecodedJSValue`; make `jsc.Strong` more efficient (#23218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `jsc.DecodedJSValue`, an extern struct which is ABI-compatible with `JSC::JSValue`. (By contrast, `jsc.JSValue` is ABI-compatible with `JSC::EncodedJSValue`.) This enables `jsc.Strong.get` to be more efficient: it no longer has to call into C⁠+⁠+. (For internal tracking: fixes ENG-20748) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/ModuleLoader.zig | 6 ++--- src/bun.js/Strong.zig | 6 ++--- src/bun.js/bindings/DecodedJSValue.zig | 33 ++++++++++++++++++++++++++ src/bun.js/bindings/EncodedJSValue.zig | 9 ------- src/bun.js/bindings/JSValue.zig | 7 ++++++ src/bun.js/bindings/StrongRef.cpp | 5 ---- src/bun.js/bindings/StrongRef.h | 1 - src/bun.js/jsc.zig | 2 +- 8 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 src/bun.js/bindings/DecodedJSValue.zig delete mode 100644 src/bun.js/bindings/EncodedJSValue.zig diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index d0bcafe343..8398f08614 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -1350,10 +1350,10 @@ pub fn transpileSourceCode( if (virtual_source) |source| { if (globalObject) |globalThis| { // attempt to avoid reading the WASM file twice. - const encoded = jsc.EncodedJSValue{ - .asPtr = globalThis, + const decoded: jsc.DecodedJSValue = .{ + .u = .{ .ptr = @ptrCast(globalThis) }, }; - const globalValue = @as(JSValue, @enumFromInt(encoded.asInt64)); + const globalValue = decoded.encode(); globalValue.put( globalThis, ZigString.static("wasmSourceBytes"), diff --git a/src/bun.js/Strong.zig b/src/bun.js/Strong.zig index 5c098b88eb..8c22aa9da4 100644 --- a/src/bun.js/Strong.zig +++ b/src/bun.js/Strong.zig @@ -121,8 +121,9 @@ pub const Impl = opaque { } pub fn get(this: *Impl) jsc.JSValue { - jsc.markBinding(@src()); - return Bun__StrongRef__get(this); + // `this` is actually a pointer to a `JSC::JSValue`; see Strong.cpp. + const js_value: *jsc.DecodedJSValue = @ptrCast(@alignCast(this)); + return js_value.encode(); } pub fn set(this: *Impl, global: *jsc.JSGlobalObject, value: jsc.JSValue) void { @@ -142,7 +143,6 @@ pub const Impl = opaque { extern fn Bun__StrongRef__delete(this: *Impl) void; extern fn Bun__StrongRef__new(*jsc.JSGlobalObject, jsc.JSValue) *Impl; - extern fn Bun__StrongRef__get(this: *Impl) jsc.JSValue; extern fn Bun__StrongRef__set(this: *Impl, *jsc.JSGlobalObject, jsc.JSValue) void; extern fn Bun__StrongRef__clear(this: *Impl) void; }; diff --git a/src/bun.js/bindings/DecodedJSValue.zig b/src/bun.js/bindings/DecodedJSValue.zig new file mode 100644 index 0000000000..4f6bb32511 --- /dev/null +++ b/src/bun.js/bindings/DecodedJSValue.zig @@ -0,0 +1,33 @@ +/// ABI-compatible with `JSC::JSValue`. +pub const DecodedJSValue = extern struct { + const Self = @This(); + + u: EncodedValueDescriptor, + + /// ABI-compatible with `JSC::EncodedValueDescriptor`. + pub const EncodedValueDescriptor = extern union { + asInt64: i64, + ptr: ?*jsc.JSCell, + asBits: extern struct { + payload: i32, + tag: i32, + }, + }; + + /// Equivalent to `JSC::JSValue::encode`. + pub fn encode(self: Self) jsc.JSValue { + return @enumFromInt(self.u.asInt64); + } +}; + +comptime { + bun.assertf(@sizeOf(usize) == 8, "EncodedValueDescriptor assumes a 64-bit system", .{}); + bun.assertf( + @import("builtin").target.cpu.arch.endian() == .little, + "EncodedValueDescriptor.asBits assumes a little-endian system", + .{}, + ); +} + +const bun = @import("bun"); +const jsc = bun.bun_js.jsc; diff --git a/src/bun.js/bindings/EncodedJSValue.zig b/src/bun.js/bindings/EncodedJSValue.zig deleted file mode 100644 index 793bc6e8c9..0000000000 --- a/src/bun.js/bindings/EncodedJSValue.zig +++ /dev/null @@ -1,9 +0,0 @@ -pub const EncodedJSValue = extern union { - asInt64: i64, - ptr: ?*JSCell, - asBits: [8]u8, - asPtr: ?*anyopaque, - asDouble: f64, -}; - -const JSCell = @import("./JSCell.zig").JSCell; diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 76b66be73b..1b36c3c4bd 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -2391,6 +2391,13 @@ pub const JSValue = enum(i64) { }; pub const backing_int = @typeInfo(JSValue).@"enum".tag_type; + + /// Equivalent to `JSC::JSValue::decode`. + pub fn decode(self: JSValue) jsc.DecodedJSValue { + var decoded: jsc.DecodedJSValue = undefined; + decoded.u.asInt64 = @intFromEnum(self); + return decoded; + } }; extern "c" fn AsyncContextFrame__withAsyncContextIfNeeded(global: *JSGlobalObject, callback: JSValue) JSValue; diff --git a/src/bun.js/bindings/StrongRef.cpp b/src/bun.js/bindings/StrongRef.cpp index 8466df5cfa..232b3ac1ee 100644 --- a/src/bun.js/bindings/StrongRef.cpp +++ b/src/bun.js/bindings/StrongRef.cpp @@ -27,11 +27,6 @@ extern "C" JSC::JSValue* Bun__StrongRef__new(JSC::JSGlobalObject* globalObject, return handleSlot; } -extern "C" JSC::EncodedJSValue Bun__StrongRef__get(JSC::JSValue* _Nonnull handleSlot) -{ - return JSC::JSValue::encode(*handleSlot); -} - extern "C" void Bun__StrongRef__set(JSC::JSValue* _Nonnull handleSlot, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue) { auto& vm = globalObject->vm(); diff --git a/src/bun.js/bindings/StrongRef.h b/src/bun.js/bindings/StrongRef.h index 9726d6895a..4701cf9c34 100644 --- a/src/bun.js/bindings/StrongRef.h +++ b/src/bun.js/bindings/StrongRef.h @@ -4,7 +4,6 @@ extern "C" void Bun__StrongRef__delete(JSC::JSValue* _Nonnull handleSlot); extern "C" JSC::JSValue* Bun__StrongRef__new(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); -extern "C" JSC::EncodedJSValue Bun__StrongRef__get(JSC::JSValue* _Nonnull handleSlot); extern "C" void Bun__StrongRef__set(JSC::JSValue* _Nonnull handleSlot, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); extern "C" void Bun__StrongRef__clear(JSC::JSValue* _Nonnull handleSlot); diff --git a/src/bun.js/jsc.zig b/src/bun.js/jsc.zig index 0e42512c45..ee13a61d0f 100644 --- a/src/bun.js/jsc.zig +++ b/src/bun.js/jsc.zig @@ -52,8 +52,8 @@ pub const CommonStrings = @import("./bindings/CommonStrings.zig").CommonStrings; pub const CustomGetterSetter = @import("./bindings/CustomGetterSetter.zig").CustomGetterSetter; pub const DOMFormData = @import("./bindings/DOMFormData.zig").DOMFormData; pub const DOMURL = @import("./bindings/DOMURL.zig").DOMURL; +pub const DecodedJSValue = @import("./bindings/DecodedJSValue.zig").DecodedJSValue; pub const DeferredError = @import("./bindings/DeferredError.zig").DeferredError; -pub const EncodedJSValue = @import("./bindings/EncodedJSValue.zig").EncodedJSValue; pub const GetterSetter = @import("./bindings/GetterSetter.zig").GetterSetter; pub const JSArray = @import("./bindings/JSArray.zig").JSArray; pub const JSArrayIterator = @import("./bindings/JSArrayIterator.zig").JSArrayIterator; From 8d28289407eef8c30529c3d3c49cb350a3c64929 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sat, 4 Oct 2025 00:31:47 -0700 Subject: [PATCH 019/191] fix(install): make negative workspace patterns work (#23229) ### What does this PR do? It's common for monorepos to exclude portions of a large glob ```json "workspaces": [ "packages/**", "!packages/**/test/**", "!packages/**/template/**" ], ``` closes #4621 (note: patterns like `"packages/!(*-standalone)"` will need to be written `"!packages/*-standalone"`) ### How did you verify your code works? Manually tested https://github.com/opentiny/tiny-engine, and added a new workspace test. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/api/glob.zig | 6 +-- src/bun.js/test/jest.zig | 3 +- src/cli/filter_arg.zig | 8 ++-- src/cli/outdated_command.zig | 6 +-- src/cli/pack_command.zig | 10 ++--- src/cli/test_command.zig | 4 +- src/cli/update_interactive_command.zig | 4 +- src/glob.zig | 40 +++++++++++++++++-- src/glob/GlobWalker.zig | 6 +-- src/glob/match.zig | 38 +----------------- .../PackageManager/install_with_manager.zig | 2 +- src/install/lockfile/Package/WorkspaceMap.zig | 32 ++++++++++++--- src/install/lockfile/Tree.zig | 2 +- src/resolver/package_json.zig | 4 +- src/shell/interpreter.zig | 3 +- src/shell/shell.zig | 3 +- test/cli/install/bun-workspaces.test.ts | 38 ++++++++++++++++++ 17 files changed, 129 insertions(+), 80 deletions(-) diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index f25381c077..0e3f37375c 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -371,7 +371,7 @@ pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame var str = try str_arg.toSlice(globalThis, arena.allocator()); defer str.deinit(); - return jsc.JSValue.jsBoolean(globImpl.match(arena.allocator(), this.pattern, str.slice()).matches()); + return jsc.JSValue.jsBoolean(bun.glob.match(this.pattern, str.slice()).matches()); } pub fn convertUtf8(codepoints: *std.ArrayList(u32), pattern: []const u8) !void { @@ -390,12 +390,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Arena = std.heap.ArenaAllocator; -const globImpl = @import("../../glob.zig"); -const GlobWalker = globImpl.BunGlobWalker; - const bun = @import("bun"); const BunString = bun.String; const CodepointIterator = bun.strings.UnsignedCodepointIterator; +const GlobWalker = bun.glob.BunGlobWalker; const jsc = bun.jsc; const JSGlobalObject = jsc.JSGlobalObject; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index d34e0c22bb..62bfe42229 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -127,9 +127,8 @@ pub const TestRunner = struct { const file_path = this.files.items(.source)[file_id].path.text; // Check if the file path matches any of the glob patterns - const glob = @import("../../glob.zig"); for (glob_patterns) |pattern| { - const result = glob.match(this.allocator, pattern, file_path); + const result = bun.glob.match(pattern, file_path); if (result == .match) return true; } return false; diff --git a/src/cli/filter_arg.zig b/src/cli/filter_arg.zig index 0d072805d6..3d0e00c6e6 100644 --- a/src/cli/filter_arg.zig +++ b/src/cli/filter_arg.zig @@ -22,7 +22,7 @@ fn globIgnoreFn(val: []const u8) bool { return false; } -const GlobWalker = Glob.GlobWalker(globIgnoreFn, Glob.walk.DirEntryAccessor, false); +const GlobWalker = glob.GlobWalker(globIgnoreFn, glob.walk.DirEntryAccessor, false); pub fn getCandidatePackagePatterns(allocator: std.mem.Allocator, log: *bun.logger.Log, out_patterns: *std.ArrayList([]u8), workdir_: []const u8, root_buf: *bun.PathBuffer) ![]const u8 { bun.ast.Expr.Data.Store.create(); @@ -177,7 +177,7 @@ pub const FilterSet = struct { pub fn matchesPath(self: *const FilterSet, path: []const u8) bool { for (self.filters) |filter| { - if (Glob.walk.matchImpl(self.allocator, filter.pattern, path).matches()) { + if (glob.match(filter.pattern, path).matches()) { return true; } } @@ -190,7 +190,7 @@ pub const FilterSet = struct { .name => name, .path => path, }; - if (Glob.walk.matchImpl(self.allocator, filter.pattern, target).matches()) { + if (glob.match(filter.pattern, target).matches()) { return true; } } @@ -275,11 +275,11 @@ pub const PackageFilterIterator = struct { const string = []const u8; -const Glob = @import("../glob.zig"); const std = @import("std"); const bun = @import("bun"); const Global = bun.Global; const JSON = bun.json; const Output = bun.Output; +const glob = bun.glob; const strings = bun.strings; diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index 98b2cf0fc4..91f5cd64e7 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -190,14 +190,14 @@ pub const OutdatedCommand = struct { const abs_res_path = path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &path_buf, &[_]string{res_path}, .posix); - if (!glob.walk.matchImpl(allocator, pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { + if (!glob.match(pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { break :matched false; } }, .name => |pattern| { const name = pkg_names[workspace_pkg_id].slice(string_buf); - if (!glob.walk.matchImpl(allocator, pattern, name).matches()) { + if (!glob.match(pattern, name).matches()) { break :matched false; } }, @@ -403,7 +403,7 @@ pub const OutdatedCommand = struct { .path => unreachable, .name => |name_pattern| { if (name_pattern.len == 0) continue; - if (!glob.walk.matchImpl(bun.default_allocator, name_pattern, dep.name.slice(string_buf)).matches()) { + if (!glob.match(name_pattern, dep.name.slice(string_buf)).matches()) { break :match false; } }, diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index d8b2f139d4..5c2b7e3d0e 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -293,7 +293,7 @@ pub const PackCommand = struct { // normally the behavior of `index.js` and `**/index.js` are the same, // but includes require `**/` const match_path = if (include.flags.@"leading **/") entry_name else entry_subpath; - switch (glob.walk.matchImpl(allocator, include.glob.slice(), match_path)) { + switch (glob.match(include.glob.slice(), match_path)) { .match => included = true, .negate_no_match, .negate_match => unreachable, else => {}, @@ -310,7 +310,7 @@ pub const PackCommand = struct { const match_path = if (exclude.flags.@"leading **/") entry_name else entry_subpath; // NOTE: These patterns have `!` so `.match` logic is // inverted here - switch (glob.walk.matchImpl(allocator, exclude.glob.slice(), match_path)) { + switch (glob.match(exclude.glob.slice(), match_path)) { .negate_no_match => included = false, else => {}, } @@ -1034,7 +1034,7 @@ pub const PackCommand = struct { // check default ignores that only apply to the root project directory for (root_default_ignore_patterns) |pattern| { - switch (glob.walk.matchImpl(bun.default_allocator, pattern, entry_name)) { + switch (glob.match(pattern, entry_name)) { .match => { // cannot be reversed return .{ @@ -1061,7 +1061,7 @@ pub const PackCommand = struct { for (default_ignore_patterns) |pattern_info| { const pattern, const can_override = pattern_info; - switch (glob.walk.matchImpl(bun.default_allocator, pattern, entry_name)) { + switch (glob.match(pattern, entry_name)) { .match => { if (can_override) { ignored = true; @@ -1103,7 +1103,7 @@ pub const PackCommand = struct { if (pattern.flags.dirs_only and entry.kind != .directory) continue; const match_path = if (pattern.flags.rel_path) rel else entry_name; - switch (glob.walk.matchImpl(bun.default_allocator, pattern.glob.slice(), match_path)) { + switch (glob.match(pattern.glob.slice(), match_path)) { .match => { ignored = true; ignore_pattern = pattern.glob.slice(); diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index c74e88db93..28c11d2bba 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1014,7 +1014,7 @@ pub const CommandLineReporter = struct { if (opts.ignore_patterns.len > 0) { var should_ignore = false; for (opts.ignore_patterns) |pattern| { - if (bun.glob.match(bun.default_allocator, pattern, relative_path).matches()) { + if (bun.glob.match(pattern, relative_path).matches()) { should_ignore = true; break; } @@ -1134,7 +1134,7 @@ pub const CommandLineReporter = struct { var should_ignore = false; for (opts.ignore_patterns) |pattern| { - if (bun.glob.match(bun.default_allocator, pattern, relative_path).matches()) { + if (bun.glob.match(pattern, relative_path).matches()) { should_ignore = true; break; } diff --git a/src/cli/update_interactive_command.zig b/src/cli/update_interactive_command.zig index 4aaa36d1a7..873b25bc5b 100644 --- a/src/cli/update_interactive_command.zig +++ b/src/cli/update_interactive_command.zig @@ -602,14 +602,14 @@ pub const UpdateInteractiveCommand = struct { const abs_res_path = path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &path_buf, &[_]string{res_path}, .posix); - if (!glob.walk.matchImpl(allocator, pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { + if (!glob.match(pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { break :matched false; } }, .name => |pattern| { const name = pkg_names[workspace_pkg_id].slice(string_buf); - if (!glob.walk.matchImpl(allocator, pattern, name).matches()) { + if (!glob.match(pattern, name).matches()) { break :matched false; } }, diff --git a/src/glob.zig b/src/glob.zig index 5519351638..07b48c5c18 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -1,8 +1,40 @@ +pub const match = @import("./glob/match.zig").match; pub const walk = @import("./glob/GlobWalker.zig"); -pub const match_impl = @import("./glob/match.zig"); -pub const match = match_impl.match; -pub const detectGlobSyntax = match_impl.detectGlobSyntax; - pub const GlobWalker = walk.GlobWalker_; pub const BunGlobWalker = GlobWalker(null, walk.SyscallAccessor, false); pub const BunGlobWalkerZ = GlobWalker(null, walk.SyscallAccessor, true); + +/// Returns true if the given string contains glob syntax, +/// excluding those escaped with backslashes +/// TODO: this doesn't play nicely with Windows directory separator and +/// backslashing, should we just require the user to supply posix filepaths? +pub fn detectGlobSyntax(potential_pattern: []const u8) bool { + // Negation only allowed in the beginning of the pattern + if (potential_pattern.len > 0 and potential_pattern[0] == '!') return true; + + // In descending order of how popular the token is + const SPECIAL_SYNTAX: [4]u8 = comptime [_]u8{ '*', '{', '[', '?' }; + + inline for (SPECIAL_SYNTAX) |token| { + var slice = potential_pattern[0..]; + while (slice.len > 0) { + if (std.mem.indexOfScalar(u8, slice, token)) |idx| { + // Check for even number of backslashes preceding the + // token to know that it's not escaped + var i = idx; + var backslash_count: u16 = 0; + + while (i > 0 and potential_pattern[i - 1] == '\\') : (i -= 1) { + backslash_count += 1; + } + + if (backslash_count % 2 == 0) return true; + slice = slice[idx + 1 ..]; + } else break; + } + } + + return false; +} + +const std = @import("std"); diff --git a/src/glob/GlobWalker.zig b/src/glob/GlobWalker.zig index 96a484c663..38f4e30aa1 100644 --- a/src/glob/GlobWalker.zig +++ b/src/glob/GlobWalker.zig @@ -1324,8 +1324,7 @@ pub fn GlobWalker_( } fn matchPatternSlow(this: *GlobWalker, pattern_component: *Component, filepath: []const u8) bool { - return match( - this.arena.allocator(), + return bun.glob.match( pattern_component.patternSlice(this.pattern), filepath, ).matches(); @@ -1686,11 +1685,8 @@ pub fn matchWildcardLiteral(literal: []const u8, path: []const u8) bool { return std.mem.eql(u8, literal, path); } -pub const matchImpl = match; - const DirIterator = @import("../bun.js/node/dir_iterator.zig"); const ResolvePath = @import("../resolver/resolve_path.zig"); -const match = @import("./match.zig").match; const bun = @import("bun"); const BunString = bun.String; diff --git a/src/glob/match.zig b/src/glob/match.zig index 391a86f455..36d50919cb 100644 --- a/src/glob/match.zig +++ b/src/glob/match.zig @@ -33,7 +33,7 @@ const Brace = struct { }; const BraceStack = bun.BoundedArray(Brace, 10); -pub const MatchResult = enum { +const MatchResult = enum { no_match, match, @@ -116,7 +116,7 @@ const Wildcard = struct { /// Used to escape any of the special characters above. // TODO: consider just taking arena and resetting to initial state, // all usages of this function pass in Arena.allocator() -pub fn match(_: Allocator, glob: []const u8, path: []const u8) MatchResult { +pub fn match(glob: []const u8, path: []const u8) MatchResult { var state = State{}; var negated = false; @@ -486,39 +486,6 @@ inline fn skipGlobstars(glob: []const u8, glob_index: *u32) void { glob_index.* -= 2; } -/// Returns true if the given string contains glob syntax, -/// excluding those escaped with backslashes -/// TODO: this doesn't play nicely with Windows directory separator and -/// backslashing, should we just require the user to supply posix filepaths? -pub fn detectGlobSyntax(potential_pattern: []const u8) bool { - // Negation only allowed in the beginning of the pattern - if (potential_pattern.len > 0 and potential_pattern[0] == '!') return true; - - // In descending order of how popular the token is - const SPECIAL_SYNTAX: [4]u8 = comptime [_]u8{ '*', '{', '[', '?' }; - - inline for (SPECIAL_SYNTAX) |token| { - var slice = potential_pattern[0..]; - while (slice.len > 0) { - if (std.mem.indexOfScalar(u8, slice, token)) |idx| { - // Check for even number of backslashes preceding the - // token to know that it's not escaped - var i = idx; - var backslash_count: u16 = 0; - - while (i > 0 and potential_pattern[i - 1] == '\\') : (i -= 1) { - backslash_count += 1; - } - - if (backslash_count % 2 == 0) return true; - slice = slice[idx + 1 ..]; - } else break; - } - } - - return false; -} - const BraceIndex = struct { start: u32 = 0, end: u32 = 0, @@ -526,4 +493,3 @@ const BraceIndex = struct { const bun = @import("bun"); const std = @import("std"); -const Allocator = std.mem.Allocator; diff --git a/src/install/PackageManager/install_with_manager.zig b/src/install/PackageManager/install_with_manager.zig index 90d016cd8c..cb4663e145 100644 --- a/src/install/PackageManager/install_with_manager.zig +++ b/src/install/PackageManager/install_with_manager.zig @@ -1064,7 +1064,7 @@ pub fn getWorkspaceFilters(manager: *PackageManager, original_cwd: []const u8) ! }, }; - switch (bun.glob.walk.matchImpl(manager.allocator, pattern, path_or_name)) { + switch (bun.glob.match(pattern, path_or_name)) { .match, .negate_match => install_root_dependencies = true, .negate_no_match => { diff --git a/src/install/lockfile/Package/WorkspaceMap.zig b/src/install/lockfile/Package/WorkspaceMap.zig index f0cbc1279e..2373d9db1d 100644 --- a/src/install/lockfile/Package/WorkspaceMap.zig +++ b/src/install/lockfile/Package/WorkspaceMap.zig @@ -130,7 +130,7 @@ pub fn processNamesArray( if (input_path.len == 0 or input_path.len == 1 and input_path[0] == '.') continue; - if (Glob.detectGlobSyntax(input_path)) { + if (glob.detectGlobSyntax(input_path)) { bun.handleOom(workspace_globs.append(input_path)); continue; } @@ -215,7 +215,7 @@ pub fn processNamesArray( if (workspace_globs.items.len > 0) { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - for (workspace_globs.items) |user_pattern| { + for (workspace_globs.items, 0..) |user_pattern, i| { defer _ = arena.reset(.retain_capacity); const glob_pattern = if (user_pattern.len == 0) "package.json" else brk: { @@ -253,7 +253,7 @@ pub fn processNamesArray( return error.GlobError; } - while (switch (try iter.next()) { + next_match: while (switch (try iter.next()) { .result => |r| r, .err => |e| { log.addErrorFmt( @@ -271,6 +271,28 @@ pub fn processNamesArray( // skip root package.json if (strings.eqlComptime(matched_path, "package.json")) continue; + { + const matched_path_without_package_json = strings.withoutTrailingSlash(strings.withoutSuffixComptime(matched_path, "package.json")); + + // check if it's negated by any remaining patterns + for (workspace_globs.items[i + 1 ..]) |next_pattern| { + switch (bun.glob.match(next_pattern, matched_path_without_package_json)) { + .no_match, + .match, + .negate_match, + => {}, + + .negate_no_match => { + debug("skipping negated path: {s}, {s}\n", .{ + matched_path_without_package_json, + next_pattern, + }); + continue :next_match; + }, + } + } + } + debug("matched path: {s}, dirname: {s}\n", .{ matched_path, entry_dir }); const abs_package_json_path = Path.joinAbsStringBufZ( @@ -375,7 +397,7 @@ fn ignoredWorkspacePaths(path: []const u8) bool { } return false; } -const GlobWalker = Glob.GlobWalker(ignoredWorkspacePaths, Glob.walk.SyscallAccessor, false); +const GlobWalker = glob.GlobWalker(ignoredWorkspacePaths, glob.walk.SyscallAccessor, false); const string = []const u8; const debug = Output.scoped(.Lockfile, .hidden); @@ -386,10 +408,10 @@ const Allocator = std.mem.Allocator; const bun = @import("bun"); const Environment = bun.Environment; -const Glob = bun.glob; const JSAst = bun.ast; const Output = bun.Output; const Path = bun.path; +const glob = bun.glob; const logger = bun.logger; const strings = bun.strings; diff --git a/src/install/lockfile/Tree.zig b/src/install/lockfile/Tree.zig index 2b5f958659..a75feae070 100644 --- a/src/install/lockfile/Tree.zig +++ b/src/install/lockfile/Tree.zig @@ -408,7 +408,7 @@ pub fn isFilteredDependencyOrWorkspace( }, }; - switch (bun.glob.match(undefined, pattern, name_or_path)) { + switch (bun.glob.match(pattern, name_or_path)) { .match, .negate_match => workspace_matched = true, .negate_no_match => { diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index e118f86f89..1ccf7529ca 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -150,7 +150,7 @@ pub const PackageJSON = struct { defer bun.default_allocator.free(normalized_path); for (glob_list.items) |pattern| { - if (glob.match(bun.default_allocator, pattern, normalized_path).matches()) { + if (glob.match(pattern, normalized_path).matches()) { return true; } } @@ -166,7 +166,7 @@ pub const PackageJSON = struct { defer bun.default_allocator.free(normalized_path); for (mixed.globs.items) |pattern| { - if (glob.match(bun.default_allocator, pattern, normalized_path).matches()) { + if (glob.match(pattern, normalized_path).matches()) { return true; } } diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index b14bcb5b99..04fe05f620 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -67,7 +67,7 @@ pub const WorkPool = jsc.WorkPool; pub const Pipe = [2]bun.FileDescriptor; pub const SmolList = shell.SmolList; -pub const GlobWalker = Glob.BunGlobWalkerZ; +pub const GlobWalker = bun.glob.BunGlobWalkerZ; pub const stdin_no = 0; pub const stdout_no = 1; @@ -1957,7 +1957,6 @@ pub fn unreachableState(context: []const u8, state: []const u8) noreturn { return bun.Output.panic("Bun shell has reached an unreachable state \"{s}\" in the {s} context. This indicates a bug, please open a GitHub issue.", .{ state, context }); } -const Glob = @import("../glob.zig"); const builtin = @import("builtin"); const WTFStringImplStruct = @import("../string.zig").WTFStringImplStruct; diff --git a/src/shell/shell.zig b/src/shell/shell.zig index e8ded9f1ae..7759256915 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -17,7 +17,7 @@ pub const IOReader = Interpreter.IOReader; pub const Yield = @import("./Yield.zig").Yield; pub const unreachableState = interpret.unreachableState; -const GlobWalker = Glob.GlobWalker_(null, true); +const GlobWalker = bun.glob.GlobWalker(null, true); // const GlobWalker = Glob.BunGlobWalker; pub const SUBSHELL_TODO_ERROR = "Subshells are not implemented, please open GitHub issue!"; @@ -4429,7 +4429,6 @@ pub const TestingAPIs = struct { pub const ShellSubprocess = @import("./subproc.zig").ShellSubprocess; -const Glob = @import("../glob.zig"); const Syscall = @import("../sys.zig"); const builtin = @import("builtin"); diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 219c2b50ac..a5a2ba94a5 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -136,6 +136,44 @@ test("dependency on workspace without version in package.json", async () => { } }, 20_000); +test("allowing negative workspace patterns", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "root", + workspaces: ["packages/*", "!packages/pkg2"], + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "doesnt-exist-oops": "1.2.3", + }, + }), + ), + ]); + + const { exited } = await runBunInstall(env, packageDir); + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + }); +}); + test("dependency on same name as workspace and dist-tag", async () => { await Promise.all([ write( From 46d6e0885b805cfb8d7148aaa13a984546178093 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sat, 4 Oct 2025 00:53:15 -0700 Subject: [PATCH 020/191] fix(pnpm migration): fix `"lockfileVersion"` number parsing (#23232) ### What does this PR do? Parsing would fail because the lockfile version might be parsing as a non-whole float instead of a string (`5.4` vs `'5.4'`) and the migration would have the wrong error. ### How did you verify your code works? Added a test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Bot Co-authored-by: Claude --- src/install/migration.zig | 25 +------------------ src/install/pnpm.zig | 8 +++--- .../install/migration/pnpm-migration.test.ts | 19 ++++++++++++++ .../pnpm/version-number-dot/package.json | 0 .../pnpm/version-number-dot/pnpm-lock.yaml | 1 + 5 files changed, 25 insertions(+), 28 deletions(-) create mode 100644 test/cli/install/migration/pnpm/version-number-dot/package.json create mode 100644 test/cli/install/migration/pnpm/version-number-dot/pnpm-lock.yaml diff --git a/src/install/migration.zig b/src/install/migration.zig index 2390d4e850..fcf417b140 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -26,14 +26,6 @@ pub fn detectAndLoadOtherLockfile( , .{}); Global.exit(1); } - if (Environment.isDebug) { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - Output.prettyErrorln("Error: {s}", .{@errorName(err)}); - log.print(Output.errorWriter()) catch {}; - Output.prettyErrorln("Invalid NPM package-lock.json\nIn a release build, this would ignore and do a fresh install.\nAborting", .{}); - Global.exit(1); - } return LoadResult{ .err = .{ .step = .migrating, .value = err, @@ -58,14 +50,6 @@ pub fn detectAndLoadOtherLockfile( defer lockfile.close(); const data = lockfile.readToEnd(allocator).unwrap() catch break :yarn; const migrate_result = @import("./yarn.zig").migrateYarnLockfile(this, manager, allocator, log, data, dir) catch |err| { - if (Environment.isDebug) { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - Output.prettyErrorln("Error: {s}", .{@errorName(err)}); - log.print(Output.errorWriter()) catch {}; - Output.prettyErrorln("Invalid yarn.lock\nIn a release build, this would ignore and do a fresh install.\nAborting", .{}); - Global.exit(1); - } return LoadResult{ .err = .{ .step = .migrating, .value = err, @@ -137,14 +121,7 @@ pub fn detectAndLoadOtherLockfile( }, else => {}, } - if (Environment.isDebug) { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - Output.prettyErrorln("Error: {s}", .{@errorName(err)}); - log.print(Output.errorWriter()) catch {}; - Output.prettyErrorln("Invalid pnpm-lock.yaml\nIn a release build, this would ignore and do a fresh install.\nAborting", .{}); - Global.exit(1); - } + log.reset(); return LoadResult{ .err = .{ .step = .migrating, .value = err, diff --git a/src/install/pnpm.zig b/src/install/pnpm.zig index eb7e759639..33e733a6ad 100644 --- a/src/install/pnpm.zig +++ b/src/install/pnpm.zig @@ -105,21 +105,21 @@ pub fn migratePnpmLockfile( return error.PnpmLockfileMissingVersion; }; - const lockfile_version_num: u32 = lockfile_version: { + const lockfile_version_num: f64 = lockfile_version: { err: { switch (lockfile_version_expr.data) { .e_number => |num| { - if (num.value < 0 or num.value > std.math.maxInt(u32)) { + if (num.value < 0) { break :err; } - break :lockfile_version @intFromFloat(std.math.divExact(f64, num.value, 1) catch break :err); + break :lockfile_version num.value; }, .e_string => |version_str| { const str = version_str.slice(allocator); const end = strings.indexOfChar(str, '.') orelse str.len; - break :lockfile_version std.fmt.parseUnsigned(u32, str[0..end], 10) catch break :err; + break :lockfile_version std.fmt.parseFloat(f64, str[0..end]) catch break :err; }, else => {}, } diff --git a/test/cli/install/migration/pnpm-migration.test.ts b/test/cli/install/migration/pnpm-migration.test.ts index 5ce3d4219e..d92a68ad23 100644 --- a/test/cli/install/migration/pnpm-migration.test.ts +++ b/test/cli/install/migration/pnpm-migration.test.ts @@ -54,6 +54,25 @@ test("basic", async () => { expect(err).not.toContain("Saved lockfile"); }); +test("version is number with dot", async () => { + const { packageDir } = await verdaccio.createTestDir({ + files: join(import.meta.dir, "pnpm/version-number-dot"), + }); + + let proc = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + env, + stdout: "pipe", + stderr: "pipe", + }); + + let [err, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + expect(err).toContain("pnpm-lock.yaml version is too old (< v7)"); +}); + describe.todo("bin", () => { test("manifests are fetched for bins", async () => { const { packageDir, packageJson } = await verdaccio.createTestDir({ diff --git a/test/cli/install/migration/pnpm/version-number-dot/package.json b/test/cli/install/migration/pnpm/version-number-dot/package.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/install/migration/pnpm/version-number-dot/pnpm-lock.yaml b/test/cli/install/migration/pnpm/version-number-dot/pnpm-lock.yaml new file mode 100644 index 0000000000..9764deb1cc --- /dev/null +++ b/test/cli/install/migration/pnpm/version-number-dot/pnpm-lock.yaml @@ -0,0 +1 @@ +lockfileVersion: 5.4 From 02d0586da59550796f89425f50b3bd47171b604b Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 00:54:24 -0700 Subject: [PATCH 021/191] Increase crash report stack trace buffer from 10 to 20 frames (#23225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Increase the stack trace buffer size in the crash handler from 10 to 20 frames to ensure more useful frames are included in crash reports sent to bun.report. ## Motivation Currently, we capture up to 10 stack frames when generating crash reports. However, many of these frames get filtered out when `StackLine.fromAddress()` returns `null` for invalid/empty frames. This results in only a small number of frames (sometimes as few as 5) actually being sent to the server. ## Changes - Increased `addr_buf` array size from `[10]usize` to `[20]usize` in `src/crash_handler.zig:307` ## Impact By capturing more frames initially, we ensure that after filtering we still have a meaningful number of frames in the crash report. This will help with debugging crashes by providing more context about the call stack. The encoding function `encodeTraceString()` has no hardcoded limits and will encode all available frames, so this change directly translates to more frames being sent to bun.report. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/crash_handler.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crash_handler.zig b/src/crash_handler.zig index 2cea2ce25e..56f4545395 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -304,7 +304,7 @@ pub fn crashHandler( writer.print("Crashed while {}\n", .{action}) catch std.posix.abort(); } - var addr_buf: [10]usize = undefined; + var addr_buf: [20]usize = undefined; var trace_buf: std.builtin.StackTrace = undefined; // If a trace was not provided, compute one now From 9993e120503693145ee1520e21c9099324ef13ec Mon Sep 17 00:00:00 2001 From: pfg Date: Sat, 4 Oct 2025 01:50:09 -0700 Subject: [PATCH 022/191] Unify timer enum (#23228) ### What does this PR do? Unify EventLoopTimer.Tag to one enum instead of two ### How did you verify your code works? Build & CI --- src/bun.js/api/Timer/EventLoopTimer.zig | 55 ++++--------------------- 1 file changed, 8 insertions(+), 47 deletions(-) diff --git a/src/bun.js/api/Timer/EventLoopTimer.zig b/src/bun.js/api/Timer/EventLoopTimer.zig index 0a0ea9dbf2..c8ed6d911b 100644 --- a/src/bun.js/api/Timer/EventLoopTimer.zig +++ b/src/bun.js/api/Timer/EventLoopTimer.zig @@ -47,7 +47,7 @@ pub fn less(_: void, a: *const Self, b: *const Self) bool { return order == .lt; } -pub const Tag = if (Environment.isWindows) enum { +pub const Tag = enum { TimerCallback, TimeoutObject, ImmediateObject, @@ -78,7 +78,7 @@ pub const Tag = if (Environment.isWindows) enum { .StatWatcherScheduler => StatWatcherScheduler, .UpgradedDuplex => uws.UpgradedDuplex, .DNSResolver => DNSResolver, - .WindowsNamedPipe => uws.WindowsNamedPipe, + .WindowsNamedPipe => if (Environment.isWindows) uws.WindowsNamedPipe else UnreachableTimer, .WTFTimer => WTFTimer, .PostgresSQLConnectionTimeout => jsc.Postgres.PostgresSQLConnection, .PostgresSQLConnectionMaxLifetime => jsc.Postgres.PostgresSQLConnection, @@ -96,52 +96,13 @@ pub const Tag = if (Environment.isWindows) enum { .EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor, }; } -} else enum { - TimerCallback, - TimeoutObject, - ImmediateObject, - StatWatcherScheduler, - UpgradedDuplex, - WTFTimer, - DNSResolver, - PostgresSQLConnectionTimeout, - PostgresSQLConnectionMaxLifetime, - MySQLConnectionTimeout, - MySQLConnectionMaxLifetime, - ValkeyConnectionTimeout, - ValkeyConnectionReconnect, - SubprocessTimeout, - DevServerSweepSourceMaps, - DevServerMemoryVisualizerTick, - AbortSignalTimeout, - DateHeaderTimer, - BunTest, - EventLoopDelayMonitor, +}; - pub fn Type(comptime T: Tag) type { - return switch (T) { - .TimerCallback => TimerCallback, - .TimeoutObject => TimeoutObject, - .ImmediateObject => ImmediateObject, - .StatWatcherScheduler => StatWatcherScheduler, - .UpgradedDuplex => uws.UpgradedDuplex, - .WTFTimer => WTFTimer, - .DNSResolver => DNSResolver, - .PostgresSQLConnectionTimeout => jsc.Postgres.PostgresSQLConnection, - .PostgresSQLConnectionMaxLifetime => jsc.Postgres.PostgresSQLConnection, - .MySQLConnectionTimeout => jsc.MySQL.MySQLConnection, - .MySQLConnectionMaxLifetime => jsc.MySQL.MySQLConnection, - .ValkeyConnectionTimeout => jsc.API.Valkey, - .ValkeyConnectionReconnect => jsc.API.Valkey, - .SubprocessTimeout => jsc.Subprocess, - .DevServerSweepSourceMaps, - .DevServerMemoryVisualizerTick, - => bun.bake.DevServer, - .AbortSignalTimeout => jsc.WebCore.AbortSignal.Timeout, - .DateHeaderTimer => jsc.API.Timer.DateHeaderTimer, - .BunTest => jsc.Jest.bun_test.BunTest, - .EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor, - }; +const UnreachableTimer = struct { + event_loop_timer: Self, + fn callback(_: *UnreachableTimer, _: *UnreachableTimer) Arm { + if (Environment.ci_assert) bun.assert(false); + return .disarm; } }; From 578a47ce4a3930bff98346a3bc90734167c2d5ed Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Sat, 4 Oct 2025 17:56:42 +0900 Subject: [PATCH 023/191] Fix segmentation fault during building stack traces string (#22902) ### What does this PR do? Bun sometimes crashes with a segmentation fault while generating stack traces. the following might be happening in `remapZigException`: 1. The first populateStackTrace (OnlyPosition) sets `frames_len` (e.g., frames_len = 5) https://github.com/oven-sh/bun/blob/613aea1787f1c8ef78d54a4dbc322b9fe59d1743/src/bun.js/bindings/bindings.cpp#L4793 ``` [frame1, frame2, frame3, frame4, frame5] ``` 2. Frame filtering in remapZigException reduces `frames_len` (e.g., frames_len = 3) https://github.com/oven-sh/bun/blob/613aea1787f1c8ef78d54a4dbc322b9fe59d1743/src/bun.js/VirtualMachine.zig#L2686-L2704 ``` [frame1, frame4, frame5, (frame4, frame5)] // frame2 and frame3 are removed by filtering; frames_len is set to 3 here, but frame4 and frame5 remain in their original positions ``` 3. The second populateStackTrace (OnlySourceLine) increases `frames_len` (e.g., frames_len = 5) https://github.com/oven-sh/bun/blob/613aea1787f1c8ef78d54a4dbc322b9fe59d1743/src/bun.js/bindings/bindings.cpp#L4793 ``` [frame1, frame4, frame5, frame4, frame5] ``` When deinit is executed on these frames, the ref count is excessively decremented (for frame4 and frame5), resulting in a UAF. ### How did you verify your code works? WIP. I'm working on creating minimal reproduction code. However, I've confirmed that `twenty-server` tests passes with this PR. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner --- src/bun.js/bindings/ZigStackFrame.zig | 4 +++ src/bun.js/bindings/bindings.cpp | 42 +++++++++++++++-------- src/bun.js/bindings/headers-handwritten.h | 12 +++++++ src/logger.zig | 10 ------ 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/bun.js/bindings/ZigStackFrame.zig b/src/bun.js/bindings/ZigStackFrame.zig index 3d9bc442ae..4082b86f25 100644 --- a/src/bun.js/bindings/ZigStackFrame.zig +++ b/src/bun.js/bindings/ZigStackFrame.zig @@ -11,6 +11,9 @@ pub const ZigStackFrame = extern struct { /// This informs formatters whether to display as a blob URL or not remapped: bool = false, + /// -1 means not set. + jsc_stack_frame_index: i32 = -1, + pub fn deinit(this: *ZigStackFrame) void { this.function_name.deref(); this.source_url.deref(); @@ -213,6 +216,7 @@ pub const ZigStackFrame = extern struct { .source_url = .empty, .position = .invalid, .is_async = false, + .jsc_stack_frame_index = -1, }; pub fn nameFormatter(this: *const ZigStackFrame, comptime enable_color: bool) NameFormatter { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index e41c9d6979..e8147330ea 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4772,25 +4772,37 @@ public: static void populateStackTrace(JSC::VM& vm, const WTF::Vector& frames, ZigStackTrace& trace, JSC::JSGlobalObject* globalObject, PopulateStackTraceFlags flags) { - uint8_t frame_i = 0; - size_t stack_frame_i = 0; - const size_t total_frame_count = frames.size(); - const uint8_t frame_count = total_frame_count < trace.frames_cap ? total_frame_count : trace.frames_cap; + if (flags == PopulateStackTraceFlags::OnlyPosition) { + uint8_t frame_i = 0; + size_t stack_frame_i = 0; + const size_t total_frame_count = frames.size(); + const uint8_t frame_count = total_frame_count < trace.frames_cap ? total_frame_count : trace.frames_cap; - while (frame_i < frame_count && stack_frame_i < total_frame_count) { - // Skip native frames - while (stack_frame_i < total_frame_count && !(frames.at(stack_frame_i).hasLineAndColumnInfo()) && !(frames.at(stack_frame_i).isWasmFrame())) { + while (frame_i < frame_count && stack_frame_i < total_frame_count) { + // Skip native frames + while (stack_frame_i < total_frame_count && !(frames.at(stack_frame_i).hasLineAndColumnInfo()) && !(frames.at(stack_frame_i).isWasmFrame())) { + stack_frame_i++; + } + if (stack_frame_i >= total_frame_count) + break; + + ZigStackFrame& frame = trace.frames_ptr[frame_i]; + frame.jsc_stack_frame_index = static_cast(stack_frame_i); + populateStackFrame(vm, trace, frames[stack_frame_i], frame, frame_i == 0, &trace.referenced_source_provider, globalObject, flags); stack_frame_i++; + frame_i++; + } + trace.frames_len = frame_i; + } else if (flags == PopulateStackTraceFlags::OnlySourceLines) { + for (uint8_t i = 0; i < trace.frames_len; i++) { + ZigStackFrame& frame = trace.frames_ptr[i]; + // A call with flags set to OnlySourceLines always follows a call with flags set to OnlyPosition, + // so jsc_stack_frame_index is always a valid value here. + ASSERT(frame.jsc_stack_frame_index >= 0); + ASSERT(static_cast(frame.jsc_stack_frame_index) < frames.size()); + populateStackFrame(vm, trace, frames[frame.jsc_stack_frame_index], frame, i == 0, &trace.referenced_source_provider, globalObject, flags); } - if (stack_frame_i >= total_frame_count) - break; - - ZigStackFrame& frame = trace.frames_ptr[frame_i]; - populateStackFrame(vm, trace, frames[stack_frame_i], frame, frame_i == 0, &trace.referenced_source_provider, globalObject, flags); - stack_frame_i++; - frame_i++; } - trace.frames_len = frame_i; } static JSC::JSValue getNonObservable(JSC::VM& vm, JSC::JSGlobalObject* global, JSC::JSObject* obj, const JSC::PropertyName& propertyName) diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index b5f56ffa0a..26d03cbb10 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -183,6 +183,18 @@ typedef struct ZigStackFrame { ZigStackFrameCode code_type; bool is_async; bool remapped; + int32_t jsc_stack_frame_index; + + ZigStackFrame() + : function_name {} + , source_url {} + , position {} + , code_type {} + , is_async(false) + , remapped(false) + , jsc_stack_frame_index(-1) + { + } } ZigStackFrame; typedef struct ZigStackTrace { diff --git a/src/logger.zig b/src/logger.zig index bd858b7458..c778cc9cad 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -1335,15 +1335,6 @@ pub const Log = struct { if (needs_newline) _ = try to.write("\n"); } - - pub fn toZigException(this: *const Log, allocator: std.mem.Allocator) *js.ZigException.Holder { - var holder = try allocator.create(js.ZigException.Holder); - holder.* = js.ZigException.Holder.init(); - var zig_exception: *js.ZigException = holder.zigException(); - zig_exception.exception = this; - zig_exception.code = js.JSErrorCode.BundlerError; - return holder; - } }; pub inline fn usize2Loc(loc: usize) Loc { @@ -1617,7 +1608,6 @@ const Output = bun.Output; const StringBuilder = bun.StringBuilder; const assert = bun.assert; const default_allocator = bun.default_allocator; -const js = bun.jsc; const jsc = bun.jsc; const strings = bun.strings; const Index = bun.ast.Index; From 9cab1fbfe0b25b5fd49db0f2ba208829f1cc51ab Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 02:17:55 -0700 Subject: [PATCH 024/191] update CLAUDE.md --- CLAUDE.md | 56 +----------------------------------------------- src/AGENTS.md | 1 + src/CLAUDE.md | 5 +++++ src/js/CLAUDE.md | 2 ++ 4 files changed, 9 insertions(+), 55 deletions(-) create mode 120000 src/AGENTS.md create mode 100644 src/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 09a8499345..54f107ec54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -177,47 +177,6 @@ Built-in JavaScript modules use special syntax and are organized as: - `internal/` - Internal modules not exposed to users - `builtins/` - Core JavaScript builtins (streams, console, etc.) -### Special Syntax in Built-in Modules - -1. **`$` prefix** - Access to private properties and JSC intrinsics: - - ```js - const arr = $Array.from(...); // Private global - map.$set(...); // Private method - const arr2 = $newArrayWithSize(5); // JSC intrinsic - ``` - -2. **`require()`** - Must use string literals, resolved at compile time: - - ```js - const fs = require("fs"); // Directly loads by numeric ID - ``` - -3. **Debug helpers**: - - `$debug()` - Like console.log but stripped in release builds - - `$assert()` - Assertions stripped in release builds - - `if($debug) {}` - Check if debug env var is set - -4. **Platform detection**: `process.platform` and `process.arch` are inlined and dead-code eliminated - -5. **Export syntax**: Use `export default` which gets converted to a return statement: - ```js - export default { - readFile, - writeFile, - }; - ``` - -Note: These are NOT ES modules. The preprocessor converts `$` to `@` (JSC's actual syntax) and handles the special functions. - -## CI - -Bun uses BuildKite for CI. To get the status of a PR, you can use the following command: - -```bash -bun ci -``` - ## Important Development Notes 1. **Never use `bun test` or `bun ` directly** - always use `bun bd test` or `bun bd `. `bun bd` compiles & runs the debug build. @@ -229,19 +188,6 @@ bun ci 7. **Avoid shell commands** - Don't use `find` or `grep` in tests; use Bun's Glob and built-in tools 8. **Memory management** - In Zig code, be careful with allocators and use defer for cleanup 9. **Cross-platform** - Run `bun run zig:check-all` to compile the Zig code on all platforms when making platform-specific changes -10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific scopes +10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific `Output.scoped(.${scopeName}, .visible)`s 11. **Be humble & honest** - NEVER overstate what you got done or what actually works in commits, PRs or in messages to the user. 12. **Branch names must start with `claude/`** - This is a requirement for the CI to work. - -## Key APIs and Features - -### Bun-Specific APIs - -- **Bun.serve()** - High-performance HTTP server -- **Bun.spawn()** - Process spawning with better performance than Node.js -- **Bun.file()** - Fast file I/O operations -- **Bun.write()** - Unified API for writing to files, stdout, etc. -- **Bun.$ (Shell)** - Cross-platform shell scripting -- **Bun.SQLite** - Native SQLite integration -- **Bun.FFI** - Call native libraries from JavaScript -- **Bun.Glob** - Fast file pattern matching diff --git a/src/AGENTS.md b/src/AGENTS.md new file mode 120000 index 0000000000..681311eb9c --- /dev/null +++ b/src/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/src/CLAUDE.md b/src/CLAUDE.md new file mode 100644 index 0000000000..d53c2b889b --- /dev/null +++ b/src/CLAUDE.md @@ -0,0 +1,5 @@ +## Zig + +- Prefer `@import` at the **bottom** of the file. +- It's `@import("bun")` not `@import("root").bun` +- You must be patient with the build. diff --git a/src/js/CLAUDE.md b/src/js/CLAUDE.md index ed175a119a..e34bbfc526 100644 --- a/src/js/CLAUDE.md +++ b/src/js/CLAUDE.md @@ -72,6 +72,8 @@ $debug("Module loaded:", name); // Debug (stripped in release) $assert(condition, "message"); // Assertions (stripped in release) ``` +**Platform detection**: `process.platform` and `process.arch` are inlined and dead-code eliminated + ## Validation and Errors ```typescript From 4424c5ed08bc690553218abbafeeca78fe1378c7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 02:20:59 -0700 Subject: [PATCH 025/191] Update CLAUDE.md --- CLAUDE.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 54f107ec54..d6c6ff3675 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,19 +143,6 @@ When implementing JavaScript classes in C++: 3. Add iso subspaces for classes with C++ fields 4. Cache structures in ZigGlobalObject -## Development Workflow - -### Code Formatting - -- `bun run prettier` - Format JS/TS files -- `bun run zig-format` - Format Zig files -- `bun run clang-format` - Format C++ files - -### Watching for Changes - -- `bun run watch` - Incremental Zig compilation with error checking -- `bun run watch-windows` - Windows-specific watch mode - ### Code Generation Code generation happens automatically as part of the build process. The main scripts are: @@ -188,6 +175,6 @@ Built-in JavaScript modules use special syntax and are organized as: 7. **Avoid shell commands** - Don't use `find` or `grep` in tests; use Bun's Glob and built-in tools 8. **Memory management** - In Zig code, be careful with allocators and use defer for cleanup 9. **Cross-platform** - Run `bun run zig:check-all` to compile the Zig code on all platforms when making platform-specific changes -10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific `Output.scoped(.${scopeName}, .visible)`s +10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific `Output.scoped(.${scopeName}, .visible)`s 11. **Be humble & honest** - NEVER overstate what you got done or what actually works in commits, PRs or in messages to the user. 12. **Branch names must start with `claude/`** - This is a requirement for the CI to work. From 3c9433f9af7864335439f45e3164bd997015a0d3 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Sat, 4 Oct 2025 02:48:50 -0700 Subject: [PATCH 026/191] fix(sqlite) enable order by and limit in delete/update statements on windows (#23227) ### What does this PR do? Enable compiler flags Update SQLite amalgamation using https://www.sqlite.org/download.html source code [sqlite-src-3500400.zip](https://www.sqlite.org/2025/sqlite-src-3500400.zip) with: ```bash ./configure CFLAGS="-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT" make sqlite3.c ``` This is the same version that before just with this adicional flag that must be enabled when generating the amalgamation so we are actually able to use this option. You can also see that without this the build will happen but the feature will not be enable https://buildkite.com/bun/bun/builds/27940, as informed in https://www.sqlite.org/howtocompile.html topic 5. ### How did you verify your code works? Add in CI two tests that check if the feature is enabled on windows --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- .github/workflows/update-sqlite3.yml | 19 +- scripts/update-sqlite-amalgamation.sh | 60 ++ src/bun.js/bindings/sqlite/CMakeLists.txt | 2 + src/bun.js/bindings/sqlite/sqlite3.c | 753 +++++++++++----------- test/js/sql/sqlite-sql.test.ts | 18 +- 5 files changed, 465 insertions(+), 387 deletions(-) create mode 100755 scripts/update-sqlite-amalgamation.sh diff --git a/.github/workflows/update-sqlite3.yml b/.github/workflows/update-sqlite3.yml index 6ee8115f7c..65321f466a 100644 --- a/.github/workflows/update-sqlite3.yml +++ b/.github/workflows/update-sqlite3.yml @@ -70,24 +70,7 @@ jobs: - name: Update SQLite if needed if: success() && steps.check-version.outputs.current_num < steps.check-version.outputs.latest_num run: | - set -euo pipefail - - TEMP_DIR=$(mktemp -d) - cd $TEMP_DIR - - echo "Downloading from: https://sqlite.org/${{ steps.check-version.outputs.latest_year }}/sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - - # Download and extract latest version - wget "https://sqlite.org/${{ steps.check-version.outputs.latest_year }}/sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - unzip "sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - cd "sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}" - - # Add header comment and copy files - echo "// clang-format off" > $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3.c - cat sqlite3.c >> $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3.c - - echo "// clang-format off" > $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3_local.h - cat sqlite3.h >> $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3_local.h + ./scripts/update-sqlite-amalgamation.sh ${{ steps.check-version.outputs.latest_num }} ${{ steps.check-version.outputs.latest_year }} - name: Create Pull Request if: success() && steps.check-version.outputs.current_num < steps.check-version.outputs.latest_num diff --git a/scripts/update-sqlite-amalgamation.sh b/scripts/update-sqlite-amalgamation.sh new file mode 100755 index 0000000000..f580f0dc5d --- /dev/null +++ b/scripts/update-sqlite-amalgamation.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script updates SQLite amalgamation files with the required compiler flags. +# It downloads the SQLite source, configures it with necessary flags, builds the +# amalgamation, and copies the generated files to the Bun source tree. +# +# Usage: +# ./scripts/update-sqlite-amalgamation.sh +# +# Example: +# ./scripts/update-sqlite-amalgamation.sh 3500400 2025 +# +# The version number is a 7-digit SQLite version (e.g., 3500400 for 3.50.4) +# The year is the release year found in the download URL + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 3500400 2025" + exit 1 +fi + +VERSION_NUM="$1" +YEAR="$2" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Create temporary directory +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +cd "$TEMP_DIR" + +echo "Downloading SQLite source version $VERSION_NUM from year $YEAR..." +DOWNLOAD_URL="https://sqlite.org/$YEAR/sqlite-src-$VERSION_NUM.zip" +echo "URL: $DOWNLOAD_URL" + +wget -q "$DOWNLOAD_URL" +unzip -q "sqlite-src-$VERSION_NUM.zip" +cd "sqlite-src-$VERSION_NUM" + +echo "Configuring SQLite with required flags..." +# These flags must be set during amalgamation generation for them to take effect +# in the parser and other compile-time generated code +CFLAGS="-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT=1 -DSQLITE_ENABLE_COLUMN_METADATA=1" +./configure CFLAGS="$CFLAGS" > /dev/null 2>&1 + +echo "Building amalgamation..." +make sqlite3.c > /dev/null 2>&1 + +echo "Copying files to Bun source tree..." +# Add clang-format off directive and copy the amalgamation +echo "// clang-format off" > "$REPO_ROOT/src/bun.js/bindings/sqlite/sqlite3.c" +cat sqlite3.c >> "$REPO_ROOT/src/bun.js/bindings/sqlite/sqlite3.c" + +echo "// clang-format off" > "$REPO_ROOT/src/bun.js/bindings/sqlite/sqlite3_local.h" +cat sqlite3.h >> "$REPO_ROOT/src/bun.js/bindings/sqlite/sqlite3_local.h" + +echo "✓ Successfully updated SQLite amalgamation files" diff --git a/src/bun.js/bindings/sqlite/CMakeLists.txt b/src/bun.js/bindings/sqlite/CMakeLists.txt index 9609feb3d3..065a862978 100644 --- a/src/bun.js/bindings/sqlite/CMakeLists.txt +++ b/src/bun.js/bindings/sqlite/CMakeLists.txt @@ -12,6 +12,8 @@ target_compile_definitions(sqlite3 PRIVATE "SQLITE_ENABLE_FTS5=1" "SQLITE_ENABLE_JSON1=1" "SQLITE_ENABLE_MATH_FUNCTIONS=1" + "SQLITE_ENABLE_UPDATE_DELETE_LIMIT=1" + "SQLITE_UDL_CAPABLE_PARSER=1" ) if(WIN32) diff --git a/src/bun.js/bindings/sqlite/sqlite3.c b/src/bun.js/bindings/sqlite/sqlite3.c index e218709dfb..fd226702de 100644 --- a/src/bun.js/bindings/sqlite/sqlite3.c +++ b/src/bun.js/bindings/sqlite/sqlite3.c @@ -29,6 +29,7 @@ #ifndef SQLITE_PRIVATE # define SQLITE_PRIVATE static #endif +#define SQLITE_UDL_CAPABLE_PARSER 1 /************** Begin file sqliteInt.h ***************************************/ /* ** 2001 September 15 @@ -175265,7 +175266,9 @@ SQLITE_PRIVATE void sqlite3WindowCodeStep( /************** End of window.c **********************************************/ /************** Begin file parse.c *******************************************/ /* This file is automatically generated by Lemon from input grammar -** source file "parse.y". +** source file "parse.y" with these options: +** +** -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT */ /* ** 2001-09-15 @@ -175811,18 +175814,18 @@ typedef union { #define sqlite3ParserCTX_FETCH Parse *pParse=yypParser->pParse; #define sqlite3ParserCTX_STORE yypParser->pParse=pParse; #define YYFALLBACK 1 -#define YYNSTATE 583 +#define YYNSTATE 587 #define YYNRULE 409 #define YYNRULE_WITH_ACTION 344 #define YYNTOKEN 187 -#define YY_MAX_SHIFT 582 -#define YY_MIN_SHIFTREDUCE 845 -#define YY_MAX_SHIFTREDUCE 1253 -#define YY_ERROR_ACTION 1254 -#define YY_ACCEPT_ACTION 1255 -#define YY_NO_ACTION 1256 -#define YY_MIN_REDUCE 1257 -#define YY_MAX_REDUCE 1665 +#define YY_MAX_SHIFT 586 +#define YY_MIN_SHIFTREDUCE 849 +#define YY_MAX_SHIFTREDUCE 1257 +#define YY_ERROR_ACTION 1258 +#define YY_ACCEPT_ACTION 1259 +#define YY_NO_ACTION 1260 +#define YY_MIN_REDUCE 1261 +#define YY_MAX_REDUCE 1669 #define YY_MIN_DSTRCTR 206 #define YY_MAX_DSTRCTR 320 /************* End control #defines *******************************************/ @@ -175909,227 +175912,227 @@ typedef union { *********** Begin parsing tables **********************************************/ #define YY_ACTTAB_COUNT (2207) static const YYACTIONTYPE yy_action[] = { - /* 0 */ 130, 127, 234, 282, 282, 1328, 576, 1307, 460, 289, - /* 10 */ 289, 576, 1622, 381, 576, 1328, 573, 576, 562, 413, - /* 20 */ 1300, 1542, 573, 481, 562, 524, 460, 459, 558, 82, - /* 30 */ 82, 983, 294, 375, 51, 51, 498, 61, 61, 984, - /* 40 */ 82, 82, 1577, 137, 138, 91, 7, 1228, 1228, 1063, - /* 50 */ 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, 413, - /* 60 */ 288, 288, 182, 288, 288, 481, 536, 288, 288, 130, - /* 70 */ 127, 234, 432, 573, 525, 562, 573, 557, 562, 1290, - /* 80 */ 573, 421, 562, 137, 138, 91, 559, 1228, 1228, 1063, - /* 90 */ 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, 296, - /* 100 */ 460, 398, 1249, 134, 134, 134, 134, 133, 133, 132, - /* 110 */ 132, 132, 131, 128, 451, 451, 1050, 1050, 1064, 1067, - /* 120 */ 1255, 1, 1, 582, 2, 1259, 581, 1174, 1259, 1174, - /* 130 */ 321, 413, 155, 321, 1584, 155, 379, 112, 481, 1341, - /* 140 */ 456, 299, 1341, 134, 134, 134, 134, 133, 133, 132, - /* 150 */ 132, 132, 131, 128, 451, 137, 138, 91, 498, 1228, - /* 160 */ 1228, 1063, 1066, 1053, 1053, 135, 135, 136, 136, 136, - /* 170 */ 136, 1204, 862, 1281, 288, 288, 283, 288, 288, 523, - /* 180 */ 523, 1250, 139, 578, 7, 578, 1345, 573, 1169, 562, - /* 190 */ 573, 1054, 562, 136, 136, 136, 136, 129, 573, 547, - /* 200 */ 562, 1169, 245, 1541, 1169, 245, 133, 133, 132, 132, - /* 210 */ 132, 131, 128, 451, 302, 134, 134, 134, 134, 133, - /* 220 */ 133, 132, 132, 132, 131, 128, 451, 1575, 1204, 1205, - /* 230 */ 1204, 7, 470, 550, 455, 413, 550, 455, 130, 127, + /* 0 */ 130, 127, 234, 282, 282, 1332, 580, 1311, 464, 289, + /* 10 */ 289, 580, 1626, 385, 580, 1332, 577, 580, 566, 417, + /* 20 */ 1304, 1546, 577, 485, 566, 528, 464, 463, 562, 82, + /* 30 */ 82, 987, 294, 379, 51, 51, 502, 61, 61, 988, + /* 40 */ 82, 82, 1581, 137, 138, 91, 7, 1232, 1232, 1067, + /* 50 */ 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, 417, + /* 60 */ 288, 288, 182, 288, 288, 485, 540, 288, 288, 130, + /* 70 */ 127, 234, 436, 577, 529, 566, 577, 561, 566, 1294, + /* 80 */ 577, 425, 566, 137, 138, 91, 563, 1232, 1232, 1067, + /* 90 */ 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, 296, + /* 100 */ 464, 402, 1253, 134, 134, 134, 134, 133, 133, 132, + /* 110 */ 132, 132, 131, 128, 455, 455, 1054, 1054, 1068, 1071, + /* 120 */ 1259, 1, 1, 586, 2, 1263, 585, 1178, 1263, 1178, + /* 130 */ 321, 417, 155, 321, 1588, 155, 383, 112, 485, 1345, + /* 140 */ 460, 299, 1345, 134, 134, 134, 134, 133, 133, 132, + /* 150 */ 132, 132, 131, 128, 455, 137, 138, 91, 502, 1232, + /* 160 */ 1232, 1067, 1070, 1057, 1057, 135, 135, 136, 136, 136, + /* 170 */ 136, 1208, 866, 1285, 288, 288, 283, 288, 288, 527, + /* 180 */ 527, 1254, 139, 582, 7, 582, 1349, 577, 1173, 566, + /* 190 */ 577, 1058, 566, 136, 136, 136, 136, 129, 577, 551, + /* 200 */ 566, 1173, 245, 1545, 1173, 245, 133, 133, 132, 132, + /* 210 */ 132, 131, 128, 455, 302, 134, 134, 134, 134, 133, + /* 220 */ 133, 132, 132, 132, 131, 128, 455, 1579, 1208, 1209, + /* 230 */ 1208, 7, 474, 554, 459, 417, 554, 459, 130, 127, /* 240 */ 234, 134, 134, 134, 134, 133, 133, 132, 132, 132, - /* 250 */ 131, 128, 451, 136, 136, 136, 136, 538, 483, 137, - /* 260 */ 138, 91, 1019, 1228, 1228, 1063, 1066, 1053, 1053, 135, - /* 270 */ 135, 136, 136, 136, 136, 1085, 576, 1204, 132, 132, - /* 280 */ 132, 131, 128, 451, 93, 214, 134, 134, 134, 134, - /* 290 */ 133, 133, 132, 132, 132, 131, 128, 451, 401, 19, + /* 250 */ 131, 128, 455, 136, 136, 136, 136, 542, 487, 137, + /* 260 */ 138, 91, 1023, 1232, 1232, 1067, 1070, 1057, 1057, 135, + /* 270 */ 135, 136, 136, 136, 136, 1089, 580, 1208, 132, 132, + /* 280 */ 132, 131, 128, 455, 93, 214, 134, 134, 134, 134, + /* 290 */ 133, 133, 132, 132, 132, 131, 128, 455, 405, 19, /* 300 */ 19, 134, 134, 134, 134, 133, 133, 132, 132, 132, - /* 310 */ 131, 128, 451, 1498, 426, 267, 344, 467, 332, 134, + /* 310 */ 131, 128, 455, 1502, 430, 267, 348, 471, 334, 134, /* 320 */ 134, 134, 134, 133, 133, 132, 132, 132, 131, 128, - /* 330 */ 451, 1281, 576, 6, 1204, 1205, 1204, 257, 576, 413, - /* 340 */ 511, 508, 507, 1279, 94, 1019, 464, 1204, 551, 551, - /* 350 */ 506, 1224, 1571, 44, 38, 51, 51, 411, 576, 413, - /* 360 */ 45, 51, 51, 137, 138, 91, 530, 1228, 1228, 1063, - /* 370 */ 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, 398, - /* 380 */ 1148, 82, 82, 137, 138, 91, 39, 1228, 1228, 1063, - /* 390 */ 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, 344, - /* 400 */ 44, 288, 288, 375, 1204, 1205, 1204, 209, 1204, 1224, - /* 410 */ 320, 567, 471, 576, 573, 576, 562, 576, 316, 264, + /* 330 */ 455, 1285, 580, 6, 1208, 1209, 1208, 257, 580, 417, + /* 340 */ 515, 512, 511, 1283, 94, 1023, 468, 1208, 555, 555, + /* 350 */ 510, 1228, 1575, 44, 38, 51, 51, 415, 580, 417, + /* 360 */ 45, 51, 51, 137, 138, 91, 534, 1232, 1232, 1067, + /* 370 */ 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, 402, + /* 380 */ 1152, 82, 82, 137, 138, 91, 39, 1232, 1232, 1067, + /* 390 */ 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, 348, + /* 400 */ 44, 288, 288, 379, 1208, 1209, 1208, 209, 1208, 1228, + /* 410 */ 320, 571, 475, 580, 577, 580, 566, 580, 316, 264, /* 420 */ 231, 46, 160, 134, 134, 134, 134, 133, 133, 132, - /* 430 */ 132, 132, 131, 128, 451, 303, 82, 82, 82, 82, - /* 440 */ 82, 82, 442, 134, 134, 134, 134, 133, 133, 132, - /* 450 */ 132, 132, 131, 128, 451, 1582, 544, 320, 567, 1250, - /* 460 */ 874, 1582, 380, 382, 413, 1204, 1205, 1204, 360, 182, - /* 470 */ 288, 288, 1576, 557, 1339, 557, 7, 557, 1277, 472, - /* 480 */ 346, 526, 531, 573, 556, 562, 439, 1511, 137, 138, - /* 490 */ 91, 219, 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, - /* 500 */ 136, 136, 136, 136, 465, 1511, 1513, 532, 413, 288, - /* 510 */ 288, 423, 512, 288, 288, 411, 288, 288, 874, 130, - /* 520 */ 127, 234, 573, 1107, 562, 1204, 573, 1107, 562, 573, - /* 530 */ 560, 562, 137, 138, 91, 1293, 1228, 1228, 1063, 1066, - /* 540 */ 1053, 1053, 135, 135, 136, 136, 136, 136, 134, 134, - /* 550 */ 134, 134, 133, 133, 132, 132, 132, 131, 128, 451, - /* 560 */ 493, 503, 1292, 1204, 257, 288, 288, 511, 508, 507, - /* 570 */ 1204, 1628, 1169, 123, 568, 275, 4, 506, 573, 1511, - /* 580 */ 562, 331, 1204, 1205, 1204, 1169, 548, 548, 1169, 261, - /* 590 */ 571, 7, 134, 134, 134, 134, 133, 133, 132, 132, - /* 600 */ 132, 131, 128, 451, 108, 533, 130, 127, 234, 1204, - /* 610 */ 448, 447, 413, 1451, 452, 983, 886, 96, 1598, 1233, - /* 620 */ 1204, 1205, 1204, 984, 1235, 1450, 565, 1204, 1205, 1204, - /* 630 */ 229, 522, 1234, 534, 1333, 1333, 137, 138, 91, 1449, - /* 640 */ 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, 136, 136, - /* 650 */ 136, 136, 373, 1595, 971, 1040, 413, 1236, 418, 1236, - /* 660 */ 879, 121, 121, 948, 373, 1595, 1204, 1205, 1204, 122, - /* 670 */ 1204, 452, 577, 452, 363, 417, 1028, 882, 373, 1595, - /* 680 */ 137, 138, 91, 462, 1228, 1228, 1063, 1066, 1053, 1053, + /* 430 */ 132, 132, 131, 128, 455, 303, 82, 82, 82, 82, + /* 440 */ 82, 82, 446, 134, 134, 134, 134, 133, 133, 132, + /* 450 */ 132, 132, 131, 128, 455, 1586, 548, 320, 571, 1254, + /* 460 */ 878, 1586, 384, 386, 417, 1208, 1209, 1208, 364, 182, + /* 470 */ 288, 288, 1580, 561, 1343, 561, 7, 561, 1281, 476, + /* 480 */ 350, 530, 535, 577, 560, 566, 443, 1515, 137, 138, + /* 490 */ 91, 219, 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, + /* 500 */ 136, 136, 136, 136, 469, 1515, 1517, 536, 417, 288, + /* 510 */ 288, 427, 516, 288, 288, 415, 288, 288, 878, 130, + /* 520 */ 127, 234, 577, 1111, 566, 1208, 577, 1111, 566, 577, + /* 530 */ 564, 566, 137, 138, 91, 1297, 1232, 1232, 1067, 1070, + /* 540 */ 1057, 1057, 135, 135, 136, 136, 136, 136, 134, 134, + /* 550 */ 134, 134, 133, 133, 132, 132, 132, 131, 128, 455, + /* 560 */ 497, 507, 1296, 1208, 257, 288, 288, 515, 512, 511, + /* 570 */ 1208, 1632, 1173, 123, 572, 275, 4, 510, 577, 1515, + /* 580 */ 566, 331, 1208, 1209, 1208, 1173, 552, 552, 1173, 261, + /* 590 */ 575, 7, 134, 134, 134, 134, 133, 133, 132, 132, + /* 600 */ 132, 131, 128, 455, 108, 537, 130, 127, 234, 1208, + /* 610 */ 452, 451, 417, 1455, 456, 987, 890, 96, 1602, 1237, + /* 620 */ 1208, 1209, 1208, 988, 1239, 1454, 569, 1208, 1209, 1208, + /* 630 */ 229, 526, 1238, 538, 1337, 1337, 137, 138, 91, 1453, + /* 640 */ 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, 136, 136, + /* 650 */ 136, 136, 377, 1599, 975, 1044, 417, 1240, 422, 1240, + /* 660 */ 883, 121, 121, 952, 377, 1599, 1208, 1209, 1208, 122, + /* 670 */ 1208, 456, 581, 456, 367, 421, 1032, 886, 377, 1599, + /* 680 */ 137, 138, 91, 466, 1232, 1232, 1067, 1070, 1057, 1057, /* 690 */ 135, 135, 136, 136, 136, 136, 134, 134, 134, 134, - /* 700 */ 133, 133, 132, 132, 132, 131, 128, 451, 1028, 1028, - /* 710 */ 1030, 1031, 35, 570, 570, 570, 197, 423, 1040, 198, - /* 720 */ 1204, 123, 568, 1204, 4, 320, 567, 1204, 1205, 1204, - /* 730 */ 40, 388, 576, 384, 882, 1029, 423, 1188, 571, 1028, + /* 700 */ 133, 133, 132, 132, 132, 131, 128, 455, 1032, 1032, + /* 710 */ 1034, 1035, 35, 574, 574, 574, 197, 427, 1044, 198, + /* 720 */ 1208, 123, 572, 1208, 4, 320, 571, 1208, 1209, 1208, + /* 730 */ 40, 392, 580, 388, 886, 1033, 427, 1192, 575, 1032, /* 740 */ 134, 134, 134, 134, 133, 133, 132, 132, 132, 131, - /* 750 */ 128, 451, 529, 1568, 1204, 19, 19, 1204, 575, 492, - /* 760 */ 413, 157, 452, 489, 1187, 1331, 1331, 5, 1204, 949, - /* 770 */ 431, 1028, 1028, 1030, 565, 22, 22, 1204, 1205, 1204, - /* 780 */ 1204, 1205, 1204, 477, 137, 138, 91, 212, 1228, 1228, - /* 790 */ 1063, 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, - /* 800 */ 1188, 48, 111, 1040, 413, 1204, 213, 970, 1041, 121, - /* 810 */ 121, 1204, 1205, 1204, 1204, 1205, 1204, 122, 221, 452, - /* 820 */ 577, 452, 44, 487, 1028, 1204, 1205, 1204, 137, 138, - /* 830 */ 91, 378, 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, + /* 750 */ 128, 455, 533, 1572, 1208, 19, 19, 1208, 579, 496, + /* 760 */ 417, 157, 456, 493, 1191, 1335, 1335, 5, 1208, 953, + /* 770 */ 435, 1032, 1032, 1034, 569, 22, 22, 1208, 1209, 1208, + /* 780 */ 1208, 1209, 1208, 481, 137, 138, 91, 212, 1232, 1232, + /* 790 */ 1067, 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, + /* 800 */ 1192, 48, 111, 1044, 417, 1208, 213, 974, 1045, 121, + /* 810 */ 121, 1208, 1209, 1208, 1208, 1209, 1208, 122, 221, 456, + /* 820 */ 581, 456, 44, 491, 1032, 1208, 1209, 1208, 137, 138, + /* 830 */ 91, 382, 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, /* 840 */ 136, 136, 136, 136, 134, 134, 134, 134, 133, 133, - /* 850 */ 132, 132, 132, 131, 128, 451, 1028, 1028, 1030, 1031, - /* 860 */ 35, 461, 1204, 1205, 1204, 1569, 1040, 377, 214, 1149, - /* 870 */ 1657, 535, 1657, 437, 902, 320, 567, 1568, 364, 320, - /* 880 */ 567, 412, 329, 1029, 519, 1188, 3, 1028, 134, 134, - /* 890 */ 134, 134, 133, 133, 132, 132, 132, 131, 128, 451, - /* 900 */ 1659, 399, 1169, 307, 893, 307, 515, 576, 413, 214, - /* 910 */ 498, 944, 1024, 540, 903, 1169, 943, 392, 1169, 1028, - /* 920 */ 1028, 1030, 406, 298, 1204, 50, 1149, 1658, 413, 1658, - /* 930 */ 145, 145, 137, 138, 91, 293, 1228, 1228, 1063, 1066, - /* 940 */ 1053, 1053, 135, 135, 136, 136, 136, 136, 1188, 1147, - /* 950 */ 514, 1568, 137, 138, 91, 1505, 1228, 1228, 1063, 1066, - /* 960 */ 1053, 1053, 135, 135, 136, 136, 136, 136, 434, 323, - /* 970 */ 435, 539, 111, 1506, 274, 291, 372, 517, 367, 516, - /* 980 */ 262, 1204, 1205, 1204, 1574, 481, 363, 576, 7, 1569, - /* 990 */ 1568, 377, 134, 134, 134, 134, 133, 133, 132, 132, - /* 1000 */ 132, 131, 128, 451, 1568, 576, 1147, 576, 232, 576, + /* 850 */ 132, 132, 132, 131, 128, 455, 1032, 1032, 1034, 1035, + /* 860 */ 35, 465, 1208, 1209, 1208, 1573, 1044, 381, 214, 1153, + /* 870 */ 1661, 539, 1661, 441, 906, 320, 571, 1572, 368, 320, + /* 880 */ 571, 416, 329, 1033, 523, 1192, 3, 1032, 134, 134, + /* 890 */ 134, 134, 133, 133, 132, 132, 132, 131, 128, 455, + /* 900 */ 1663, 403, 1173, 307, 897, 307, 519, 580, 417, 214, + /* 910 */ 502, 948, 1028, 544, 907, 1173, 947, 396, 1173, 1032, + /* 920 */ 1032, 1034, 410, 298, 1208, 50, 1153, 1662, 417, 1662, + /* 930 */ 145, 145, 137, 138, 91, 293, 1232, 1232, 1067, 1070, + /* 940 */ 1057, 1057, 135, 135, 136, 136, 136, 136, 1192, 1151, + /* 950 */ 518, 1572, 137, 138, 91, 1509, 1232, 1232, 1067, 1070, + /* 960 */ 1057, 1057, 135, 135, 136, 136, 136, 136, 438, 323, + /* 970 */ 439, 543, 111, 1510, 274, 291, 376, 521, 371, 520, + /* 980 */ 262, 1208, 1209, 1208, 1578, 485, 367, 580, 7, 1573, + /* 990 */ 1572, 381, 134, 134, 134, 134, 133, 133, 132, 132, + /* 1000 */ 132, 131, 128, 455, 1572, 580, 1151, 580, 232, 580, /* 1010 */ 19, 19, 134, 134, 134, 134, 133, 133, 132, 132, - /* 1020 */ 132, 131, 128, 451, 1169, 433, 576, 1207, 19, 19, - /* 1030 */ 19, 19, 19, 19, 1627, 576, 911, 1169, 47, 120, - /* 1040 */ 1169, 117, 413, 306, 498, 438, 1125, 206, 336, 19, - /* 1050 */ 19, 1435, 49, 449, 449, 449, 1368, 315, 81, 81, - /* 1060 */ 576, 304, 413, 1570, 207, 377, 137, 138, 91, 115, - /* 1070 */ 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, 136, 136, - /* 1080 */ 136, 136, 576, 82, 82, 1207, 137, 138, 91, 1340, - /* 1090 */ 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, 136, 136, - /* 1100 */ 136, 136, 1569, 386, 377, 82, 82, 463, 1126, 1552, - /* 1110 */ 333, 463, 335, 131, 128, 451, 1569, 161, 377, 16, - /* 1120 */ 317, 387, 428, 1127, 448, 447, 134, 134, 134, 134, - /* 1130 */ 133, 133, 132, 132, 132, 131, 128, 451, 1128, 576, - /* 1140 */ 1105, 10, 445, 267, 576, 1554, 134, 134, 134, 134, - /* 1150 */ 133, 133, 132, 132, 132, 131, 128, 451, 532, 576, - /* 1160 */ 922, 576, 19, 19, 576, 1573, 576, 147, 147, 7, - /* 1170 */ 923, 1236, 498, 1236, 576, 487, 413, 552, 285, 1224, - /* 1180 */ 969, 215, 82, 82, 66, 66, 1435, 67, 67, 21, - /* 1190 */ 21, 1110, 1110, 495, 334, 297, 413, 53, 53, 297, - /* 1200 */ 137, 138, 91, 119, 1228, 1228, 1063, 1066, 1053, 1053, - /* 1210 */ 135, 135, 136, 136, 136, 136, 413, 1336, 1311, 446, - /* 1220 */ 137, 138, 91, 227, 1228, 1228, 1063, 1066, 1053, 1053, - /* 1230 */ 135, 135, 136, 136, 136, 136, 574, 1224, 936, 936, - /* 1240 */ 137, 126, 91, 141, 1228, 1228, 1063, 1066, 1053, 1053, - /* 1250 */ 135, 135, 136, 136, 136, 136, 533, 429, 472, 346, + /* 1020 */ 132, 131, 128, 455, 1173, 437, 580, 1211, 19, 19, + /* 1030 */ 19, 19, 19, 19, 1631, 580, 915, 1173, 47, 120, + /* 1040 */ 1173, 117, 417, 306, 502, 442, 1129, 206, 340, 19, + /* 1050 */ 19, 1439, 49, 453, 453, 453, 1372, 315, 81, 81, + /* 1060 */ 580, 304, 417, 1574, 207, 381, 137, 138, 91, 115, + /* 1070 */ 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, 136, 136, + /* 1080 */ 136, 136, 580, 82, 82, 1211, 137, 138, 91, 1344, + /* 1090 */ 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, 136, 136, + /* 1100 */ 136, 136, 1573, 390, 381, 82, 82, 467, 1130, 1556, + /* 1110 */ 337, 467, 339, 131, 128, 455, 1573, 161, 381, 16, + /* 1120 */ 317, 391, 432, 1131, 452, 451, 134, 134, 134, 134, + /* 1130 */ 133, 133, 132, 132, 132, 131, 128, 455, 1132, 580, + /* 1140 */ 1109, 10, 449, 267, 580, 1558, 134, 134, 134, 134, + /* 1150 */ 133, 133, 132, 132, 132, 131, 128, 455, 536, 580, + /* 1160 */ 926, 580, 19, 19, 580, 1577, 580, 147, 147, 7, + /* 1170 */ 927, 1240, 502, 1240, 580, 491, 417, 556, 285, 1228, + /* 1180 */ 973, 215, 82, 82, 66, 66, 1439, 67, 67, 21, + /* 1190 */ 21, 1114, 1114, 499, 338, 297, 417, 53, 53, 297, + /* 1200 */ 137, 138, 91, 119, 1232, 1232, 1067, 1070, 1057, 1057, + /* 1210 */ 135, 135, 136, 136, 136, 136, 417, 1340, 1315, 450, + /* 1220 */ 137, 138, 91, 227, 1232, 1232, 1067, 1070, 1057, 1057, + /* 1230 */ 135, 135, 136, 136, 136, 136, 578, 1228, 940, 940, + /* 1240 */ 137, 126, 91, 141, 1232, 1232, 1067, 1070, 1057, 1057, + /* 1250 */ 135, 135, 136, 136, 136, 136, 537, 433, 476, 350, /* 1260 */ 134, 134, 134, 134, 133, 133, 132, 132, 132, 131, - /* 1270 */ 128, 451, 576, 457, 233, 343, 1435, 403, 498, 1550, + /* 1270 */ 128, 455, 580, 461, 233, 347, 1439, 407, 502, 1554, /* 1280 */ 134, 134, 134, 134, 133, 133, 132, 132, 132, 131, - /* 1290 */ 128, 451, 576, 324, 576, 82, 82, 487, 576, 969, + /* 1290 */ 128, 455, 580, 324, 580, 82, 82, 491, 580, 973, /* 1300 */ 134, 134, 134, 134, 133, 133, 132, 132, 132, 131, - /* 1310 */ 128, 451, 288, 288, 546, 68, 68, 54, 54, 553, - /* 1320 */ 413, 69, 69, 351, 6, 573, 944, 562, 410, 409, - /* 1330 */ 1435, 943, 450, 545, 260, 259, 258, 576, 158, 576, - /* 1340 */ 413, 222, 1180, 479, 969, 138, 91, 430, 1228, 1228, - /* 1350 */ 1063, 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, - /* 1360 */ 70, 70, 71, 71, 576, 1126, 91, 576, 1228, 1228, - /* 1370 */ 1063, 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, - /* 1380 */ 1127, 166, 850, 851, 852, 1282, 419, 72, 72, 108, - /* 1390 */ 73, 73, 1310, 358, 1180, 1128, 576, 305, 576, 123, - /* 1400 */ 568, 494, 4, 488, 134, 134, 134, 134, 133, 133, - /* 1410 */ 132, 132, 132, 131, 128, 451, 571, 564, 534, 55, - /* 1420 */ 55, 56, 56, 576, 134, 134, 134, 134, 133, 133, - /* 1430 */ 132, 132, 132, 131, 128, 451, 576, 1104, 233, 1104, - /* 1440 */ 452, 1602, 582, 2, 1259, 576, 57, 57, 576, 321, - /* 1450 */ 576, 155, 565, 1435, 485, 353, 576, 356, 1341, 59, - /* 1460 */ 59, 576, 44, 969, 569, 419, 576, 238, 60, 60, - /* 1470 */ 261, 74, 74, 75, 75, 287, 231, 576, 1366, 76, - /* 1480 */ 76, 1040, 420, 184, 20, 20, 576, 121, 121, 77, - /* 1490 */ 77, 97, 218, 288, 288, 122, 125, 452, 577, 452, - /* 1500 */ 143, 143, 1028, 576, 520, 576, 573, 576, 562, 144, - /* 1510 */ 144, 474, 227, 1244, 478, 123, 568, 576, 4, 320, - /* 1520 */ 567, 245, 411, 576, 443, 411, 78, 78, 62, 62, - /* 1530 */ 79, 79, 571, 319, 1028, 1028, 1030, 1031, 35, 418, - /* 1540 */ 63, 63, 576, 290, 411, 9, 80, 80, 1144, 576, - /* 1550 */ 400, 576, 486, 455, 576, 1223, 452, 576, 325, 342, - /* 1560 */ 576, 111, 576, 1188, 242, 64, 64, 473, 565, 576, - /* 1570 */ 23, 576, 170, 170, 171, 171, 576, 87, 87, 328, - /* 1580 */ 65, 65, 542, 83, 83, 146, 146, 541, 123, 568, - /* 1590 */ 341, 4, 84, 84, 168, 168, 576, 1040, 576, 148, - /* 1600 */ 148, 576, 1380, 121, 121, 571, 1021, 576, 266, 576, - /* 1610 */ 424, 122, 576, 452, 577, 452, 576, 553, 1028, 142, - /* 1620 */ 142, 169, 169, 576, 162, 162, 528, 889, 371, 452, - /* 1630 */ 152, 152, 151, 151, 1379, 149, 149, 109, 370, 150, - /* 1640 */ 150, 565, 576, 480, 576, 266, 86, 86, 576, 1092, - /* 1650 */ 1028, 1028, 1030, 1031, 35, 542, 482, 576, 266, 466, - /* 1660 */ 543, 123, 568, 1616, 4, 88, 88, 85, 85, 475, - /* 1670 */ 1040, 52, 52, 222, 901, 900, 121, 121, 571, 1188, - /* 1680 */ 58, 58, 244, 1032, 122, 889, 452, 577, 452, 908, - /* 1690 */ 909, 1028, 300, 347, 504, 111, 263, 361, 165, 111, - /* 1700 */ 111, 1088, 452, 263, 974, 1153, 266, 1092, 986, 987, - /* 1710 */ 942, 939, 125, 125, 565, 1103, 872, 1103, 159, 941, - /* 1720 */ 1309, 125, 1557, 1028, 1028, 1030, 1031, 35, 542, 337, - /* 1730 */ 1530, 205, 1529, 541, 499, 1589, 490, 348, 1376, 352, - /* 1740 */ 355, 1032, 357, 1040, 359, 1324, 1308, 366, 563, 121, - /* 1750 */ 121, 376, 1188, 1389, 1434, 1362, 280, 122, 1374, 452, - /* 1760 */ 577, 452, 167, 1439, 1028, 1289, 1280, 1268, 1267, 1269, - /* 1770 */ 1609, 1359, 312, 313, 314, 397, 12, 237, 224, 1421, - /* 1780 */ 295, 1416, 1409, 1426, 339, 484, 340, 509, 1371, 1612, - /* 1790 */ 1372, 1425, 1244, 404, 301, 228, 1028, 1028, 1030, 1031, - /* 1800 */ 35, 1601, 1192, 454, 345, 1307, 292, 369, 1502, 1501, - /* 1810 */ 270, 396, 396, 395, 277, 393, 1370, 1369, 859, 1549, - /* 1820 */ 186, 123, 568, 235, 4, 1188, 391, 210, 211, 223, - /* 1830 */ 1547, 239, 1241, 327, 422, 96, 220, 195, 571, 180, - /* 1840 */ 188, 326, 468, 469, 190, 191, 502, 192, 193, 566, - /* 1850 */ 247, 109, 1430, 491, 199, 251, 102, 281, 402, 476, - /* 1860 */ 405, 1496, 452, 497, 253, 1422, 13, 1428, 14, 1427, - /* 1870 */ 203, 1507, 241, 500, 565, 354, 407, 92, 95, 1270, - /* 1880 */ 175, 254, 518, 43, 1327, 255, 1326, 1325, 436, 1518, - /* 1890 */ 350, 1318, 104, 229, 893, 1626, 440, 441, 1625, 408, - /* 1900 */ 240, 1296, 268, 1040, 310, 269, 1297, 527, 444, 121, - /* 1910 */ 121, 368, 1295, 1594, 1624, 311, 1394, 122, 1317, 452, - /* 1920 */ 577, 452, 374, 1580, 1028, 1393, 140, 553, 11, 90, - /* 1930 */ 568, 385, 4, 116, 318, 414, 1579, 110, 1483, 537, - /* 1940 */ 320, 567, 1350, 555, 42, 579, 571, 1349, 1198, 383, - /* 1950 */ 276, 390, 216, 389, 278, 279, 1028, 1028, 1030, 1031, - /* 1960 */ 35, 172, 580, 1265, 458, 1260, 415, 416, 185, 156, - /* 1970 */ 452, 1534, 1535, 173, 1533, 1532, 89, 308, 225, 226, - /* 1980 */ 846, 174, 565, 453, 217, 1188, 322, 236, 1102, 154, - /* 1990 */ 1100, 330, 187, 176, 1223, 243, 189, 925, 338, 246, - /* 2000 */ 1116, 194, 177, 425, 178, 427, 98, 196, 99, 100, - /* 2010 */ 101, 1040, 179, 1119, 1115, 248, 249, 121, 121, 163, - /* 2020 */ 24, 250, 349, 1238, 496, 122, 1108, 452, 577, 452, - /* 2030 */ 1192, 454, 1028, 266, 292, 200, 252, 201, 861, 396, - /* 2040 */ 396, 395, 277, 393, 15, 501, 859, 370, 292, 256, - /* 2050 */ 202, 554, 505, 396, 396, 395, 277, 393, 103, 239, - /* 2060 */ 859, 327, 25, 26, 1028, 1028, 1030, 1031, 35, 326, - /* 2070 */ 362, 510, 891, 239, 365, 327, 513, 904, 105, 309, - /* 2080 */ 164, 181, 27, 326, 106, 521, 107, 1185, 1069, 1155, - /* 2090 */ 17, 1154, 230, 1188, 284, 286, 265, 204, 125, 1171, - /* 2100 */ 241, 28, 978, 972, 29, 41, 1175, 1179, 175, 1173, - /* 2110 */ 30, 43, 31, 8, 241, 1178, 32, 1160, 208, 549, - /* 2120 */ 33, 111, 175, 1083, 1070, 43, 1068, 1072, 240, 113, - /* 2130 */ 114, 34, 561, 118, 1124, 271, 1073, 36, 18, 572, - /* 2140 */ 1033, 873, 240, 124, 37, 935, 272, 273, 1617, 183, - /* 2150 */ 153, 394, 1194, 1193, 1256, 1256, 1256, 1256, 1256, 1256, - /* 2160 */ 1256, 1256, 1256, 414, 1256, 1256, 1256, 1256, 320, 567, - /* 2170 */ 1256, 1256, 1256, 1256, 1256, 1256, 1256, 414, 1256, 1256, - /* 2180 */ 1256, 1256, 320, 567, 1256, 1256, 1256, 1256, 1256, 1256, - /* 2190 */ 1256, 1256, 458, 1256, 1256, 1256, 1256, 1256, 1256, 1256, - /* 2200 */ 1256, 1256, 1256, 1256, 1256, 1256, 458, + /* 1310 */ 128, 455, 288, 288, 550, 68, 68, 54, 54, 557, + /* 1320 */ 417, 69, 69, 355, 6, 577, 948, 566, 414, 413, + /* 1330 */ 1439, 947, 454, 549, 260, 259, 258, 580, 158, 580, + /* 1340 */ 417, 222, 1184, 483, 973, 138, 91, 434, 1232, 1232, + /* 1350 */ 1067, 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, + /* 1360 */ 70, 70, 71, 71, 580, 1130, 91, 580, 1232, 1232, + /* 1370 */ 1067, 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, + /* 1380 */ 1131, 166, 854, 855, 856, 1286, 423, 72, 72, 108, + /* 1390 */ 73, 73, 1314, 362, 1184, 1132, 580, 305, 580, 123, + /* 1400 */ 572, 498, 4, 492, 134, 134, 134, 134, 133, 133, + /* 1410 */ 132, 132, 132, 131, 128, 455, 575, 568, 538, 55, + /* 1420 */ 55, 56, 56, 580, 134, 134, 134, 134, 133, 133, + /* 1430 */ 132, 132, 132, 131, 128, 455, 580, 1108, 233, 1108, + /* 1440 */ 456, 1606, 586, 2, 1263, 580, 57, 57, 580, 321, + /* 1450 */ 580, 155, 569, 1439, 489, 357, 580, 360, 1345, 59, + /* 1460 */ 59, 580, 44, 973, 573, 423, 580, 238, 60, 60, + /* 1470 */ 261, 74, 74, 75, 75, 287, 231, 580, 1370, 76, + /* 1480 */ 76, 1044, 424, 184, 20, 20, 580, 121, 121, 77, + /* 1490 */ 77, 97, 218, 288, 288, 122, 125, 456, 581, 456, + /* 1500 */ 143, 143, 1032, 580, 524, 580, 577, 580, 566, 144, + /* 1510 */ 144, 478, 227, 1248, 482, 123, 572, 580, 4, 320, + /* 1520 */ 571, 245, 415, 580, 447, 415, 78, 78, 62, 62, + /* 1530 */ 79, 79, 575, 319, 1032, 1032, 1034, 1035, 35, 422, + /* 1540 */ 63, 63, 580, 290, 415, 9, 80, 80, 1148, 580, + /* 1550 */ 404, 580, 490, 459, 580, 1227, 456, 580, 325, 346, + /* 1560 */ 580, 111, 580, 1192, 242, 64, 64, 477, 569, 580, + /* 1570 */ 23, 580, 170, 170, 171, 171, 580, 87, 87, 328, + /* 1580 */ 65, 65, 546, 83, 83, 146, 146, 545, 123, 572, + /* 1590 */ 345, 4, 84, 84, 168, 168, 580, 1044, 580, 148, + /* 1600 */ 148, 580, 1384, 121, 121, 575, 1025, 580, 266, 580, + /* 1610 */ 428, 122, 580, 456, 581, 456, 580, 557, 1032, 142, + /* 1620 */ 142, 169, 169, 580, 162, 162, 532, 893, 375, 456, + /* 1630 */ 152, 152, 151, 151, 1383, 149, 149, 109, 374, 150, + /* 1640 */ 150, 569, 580, 484, 580, 266, 86, 86, 580, 1096, + /* 1650 */ 1032, 1032, 1034, 1035, 35, 546, 486, 580, 266, 470, + /* 1660 */ 547, 123, 572, 1620, 4, 88, 88, 85, 85, 479, + /* 1670 */ 1044, 52, 52, 222, 905, 904, 121, 121, 575, 1192, + /* 1680 */ 58, 58, 244, 1036, 122, 893, 456, 581, 456, 912, + /* 1690 */ 913, 1032, 300, 351, 508, 111, 263, 365, 165, 111, + /* 1700 */ 111, 1092, 456, 263, 978, 1157, 266, 1096, 990, 991, + /* 1710 */ 946, 943, 125, 125, 569, 1107, 876, 1107, 159, 945, + /* 1720 */ 1313, 125, 1561, 1032, 1032, 1034, 1035, 35, 546, 341, + /* 1730 */ 1534, 205, 1533, 545, 503, 1593, 494, 352, 1380, 356, + /* 1740 */ 359, 1036, 361, 1044, 363, 1328, 1312, 370, 567, 121, + /* 1750 */ 121, 380, 1192, 1393, 1438, 1366, 280, 122, 1378, 456, + /* 1760 */ 581, 456, 167, 1443, 1032, 1293, 1284, 1272, 1271, 1273, + /* 1770 */ 1613, 1363, 312, 313, 314, 401, 12, 237, 224, 1425, + /* 1780 */ 295, 333, 336, 1430, 343, 488, 344, 513, 1375, 1616, + /* 1790 */ 1376, 1429, 1248, 408, 301, 228, 1032, 1032, 1034, 1035, + /* 1800 */ 35, 1605, 1196, 458, 349, 1311, 292, 373, 1506, 1505, + /* 1810 */ 270, 400, 400, 399, 277, 397, 1374, 1373, 863, 1553, + /* 1820 */ 186, 123, 572, 235, 4, 1192, 395, 210, 211, 223, + /* 1830 */ 1551, 239, 1245, 327, 426, 96, 220, 195, 575, 140, + /* 1840 */ 557, 326, 180, 472, 1420, 332, 188, 1413, 335, 570, + /* 1850 */ 190, 191, 192, 193, 473, 506, 247, 109, 1434, 495, + /* 1860 */ 251, 199, 456, 406, 480, 1426, 13, 409, 102, 14, + /* 1870 */ 501, 1511, 241, 1432, 569, 1431, 203, 92, 95, 1500, + /* 1880 */ 175, 281, 358, 43, 504, 253, 411, 254, 522, 1331, + /* 1890 */ 104, 1274, 1522, 354, 440, 1322, 255, 1330, 1630, 1629, + /* 1900 */ 240, 897, 229, 1044, 444, 1321, 445, 448, 269, 121, + /* 1910 */ 121, 1329, 531, 268, 310, 412, 1398, 122, 1301, 456, + /* 1920 */ 581, 456, 1300, 372, 1032, 1299, 1628, 378, 311, 90, + /* 1930 */ 572, 11, 4, 1397, 1598, 418, 1487, 389, 116, 110, + /* 1940 */ 320, 571, 1584, 559, 318, 541, 575, 1583, 42, 1354, + /* 1950 */ 387, 583, 216, 1353, 1202, 276, 1032, 1032, 1034, 1035, + /* 1960 */ 35, 393, 394, 278, 462, 279, 419, 584, 1538, 1269, + /* 1970 */ 456, 1264, 172, 420, 173, 1539, 1537, 1536, 156, 308, + /* 1980 */ 225, 89, 569, 850, 226, 1192, 457, 174, 217, 236, + /* 1990 */ 322, 154, 1106, 1104, 187, 330, 176, 1227, 929, 189, + /* 2000 */ 243, 342, 246, 1120, 194, 177, 178, 429, 431, 98, + /* 2010 */ 196, 1044, 99, 185, 100, 101, 179, 121, 121, 1123, + /* 2020 */ 248, 249, 1119, 163, 250, 122, 24, 456, 581, 456, + /* 2030 */ 1196, 458, 1032, 353, 292, 266, 1112, 200, 1242, 400, + /* 2040 */ 400, 399, 277, 397, 500, 252, 863, 201, 292, 15, + /* 2050 */ 865, 558, 505, 400, 400, 399, 277, 397, 374, 239, + /* 2060 */ 863, 327, 256, 202, 1032, 1032, 1034, 1035, 35, 326, + /* 2070 */ 103, 25, 26, 239, 509, 327, 366, 514, 369, 895, + /* 2080 */ 908, 517, 105, 326, 309, 164, 525, 106, 181, 27, + /* 2090 */ 1189, 1073, 17, 1192, 107, 1159, 1158, 284, 230, 286, + /* 2100 */ 241, 204, 125, 1175, 982, 265, 1182, 976, 175, 28, + /* 2110 */ 1179, 43, 29, 1177, 241, 30, 31, 8, 1183, 32, + /* 2120 */ 1164, 41, 175, 208, 553, 43, 111, 33, 240, 1087, + /* 2130 */ 1074, 113, 114, 1072, 1076, 34, 1077, 565, 1128, 118, + /* 2140 */ 271, 36, 240, 18, 939, 1037, 877, 272, 124, 37, + /* 2150 */ 398, 1198, 1197, 576, 183, 273, 153, 1621, 1260, 1260, + /* 2160 */ 1260, 1260, 1260, 418, 1260, 1260, 1260, 1260, 320, 571, + /* 2170 */ 1260, 1260, 1260, 1260, 1260, 1260, 1260, 418, 1260, 1260, + /* 2180 */ 1260, 1260, 320, 571, 1260, 1260, 1260, 1260, 1260, 1260, + /* 2190 */ 1260, 1260, 462, 1260, 1260, 1260, 1260, 1260, 1260, 1260, + /* 2200 */ 1260, 1260, 1260, 1260, 1260, 1260, 462, }; static const YYCODETYPE yy_lookahead[] = { /* 0 */ 277, 278, 279, 241, 242, 225, 195, 227, 195, 241, @@ -176315,39 +176318,39 @@ static const YYCODETYPE yy_lookahead[] = { /* 1800 */ 158, 0, 1, 2, 247, 227, 5, 221, 221, 221, /* 1810 */ 142, 10, 11, 12, 13, 14, 262, 262, 17, 202, /* 1820 */ 300, 19, 20, 300, 22, 183, 247, 251, 251, 245, - /* 1830 */ 202, 30, 38, 32, 202, 152, 151, 22, 36, 43, - /* 1840 */ 236, 40, 18, 202, 239, 239, 18, 239, 239, 283, - /* 1850 */ 201, 150, 236, 202, 236, 201, 159, 202, 248, 248, - /* 1860 */ 248, 248, 60, 63, 201, 275, 273, 275, 273, 275, - /* 1870 */ 22, 286, 71, 223, 72, 202, 223, 297, 297, 202, - /* 1880 */ 79, 201, 116, 82, 220, 201, 220, 220, 65, 293, - /* 1890 */ 292, 229, 22, 166, 127, 226, 24, 114, 226, 223, - /* 1900 */ 99, 222, 202, 101, 285, 92, 220, 308, 83, 107, - /* 1910 */ 108, 220, 220, 316, 220, 285, 268, 115, 229, 117, - /* 1920 */ 118, 119, 223, 321, 122, 268, 149, 146, 22, 19, - /* 1930 */ 20, 202, 22, 159, 282, 134, 321, 148, 280, 147, - /* 1940 */ 139, 140, 252, 141, 25, 204, 36, 252, 13, 251, - /* 1950 */ 196, 248, 250, 249, 196, 6, 154, 155, 156, 157, - /* 1960 */ 158, 209, 194, 194, 163, 194, 306, 306, 303, 224, - /* 1970 */ 60, 215, 215, 209, 215, 215, 215, 224, 216, 216, - /* 1980 */ 4, 209, 72, 3, 22, 183, 164, 15, 23, 16, - /* 1990 */ 23, 140, 152, 131, 25, 24, 143, 20, 16, 145, - /* 2000 */ 1, 143, 131, 62, 131, 37, 54, 152, 54, 54, - /* 2010 */ 54, 101, 131, 117, 1, 34, 142, 107, 108, 5, - /* 2020 */ 22, 116, 162, 76, 41, 115, 69, 117, 118, 119, - /* 2030 */ 1, 2, 122, 25, 5, 69, 142, 116, 20, 10, - /* 2040 */ 11, 12, 13, 14, 24, 19, 17, 132, 5, 126, - /* 2050 */ 22, 141, 68, 10, 11, 12, 13, 14, 22, 30, - /* 2060 */ 17, 32, 22, 22, 154, 155, 156, 157, 158, 40, - /* 2070 */ 23, 68, 60, 30, 24, 32, 97, 28, 22, 68, - /* 2080 */ 23, 37, 34, 40, 150, 22, 25, 23, 23, 23, - /* 2090 */ 22, 98, 142, 183, 23, 23, 34, 22, 25, 89, - /* 2100 */ 71, 34, 117, 144, 34, 22, 76, 76, 79, 87, - /* 2110 */ 34, 82, 34, 44, 71, 94, 34, 23, 25, 24, - /* 2120 */ 34, 25, 79, 23, 23, 82, 23, 23, 99, 143, - /* 2130 */ 143, 22, 25, 25, 23, 22, 11, 22, 22, 25, - /* 2140 */ 23, 23, 99, 22, 22, 136, 142, 142, 142, 25, - /* 2150 */ 23, 15, 1, 1, 323, 323, 323, 323, 323, 323, + /* 1830 */ 202, 30, 38, 32, 202, 152, 151, 22, 36, 149, + /* 1840 */ 146, 40, 43, 18, 252, 251, 236, 252, 251, 283, + /* 1850 */ 239, 239, 239, 239, 202, 18, 201, 150, 236, 202, + /* 1860 */ 201, 236, 60, 248, 248, 275, 273, 248, 159, 273, + /* 1870 */ 63, 286, 71, 275, 72, 275, 22, 297, 297, 248, + /* 1880 */ 79, 202, 202, 82, 223, 201, 223, 201, 116, 220, + /* 1890 */ 22, 202, 293, 292, 65, 229, 201, 220, 226, 226, + /* 1900 */ 99, 127, 166, 101, 24, 229, 114, 83, 92, 107, + /* 1910 */ 108, 220, 308, 202, 285, 223, 268, 115, 220, 117, + /* 1920 */ 118, 119, 222, 220, 122, 220, 220, 223, 285, 19, + /* 1930 */ 20, 22, 22, 268, 316, 134, 280, 202, 159, 148, + /* 1940 */ 139, 140, 321, 141, 282, 147, 36, 321, 25, 252, + /* 1950 */ 251, 204, 250, 252, 13, 196, 154, 155, 156, 157, + /* 1960 */ 158, 249, 248, 196, 163, 6, 306, 194, 215, 194, + /* 1970 */ 60, 194, 209, 306, 209, 215, 215, 215, 224, 224, + /* 1980 */ 216, 215, 72, 4, 216, 183, 3, 209, 22, 15, + /* 1990 */ 164, 16, 23, 23, 152, 140, 131, 25, 20, 143, + /* 2000 */ 24, 16, 145, 1, 143, 131, 131, 62, 37, 54, + /* 2010 */ 152, 101, 54, 303, 54, 54, 131, 107, 108, 117, + /* 2020 */ 34, 142, 1, 5, 116, 115, 22, 117, 118, 119, + /* 2030 */ 1, 2, 122, 162, 5, 25, 69, 69, 76, 10, + /* 2040 */ 11, 12, 13, 14, 41, 142, 17, 116, 5, 24, + /* 2050 */ 20, 141, 19, 10, 11, 12, 13, 14, 132, 30, + /* 2060 */ 17, 32, 126, 22, 154, 155, 156, 157, 158, 40, + /* 2070 */ 22, 22, 22, 30, 68, 32, 23, 68, 24, 60, + /* 2080 */ 28, 97, 22, 40, 68, 23, 22, 150, 37, 34, + /* 2090 */ 23, 23, 22, 183, 25, 23, 98, 23, 142, 23, + /* 2100 */ 71, 22, 25, 89, 117, 34, 94, 144, 79, 34, + /* 2110 */ 76, 82, 34, 87, 71, 34, 34, 44, 76, 34, + /* 2120 */ 23, 22, 79, 25, 24, 82, 25, 34, 99, 23, + /* 2130 */ 23, 143, 143, 23, 23, 22, 11, 25, 23, 25, + /* 2140 */ 22, 22, 99, 22, 136, 23, 23, 142, 22, 22, + /* 2150 */ 15, 1, 1, 25, 25, 142, 23, 142, 323, 323, /* 2160 */ 323, 323, 323, 134, 323, 323, 323, 323, 139, 140, /* 2170 */ 323, 323, 323, 323, 323, 323, 323, 134, 323, 323, /* 2180 */ 323, 323, 139, 140, 323, 323, 323, 323, 323, 323, @@ -176366,16 +176369,16 @@ static const YYCODETYPE yy_lookahead[] = { /* 2310 */ 323, 323, 323, 323, 323, 323, 323, 323, 323, 323, /* 2320 */ 323, 323, 323, 323, 323, 323, 323, 323, 323, 323, /* 2330 */ 323, 323, 323, 323, 323, 323, 323, 323, 323, 323, - /* 2340 */ 323, 187, 187, 187, 187, 187, 187, 187, 187, 187, + /* 2340 */ 323, 323, 323, 323, 323, 187, 187, 187, 187, 187, /* 2350 */ 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, /* 2360 */ 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, /* 2370 */ 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, /* 2380 */ 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, /* 2390 */ 187, 187, 187, 187, }; -#define YY_SHIFT_COUNT (582) +#define YY_SHIFT_COUNT (586) #define YY_SHIFT_MIN (0) -#define YY_SHIFT_MAX (2152) +#define YY_SHIFT_MAX (2151) static const unsigned short int yy_shift_ofst[] = { /* 0 */ 2029, 1801, 2043, 1380, 1380, 318, 271, 1496, 1569, 1642, /* 10 */ 702, 702, 702, 740, 318, 318, 318, 318, 318, 0, @@ -176410,36 +176413,36 @@ static const unsigned short int yy_shift_ofst[] = { /* 300 */ 667, 667, 1487, 667, 1198, 1435, 777, 1011, 1423, 584, /* 310 */ 584, 584, 1273, 1273, 1273, 1273, 1471, 1471, 880, 1530, /* 320 */ 1190, 1095, 1731, 1731, 1668, 1668, 1794, 1794, 1668, 1683, - /* 330 */ 1685, 1815, 1796, 1824, 1824, 1824, 1824, 1668, 1828, 1701, - /* 340 */ 1685, 1685, 1701, 1815, 1796, 1701, 1796, 1701, 1668, 1828, - /* 350 */ 1697, 1800, 1668, 1828, 1848, 1668, 1828, 1668, 1828, 1848, - /* 360 */ 1766, 1766, 1766, 1823, 1870, 1870, 1848, 1766, 1767, 1766, - /* 370 */ 1823, 1766, 1766, 1727, 1872, 1783, 1783, 1848, 1668, 1813, - /* 380 */ 1813, 1825, 1825, 1777, 1781, 1906, 1668, 1774, 1777, 1789, - /* 390 */ 1792, 1701, 1919, 1935, 1935, 1949, 1949, 1949, 2207, 2207, - /* 400 */ 2207, 2207, 2207, 2207, 2207, 2207, 2207, 2207, 2207, 2207, - /* 410 */ 2207, 2207, 2207, 69, 1032, 79, 357, 1377, 1206, 400, - /* 420 */ 1525, 835, 332, 1540, 1437, 1539, 1536, 1548, 1583, 1620, - /* 430 */ 1633, 1670, 1671, 1674, 1567, 1553, 1682, 1506, 1675, 1358, - /* 440 */ 1607, 1589, 1678, 1681, 1624, 1687, 1688, 1283, 1561, 1693, - /* 450 */ 1696, 1623, 1521, 1976, 1980, 1962, 1822, 1972, 1973, 1965, - /* 460 */ 1967, 1851, 1840, 1862, 1969, 1969, 1971, 1853, 1977, 1854, - /* 470 */ 1982, 1999, 1858, 1871, 1969, 1873, 1941, 1968, 1969, 1855, - /* 480 */ 1952, 1954, 1955, 1956, 1881, 1896, 1981, 1874, 2013, 2014, - /* 490 */ 1998, 1905, 1860, 1957, 2008, 1966, 1947, 1983, 1894, 1921, - /* 500 */ 2020, 2018, 2026, 1915, 1923, 2028, 1984, 2036, 2040, 2047, - /* 510 */ 2041, 2003, 2012, 2050, 1979, 2049, 2056, 2011, 2044, 2057, - /* 520 */ 2048, 1934, 2063, 2064, 2065, 2061, 2066, 2068, 1993, 1950, - /* 530 */ 2071, 2072, 1985, 2062, 2075, 1959, 2073, 2067, 2070, 2076, - /* 540 */ 2078, 2010, 2030, 2022, 2069, 2031, 2021, 2082, 2094, 2083, - /* 550 */ 2095, 2093, 2096, 2086, 1986, 1987, 2100, 2073, 2101, 2103, - /* 560 */ 2104, 2109, 2107, 2108, 2111, 2113, 2125, 2115, 2116, 2117, - /* 570 */ 2118, 2121, 2122, 2114, 2009, 2004, 2005, 2006, 2124, 2127, - /* 580 */ 2136, 2151, 2152, + /* 330 */ 1685, 1815, 1690, 1694, 1799, 1690, 1694, 1825, 1825, 1825, + /* 340 */ 1825, 1668, 1837, 1707, 1685, 1685, 1707, 1815, 1799, 1707, + /* 350 */ 1799, 1707, 1668, 1837, 1709, 1807, 1668, 1837, 1854, 1668, + /* 360 */ 1837, 1668, 1837, 1854, 1772, 1772, 1772, 1829, 1868, 1868, + /* 370 */ 1854, 1772, 1774, 1772, 1829, 1772, 1772, 1736, 1880, 1792, + /* 380 */ 1792, 1854, 1668, 1816, 1816, 1824, 1824, 1690, 1694, 1909, + /* 390 */ 1668, 1779, 1690, 1791, 1798, 1707, 1923, 1941, 1941, 1959, + /* 400 */ 1959, 1959, 2207, 2207, 2207, 2207, 2207, 2207, 2207, 2207, + /* 410 */ 2207, 2207, 2207, 2207, 2207, 2207, 2207, 69, 1032, 79, + /* 420 */ 357, 1377, 1206, 400, 1525, 835, 332, 1540, 1437, 1539, + /* 430 */ 1536, 1548, 1583, 1620, 1633, 1670, 1671, 1674, 1567, 1553, + /* 440 */ 1682, 1506, 1675, 1358, 1607, 1589, 1678, 1681, 1624, 1687, + /* 450 */ 1688, 1283, 1561, 1693, 1696, 1623, 1521, 1979, 1983, 1966, + /* 460 */ 1826, 1974, 1975, 1969, 1970, 1855, 1842, 1865, 1972, 1972, + /* 470 */ 1976, 1856, 1978, 1857, 1985, 2002, 1861, 1874, 1972, 1875, + /* 480 */ 1945, 1971, 1972, 1858, 1955, 1958, 1960, 1961, 1885, 1902, + /* 490 */ 1986, 1879, 2021, 2018, 2004, 1908, 1871, 1967, 2010, 1968, + /* 500 */ 1962, 2003, 1903, 1931, 2025, 2030, 2033, 1926, 1936, 2041, + /* 510 */ 2006, 2048, 2049, 2053, 2050, 2009, 2019, 2054, 1984, 2052, + /* 520 */ 2060, 2016, 2051, 2062, 2055, 1937, 2064, 2067, 2068, 2069, + /* 530 */ 2072, 2070, 1998, 1956, 2074, 2076, 1987, 2071, 2079, 1963, + /* 540 */ 2077, 2075, 2078, 2081, 2082, 2014, 2034, 2026, 2073, 2042, + /* 550 */ 2012, 2085, 2097, 2099, 2100, 2098, 2101, 2093, 1988, 1989, + /* 560 */ 2106, 2077, 2107, 2110, 2111, 2113, 2112, 2114, 2115, 2118, + /* 570 */ 2125, 2119, 2121, 2122, 2123, 2126, 2127, 2128, 2008, 2005, + /* 580 */ 2013, 2015, 2129, 2133, 2135, 2150, 2151, }; -#define YY_REDUCE_COUNT (412) +#define YY_REDUCE_COUNT (416) #define YY_REDUCE_MIN (-277) -#define YY_REDUCE_MAX (1772) +#define YY_REDUCE_MAX (1778) static const short yy_reduce_ofst[] = { /* 0 */ -67, 1252, -64, -178, -181, 160, 1071, 143, -184, 137, /* 10 */ 218, 220, 222, -174, 229, 268, 272, 275, 324, -208, @@ -176474,76 +176477,76 @@ static const short yy_reduce_ofst[] = { /* 300 */ 1509, 1517, 1546, 1519, 1557, 1489, 1565, 1564, 1578, 1586, /* 310 */ 1587, 1588, 1526, 1528, 1554, 1555, 1576, 1577, 1566, 1579, /* 320 */ 1584, 1591, 1520, 1523, 1617, 1628, 1580, 1581, 1632, 1585, - /* 330 */ 1590, 1593, 1604, 1605, 1606, 1608, 1609, 1641, 1649, 1610, - /* 340 */ 1592, 1594, 1611, 1595, 1616, 1612, 1618, 1613, 1651, 1654, - /* 350 */ 1596, 1598, 1655, 1663, 1650, 1673, 1680, 1677, 1684, 1653, - /* 360 */ 1664, 1666, 1667, 1662, 1669, 1672, 1676, 1686, 1679, 1691, - /* 370 */ 1689, 1692, 1694, 1597, 1599, 1619, 1630, 1699, 1700, 1602, - /* 380 */ 1615, 1648, 1657, 1690, 1698, 1658, 1729, 1652, 1695, 1702, - /* 390 */ 1704, 1703, 1741, 1754, 1758, 1768, 1769, 1771, 1660, 1661, - /* 400 */ 1665, 1752, 1756, 1757, 1759, 1760, 1764, 1745, 1753, 1762, - /* 410 */ 1763, 1761, 1772, + /* 330 */ 1590, 1593, 1592, 1594, 1610, 1595, 1597, 1611, 1612, 1613, + /* 340 */ 1614, 1652, 1655, 1615, 1598, 1600, 1616, 1596, 1622, 1619, + /* 350 */ 1625, 1631, 1657, 1659, 1599, 1601, 1679, 1684, 1661, 1680, + /* 360 */ 1686, 1689, 1695, 1663, 1669, 1677, 1691, 1666, 1672, 1673, + /* 370 */ 1692, 1698, 1700, 1703, 1676, 1705, 1706, 1618, 1604, 1629, + /* 380 */ 1643, 1704, 1711, 1621, 1626, 1648, 1665, 1697, 1699, 1656, + /* 390 */ 1735, 1662, 1701, 1702, 1712, 1714, 1747, 1759, 1767, 1773, + /* 400 */ 1775, 1777, 1660, 1667, 1710, 1763, 1753, 1760, 1761, 1762, + /* 410 */ 1765, 1754, 1755, 1764, 1768, 1766, 1778, }; static const YYACTIONTYPE yy_default[] = { - /* 0 */ 1663, 1663, 1663, 1491, 1254, 1367, 1254, 1254, 1254, 1254, - /* 10 */ 1491, 1491, 1491, 1254, 1254, 1254, 1254, 1254, 1254, 1397, - /* 20 */ 1397, 1544, 1287, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 30 */ 1254, 1254, 1254, 1254, 1254, 1490, 1254, 1254, 1254, 1254, - /* 40 */ 1578, 1578, 1254, 1254, 1254, 1254, 1254, 1563, 1562, 1254, - /* 50 */ 1254, 1254, 1406, 1254, 1413, 1254, 1254, 1254, 1254, 1254, - /* 60 */ 1492, 1493, 1254, 1254, 1254, 1254, 1543, 1545, 1508, 1420, - /* 70 */ 1419, 1418, 1417, 1526, 1385, 1411, 1404, 1408, 1487, 1488, - /* 80 */ 1486, 1641, 1493, 1492, 1254, 1407, 1455, 1471, 1454, 1254, - /* 90 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 100 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 110 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 120 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 130 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 140 */ 1254, 1254, 1463, 1470, 1469, 1468, 1477, 1467, 1464, 1457, - /* 150 */ 1456, 1458, 1459, 1278, 1254, 1275, 1329, 1254, 1254, 1254, - /* 160 */ 1254, 1254, 1460, 1287, 1448, 1447, 1446, 1254, 1474, 1461, - /* 170 */ 1473, 1472, 1551, 1615, 1614, 1509, 1254, 1254, 1254, 1254, - /* 180 */ 1254, 1254, 1578, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 190 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 200 */ 1254, 1254, 1254, 1254, 1254, 1387, 1578, 1578, 1254, 1287, - /* 210 */ 1578, 1578, 1388, 1388, 1283, 1283, 1391, 1558, 1358, 1358, - /* 220 */ 1358, 1358, 1367, 1358, 1254, 1254, 1254, 1254, 1254, 1254, - /* 230 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1548, - /* 240 */ 1546, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 250 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 260 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1363, 1254, - /* 270 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1608, - /* 280 */ 1254, 1521, 1343, 1363, 1363, 1363, 1363, 1365, 1344, 1342, - /* 290 */ 1357, 1288, 1261, 1655, 1423, 1412, 1364, 1412, 1652, 1410, - /* 300 */ 1423, 1423, 1410, 1423, 1364, 1652, 1304, 1630, 1299, 1397, - /* 310 */ 1397, 1397, 1387, 1387, 1387, 1387, 1391, 1391, 1489, 1364, - /* 320 */ 1357, 1254, 1655, 1655, 1373, 1373, 1654, 1654, 1373, 1509, - /* 330 */ 1638, 1432, 1332, 1338, 1338, 1338, 1338, 1373, 1272, 1410, - /* 340 */ 1638, 1638, 1410, 1432, 1332, 1410, 1332, 1410, 1373, 1272, - /* 350 */ 1525, 1649, 1373, 1272, 1499, 1373, 1272, 1373, 1272, 1499, - /* 360 */ 1330, 1330, 1330, 1319, 1254, 1254, 1499, 1330, 1304, 1330, - /* 370 */ 1319, 1330, 1330, 1596, 1254, 1503, 1503, 1499, 1373, 1588, - /* 380 */ 1588, 1400, 1400, 1405, 1391, 1494, 1373, 1254, 1405, 1403, - /* 390 */ 1401, 1410, 1322, 1611, 1611, 1607, 1607, 1607, 1660, 1660, - /* 400 */ 1558, 1623, 1287, 1287, 1287, 1287, 1623, 1306, 1306, 1288, - /* 410 */ 1288, 1287, 1623, 1254, 1254, 1254, 1254, 1254, 1254, 1618, - /* 420 */ 1254, 1553, 1510, 1377, 1254, 1254, 1254, 1254, 1254, 1254, - /* 430 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 440 */ 1564, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 450 */ 1254, 1254, 1437, 1254, 1257, 1555, 1254, 1254, 1254, 1254, - /* 460 */ 1254, 1254, 1254, 1254, 1414, 1415, 1378, 1254, 1254, 1254, - /* 470 */ 1254, 1254, 1254, 1254, 1429, 1254, 1254, 1254, 1424, 1254, - /* 480 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1651, 1254, 1254, - /* 490 */ 1254, 1254, 1254, 1254, 1524, 1523, 1254, 1254, 1375, 1254, - /* 500 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 510 */ 1254, 1254, 1302, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 520 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 530 */ 1254, 1254, 1254, 1254, 1254, 1254, 1402, 1254, 1254, 1254, - /* 540 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 550 */ 1254, 1593, 1392, 1254, 1254, 1254, 1254, 1642, 1254, 1254, - /* 560 */ 1254, 1254, 1352, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 570 */ 1254, 1254, 1254, 1634, 1346, 1438, 1254, 1441, 1276, 1254, - /* 580 */ 1266, 1254, 1254, + /* 0 */ 1667, 1667, 1667, 1495, 1258, 1371, 1258, 1258, 1258, 1258, + /* 10 */ 1495, 1495, 1495, 1258, 1258, 1258, 1258, 1258, 1258, 1401, + /* 20 */ 1401, 1548, 1291, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 30 */ 1258, 1258, 1258, 1258, 1258, 1494, 1258, 1258, 1258, 1258, + /* 40 */ 1582, 1582, 1258, 1258, 1258, 1258, 1258, 1567, 1566, 1258, + /* 50 */ 1258, 1258, 1410, 1258, 1417, 1258, 1258, 1258, 1258, 1258, + /* 60 */ 1496, 1497, 1258, 1258, 1258, 1258, 1547, 1549, 1512, 1424, + /* 70 */ 1423, 1422, 1421, 1530, 1389, 1415, 1408, 1412, 1491, 1492, + /* 80 */ 1490, 1645, 1497, 1496, 1258, 1411, 1459, 1475, 1458, 1258, + /* 90 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 100 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 110 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 120 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 130 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 140 */ 1258, 1258, 1467, 1474, 1473, 1472, 1481, 1471, 1468, 1461, + /* 150 */ 1460, 1462, 1463, 1282, 1258, 1279, 1333, 1258, 1258, 1258, + /* 160 */ 1258, 1258, 1464, 1291, 1452, 1451, 1450, 1258, 1478, 1465, + /* 170 */ 1477, 1476, 1555, 1619, 1618, 1513, 1258, 1258, 1258, 1258, + /* 180 */ 1258, 1258, 1582, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 190 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 200 */ 1258, 1258, 1258, 1258, 1258, 1391, 1582, 1582, 1258, 1291, + /* 210 */ 1582, 1582, 1392, 1392, 1287, 1287, 1395, 1562, 1362, 1362, + /* 220 */ 1362, 1362, 1371, 1362, 1258, 1258, 1258, 1258, 1258, 1258, + /* 230 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1552, + /* 240 */ 1550, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 250 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 260 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1367, 1258, + /* 270 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1612, + /* 280 */ 1258, 1525, 1347, 1367, 1367, 1367, 1367, 1369, 1348, 1346, + /* 290 */ 1361, 1292, 1265, 1659, 1427, 1416, 1368, 1416, 1656, 1414, + /* 300 */ 1427, 1427, 1414, 1427, 1368, 1656, 1308, 1634, 1303, 1401, + /* 310 */ 1401, 1401, 1391, 1391, 1391, 1391, 1395, 1395, 1493, 1368, + /* 320 */ 1361, 1258, 1659, 1659, 1377, 1377, 1658, 1658, 1377, 1513, + /* 330 */ 1642, 1436, 1409, 1395, 1336, 1409, 1395, 1342, 1342, 1342, + /* 340 */ 1342, 1377, 1276, 1414, 1642, 1642, 1414, 1436, 1336, 1414, + /* 350 */ 1336, 1414, 1377, 1276, 1529, 1653, 1377, 1276, 1503, 1377, + /* 360 */ 1276, 1377, 1276, 1503, 1334, 1334, 1334, 1323, 1258, 1258, + /* 370 */ 1503, 1334, 1308, 1334, 1323, 1334, 1334, 1600, 1258, 1507, + /* 380 */ 1507, 1503, 1377, 1592, 1592, 1404, 1404, 1409, 1395, 1498, + /* 390 */ 1377, 1258, 1409, 1407, 1405, 1414, 1326, 1615, 1615, 1611, + /* 400 */ 1611, 1611, 1664, 1664, 1562, 1627, 1291, 1291, 1291, 1291, + /* 410 */ 1627, 1310, 1310, 1292, 1292, 1291, 1627, 1258, 1258, 1258, + /* 420 */ 1258, 1258, 1258, 1622, 1258, 1557, 1514, 1381, 1258, 1258, + /* 430 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 440 */ 1258, 1258, 1258, 1258, 1568, 1258, 1258, 1258, 1258, 1258, + /* 450 */ 1258, 1258, 1258, 1258, 1258, 1258, 1441, 1258, 1261, 1559, + /* 460 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1418, 1419, + /* 470 */ 1382, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1433, 1258, + /* 480 */ 1258, 1258, 1428, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 490 */ 1258, 1655, 1258, 1258, 1258, 1258, 1258, 1258, 1528, 1527, + /* 500 */ 1258, 1258, 1379, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 510 */ 1258, 1258, 1258, 1258, 1258, 1258, 1306, 1258, 1258, 1258, + /* 520 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 530 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 540 */ 1406, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 550 */ 1258, 1258, 1258, 1258, 1258, 1597, 1396, 1258, 1258, 1258, + /* 560 */ 1258, 1646, 1258, 1258, 1258, 1258, 1356, 1258, 1258, 1258, + /* 570 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1638, 1350, 1442, + /* 580 */ 1258, 1445, 1280, 1258, 1270, 1258, 1258, }; /********** End of lemon-generated parsing tables *****************************/ @@ -177315,14 +177318,14 @@ static const char *const yyRuleName[] = { /* 149 */ "limit_opt ::= LIMIT expr", /* 150 */ "limit_opt ::= LIMIT expr OFFSET expr", /* 151 */ "limit_opt ::= LIMIT expr COMMA expr", - /* 152 */ "cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret", + /* 152 */ "cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt limit_opt", /* 153 */ "where_opt ::=", /* 154 */ "where_opt ::= WHERE expr", /* 155 */ "where_opt_ret ::=", /* 156 */ "where_opt_ret ::= WHERE expr", /* 157 */ "where_opt_ret ::= RETURNING selcollist", /* 158 */ "where_opt_ret ::= WHERE expr RETURNING selcollist", - /* 159 */ "cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret", + /* 159 */ "cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret orderby_opt limit_opt", /* 160 */ "setlist ::= setlist COMMA nm EQ expr", /* 161 */ "setlist ::= setlist COMMA LP idlist RP EQ expr", /* 162 */ "setlist ::= nm EQ expr", @@ -178240,14 +178243,14 @@ static const YYCODETYPE yyRuleInfoLhs[] = { 252, /* (149) limit_opt ::= LIMIT expr */ 252, /* (150) limit_opt ::= LIMIT expr OFFSET expr */ 252, /* (151) limit_opt ::= LIMIT expr COMMA expr */ - 192, /* (152) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */ + 192, /* (152) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt limit_opt */ 248, /* (153) where_opt ::= */ 248, /* (154) where_opt ::= WHERE expr */ 270, /* (155) where_opt_ret ::= */ 270, /* (156) where_opt_ret ::= WHERE expr */ 270, /* (157) where_opt_ret ::= RETURNING selcollist */ 270, /* (158) where_opt_ret ::= WHERE expr RETURNING selcollist */ - 192, /* (159) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */ + 192, /* (159) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret orderby_opt limit_opt */ 271, /* (160) setlist ::= setlist COMMA nm EQ expr */ 271, /* (161) setlist ::= setlist COMMA LP idlist RP EQ expr */ 271, /* (162) setlist ::= nm EQ expr */ @@ -178654,14 +178657,14 @@ static const signed char yyRuleInfoNRhs[] = { -2, /* (149) limit_opt ::= LIMIT expr */ -4, /* (150) limit_opt ::= LIMIT expr OFFSET expr */ -4, /* (151) limit_opt ::= LIMIT expr COMMA expr */ - -6, /* (152) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */ + -8, /* (152) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt limit_opt */ 0, /* (153) where_opt ::= */ -2, /* (154) where_opt ::= WHERE expr */ 0, /* (155) where_opt_ret ::= */ -2, /* (156) where_opt_ret ::= WHERE expr */ -2, /* (157) where_opt_ret ::= RETURNING selcollist */ -4, /* (158) where_opt_ret ::= WHERE expr RETURNING selcollist */ - -9, /* (159) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */ + -11, /* (159) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret orderby_opt limit_opt */ -5, /* (160) setlist ::= setlist COMMA nm EQ expr */ -7, /* (161) setlist ::= setlist COMMA LP idlist RP EQ expr */ -3, /* (162) setlist ::= nm EQ expr */ @@ -179579,10 +179582,17 @@ static YYACTIONTYPE yy_reduce( case 151: /* limit_opt ::= LIMIT expr COMMA expr */ {yymsp[-3].minor.yy590 = sqlite3PExpr(pParse,TK_LIMIT,yymsp[0].minor.yy590,yymsp[-2].minor.yy590);} break; - case 152: /* cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */ + case 152: /* cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt limit_opt */ { - sqlite3SrcListIndexedBy(pParse, yymsp[-2].minor.yy563, &yymsp[-1].minor.yy0); - sqlite3DeleteFrom(pParse,yymsp[-2].minor.yy563,yymsp[0].minor.yy590,0,0); + sqlite3SrcListIndexedBy(pParse, yymsp[-4].minor.yy563, &yymsp[-3].minor.yy0); +#ifndef SQLITE_ENABLE_UPDATE_DELETE_LIMIT + if( yymsp[-1].minor.yy402 || yymsp[0].minor.yy590 ){ + updateDeleteLimitError(pParse,yymsp[-1].minor.yy402,yymsp[0].minor.yy590); + yymsp[-1].minor.yy402 = 0; + yymsp[0].minor.yy590 = 0; + } +#endif + sqlite3DeleteFrom(pParse,yymsp[-4].minor.yy563,yymsp[-2].minor.yy590,yymsp[-1].minor.yy402,yymsp[0].minor.yy590); } break; case 157: /* where_opt_ret ::= RETURNING selcollist */ @@ -179591,12 +179601,11 @@ static YYACTIONTYPE yy_reduce( case 158: /* where_opt_ret ::= WHERE expr RETURNING selcollist */ {sqlite3AddReturning(pParse,yymsp[0].minor.yy402); yymsp[-3].minor.yy590 = yymsp[-2].minor.yy590;} break; - case 159: /* cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */ + case 159: /* cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret orderby_opt limit_opt */ { - sqlite3SrcListIndexedBy(pParse, yymsp[-5].minor.yy563, &yymsp[-4].minor.yy0); - sqlite3ExprListCheckLength(pParse,yymsp[-2].minor.yy402,"set list"); - if( yymsp[-1].minor.yy563 ){ - SrcList *pFromClause = yymsp[-1].minor.yy563; + sqlite3SrcListIndexedBy(pParse, yymsp[-7].minor.yy563, &yymsp[-6].minor.yy0); + if( yymsp[-3].minor.yy563 ){ + SrcList *pFromClause = yymsp[-3].minor.yy563; if( pFromClause->nSrc>1 ){ Select *pSubquery; Token as; @@ -179605,9 +179614,17 @@ static YYACTIONTYPE yy_reduce( as.z = 0; pFromClause = sqlite3SrcListAppendFromTerm(pParse,0,0,0,&as,pSubquery,0); } - yymsp[-5].minor.yy563 = sqlite3SrcListAppendList(pParse, yymsp[-5].minor.yy563, pFromClause); + yymsp[-7].minor.yy563 = sqlite3SrcListAppendList(pParse, yymsp[-7].minor.yy563, pFromClause); } - sqlite3Update(pParse,yymsp[-5].minor.yy563,yymsp[-2].minor.yy402,yymsp[0].minor.yy590,yymsp[-6].minor.yy502,0,0,0); + sqlite3ExprListCheckLength(pParse,yymsp[-4].minor.yy402,"set list"); +#ifndef SQLITE_ENABLE_UPDATE_DELETE_LIMIT + if( yymsp[-1].minor.yy402 || yymsp[0].minor.yy590 ){ + updateDeleteLimitError(pParse,yymsp[-1].minor.yy402,yymsp[0].minor.yy590); + yymsp[-1].minor.yy402 = 0; + yymsp[0].minor.yy590 = 0; + } +#endif + sqlite3Update(pParse,yymsp[-7].minor.yy563,yymsp[-4].minor.yy402,yymsp[-2].minor.yy590,yymsp[-8].minor.yy502,yymsp[-1].minor.yy402,yymsp[0].minor.yy590,0); } break; case 160: /* setlist ::= setlist COMMA nm EQ expr */ diff --git a/test/js/sql/sqlite-sql.test.ts b/test/js/sql/sqlite-sql.test.ts index 539f4854c6..ff6653a959 100644 --- a/test/js/sql/sqlite-sql.test.ts +++ b/test/js/sql/sqlite-sql.test.ts @@ -1214,7 +1214,23 @@ describe("SQLite-specific features", () => { await sql`insert into "users" ("id", "name", "verified", "created_at") values (null, ${"John"}, ${0}, strftime('%s', 'now')) returning upper("name")`; expect(upperName).toBe("JOHN"); }); - + test("order by and limit in delete statements", async () => { + await using sql = new SQL("sqlite://:memory:"); + await sql`CREATE TABLE users (id INTEGER, name TEXT)`; + await sql`INSERT INTO users VALUES (1, 'John'), (2, 'Jane'), (3, 'Austin')`; + const result = await sql`delete from "users" where "users"."id" = ${1} order by "users"."name" asc limit ${1}`; + expect(result.count).toBe(1); + expect(result.command).toBe("DELETE"); + }); + test("order by and limit in update statements", async () => { + await using sql = new SQL("sqlite://:memory:"); + await sql`CREATE TABLE users (id INTEGER, name TEXT)`; + await sql`INSERT INTO users VALUES (1, 'John'), (2, 'Jane'), (3, 'Austin')`; + const result = + await sql`update "users" set "name" = 'John' where "users"."id" = ${1} order by "users"."name" asc limit ${1}`; + expect(result.count).toBe(1); + expect(result.command).toBe("UPDATE"); + }); test("last_insert_rowid()", async () => { await sql`CREATE TABLE rowid_test (id INTEGER PRIMARY KEY, value TEXT)`; await sql`INSERT INTO rowid_test (value) VALUES ('test')`; From 2e86f74764e2f24958417593fc36516c071d3838 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 02:51:12 -0700 Subject: [PATCH 027/191] Update no-validate-leaksan.txt --- test/no-validate-leaksan.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/no-validate-leaksan.txt b/test/no-validate-leaksan.txt index 217b98d0dc..876d7b356c 100644 --- a/test/no-validate-leaksan.txt +++ b/test/no-validate-leaksan.txt @@ -367,6 +367,9 @@ test/js/bun/util/inspect.test.js test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js test/js/node/vm/vm.test.ts +# VM has terminated +test/js/node/test/parallel/test-net-during-close.js + # JSC::BuiltinNames::~BuiltinNames test/js/bun/shell/shell-hang.test.ts test/js/bun/util/reportError.test.ts From 6c8635da63e0519478499beca89cd15cf44bd94c Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sat, 4 Oct 2025 02:59:47 -0700 Subject: [PATCH 028/191] fix(install): isolated installs with transitive self dependencies (#23222) ### What does this PR do? Packages with self dependencies at a different version were colliding with the current version in the store node_modules. This pr nests them in another node_modules Example: self-dep@1.0.2 has a dependency on self-dep@1.0.1. self-dep@1.0.2 is placed here in: `./node_modules/.bun/self-dep@1.0.2/node_modules/self-dep` and it's self-dep dependency symlink is now placed in: `./node_modules/.bun/self-dep@1.0.2/node_modules/self-dep/node_modules/self-dep` fixes #22681 ### How did you verify your code works? Manually tested the linked issue is working, and added a test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/install/isolated_install/Installer.zig | 50 +++++++++++++- test/cli/install/isolated-install.test.ts | 35 ++++++++++ .../registry/packages/self-dep/package.json | 65 ++++++++++++++++++ .../packages/self-dep/self-dep-1.0.1.tgz | Bin 0 -> 143 bytes .../packages/self-dep/self-dep-1.0.2.tgz | Bin 0 -> 166 bytes 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 test/cli/install/registry/packages/self-dep/package.json create mode 100644 test/cli/install/registry/packages/self-dep/self-dep-1.0.1.tgz create mode 100644 test/cli/install/registry/packages/self-dep/self-dep-1.0.2.tgz diff --git a/src/install/isolated_install/Installer.zig b/src/install/isolated_install/Installer.zig index 42c1ccabef..e84e67eb40 100644 --- a/src/install/isolated_install/Installer.zig +++ b/src/install/isolated_install/Installer.zig @@ -741,13 +741,23 @@ pub const Installer = struct { const dependencies = lockfile.buffers.dependencies.items; for (entry_dependencies[this.entry_id.get()].slice()) |dep| { - const dep_name = dependencies[dep.dep_id].name; + const dep_name = dependencies[dep.dep_id].name.slice(string_buf); var dest: bun.Path(.{ .sep = .auto }) = .initTopLevelDir(); defer dest.deinit(); installer.appendStoreNodeModulesPath(&dest, this.entry_id); - dest.append(dep_name.slice(string_buf)); + + dest.append(dep_name); + + if (installer.entryStoreNodeModulesPackageName(dep_id, pkg_id, &pkg_res, pkg_names)) |entry_node_modules_name| { + if (strings.eqlLong(dep_name, entry_node_modules_name, true)) { + // nest the dependency in another node_modules if the name is the same as the entry name + // in the store node_modules to avoid collision + dest.append("node_modules"); + dest.append(dep_name); + } + } var dep_store_path: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir(); defer dep_store_path.deinit(); @@ -1283,6 +1293,8 @@ pub const Installer = struct { } else { buf.append(pkg_name.slice(string_buf)); } + } else { + // append nothing. buf is already top_level_dir } }, .workspace => { @@ -1306,6 +1318,38 @@ pub const Installer = struct { }, } } + + /// The directory name for the entry store node_modules install + /// folder. + /// ./node_modules/.bun/jquery@3.7.1/node_modules/jquery + /// ^ this one + /// Need to know this to avoid collisions with dependencies + /// with the same name as the package. + pub fn entryStoreNodeModulesPackageName( + this: *const Installer, + dep_id: DependencyID, + pkg_id: PackageID, + pkg_res: *const Resolution, + pkg_names: []const String, + ) ?[]const u8 { + const string_buf = this.lockfile.buffers.string_bytes.items; + + return switch (pkg_res.tag) { + .root => { + if (dep_id != invalid_dependency_id) { + const pkg_name = pkg_names[pkg_id]; + if (pkg_name.isEmpty()) { + return std.fs.path.basename(bun.fs.FileSystem.instance.top_level_dir); + } + return pkg_name.slice(string_buf); + } + return null; + }, + .workspace => null, + .symlink => null, + else => pkg_names[pkg_id].slice(string_buf), + }; + } }; const string = []const u8; @@ -1332,6 +1376,8 @@ const String = bun.Semver.String; const install = bun.install; const Bin = install.Bin; +const DependencyID = install.DependencyID; +const PackageID = install.PackageID; const PackageInstall = install.PackageInstall; const PackageManager = install.PackageManager; const PackageNameHash = install.PackageNameHash; diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index 3766f0c60c..1217a7bf71 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -226,6 +226,41 @@ test("handles cyclic dependencies", async () => { }); }); +test("package with dependency on previous self works", async () => { + const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { isolated: true } }); + + await write( + packageJson, + JSON.stringify({ + name: "test-transitive-self-dep", + dependencies: { + "self-dep": "1.0.2", + }, + }), + ); + + await runBunInstall(bunEnv, packageDir); + + expect( + await Promise.all([ + file(join(packageDir, "node_modules", "self-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "self-dep", "node_modules", "self-dep", "package.json")).json(), + ]), + ).toEqual([ + { + name: "self-dep", + version: "1.0.2", + dependencies: { + "self-dep": "1.0.1", + }, + }, + { + name: "self-dep", + version: "1.0.1", + }, + ]); +}); + test("can install folder dependencies", async () => { const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { isolated: true } }); diff --git a/test/cli/install/registry/packages/self-dep/package.json b/test/cli/install/registry/packages/self-dep/package.json new file mode 100644 index 0000000000..41c7bf85c8 --- /dev/null +++ b/test/cli/install/registry/packages/self-dep/package.json @@ -0,0 +1,65 @@ +{ + "name": "self-dep", + "versions": { + "1.0.1": { + "name": "self-dep", + "version": "1.0.1", + "_id": "self-dep@1.0.1", + "_integrity": "sha512-X9HQMiuvWXqhxJExWwQz5X+z901PvtvYVVAsEli2k5FDVOg2j6opnkQjha5iDTWbqBrG1m95N3rKwmq3OftU/Q==", + "_nodeVersion": "24.3.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-X9HQMiuvWXqhxJExWwQz5X+z901PvtvYVVAsEli2k5FDVOg2j6opnkQjha5iDTWbqBrG1m95N3rKwmq3OftU/Q==", + "shasum": "b95f3e460b2f2ede25a18cc3c25bc226a3edfc71", + "dist": { + "integrity": "sha512-X9HQMiuvWXqhxJExWwQz5X+z901PvtvYVVAsEli2k5FDVOg2j6opnkQjha5iDTWbqBrG1m95N3rKwmq3OftU/Q==", + "shasum": "b95f3e460b2f2ede25a18cc3c25bc226a3edfc71", + "tarball": "http://http://localhost:4873/self-dep/-/self-dep-1.0.1.tgz" + }, + "contributors": [] + }, + "1.0.2": { + "name": "self-dep", + "version": "1.0.2", + "dependencies": { + "self-dep": "1.0.1" + }, + "_id": "self-dep@1.0.2", + "_integrity": "sha512-idMxfr8aIs5CwIVOMTykKRK7MBURv62AjBZ+zRH2zPOZMsWbe+sBXha0zPhQNfP7cUWccF3yiSvs0AQwQXGKfA==", + "_nodeVersion": "24.3.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-idMxfr8aIs5CwIVOMTykKRK7MBURv62AjBZ+zRH2zPOZMsWbe+sBXha0zPhQNfP7cUWccF3yiSvs0AQwQXGKfA==", + "shasum": "d1bc984e927fd960511dbef211551408e3bb2f72", + "dist": { + "integrity": "sha512-idMxfr8aIs5CwIVOMTykKRK7MBURv62AjBZ+zRH2zPOZMsWbe+sBXha0zPhQNfP7cUWccF3yiSvs0AQwQXGKfA==", + "shasum": "d1bc984e927fd960511dbef211551408e3bb2f72", + "tarball": "http://http://localhost:4873/self-dep/-/self-dep-1.0.2.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-10-03T21:30:17.221Z", + "created": "2025-10-03T21:30:02.446Z", + "1.0.1": "2025-10-03T21:30:02.446Z", + "1.0.2": "2025-10-03T21:30:17.221Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.2" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "self-dep-1.0.1.tgz": { + "shasum": "b95f3e460b2f2ede25a18cc3c25bc226a3edfc71", + "version": "1.0.1" + }, + "self-dep-1.0.2.tgz": { + "shasum": "d1bc984e927fd960511dbef211551408e3bb2f72", + "version": "1.0.2" + } + }, + "_rev": "", + "_id": "self-dep", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/self-dep/self-dep-1.0.1.tgz b/test/cli/install/registry/packages/self-dep/self-dep-1.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..1e8490d8548dac8e88d76ccc1df3584aab36d5c6 GIT binary patch literal 143 zcmV;A0C4{wiwFP!00002|LxDq3c@fDh2dHEDTb^yJwww7zD>|5-qO&ho8r4maHZ=a zDD!Q8m|2}1Hm9(UZGP1r%aCYh0K9Wt3*fT=de7*34-xO-7}5z=#Go&@m1`IYm|^7G xxF0b!%qE3PG;1~`o_EV_%w|!q_c)frkm^G$teECON-3rO#1kd1FfafJ004JlK{x;a literal 0 HcmV?d00001 diff --git a/test/cli/install/registry/packages/self-dep/self-dep-1.0.2.tgz b/test/cli/install/registry/packages/self-dep/self-dep-1.0.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a42ab9bcc645dfe13a905217c01450e61df04ef0 GIT binary patch literal 166 zcmV;X09pSZiwFP!00002|LxDq3c@fD1<PGtS66!)- z7g5OBT$sn=dfS}rK{kHQ{1|$t76!nSCB+2rnE)3Rq1YKP8-tR-1*1{~^##{3+Cc#e zlzc1qC+-=McJ?B=CLQFwU$^4*Do$@QgsyjS!8!8nJZ;5`YsEF41YJ}7r>N0A&_V00000 literal 0 HcmV?d00001 From 46e7a3b3c5b2e22de110e8e8c38980a82b71b71b Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 04:57:29 -0700 Subject: [PATCH 029/191] Implement birthtime support on Linux using statx syscall (#23209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds birthtime (file creation time) support on Linux using the `statx` syscall - Stores birthtime in architecture-specific unused fields of the kernel Stat struct (x86_64 and aarch64) - Falls back to traditional `stat` on kernels < 4.11 that don't support `statx` - Includes comprehensive tests validating birthtime behavior Fixes #6585 ## Implementation Details **src/sys.zig:** - Added `StatxField` enum for field selection - Implemented `statxImpl()`, `fstatx()`, `statx()`, and `lstatx()` functions - Stores birthtime in unused padding fields (architecture-specific for x86_64 and aarch64) - Graceful fallback to traditional stat if statx is not supported **src/bun.js/node/node_fs.zig:** - Updated `stat()`, `fstat()`, and `lstat()` to use statx functions on Linux **src/bun.js/node/Stat.zig:** - Added `getBirthtime()` helper to extract birthtime from architecture-specific storage **test/js/node/fs/fs-birthtime-linux.test.ts:** - Tests non-zero birthtime values - Verifies birthtime immutability across file modifications - Validates consistency across stat/lstat/fstat - Tests BigInt stats with nanosecond precision - Verifies birthtime ordering relative to other timestamps ## Test Plan - [x] Run `bun bd test test/js/node/fs/fs-birthtime-linux.test.ts` - all 5 tests pass - [x] Compare behavior with Node.js - identical behavior - [x] Compare with system Bun - system Bun returns epoch, new implementation returns real birthtime 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/node/Stat.zig | 32 +++--- src/bun.js/node/node_fs.zig | 83 ++++++++++---- src/bun.js/node/node_fs_stat_watcher.zig | 60 +++++++--- src/sys.zig | 127 +++++++++++++++++++++ src/sys/PosixStat.zig | 96 ++++++++++++++++ src/sys_uv.zig | 1 + test/js/bun/util/bun-file.test.ts | 6 +- test/js/node/fs/fs-birthtime-linux.test.ts | 108 ++++++++++++++++++ 8 files changed, 455 insertions(+), 58 deletions(-) create mode 100644 src/sys/PosixStat.zig create mode 100644 test/js/node/fs/fs-birthtime-linux.test.ts diff --git a/src/bun.js/node/Stat.zig b/src/bun.js/node/Stat.zig index 45e3492ec0..1ffb7271b6 100644 --- a/src/bun.js/node/Stat.zig +++ b/src/bun.js/node/Stat.zig @@ -4,11 +4,15 @@ pub fn StatType(comptime big: bool) type { pub const new = bun.TrivialNew(@This()); pub const deinit = bun.TrivialDeinit(@This()); - value: bun.Stat, + value: Syscall.PosixStat, - const StatTimespec = if (Environment.isWindows) bun.windows.libuv.uv_timespec_t else std.posix.timespec; + const StatTimespec = bun.timespec; const Float = if (big) i64 else f64; + pub inline fn init(stat_: *const Syscall.PosixStat) @This() { + return .{ .value = stat_.* }; + } + inline fn toNanoseconds(ts: StatTimespec) u64 { if (ts.sec < 0) { return @intCast(@max(bun.timespec.nsSigned(&bun.timespec{ @@ -29,8 +33,8 @@ pub fn StatType(comptime big: bool) type { // > libuv calculates tv_sec and tv_nsec from it and converts to signed long, // > which causes Y2038 overflow. On the other platforms it is safe to treat // > negative values as pre-epoch time. - const tv_sec = if (Environment.isWindows) @as(u32, @bitCast(ts.sec)) else ts.sec; - const tv_nsec = if (Environment.isWindows) @as(u32, @bitCast(ts.nsec)) else ts.nsec; + const tv_sec = if (Environment.isWindows) @as(u32, @bitCast(@as(i32, @truncate(ts.sec)))) else ts.sec; + const tv_nsec = if (Environment.isWindows) @as(u32, @bitCast(@as(i32, @truncate(ts.nsec)))) else ts.nsec; if (big) { const sec: i64 = tv_sec; const nsec: i64 = tv_nsec; @@ -44,6 +48,10 @@ pub fn StatType(comptime big: bool) type { } } + fn getBirthtime(stat_: *const Syscall.PosixStat) StatTimespec { + return stat_.birthtim; + } + pub fn toJS(this: *const @This(), globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { return statToJS(&this.value, globalObject); } @@ -56,7 +64,7 @@ pub fn StatType(comptime big: bool) type { return @intCast(@min(@max(value, 0), std.math.maxInt(i64))); } - fn statToJS(stat_: *const bun.Stat, globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { + fn statToJS(stat_: *const Syscall.PosixStat, globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { const aTime = stat_.atime(); const mTime = stat_.mtime(); const cTime = stat_.ctime(); @@ -70,14 +78,15 @@ pub fn StatType(comptime big: bool) type { const size: i64 = clampedInt64(stat_.size); const blksize: i64 = clampedInt64(stat_.blksize); const blocks: i64 = clampedInt64(stat_.blocks); + const bTime = getBirthtime(stat_); const atime_ms: Float = toTimeMS(aTime); const mtime_ms: Float = toTimeMS(mTime); const ctime_ms: Float = toTimeMS(cTime); + const birthtime_ms: Float = toTimeMS(bTime); const atime_ns: u64 = if (big) toNanoseconds(aTime) else 0; const mtime_ns: u64 = if (big) toNanoseconds(mTime) else 0; const ctime_ns: u64 = if (big) toNanoseconds(cTime) else 0; - const birthtime_ms: Float = if (Environment.isLinux) 0 else toTimeMS(stat_.birthtime()); - const birthtime_ns: u64 = if (big and !Environment.isLinux) toNanoseconds(stat_.birthtime()) else 0; + const birthtime_ns: u64 = if (big) toNanoseconds(bTime) else 0; if (big) { return bun.jsc.fromJSHostCall(globalObject, @src(), Bun__createJSBigIntStatsObject, .{ @@ -121,12 +130,6 @@ pub fn StatType(comptime big: bool) type { birthtime_ms, ); } - - pub fn init(stat_: *const bun.Stat) @This() { - return @This(){ - .value = stat_.*, - }; - } }; } extern fn Bun__JSBigIntStatsObjectConstructor(*jsc.JSGlobalObject) jsc.JSValue; @@ -180,7 +183,7 @@ pub const Stats = union(enum) { big: StatsBig, small: StatsSmall, - pub inline fn init(stat_: *const bun.Stat, big: bool) Stats { + pub inline fn init(stat_: *const Syscall.PosixStat, big: bool) Stats { if (big) { return .{ .big = StatsBig.init(stat_) }; } else { @@ -207,4 +210,5 @@ const std = @import("std"); const bun = @import("bun"); const Environment = bun.Environment; +const Syscall = bun.sys; const jsc = bun.jsc; diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index ffd20551e9..174563b5c7 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -3799,10 +3799,17 @@ pub const NodeFS = struct { } pub fn fstat(_: *NodeFS, args: Arguments.Fstat, _: Flavor) Maybe(Return.Fstat) { - return switch (Syscall.fstat(args.fd)) { - .result => |*result| .{ .result = .init(result, args.big_int) }, - .err => |err| .{ .err = err }, - }; + if (Environment.isLinux and Syscall.supports_statx_on_linux.load(.monotonic)) { + return switch (Syscall.fstatx(args.fd, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks })) { + .result => |result| .{ .result = .init(&result, args.big_int) }, + .err => |err| .{ .err = err }, + }; + } else { + return switch (Syscall.fstat(args.fd)) { + .result => |result| .{ .result = .init(&Syscall.PosixStat.init(&result), args.big_int) }, + .err => |err| .{ .err = err }, + }; + } } pub fn fsync(_: *NodeFS, args: Arguments.Fsync, _: Flavor) Maybe(Return.Fsync) { @@ -3876,15 +3883,27 @@ pub const NodeFS = struct { } pub fn lstat(this: *NodeFS, args: Arguments.Lstat, _: Flavor) Maybe(Return.Lstat) { - return switch (Syscall.lstat(args.path.sliceZ(&this.sync_error_buf))) { - .result => |*result| Maybe(Return.Lstat){ .result = .{ .stats = .init(result, args.big_int) } }, - .err => |err| brk: { - if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { - return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; - } - break :brk Maybe(Return.Lstat){ .err = err.withPath(args.path.slice()) }; - }, - }; + if (Environment.isLinux and Syscall.supports_statx_on_linux.load(.monotonic)) { + return switch (Syscall.lstatx(args.path.sliceZ(&this.sync_error_buf), &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks })) { + .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = .init(&result, args.big_int) } }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; + } + break :brk Maybe(Return.Lstat){ .err = err.withPath(args.path.slice()) }; + }, + }; + } else { + return switch (Syscall.lstat(args.path.sliceZ(&this.sync_error_buf))) { + .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = .init(&Syscall.PosixStat.init(&result), args.big_int) } }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; + } + break :brk Maybe(Return.Lstat){ .err = err.withPath(args.path.slice()) }; + }, + }; + } } pub fn mkdir(this: *NodeFS, args: Arguments.Mkdir, _: Flavor) Maybe(Return.Mkdir) { @@ -5701,21 +5720,35 @@ pub const NodeFS = struct { const path = args.path.sliceZ(&this.sync_error_buf); if (bun.StandaloneModuleGraph.get()) |graph| { if (graph.stat(path)) |*result| { - return .{ .result = .{ .stats = .init(result, args.big_int) } }; + return .{ .result = .{ .stats = .init(&Syscall.PosixStat.init(result), args.big_int) } }; } } - return switch (Syscall.stat(path)) { - .result => |*result| .{ - .result = .{ .stats = .init(result, args.big_int) }, - }, - .err => |err| brk: { - if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { - return .{ .result = .{ .not_found = {} } }; - } - break :brk .{ .err = err.withPath(args.path.slice()) }; - }, - }; + if (Environment.isLinux and Syscall.supports_statx_on_linux.load(.monotonic)) { + return switch (Syscall.statx(path, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks })) { + .result => |result| .{ + .result = .{ .stats = .init(&result, args.big_int) }, + }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return .{ .result = .{ .not_found = {} } }; + } + break :brk .{ .err = err.withPath(args.path.slice()) }; + }, + }; + } else { + return switch (Syscall.stat(path)) { + .result => |result| .{ + .result = .{ .stats = .init(&Syscall.PosixStat.init(&result), args.big_int) }, + }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return .{ .result = .{ .not_found = {} } }; + } + break :brk .{ .err = err.withPath(args.path.slice()) }; + }, + }; + } } pub fn symlink(this: *NodeFS, args: Arguments.Symlink, _: Flavor) Maybe(Return.Symlink) { diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index c10a391f3f..8ff99bde77 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -1,6 +1,6 @@ const log = bun.Output.scoped(.StatWatcher, .visible); -fn statToJSStats(globalThis: *jsc.JSGlobalObject, stats: *const bun.Stat, bigint: bool) bun.JSError!jsc.JSValue { +fn statToJSStats(globalThis: *jsc.JSGlobalObject, stats: *const bun.sys.PosixStat, bigint: bool) bun.JSError!jsc.JSValue { if (bigint) { return StatsBig.init(stats).toJS(globalThis); } else { @@ -192,7 +192,7 @@ pub const StatWatcher = struct { poll_ref: bun.Async.KeepAlive = .{}, - last_stat: bun.Stat, + last_stat: bun.sys.PosixStat, last_jsvalue: jsc.Strong.Optional, scheduler: bun.ptr.RefPtr(StatWatcherScheduler), @@ -352,7 +352,15 @@ pub const StatWatcher = struct { return; } - const stat = bun.sys.stat(this.path); + const stat: bun.sys.Maybe(bun.sys.PosixStat) = if (bun.Environment.isLinux and bun.sys.supports_statx_on_linux.load(.monotonic)) + bun.sys.statx(this.path, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks }) + else brk: { + const result = bun.sys.stat(this.path); + break :brk switch (result) { + .result => |r| bun.sys.Maybe(bun.sys.PosixStat){ .result = bun.sys.PosixStat.init(&r) }, + .err => |e| bun.sys.Maybe(bun.sys.PosixStat){ .err = e }, + }; + }; switch (stat) { .result => |res| { // we store the stat, but do not call the callback @@ -362,7 +370,7 @@ pub const StatWatcher = struct { .err => { // on enoent, eperm, we call cb with two zeroed stat objects // and store previous stat as a zeroed stat object, and then call the callback. - this.last_stat = std.mem.zeroes(bun.Stat); + this.last_stat = std.mem.zeroes(bun.sys.PosixStat); this.enqueueTaskConcurrent(jsc.ConcurrentTask.fromCallback(this, initialStatErrorOnMainThread)); }, } @@ -406,23 +414,39 @@ pub const StatWatcher = struct { /// Called from any thread pub fn restat(this: *StatWatcher) void { log("recalling stat", .{}); - const stat = bun.sys.stat(this.path); + const stat: bun.sys.Maybe(bun.sys.PosixStat) = if (bun.Environment.isLinux and bun.sys.supports_statx_on_linux.load(.monotonic)) + bun.sys.statx(this.path, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks }) + else brk: { + const result = bun.sys.stat(this.path); + break :brk switch (result) { + .result => |r| bun.sys.Maybe(bun.sys.PosixStat){ .result = bun.sys.PosixStat.init(&r) }, + .err => |e| bun.sys.Maybe(bun.sys.PosixStat){ .err = e }, + }; + }; const res = switch (stat) { .result => |res| res, - .err => std.mem.zeroes(bun.Stat), + .err => std.mem.zeroes(bun.sys.PosixStat), }; - var compare = res; - const StatT = @TypeOf(compare); - if (@hasField(StatT, "st_atim")) { - compare.st_atim = this.last_stat.st_atim; - } else if (@hasField(StatT, "st_atimespec")) { - compare.st_atimespec = this.last_stat.st_atimespec; - } else if (@hasField(StatT, "atim")) { - compare.atim = this.last_stat.atim; - } - - if (std.mem.eql(u8, std.mem.asBytes(&compare), std.mem.asBytes(&this.last_stat))) return; + // Ignore atime changes when comparing stats + // Compare field-by-field to avoid false positives from padding bytes + if (res.dev == this.last_stat.dev and + res.ino == this.last_stat.ino and + res.mode == this.last_stat.mode and + res.nlink == this.last_stat.nlink and + res.uid == this.last_stat.uid and + res.gid == this.last_stat.gid and + res.rdev == this.last_stat.rdev and + res.size == this.last_stat.size and + res.blksize == this.last_stat.blksize and + res.blocks == this.last_stat.blocks and + res.mtim.sec == this.last_stat.mtim.sec and + res.mtim.nsec == this.last_stat.mtim.nsec and + res.ctim.sec == this.last_stat.ctim.sec and + res.ctim.nsec == this.last_stat.ctim.nsec and + res.birthtim.sec == this.last_stat.birthtim.sec and + res.birthtim.nsec == this.last_stat.birthtim.nsec) + return; this.last_stat = res; this.enqueueTaskConcurrent(jsc.ConcurrentTask.fromCallback(this, swapAndCallListenerOnMainThread)); @@ -480,7 +504,7 @@ pub const StatWatcher = struct { // Instant.now will not fail on our target platforms. .last_check = std.time.Instant.now() catch unreachable, // InitStatTask is responsible for setting this - .last_stat = std.mem.zeroes(bun.Stat), + .last_stat = std.mem.zeroes(bun.sys.PosixStat), .last_jsvalue = .empty, .scheduler = vm.rareData().nodeFSStatWatcherScheduler(vm), .ref_count = .init(), diff --git a/src/sys.zig b/src/sys.zig index 4bf87f89f5..2062abc9ed 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -300,6 +300,7 @@ pub const Tag = enum(u8) { }; pub const Error = @import("./sys/Error.zig"); +pub const PosixStat = @import("./sys/PosixStat.zig").PosixStat; pub fn Maybe(comptime ReturnTypeT: type) type { return bun.api.node.Maybe(ReturnTypeT, Error); @@ -501,6 +502,7 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { log("stat({s}) = {d}", .{ bun.asByteSlice(path), rc }); if (Maybe(bun.Stat).errnoSysP(rc, .stat, path)) |err| return err; + return Maybe(bun.Stat){ .result = stat_ }; } } @@ -551,8 +553,133 @@ pub fn fstat(fd: bun.FileDescriptor) Maybe(bun.Stat) { log("fstat({}) = {d}", .{ fd, rc }); if (Maybe(bun.Stat).errnoSysFd(rc, .fstat, fd)) |err| return err; + return Maybe(bun.Stat){ .result = stat_ }; } + +pub const StatxField = enum(comptime_int) { + type = linux.STATX_TYPE, + mode = linux.STATX_MODE, + nlink = linux.STATX_NLINK, + uid = linux.STATX_UID, + gid = linux.STATX_GID, + atime = linux.STATX_ATIME, + mtime = linux.STATX_MTIME, + ctime = linux.STATX_CTIME, + btime = linux.STATX_BTIME, + ino = linux.STATX_INO, + size = linux.STATX_SIZE, + blocks = linux.STATX_BLOCKS, +}; + +// Linux Kernel v4.11 +pub var supports_statx_on_linux = std.atomic.Value(bool).init(true); + +/// Linux kernel makedev encoding for device numbers +/// From glibc sys/sysmacros.h and Linux kernel +/// dev_t layout (64 bits): +/// Bits 31-20: major high (12 bits) +/// Bits 19-8: minor high (12 bits) +/// Bits 7-0: minor low (8 bits) +inline fn makedev(major: u32, minor: u32) u64 { + const maj: u64 = major & 0xFFF; + const min: u64 = minor & 0xFFFFF; + return (maj << 8) | (min & 0xFF) | ((min & 0xFFF00) << 12); +} + +fn statxImpl(fd: bun.FileDescriptor, path: ?[*:0]const u8, flags: u32, mask: u32) Maybe(PosixStat) { + if (comptime !Environment.isLinux) { + @compileError("statx is only supported on Linux"); + } + + var buf: linux.Statx = undefined; + + while (true) { + const rc = linux.statx(@intCast(fd.cast()), if (path) |p| p else "", flags, mask, &buf); + + if (Maybe(PosixStat).errnoSys(rc, .statx)) |err| { + // Retry on EINTR + if (err.getErrno() == .INTR) continue; + + // Handle unsupported statx by setting flag and falling back + if (err.getErrno() == .NOSYS or err.getErrno() == .OPNOTSUPP) { + supports_statx_on_linux.store(false, .monotonic); + if (path) |p| { + const path_span = bun.span(p); + const fallback = if (flags & linux.AT.SYMLINK_NOFOLLOW != 0) lstat(path_span) else stat(path_span); + return switch (fallback) { + .result => |s| .{ .result = PosixStat.init(&s) }, + .err => |e| .{ .err = e }, + }; + } else { + return switch (fstat(fd)) { + .result => |s| .{ .result = PosixStat.init(&s) }, + .err => |e| .{ .err = e }, + }; + } + } + + return err; + } + + // Convert statx buffer to PosixStat structure + const stat_ = PosixStat{ + .dev = makedev(buf.dev_major, buf.dev_minor), + .ino = buf.ino, + .mode = buf.mode, + .nlink = buf.nlink, + .uid = buf.uid, + .gid = buf.gid, + .rdev = makedev(buf.rdev_major, buf.rdev_minor), + .size = @bitCast(buf.size), + .blksize = @intCast(buf.blksize), + .blocks = @bitCast(buf.blocks), + .atim = .{ .sec = buf.atime.sec, .nsec = buf.atime.nsec }, + .mtim = .{ .sec = buf.mtime.sec, .nsec = buf.mtime.nsec }, + .ctim = .{ .sec = buf.ctime.sec, .nsec = buf.ctime.nsec }, + .birthtim = if (buf.mask & linux.STATX_BTIME != 0) + .{ .sec = buf.btime.sec, .nsec = buf.btime.nsec } + else + .{ .sec = 0, .nsec = 0 }, + }; + + return .{ .result = stat_ }; + } +} + +pub fn fstatx(fd: bun.FileDescriptor, comptime fields: []const StatxField) Maybe(PosixStat) { + const mask: u32 = comptime brk: { + var i: u32 = 0; + for (fields) |field| { + i |= @intFromEnum(field); + } + break :brk i; + }; + return statxImpl(fd, null, linux.AT.EMPTY_PATH, mask); +} + +pub fn statx(path: [*:0]const u8, comptime fields: []const StatxField) Maybe(PosixStat) { + const mask: u32 = comptime brk: { + var i: u32 = 0; + for (fields) |field| { + i |= @intFromEnum(field); + } + break :brk i; + }; + return statxImpl(bun.FD.fromNative(std.posix.AT.FDCWD), path, 0, mask); +} + +pub fn lstatx(path: [*:0]const u8, comptime fields: []const StatxField) Maybe(PosixStat) { + const mask: u32 = comptime brk: { + var i: u32 = 0; + for (fields) |field| { + i |= @intFromEnum(field); + } + break :brk i; + }; + return statxImpl(bun.FD.fromNative(std.posix.AT.FDCWD), path, linux.AT.SYMLINK_NOFOLLOW, mask); +} + pub fn lutimes(path: [:0]const u8, atime: jsc.Node.TimeLike, mtime: jsc.Node.TimeLike) Maybe(void) { if (comptime Environment.isWindows) { return sys_uv.lutimes(path, atime, mtime); diff --git a/src/sys/PosixStat.zig b/src/sys/PosixStat.zig new file mode 100644 index 0000000000..c8975032af --- /dev/null +++ b/src/sys/PosixStat.zig @@ -0,0 +1,96 @@ +/// POSIX-like stat structure with birthtime support for node:fs +/// This extends the standard POSIX stat with birthtime (creation time) +pub const PosixStat = extern struct { + dev: u64, + ino: u64, + mode: u32, + nlink: u64, + uid: u32, + gid: u32, + rdev: u64, + size: i64, + blksize: i64, + blocks: i64, + + /// Access time + atim: bun.timespec, + /// Modification time + mtim: bun.timespec, + /// Change time (metadata) + ctim: bun.timespec, + /// Birth time (creation time) - may be zero if not supported + birthtim: bun.timespec, + + /// Convert platform-specific bun.Stat to PosixStat + pub fn init(stat_: *const bun.Stat) PosixStat { + if (Environment.isWindows) { + // Windows: all fields need casting + const atime_val = stat_.atime(); + const mtime_val = stat_.mtime(); + const ctime_val = stat_.ctime(); + const birthtime_val = stat_.birthtime(); + + return PosixStat{ + .dev = @intCast(stat_.dev), + .ino = @intCast(stat_.ino), + .mode = @intCast(stat_.mode), + .nlink = @intCast(stat_.nlink), + .uid = @intCast(stat_.uid), + .gid = @intCast(stat_.gid), + .rdev = @intCast(stat_.rdev), + .size = @intCast(stat_.size), + .blksize = @intCast(stat_.blksize), + .blocks = @intCast(stat_.blocks), + .atim = .{ .sec = atime_val.sec, .nsec = atime_val.nsec }, + .mtim = .{ .sec = mtime_val.sec, .nsec = mtime_val.nsec }, + .ctim = .{ .sec = ctime_val.sec, .nsec = ctime_val.nsec }, + .birthtim = .{ .sec = birthtime_val.sec, .nsec = birthtime_val.nsec }, + }; + } else { + // POSIX (Linux/macOS): use accessor methods and cast types + const atime_val = stat_.atime(); + const mtime_val = stat_.mtime(); + const ctime_val = stat_.ctime(); + const birthtime_val = if (Environment.isLinux) + bun.timespec.epoch + else + stat_.birthtime(); + + return PosixStat{ + .dev = @intCast(stat_.dev), + .ino = @intCast(stat_.ino), + .mode = @intCast(stat_.mode), + .nlink = @intCast(stat_.nlink), + .uid = @intCast(stat_.uid), + .gid = @intCast(stat_.gid), + .rdev = @intCast(stat_.rdev), + .size = @intCast(stat_.size), + .blksize = @intCast(stat_.blksize), + .blocks = @intCast(stat_.blocks), + .atim = .{ .sec = atime_val.sec, .nsec = atime_val.nsec }, + .mtim = .{ .sec = mtime_val.sec, .nsec = mtime_val.nsec }, + .ctim = .{ .sec = ctime_val.sec, .nsec = ctime_val.nsec }, + .birthtim = .{ .sec = birthtime_val.sec, .nsec = birthtime_val.nsec }, + }; + } + } + + pub fn atime(self: *const PosixStat) bun.timespec { + return self.atim; + } + + pub fn mtime(self: *const PosixStat) bun.timespec { + return self.mtim; + } + + pub fn ctime(self: *const PosixStat) bun.timespec { + return self.ctim; + } + + pub fn birthtime(self: *const PosixStat) bun.timespec { + return self.birthtim; + } +}; + +const bun = @import("bun"); +const Environment = bun.Environment; diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 4e01d9bb5f..48f548c1f0 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -7,6 +7,7 @@ comptime { pub const log = bun.sys.syslog; pub const Error = bun.sys.Error; +pub const PosixStat = bun.sys.PosixStat; // libuv dont support openat (https://github.com/libuv/libuv/issues/4167) pub const openat = bun.sys.openat; diff --git a/test/js/bun/util/bun-file.test.ts b/test/js/bun/util/bun-file.test.ts index 48d543f58b..d4e8d5f1b3 100644 --- a/test/js/bun/util/bun-file.test.ts +++ b/test/js/bun/util/bun-file.test.ts @@ -15,7 +15,11 @@ test("delete() and stat() should work with unicode paths", async () => { expect(async () => { await Bun.file(filename).stat(); - }).toThrow(`ENOENT: no such file or directory, stat '${filename}'`); + }).toThrow( + process.platform === "linux" + ? `ENOENT: no such file or directory, statx '${filename}'` + : `ENOENT: no such file or directory, stat '${filename}'`, + ); await Bun.write(filename, "HI"); diff --git a/test/js/node/fs/fs-birthtime-linux.test.ts b/test/js/node/fs/fs-birthtime-linux.test.ts new file mode 100644 index 0000000000..e1cd536902 --- /dev/null +++ b/test/js/node/fs/fs-birthtime-linux.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "bun:test"; +import { isLinux, tempDirWithFiles } from "harness"; +import { chmodSync, closeSync, fstatSync, lstatSync, openSync, statSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +describe.skipIf(!isLinux)("birthtime", () => { + it("should return non-zero birthtime on Linux", () => { + const dir = tempDirWithFiles("birthtime-test", { + "test.txt": "initial content", + }); + + const filepath = join(dir, "test.txt"); + const stats = statSync(filepath); + + // On Linux with statx support, birthtime should be > 0 + expect(stats.birthtimeMs).toBeGreaterThan(0); + expect(stats.birthtime.getTime()).toBeGreaterThan(0); + expect(stats.birthtime.getFullYear()).toBeGreaterThanOrEqual(2025); + }); + + it("birthtime should remain constant while other timestamps change", () => { + const dir = tempDirWithFiles("birthtime-immutable", {}); + const filepath = join(dir, "immutable-test.txt"); + + // Create file and capture birthtime + writeFileSync(filepath, "original"); + const initialStats = statSync(filepath); + const birthtime = initialStats.birthtimeMs; + + expect(birthtime).toBeGreaterThan(0); + + // Wait a bit to ensure timestamps would differ + Bun.sleepSync(10); + + // Modify content (updates mtime and ctime) + writeFileSync(filepath, "modified"); + const afterModify = statSync(filepath); + + expect(afterModify.birthtimeMs).toBe(birthtime); + expect(afterModify.mtimeMs).toBeGreaterThan(initialStats.mtimeMs); + + // Wait again + Bun.sleepSync(10); + + // Change permissions (updates ctime) + chmodSync(filepath, 0o755); + const afterChmod = statSync(filepath); + + expect(afterChmod.birthtimeMs).toBe(birthtime); + expect(afterChmod.ctimeMs).toBeGreaterThan(afterModify.ctimeMs); + }); + + it("birthtime should work with lstat and fstat", () => { + const dir = tempDirWithFiles("birthtime-variants", { + "test.txt": "content", + }); + + const filepath = join(dir, "test.txt"); + + const statResult = statSync(filepath); + const lstatResult = lstatSync(filepath); + const fd = openSync(filepath, "r"); + const fstatResult = fstatSync(fd); + closeSync(fd); + + // All three should return the same birthtime + expect(statResult.birthtimeMs).toBeGreaterThan(0); + expect(lstatResult.birthtimeMs).toBe(statResult.birthtimeMs); + expect(fstatResult.birthtimeMs).toBe(statResult.birthtimeMs); + + expect(statResult.birthtime.getTime()).toBe(lstatResult.birthtime.getTime()); + expect(statResult.birthtime.getTime()).toBe(fstatResult.birthtime.getTime()); + }); + + it("birthtime should work with BigInt stats", () => { + const dir = tempDirWithFiles("birthtime-bigint", { + "test.txt": "content", + }); + + const filepath = join(dir, "test.txt"); + + const regularStats = statSync(filepath); + const bigintStats = statSync(filepath, { bigint: true }); + + expect(bigintStats.birthtimeMs).toBeGreaterThan(0n); + expect(bigintStats.birthtimeNs).toBeGreaterThan(0n); + + // birthtimeMs should be close (within rounding) + const regularMs = BigInt(Math.floor(regularStats.birthtimeMs)); + expect(bigintStats.birthtimeMs).toBe(regularMs); + + // birthtimeNs should have nanosecond precision + expect(bigintStats.birthtimeNs).toBeGreaterThanOrEqual(bigintStats.birthtimeMs * 1000000n); + }); + + it("birthtime should be less than or equal to all other timestamps on creation", () => { + const dir = tempDirWithFiles("birthtime-ordering", {}); + const filepath = join(dir, "new-file.txt"); + + writeFileSync(filepath, "new content"); + const stats = statSync(filepath); + + // birthtime should be <= all other times since it's when file was created + expect(stats.birthtimeMs).toBeLessThanOrEqual(stats.mtimeMs); + expect(stats.birthtimeMs).toBeLessThanOrEqual(stats.atimeMs); + expect(stats.birthtimeMs).toBeLessThanOrEqual(stats.ctimeMs); + }); +}); From 3c96c8a63d60777e523e7cd06e451ec148df88b5 Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 05:25:30 -0700 Subject: [PATCH 030/191] Add Claude Code hooks to prevent common development mistakes (#23241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Added Claude Code hooks to prevent common development mistakes when working on the Bun codebase. ## Changes - Created `.claude/hooks/pre-bash-zig-build.js` - A pre-bash hook that validates commands - Created `.claude/settings.json` - Hook configuration ## Prevented Mistakes 1. **Running `zig build obj` directly** → Redirects to use `bun bd` 2. **Using `bun test` in development** → Must use `bun bd test` (or set `USE_SYSTEM_BUN=1`) 3. **Combining snapshot updates with test filters** → Prevents `-u`/`--update-snapshots` with `-t`/`--test-name-pattern` 4. **Running `bun bd` with timeout** → Build needs time to complete without timeout 5. **Running `bun bd test` from repo root** → Must specify a test file path to avoid running all tests ## Test plan - [x] Tested all validation rules with various command combinations - [x] Verified USE_SYSTEM_BUN=1 bypass works - [x] Verified file path detection works correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- .claude/hooks/post-edit-zig-format.js | 90 +++++++++++++ .claude/hooks/pre-bash-zig-build.js | 175 ++++++++++++++++++++++++++ .claude/settings.json | 26 ++++ 3 files changed, 291 insertions(+) create mode 100755 .claude/hooks/post-edit-zig-format.js create mode 100755 .claude/hooks/pre-bash-zig-build.js create mode 100644 .claude/settings.json diff --git a/.claude/hooks/post-edit-zig-format.js b/.claude/hooks/post-edit-zig-format.js new file mode 100755 index 0000000000..3cf96837b9 --- /dev/null +++ b/.claude/hooks/post-edit-zig-format.js @@ -0,0 +1,90 @@ +#!/usr/bin/env bun +import { extname } from "path"; +import { spawnSync } from "child_process"; + +const input = await Bun.stdin.json(); + +const toolName = input.tool_name; +const toolInput = input.tool_input || {}; +const filePath = toolInput.file_path; + +// Only process Write, Edit, and MultiEdit tools +if (!["Write", "Edit", "MultiEdit"].includes(toolName)) { + process.exit(0); +} + +const ext = extname(filePath); + +// Only format known files +if (!filePath) { + process.exit(0); +} + +function formatZigFile() { + try { + // Format the Zig file + const result = spawnSync("vendor/zig/zig.exe", ["fmt", filePath], { + cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(), + encoding: "utf-8", + }); + + if (result.error) { + console.error(`Failed to format ${filePath}: ${result.error.message}`); + process.exit(0); + } + + if (result.status !== 0) { + console.error(`zig fmt failed for ${filePath}:`); + if (result.stderr) { + console.error(result.stderr); + } + process.exit(0); + } + } catch (error) {} +} + +function formatTypeScriptFile() { + try { + // Format the TypeScript file + const result = spawnSync( + "./node_modules/.bin/prettier", + ["--plugin=prettier-plugin-organize-imports", "--config", ".prettierrc", "--write", filePath], + { + cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(), + encoding: "utf-8", + }, + ); + } catch (error) {} +} + +if (ext === ".zig") { + formatZigFile(); +} else if ( + [ + ".cjs", + ".css", + ".html", + ".js", + ".json", + ".jsonc", + ".jsx", + ".less", + ".mjs", + ".pcss", + ".postcss", + ".sass", + ".scss", + ".styl", + ".stylus", + ".toml", + ".ts", + ".tsx", + ".yaml", + ].includes(ext) +) { + formatTypeScriptFile(); +} else if (ext === ".cpp" || ext === ".c" || ext === ".h") { + formatCppFile(); +} + +process.exit(0); diff --git a/.claude/hooks/pre-bash-zig-build.js b/.claude/hooks/pre-bash-zig-build.js new file mode 100755 index 0000000000..24b47b06fc --- /dev/null +++ b/.claude/hooks/pre-bash-zig-build.js @@ -0,0 +1,175 @@ +#!/usr/bin/env bun +import { basename, extname } from "path"; + +const input = await Bun.stdin.json(); + +const toolName = input.tool_name; +const toolInput = input.tool_input || {}; +const command = toolInput.command || ""; +const timeout = toolInput.timeout; +const cwd = input.cwd || ""; + +// Get environment variables from the hook context +// Note: We check process.env directly as env vars are inherited +let useSystemBun = process.env.USE_SYSTEM_BUN; + +if (toolName !== "Bash" || !command) { + process.exit(0); +} + +function denyWithReason(reason) { + const output = { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: reason, + }, + }; + console.log(JSON.stringify(output)); + process.exit(0); +} + +// Parse the command to extract argv0 and positional args +let tokens; +try { + // Simple shell parsing - split on spaces but respect quotes (both single and double) + tokens = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map(t => t.replace(/^['"]|['"]$/g, "")) || []; +} catch { + process.exit(0); +} + +if (tokens.length === 0) { + process.exit(0); +} + +// Strip inline environment variable assignments (e.g., FOO=1 bun test) +const inlineEnv = new Map(); +let commandStart = 0; +while ( + commandStart < tokens.length && + /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[commandStart]) && + !tokens[commandStart].includes("/") +) { + const [name, value = ""] = tokens[commandStart].split("=", 2); + inlineEnv.set(name, value); + commandStart++; +} +if (commandStart >= tokens.length) { + process.exit(0); +} +tokens = tokens.slice(commandStart); +useSystemBun = inlineEnv.get("USE_SYSTEM_BUN") ?? useSystemBun; + +// Get the executable name (argv0) +const argv0 = basename(tokens[0], extname(tokens[0])); + +// Check if it's zig or zig.exe +if (argv0 === "zig") { + // Filter out flags (starting with -) to get positional arguments + const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-")); + + // Check if the positional args contain "build" followed by "obj" + if (positionalArgs.length >= 2 && positionalArgs[0] === "build" && positionalArgs[1] === "obj") { + denyWithReason("error: Use `bun bd` to build Bun and wait patiently"); + } +} + +// Check if argv0 is timeout and the command is "bun bd" +if (argv0 === "timeout") { + // Find the actual command after timeout and its arguments + const timeoutArgEndIndex = tokens.slice(1).findIndex(t => !t.startsWith("-") && !/^\d/.test(t)); + if (timeoutArgEndIndex === -1) { + process.exit(0); + } + + const actualCommandIndex = timeoutArgEndIndex + 1; + if (actualCommandIndex >= tokens.length) { + process.exit(0); + } + + const actualCommand = basename(tokens[actualCommandIndex]); + const restArgs = tokens.slice(actualCommandIndex + 1); + + // Check if it's "bun bd" or "bun-debug bd" without other positional args + if (actualCommand === "bun" || actualCommand.includes("bun-debug")) { + const positionalArgs = restArgs.filter(arg => !arg.startsWith("-")); + if (positionalArgs.length === 1 && positionalArgs[0] === "bd") { + denyWithReason("error: Run `bun bd` without a timeout"); + } + } +} + +// Check if command is "bun .* test" or "bun-debug test" with -u/--update-snapshots AND -t/--test-name-pattern +if (argv0 === "bun" || argv0.includes("bun-debug")) { + const allArgs = tokens.slice(1); + + // Check if "test" is in positional args or "bd" followed by "test" + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + const hasTest = positionalArgs.includes("test") || (positionalArgs[0] === "bd" && positionalArgs[1] === "test"); + + if (hasTest) { + const hasUpdateSnapshots = allArgs.some(arg => arg === "-u" || arg === "--update-snapshots"); + const hasTestNamePattern = allArgs.some(arg => arg === "-t" || arg === "--test-name-pattern"); + + if (hasUpdateSnapshots && hasTestNamePattern) { + denyWithReason("error: Cannot use -u/--update-snapshots with -t/--test-name-pattern"); + } + } +} + +// Check if timeout option is set for "bun bd" command +if (timeout !== undefined && (argv0 === "bun" || argv0.includes("bun-debug"))) { + const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-")); + if (positionalArgs.length === 1 && positionalArgs[0] === "bd") { + denyWithReason("error: Run `bun bd` without a timeout"); + } +} + +// Check if running "bun test " without USE_SYSTEM_BUN=1 +if ((argv0 === "bun" || argv0.includes("bun-debug")) && useSystemBun !== "1") { + const allArgs = tokens.slice(1); + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + + // Check if it's "test" (not "bd test") + if (positionalArgs.length >= 1 && positionalArgs[0] === "test" && positionalArgs[0] !== "bd") { + denyWithReason( + "error: In development, use `bun bd test ` to test your changes. If you meant to use a release version, set USE_SYSTEM_BUN=1", + ); + } +} + +// Check if running "bun bd test" from bun repo root or test folder without a file path +if (argv0 === "bun" || argv0.includes("bun-debug")) { + const allArgs = tokens.slice(1); + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + + // Check if it's "bd test" + if (positionalArgs.length >= 2 && positionalArgs[0] === "bd" && positionalArgs[1] === "test") { + // Check if cwd is the bun repo root or test folder + const isBunRepoRoot = cwd === "/workspace/bun" || cwd.endsWith("/bun"); + const isTestFolder = cwd.endsWith("/bun/test"); + + if (isBunRepoRoot || isTestFolder) { + // Check if there's a file path argument (looks like a path: contains / or has test extension) + const hasFilePath = positionalArgs + .slice(2) + .some( + arg => + arg.includes("/") || + arg.endsWith(".test.ts") || + arg.endsWith(".test.js") || + arg.endsWith(".test.tsx") || + arg.endsWith(".test.jsx"), + ); + + if (!hasFilePath) { + denyWithReason( + "error: `bun bd test` from repo root or test folder will run all tests. Use `bun bd test ` with a specific test file.", + ); + } + } + } +} + +// Allow the command to proceed +process.exit(0); diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..387633d5fd --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-bash-zig-build.js" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-zig-format.js" + } + ] + } + ] + } +} From 13a3c4de607e84af4accc7a6b3d50a85415ac2bd Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 05:56:21 -0700 Subject: [PATCH 031/191] fix(install): fetch os/cpu metadata during yarn.lock migration (#23143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary During `yarn.lock` migration, OS/CPU package metadata was not being fetched from the npm registry when missing from `yarn.lock`. This caused packages with platform-specific requirements to not be properly marked, potentially leading to incorrect package installation behavior. ## Changes Updated `fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration` to conditionally fetch OS/CPU metadata: - **For yarn.lock migration**: Fetches OS/CPU metadata from npm registry when not present in yarn.lock (`update_os_cpu = true`) - **For pnpm-lock.yaml migration**: Skips OS/CPU fetching since pnpm-lock.yaml already includes this data (`update_os_cpu = false`) ### Files Modified - `src/install/lockfile.zig` - Added comptime `update_os_cpu` parameter and conditional logic to fetch OS/CPU metadata - `src/install/yarn.zig` - Pass `true` to enable OS/CPU fetching for yarn migrations - `src/install/pnpm.zig` - Pass `false` to skip OS/CPU fetching for pnpm migrations (already parsed from lockfile) ## Why This Approach - `yarn.lock` format often doesn't include OS/CPU constraints, requiring us to fetch from npm registry - `pnpm-lock.yaml` already parses OS/CPU during migration (lines 618-621 in pnpm.zig), making additional fetching redundant - Using a comptime parameter allows the compiler to optimize away the unused code path ## Testing - ✅ Debug build compiles successfully - Tested that the function correctly updates `pkg_meta.os` and `pkg_meta.arch` only when: - `update_os_cpu` is `true` (yarn migration) - Current values are `.all` (not already set) - Package metadata is available from npm registry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Dylan Conway Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/install/lockfile.zig | 101 ++++++++++++----- src/install/pnpm.zig | 2 +- src/install/yarn.zig | 2 +- .../yarn-lock-migration.test.ts.snap | 37 +++++- .../migration/yarn-lock-migration.test.ts | 107 ++++++++++++++++++ 5 files changed, 217 insertions(+), 32 deletions(-) diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index ee541146b4..60f0837180 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -951,7 +951,7 @@ const PendingResolution = struct { const PendingResolutions = std.ArrayList(PendingResolution); -pub fn fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(this: *Lockfile, manager: *PackageManager) OOM!void { +pub fn fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(this: *Lockfile, manager: *PackageManager, comptime update_os_cpu: bool) OOM!void { manager.populateManifestCache(.all) catch return; const pkgs = this.packages.slice(); @@ -960,40 +960,87 @@ pub fn fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(this: *Lockfile, ma const pkg_name_hashes = pkgs.items(.name_hash); const pkg_resolutions = pkgs.items(.resolution); const pkg_bins = pkgs.items(.bin); + const pkg_metas = if (update_os_cpu) pkgs.items(.meta) else undefined; - for (pkg_names, pkg_name_hashes, pkg_resolutions, pkg_bins) |pkg_name, pkg_name_hash, pkg_res, *pkg_bin| { - switch (pkg_res.tag) { - .npm => { - const manifest = manager.manifests.byNameHash( - manager, - manager.scopeForPackageName(pkg_name.slice(this.buffers.string_bytes.items)), - pkg_name_hash, - .load_from_memory_fallback_to_disk, - ) orelse { - continue; - }; + if (update_os_cpu) { + for (pkg_names, pkg_name_hashes, pkg_resolutions, pkg_bins, pkg_metas) |pkg_name, pkg_name_hash, pkg_res, *pkg_bin, *pkg_meta| { + switch (pkg_res.tag) { + .npm => { + const manifest = manager.manifests.byNameHash( + manager, + manager.scopeForPackageName(pkg_name.slice(this.buffers.string_bytes.items)), + pkg_name_hash, + .load_from_memory_fallback_to_disk, + ) orelse { + continue; + }; - const pkg = manifest.findByVersion(pkg_res.value.npm.version) orelse { - continue; - }; + const pkg = manifest.findByVersion(pkg_res.value.npm.version) orelse { + continue; + }; - var builder = manager.lockfile.stringBuilder(); + var builder = manager.lockfile.stringBuilder(); - var bin_extern_strings_count: u32 = 0; + var bin_extern_strings_count: u32 = 0; - bin_extern_strings_count += pkg.package.bin.count(manifest.string_buf, manifest.extern_strings_bin_entries, @TypeOf(&builder), &builder); + bin_extern_strings_count += pkg.package.bin.count(manifest.string_buf, manifest.extern_strings_bin_entries, @TypeOf(&builder), &builder); - try builder.allocate(); - defer builder.clamp(); + try builder.allocate(); + defer builder.clamp(); - var extern_strings_list = &manager.lockfile.buffers.extern_strings; - try extern_strings_list.ensureUnusedCapacity(manager.lockfile.allocator, bin_extern_strings_count); - extern_strings_list.items.len += bin_extern_strings_count; - const extern_strings = extern_strings_list.items[extern_strings_list.items.len - bin_extern_strings_count ..]; + var extern_strings_list = &manager.lockfile.buffers.extern_strings; + try extern_strings_list.ensureUnusedCapacity(manager.lockfile.allocator, bin_extern_strings_count); + extern_strings_list.items.len += bin_extern_strings_count; + const extern_strings = extern_strings_list.items[extern_strings_list.items.len - bin_extern_strings_count ..]; - pkg_bin.* = pkg.package.bin.clone(manifest.string_buf, manifest.extern_strings_bin_entries, extern_strings_list.items, extern_strings, @TypeOf(&builder), &builder); - }, - else => {}, + pkg_bin.* = pkg.package.bin.clone(manifest.string_buf, manifest.extern_strings_bin_entries, extern_strings_list.items, extern_strings, @TypeOf(&builder), &builder); + + // Update os/cpu metadata if not already set + if (pkg_meta.os == .all) { + pkg_meta.os = pkg.package.os; + } + if (pkg_meta.arch == .all) { + pkg_meta.arch = pkg.package.cpu; + } + }, + else => {}, + } + } + } else { + for (pkg_names, pkg_name_hashes, pkg_resolutions, pkg_bins) |pkg_name, pkg_name_hash, pkg_res, *pkg_bin| { + switch (pkg_res.tag) { + .npm => { + const manifest = manager.manifests.byNameHash( + manager, + manager.scopeForPackageName(pkg_name.slice(this.buffers.string_bytes.items)), + pkg_name_hash, + .load_from_memory_fallback_to_disk, + ) orelse { + continue; + }; + + const pkg = manifest.findByVersion(pkg_res.value.npm.version) orelse { + continue; + }; + + var builder = manager.lockfile.stringBuilder(); + + var bin_extern_strings_count: u32 = 0; + + bin_extern_strings_count += pkg.package.bin.count(manifest.string_buf, manifest.extern_strings_bin_entries, @TypeOf(&builder), &builder); + + try builder.allocate(); + defer builder.clamp(); + + var extern_strings_list = &manager.lockfile.buffers.extern_strings; + try extern_strings_list.ensureUnusedCapacity(manager.lockfile.allocator, bin_extern_strings_count); + extern_strings_list.items.len += bin_extern_strings_count; + const extern_strings = extern_strings_list.items[extern_strings_list.items.len - bin_extern_strings_count ..]; + + pkg_bin.* = pkg.package.bin.clone(manifest.string_buf, manifest.extern_strings_bin_entries, extern_strings_list.items, extern_strings, @TypeOf(&builder), &builder); + }, + else => {}, + } } } } diff --git a/src/install/pnpm.zig b/src/install/pnpm.zig index 33e733a6ad..e1b7cf0e8d 100644 --- a/src/install/pnpm.zig +++ b/src/install/pnpm.zig @@ -824,7 +824,7 @@ pub fn migratePnpmLockfile( try lockfile.resolve(log); - try lockfile.fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(manager); + try lockfile.fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(manager, false); try updatePackageJsonAfterMigration(allocator, manager, log, dir, found_patches); diff --git a/src/install/yarn.zig b/src/install/yarn.zig index 21af400e75..23e71140d5 100644 --- a/src/install/yarn.zig +++ b/src/install/yarn.zig @@ -1671,7 +1671,7 @@ pub fn migrateYarnLockfile( try this.resolve(log); - try this.fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(manager); + try this.fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(manager, true); if (Environment.allow_assert) { try this.verifyData(); diff --git a/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap b/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap index 3cbbd10486..3d4499931d 100644 --- a/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap +++ b/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap @@ -79,7 +79,7 @@ exports[`yarn.lock migration basic complex yarn.lock with multiple dependencies "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "fsevents": ["fsevents@2.3.3", "", {}, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "get-intrinsic": ["get-intrinsic@1.2.2", "", { "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA=="], @@ -290,7 +290,7 @@ exports[`yarn.lock migration basic migration with realistic complex yarn.lock: c "eslint": ["eslint@8.35.0", "", { "dependencies": { "@eslint/eslintrc": "^2.0.0", "@eslint/js": "8.35.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", "espree": "^9.4.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw=="], - "fsevents": ["fsevents@2.3.2", "", {}, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1176,7 +1176,7 @@ exports[`bun pm migrate for existing yarn.lock yarn-cli-repo: yarn-cli-repo 1`] "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="], - "fsevents": ["fsevents@1.2.4", "", { "dependencies": { "nan": "^2.9.2", "node-pre-gyp": "^0.10.0" } }, "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg=="], + "fsevents": ["fsevents@1.2.4", "", { "dependencies": { "nan": "^2.9.2", "node-pre-gyp": "^0.10.0" }, "os": "darwin" }, "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg=="], "function-bind": ["function-bind@1.1.1", "", {}, "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="], @@ -3161,3 +3161,34 @@ exports[`bun pm migrate for existing yarn.lock yarn-stuff/abbrev-link-target: ya } " `; + +exports[`bun pm migrate for existing yarn.lock yarn.lock with packages that have os/cpu requirements: os-cpu-yarn-migration 1`] = ` +"{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "os-cpu-test", + "dependencies": { + "fsevents": "^2.3.2", + "esbuild": "^0.17.0", + }, + }, + }, + "packages": { + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Mj/VEUqd5+2h0EBPdMzNdGXnGxbLPg6H5TF8xsHY4X5UAP0FUbDKJhtKu+6iLpIjKjWEvb5XrFyZdVy9OTg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="], + + "esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + } +} +" +`; diff --git a/test/cli/install/migration/yarn-lock-migration.test.ts b/test/cli/install/migration/yarn-lock-migration.test.ts index 54329d56ec..da3fbd786e 100644 --- a/test/cli/install/migration/yarn-lock-migration.test.ts +++ b/test/cli/install/migration/yarn-lock-migration.test.ts @@ -1372,4 +1372,111 @@ describe("bun pm migrate for existing yarn.lock", () => { const bunLockContent = await Bun.file(join(tempDir, "bun.lock")).text(); expect(bunLockContent).toMatchSnapshot(folder); }); + + test("yarn.lock with packages that have os/cpu requirements", async () => { + const tempDir = tempDirWithFiles("yarn-migration-os-cpu", { + "package.json": JSON.stringify( + { + name: "os-cpu-test", + version: "1.0.0", + dependencies: { + fsevents: "^2.3.2", + esbuild: "^0.17.0", + }, + }, + null, + 2, + ), + "yarn.lock": `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@esbuild/android-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" + integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== + +"@esbuild/darwin-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" + integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== + +"@esbuild/darwin-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" + integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== + +"@esbuild/linux-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" + integrity sha512-ct1Mj/VEUqd5+2h0EBPdMzNdGXnGxbLPg6H5TF8xsHY4X5UAP0FUbDKJhtKu+6iLpIjKjWEvb5XrFyZdVy9OTg== + +"@esbuild/linux-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" + integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== + +esbuild@^0.17.0: + version "0.17.19" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955" + integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw== + optionalDependencies: + "@esbuild/android-arm" "0.17.19" + "@esbuild/android-arm64" "0.17.19" + "@esbuild/android-x64" "0.17.19" + "@esbuild/darwin-arm64" "0.17.19" + "@esbuild/darwin-x64" "0.17.19" + "@esbuild/freebsd-arm64" "0.17.19" + "@esbuild/freebsd-x64" "0.17.19" + "@esbuild/linux-arm" "0.17.19" + "@esbuild/linux-arm64" "0.17.19" + "@esbuild/linux-ia32" "0.17.19" + "@esbuild/linux-loong64" "0.17.19" + "@esbuild/linux-mips64el" "0.17.19" + "@esbuild/linux-ppc64" "0.17.19" + "@esbuild/linux-riscv64" "0.17.19" + "@esbuild/linux-s390x" "0.17.19" + "@esbuild/linux-x64" "0.17.19" + "@esbuild/netbsd-x64" "0.17.19" + "@esbuild/openbsd-x64" "0.17.19" + "@esbuild/sunos-x64" "0.17.19" + "@esbuild/win32-arm64" "0.17.19" + "@esbuild/win32-ia32" "0.17.19" + "@esbuild/win32-x64" "0.17.19" + +fsevents@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +`, + }); + + // Run bun pm migrate + const migrateResult = await Bun.spawn({ + cmd: [bunExe(), "pm", "migrate", "-f"], + cwd: tempDir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(migrateResult.stdout).text(), + new Response(migrateResult.stderr).text(), + migrateResult.exited, + ]); + + expect(exitCode).toBe(0); + expect(fs.existsSync(join(tempDir, "bun.lock"))).toBe(true); + + const bunLockContent = fs.readFileSync(join(tempDir, "bun.lock"), "utf8"); + expect(bunLockContent).toMatchSnapshot("os-cpu-yarn-migration"); + + // Verify that the lockfile contains the expected os/cpu metadata by checking the snapshot + // fsevents should have darwin os constraint, esbuild packages should have arch constraints + expect(bunLockContent).toContain("fsevents"); + expect(bunLockContent).toContain("@esbuild/linux-arm64"); + expect(bunLockContent).toContain("@esbuild/darwin-arm64"); + }); }); From db37c36d31e7bdcf8e369538c0d672d28faf5030 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 06:07:38 -0700 Subject: [PATCH 032/191] Update post-edit-zig-format.js --- .claude/hooks/post-edit-zig-format.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/.claude/hooks/post-edit-zig-format.js b/.claude/hooks/post-edit-zig-format.js index 3cf96837b9..e70d5eb99e 100755 --- a/.claude/hooks/post-edit-zig-format.js +++ b/.claude/hooks/post-edit-zig-format.js @@ -83,8 +83,6 @@ if (ext === ".zig") { ].includes(ext) ) { formatTypeScriptFile(); -} else if (ext === ".cpp" || ext === ".c" || ext === ".h") { - formatCppFile(); } process.exit(0); From 624911180f36cad33ed44f7d970ad20b85bede48 Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 06:51:21 -0700 Subject: [PATCH 033/191] fix(outdated): show catalog info without requiring --filter or -r (#23039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The `bun outdated` command now displays catalog dependencies with their workspace grouping even when run without the `--filter` or `-r` flags. ## What changed - Added detection for catalog dependencies in the outdated packages list - The workspace column is now shown when: - Using `--filter` or `-r` flags (existing behavior) - OR when there are catalog dependencies to display (new behavior) - When there are no catalog dependencies and no filtering, the workspace column remains hidden as before ## Why Previously, running `bun outdated` without any flags would not show which workspaces were using catalog dependencies, making it unclear where catalog entries were being used. This fix ensures catalog dependencies are properly grouped and displayed with their workspace information. ## Test ```bash # Create a workspace project with catalog dependencies mkdir test-catalog && cd test-catalog cat > package.json << 'JSON' { "name": "test-catalog", "workspaces": ["packages/*"], "catalog": { "react": "^17.0.0" } } JSON mkdir -p packages/{app1,app2} echo '{"name":"app1","dependencies":{"react":"catalog:"}}' > packages/app1/package.json echo '{"name":"app2","dependencies":{"react":"catalog:"}}' > packages/app2/package.json bun install bun outdated # Should now show catalog grouping without needing --filter ``` 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/cli/outdated_command.zig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index 91f5cd64e7..4b722051fa 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -484,12 +484,17 @@ pub const OutdatedCommand = struct { // Recalculate max workspace length after grouping var new_max_workspace: usize = max_workspace; + var has_catalog_deps = false; for (grouped_ids.items) |item| { if (item.grouped_workspace_names) |names| { if (names.len > new_max_workspace) new_max_workspace = names.len; + has_catalog_deps = true; } } + // Show workspace column if filtered OR if there are catalog dependencies + const show_workspace_column = was_filtered or has_catalog_deps; + const package_column_inside_length = @max("Packages".len, max_name); const current_column_inside_length = @max("Current".len, max_current); const update_column_inside_length = @max("Update".len, max_update); @@ -500,7 +505,7 @@ pub const OutdatedCommand = struct { const column_right_pad = 1; const table = Table("blue", column_left_pad, column_right_pad, enable_ansi_colors).init( - &if (was_filtered) + &if (show_workspace_column) [_][]const u8{ "Package", "Current", @@ -515,7 +520,7 @@ pub const OutdatedCommand = struct { "Update", "Latest", }, - &if (was_filtered) + &if (show_workspace_column) [_]usize{ package_column_inside_length, current_column_inside_length, @@ -621,7 +626,7 @@ pub const OutdatedCommand = struct { version_buf.clearRetainingCapacity(); } - if (was_filtered) { + if (show_workspace_column) { Output.pretty("{s}", .{table.symbols.verticalEdge()}); for (0..column_left_pad) |_| Output.pretty(" ", .{}); From f0eb0472e6e43c4bd07738b762c8866814db4bec Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 06:52:20 -0700 Subject: [PATCH 034/191] Allow `--splitting` and `--compile` together (#23017) ### What does this PR do? ### How did you verify your code works? --- src/StandaloneModuleGraph.zig | 21 ++++++++++ src/bake/DevServer.zig | 1 + src/bundler/Chunk.zig | 13 ++++-- .../generateChunksInParallel.zig | 1 + .../linker_context/writeOutputFilesToDisk.zig | 1 + src/cli/build_command.zig | 6 --- .../bundler/bundler_compile_splitting.test.ts | 40 +++++++++++++++++++ test/bundler/expectBundled.ts | 28 ++++++++----- 8 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 test/bundler/bundler_compile_splitting.test.ts diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 0da83a40e6..9b045a1fad 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -431,6 +431,27 @@ pub const StandaloneModuleGraph = struct { } }; + if (comptime bun.Environment.is_canary or bun.Environment.isDebug) { + if (bun.getenvZ("BUN_FEATURE_FLAG_DUMP_CODE")) |dump_code_dir| { + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); + const dest_z = bun.path.joinAbsStringBufZ(dump_code_dir, buf, &.{dest_path}, .auto); + + // Scoped block to handle dump failures without skipping module emission + dump: { + const file = bun.sys.File.makeOpen(dest_z, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664).unwrap() catch |err| { + Output.prettyErrorln("error: failed to open {s}: {s}", .{ dest_path, @errorName(err) }); + break :dump; + }; + defer file.close(); + file.writeAll(output_file.value.buffer.bytes).unwrap() catch |err| { + Output.prettyErrorln("error: failed to write {s}: {s}", .{ dest_path, @errorName(err) }); + break :dump; + }; + } + } + } + var module = CompiledModuleGraphFile{ .name = string_builder.fmtAppendCountZ("{s}{s}", .{ prefix, diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index e45dc06831..9264eeafa6 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2334,6 +2334,7 @@ pub fn finalizeBundle( result.chunks, null, false, + false, ); // Create an entry for this file. diff --git a/src/bundler/Chunk.zig b/src/bundler/Chunk.zig index ac17d003f3..cdf6eec74c 100644 --- a/src/bundler/Chunk.zig +++ b/src/bundler/Chunk.zig @@ -142,6 +142,7 @@ pub const Chunk = struct { chunk: *Chunk, chunks: []Chunk, display_size: ?*usize, + force_absolute_path: bool, enable_source_map_shifts: bool, ) bun.OOM!CodeResult { return switch (enable_source_map_shifts) { @@ -153,6 +154,7 @@ pub const Chunk = struct { chunk, chunks, display_size, + force_absolute_path, source_map_shifts, ), }; @@ -167,10 +169,13 @@ pub const Chunk = struct { chunk: *Chunk, chunks: []Chunk, display_size: ?*usize, + force_absolute_path: bool, comptime enable_source_map_shifts: bool, ) bun.OOM!CodeResult { const additional_files = graph.input_files.items(.additional_files); const unique_key_for_additional_files = graph.input_files.items(.unique_key_for_additional_file); + const relative_platform_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(relative_platform_buf); switch (this.*) { .pieces => |*pieces| { const entry_point_chunks_for_scb = linker_graph.files.items(.entry_point_chunk_index); @@ -224,10 +229,10 @@ pub const Chunk = struct { const cheap_normalizer = cheapPrefixNormalizer( import_prefix, - if (from_chunk_dir.len == 0) + if (from_chunk_dir.len == 0 or force_absolute_path) file_path else - bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), + bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false), ); count += cheap_normalizer[0].len + cheap_normalizer[1].len; }, @@ -316,10 +321,10 @@ pub const Chunk = struct { bun.path.platformToPosixInPlace(u8, @constCast(file_path)); const cheap_normalizer = cheapPrefixNormalizer( import_prefix, - if (from_chunk_dir.len == 0) + if (from_chunk_dir.len == 0 or force_absolute_path) file_path else - bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), + bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false), ); if (cheap_normalizer[0].len > 0) { diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index 1cc1a05bf1..dcb091fb0c 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -340,6 +340,7 @@ pub fn generateChunksInParallel( chunk, chunks, &display_size, + c.resolver.opts.compile and !chunk.is_browser_chunk_from_server_build, chunk.content.sourcemap(c.options.source_maps) != .none, ); var code_result = _code_result catch @panic("Failed to allocate memory for output file"); diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.zig b/src/bundler/linker_context/writeOutputFilesToDisk.zig index 2592cb57af..e49fd8c7e1 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.zig +++ b/src/bundler/linker_context/writeOutputFilesToDisk.zig @@ -73,6 +73,7 @@ pub fn writeOutputFilesToDisk( chunk, chunks, &display_size, + c.resolver.opts.compile and !chunk.is_browser_chunk_from_server_build, chunk.content.sourcemap(c.options.source_maps) != .none, ) catch |err| bun.Output.panic("Failed to create output chunk: {s}", .{@errorName(err)}); diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 6637e7007f..5590ef1581 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -96,12 +96,6 @@ pub const BuildCommand = struct { var was_renamed_from_index = false; if (ctx.bundler_options.compile) { - if (ctx.bundler_options.code_splitting) { - Output.prettyErrorln("error: cannot use --compile with --splitting", .{}); - Global.exit(1); - return; - } - if (ctx.bundler_options.outdir.len > 0) { Output.prettyErrorln("error: cannot use --compile with --outdir", .{}); Global.exit(1); diff --git a/test/bundler/bundler_compile_splitting.test.ts b/test/bundler/bundler_compile_splitting.test.ts new file mode 100644 index 0000000000..f80d0bfdc9 --- /dev/null +++ b/test/bundler/bundler_compile_splitting.test.ts @@ -0,0 +1,40 @@ +import { describe } from "bun:test"; +import { itBundled } from "./expectBundled"; + +describe("bundler", () => { + describe("compile with splitting", () => { + itBundled("compile/splitting/RelativePathsAcrossChunks", { + compile: true, + splitting: true, + backend: "cli", + files: { + "/src/app/entry.ts": /* js */ ` + console.log('app entry'); + import('../components/header').then(m => m.render()); + `, + "/src/components/header.ts": /* js */ ` + export async function render() { + console.log('header rendering'); + const nav = await import('./nav/menu'); + nav.show(); + } + `, + "/src/components/nav/menu.ts": /* js */ ` + export async function show() { + console.log('menu showing'); + const items = await import('./items'); + console.log('items:', items.list); + } + `, + "/src/components/nav/items.ts": /* js */ ` + export const list = ['home', 'about', 'contact'].join(','); + `, + }, + entryPoints: ["/src/app/entry.ts"], + outdir: "/build", + run: { + stdout: "app entry\nheader rendering\nmenu showing\nitems: home,about,contact", + }, + }); + }); +}); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index a45de6d53a..972f5feff8 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -524,7 +524,15 @@ function expectBundled( if (metafile === true) metafile = "/metafile.json"; if (bundleErrors === true) bundleErrors = {}; if (bundleWarnings === true) bundleWarnings = {}; - const useOutFile = generateOutput == false ? false : outfile ? true : outdir ? false : entryPoints.length === 1; + const useOutFile = compile + ? true + : generateOutput == false + ? false + : outfile + ? true + : outdir + ? false + : entryPoints.length === 1; if (bundling === false && entryPoints.length > 1) { throw new Error("bundling:false only supports a single entry point"); @@ -1087,14 +1095,16 @@ function expectBundled( define: define ?? {}, throw: _throw ?? false, compile, - jsx: jsx ? { - runtime: jsx.runtime, - importSource: jsx.importSource, - factory: jsx.factory, - fragment: jsx.fragment, - sideEffects: jsx.sideEffects, - development: jsx.development, - } : undefined, + jsx: jsx + ? { + runtime: jsx.runtime, + importSource: jsx.importSource, + factory: jsx.factory, + fragment: jsx.fragment, + sideEffects: jsx.sideEffects, + development: jsx.development, + } + : undefined, } as BuildConfig; if (dotenv) { From 83060e4b3ed89645eac4c28339e4c5883925a2bf Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 5 Oct 2025 04:28:25 -0700 Subject: [PATCH 035/191] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b0c2fa643d..4b95245f9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.claude/settings.local.json .DS_Store .env .envrc @@ -190,4 +191,4 @@ scratch*.{js,ts,tsx,cjs,mjs} scripts/lldb-inline # We regenerate these in all the build scripts -cmake/sources/*.txt \ No newline at end of file +cmake/sources/*.txt From 67647c35220c56df1449e4a310d9b7e1ba071aed Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 5 Oct 2025 05:07:59 -0700 Subject: [PATCH 036/191] test(valkey): Improvements to valkey IPC interlock (#23252) ### What does this PR do? Adds a stronger IPC interlock in the failing subscriber test. ### How did you verify your code works? Hopefully CI. --- test/js/valkey/test-utils.ts | 34 ++++++++++++++------- test/js/valkey/valkey.failing-subscriber.ts | 4 ++- test/js/valkey/valkey.test.ts | 31 ++++++++++++++++--- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/test/js/valkey/test-utils.ts b/test/js/valkey/test-utils.ts index ee09d55884..e3443ea015 100644 --- a/test/js/valkey/test-utils.ts +++ b/test/js/valkey/test-utils.ts @@ -663,21 +663,33 @@ export function awaitableCounter(timeoutMs: number = 1000) { let activeResolvers: [number, NodeJS.Timeout, (value: number) => void][] = []; let currentCount = 0; - return { - increment: () => { - currentCount++; + const incrementBy = (count: number) => { + currentCount += count; - for (const [value, alarm, resolve] of activeResolvers) { - alarm.close(); + for (const [value, alarm, resolve] of activeResolvers) { + alarm.close(); - if (currentCount >= value) { - resolve(currentCount); - } + if (currentCount >= value) { + resolve(currentCount); } + } - // Remove resolved promises - activeResolvers = activeResolvers.filter(([value]) => currentCount < value); - }, + // Remove resolved promises + const remaining: typeof activeResolvers = []; + for (const [value, alarm, resolve] of activeResolvers) { + if (currentCount >= value) { + alarm.close(); + resolve(currentCount); + } else { + remaining.push([value, alarm, resolve]); + } + } + activeResolvers = remaining; + }; + + return { + incrementBy: incrementBy, + increment: incrementBy.bind(null, 1), count: () => currentCount, untilValue: (value: number) => diff --git a/test/js/valkey/valkey.failing-subscriber.ts b/test/js/valkey/valkey.failing-subscriber.ts index 379718e43c..461a059dcf 100644 --- a/test/js/valkey/valkey.failing-subscriber.ts +++ b/test/js/valkey/valkey.failing-subscriber.ts @@ -33,6 +33,7 @@ process.on("message", (msg: any) => { const CHANNEL = "error-callback-channel"; // We will wait for the parent process to tell us to start. +trySend({ event: "waiting-for-url" }); const { url, tlsPaths } = await redisUrl; const subscriber = new RedisClient(url, { tls: tlsPaths @@ -44,7 +45,6 @@ const subscriber = new RedisClient(url, { : undefined, }); await subscriber.connect(); -trySend({ event: "ready" }); let counter = 0; await subscriber.subscribe(CHANNEL, () => { @@ -58,3 +58,5 @@ await subscriber.subscribe(CHANNEL, () => { process.on("uncaughtException", e => { trySend({ event: "exception", exMsg: e.message }); }); + +trySend({ event: "ready" }); diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 2c50270127..df4132a30c 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -6570,13 +6570,34 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { ); }); + test("high volume pub/sub", async () => { + const channel = testChannel(); + + const MESSAGE_COUNT = 1000; + const MESSAGE_SIZE = 1024 * 1024; + + let byteCounter = awaitableCounter(5_000); // 5s timeout + const subscriber = await ctx.redis.duplicate(); + await subscriber.subscribe(channel, message => { + byteCounter.incrementBy(message.length); + }); + + for (let i = 0; i < MESSAGE_COUNT; i++) { + await ctx.redis.publish(channel, "X".repeat(MESSAGE_SIZE)); + } + + expect(await byteCounter.untilValue(MESSAGE_COUNT * MESSAGE_SIZE)).toBe(MESSAGE_COUNT * MESSAGE_SIZE); + subscriber.close(); + }); + test("callback errors don't crash the client", async () => { const channel = "error-callback-channel"; - const STEP_SUBSCRIBED = 1; - const STEP_FIRST_MESSAGE = 2; - const STEP_SECOND_MESSAGE = 3; - const STEP_THIRD_MESSAGE = 4; + const STEP_WAITING_FOR_URL = 1; + const STEP_SUBSCRIBED = 2; + const STEP_FIRST_MESSAGE = 3; + const STEP_SECOND_MESSAGE = 4; + const STEP_THIRD_MESSAGE = 5; const stepCounter = awaitableCounter(); let currentMessage: any = {}; @@ -6595,6 +6616,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }, }); + await stepCounter.untilValue(STEP_WAITING_FOR_URL); + expect(currentMessage.event).toBe("waiting-for-url"); subscriberProc.send({ event: "start", url: connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL, From f0295ce0a55acc43d898b6769f1e3c14a793968c Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 5 Oct 2025 17:22:37 -0700 Subject: [PATCH 037/191] Fix bunfig.toml parsing with UTF-8 BOM (#23276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #23275 ### What does this PR do? This PR fixes a bug where `bunfig.toml` files starting with a UTF-8 BOM (byte order mark, `U+FEFF` or bytes `0xEF 0xBB 0xBF`) would fail to parse with an "Unexpected" error. The fix uses Bun's existing `File.toSource()` function with `convert_bom: true` option when loading config files. This properly detects and strips the BOM before parsing, matching the behavior of other file readers in Bun (like the JavaScript lexer which treats `0xFEFF` as whitespace). **Changes:** - Modified `src/cli/Arguments.zig` to use `bun.sys.File.toSource()` with BOM conversion instead of manually reading the file - Simplified the config loading code by removing intermediate file handle and buffer logic ### How did you verify your code works? Added comprehensive regression tests in `test/regression/issue/23275.test.ts` that verify: 1. ✅ `bunfig.toml` with UTF-8 BOM parses correctly without errors 2. ✅ `bunfig.toml` without BOM still works (regression test) 3. ✅ `bunfig.toml` with BOM and actual config content parses the content correctly All three tests pass with the debug build: ``` 3 pass 0 fail 11 expect() calls Ran 3 tests across 1 file. [6.41s] ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/cli/Arguments.zig | 18 ++---- test/internal/ban-limits.json | 2 +- test/regression/issue/23275.test.ts | 99 +++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 test/regression/issue/23275.test.ts diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 4b55ba7446..1d4bea63b8 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -212,11 +212,11 @@ pub const test_only_params = [_]ParamType{ pub const test_params = test_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; pub fn loadConfigPath(allocator: std.mem.Allocator, auto_loaded: bool, config_path: [:0]const u8, ctx: Command.Context, comptime cmd: Command.Tag) !void { - var config_file = switch (bun.sys.openA(config_path, bun.O.RDONLY, 0)) { - .result => |fd| fd.stdFile(), + const source = switch (bun.sys.File.toSource(config_path, allocator, .{ .convert_bom = true })) { + .result => |s| s, .err => |err| { if (auto_loaded) return; - Output.prettyErrorln("{}\nwhile opening config \"{s}\"", .{ + Output.prettyErrorln("{}\nwhile reading config \"{s}\"", .{ err, config_path, }); @@ -224,16 +224,6 @@ pub fn loadConfigPath(allocator: std.mem.Allocator, auto_loaded: bool, config_pa }, }; - defer config_file.close(); - const contents = config_file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { - if (auto_loaded) return; - Output.prettyErrorln("error: {s} reading config \"{s}\"", .{ - @errorName(err), - config_path, - }); - Global.exit(1); - }; - js_ast.Stmt.Data.Store.create(); js_ast.Expr.Data.Store.create(); defer { @@ -245,7 +235,7 @@ pub fn loadConfigPath(allocator: std.mem.Allocator, auto_loaded: bool, config_pa ctx.log.level = original_level; } ctx.log.level = logger.Log.Level.warn; - try Bunfig.parse(allocator, &logger.Source.initPathString(bun.asByteSlice(config_path), contents), ctx, cmd); + try Bunfig.parse(allocator, &source, ctx, cmd); } fn getHomeConfigPath(buf: *bun.PathBuffer) ?[:0]const u8 { diff --git a/test/internal/ban-limits.json b/test/internal/ban-limits.json index 39f2c58676..9102540fcb 100644 --- a/test/internal/ban-limits.json +++ b/test/internal/ban-limits.json @@ -8,7 +8,7 @@ ".jsBoolean(false)": 0, ".jsBoolean(true)": 0, ".stdDir()": 41, - ".stdFile()": 18, + ".stdFile()": 17, "// autofix": 167, ": [^=]+= undefined,$": 256, "== alloc.ptr": 0, diff --git a/test/regression/issue/23275.test.ts b/test/regression/issue/23275.test.ts new file mode 100644 index 0000000000..d6c5db1168 --- /dev/null +++ b/test/regression/issue/23275.test.ts @@ -0,0 +1,99 @@ +// https://github.com/oven-sh/bun/issues/23275 +// UTF-8 BOM in bunfig.toml should not cause parsing errors + +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("bunfig.toml with UTF-8 BOM should parse correctly", async () => { + // UTF-8 BOM is the byte sequence: 0xEF 0xBB 0xBF + const utf8BOM = "\uFEFF"; + + using dir = tempDir("bunfig-bom", { + "bunfig.toml": + utf8BOM + + ` +[install] +exact = true +`, + "index.ts": `console.log("test");`, + "package.json": JSON.stringify({ + name: "test-bom", + version: "1.0.0", + }), + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should not have the "Unexpected" error that was reported in the issue + expect(stderr).not.toContain("Unexpected"); + expect(stderr).not.toContain("error:"); + expect(stdout).toContain("test"); + expect(exitCode).toBe(0); +}); + +test("bunfig.toml without BOM should still work", async () => { + using dir = tempDir("bunfig-no-bom", { + "bunfig.toml": ` +[install] +exact = true +`, + "index.ts": `console.log("test");`, + "package.json": JSON.stringify({ + name: "test-no-bom", + version: "1.0.0", + }), + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).not.toContain("Unexpected"); + expect(stderr).not.toContain("error:"); + expect(stdout).toContain("test"); + expect(exitCode).toBe(0); +}); + +test("bunfig.toml with BOM and actual content should parse the content correctly", async () => { + const utf8BOM = "\uFEFF"; + + using dir = tempDir("bunfig-bom-content", { + "bunfig.toml": + utf8BOM + + ` +logLevel = "debug" + +[install] +production = true +`, + "index.ts": `console.log("hello");`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("hello"); + expect(stderr).not.toContain("Unexpected"); + expect(exitCode).toBe(0); +}); From fcbd57ac48c157f43fa5216d484161a400a8ea2b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sun, 5 Oct 2025 17:23:59 -0700 Subject: [PATCH 038/191] Bring `Bun.YAML` to 90% passing yaml-test-suite (#23265) ### What does this PR do? Fixes bugs in the parser bringing it to 90% passing the official [yaml-test-suite](https://github.com/yaml/yaml-test-suite) (362/400 passing tests) Still missing from our parser: |- and |+ (about 5%), and cyclic references. Translates the yaml-test-suite to our tests. fixes #22659 fixes #22392 fixes #22286 ### How did you verify your code works? Added tests for yaml-test-suite and each of the linked issues --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/interchange/yaml.zig | 828 ++- .../import-attributes.test.ts | 2 +- .../bun/yaml/__snapshots__/yaml.test.ts.snap | 171 + test/js/bun/yaml/fixtures/AHatInTime.yaml | 167 + .../yaml/translate_yaml_test_suite_to_bun.py | 1049 +++ test/js/bun/yaml/yaml-test-suite.test.ts | 6405 +++++++++++++++++ test/js/bun/yaml/yaml.test.ts | 61 +- 7 files changed, 8438 insertions(+), 245 deletions(-) create mode 100644 test/js/bun/yaml/__snapshots__/yaml.test.ts.snap create mode 100644 test/js/bun/yaml/fixtures/AHatInTime.yaml create mode 100644 test/js/bun/yaml/translate_yaml_test_suite_to_bun.py create mode 100644 test/js/bun/yaml/yaml-test-suite.test.ts diff --git a/src/interchange/yaml.zig b/src/interchange/yaml.zig index b76a0af3a8..28f7de06fa 100644 --- a/src/interchange/yaml.zig +++ b/src/interchange/yaml.zig @@ -278,6 +278,8 @@ pub fn Parser(comptime enc: Encoding) type { context: Context.Stack, block_indents: Indent.Stack, + explicit_document_start_line: ?Line, + // anchors: Anchors, anchors: bun.StringHashMap(Expr), // aliases: PendingAliases, @@ -299,13 +301,12 @@ pub fn Parser(comptime enc: Encoding) type { stack_check: bun.StackCheck, - const Whitespace = struct { - pos: Pos, - unit: enc.unit(), - - pub const space: Whitespace = .{ .unit = ' ', .pos = .zero }; - pub const tab: Whitespace = .{ .unit = '\t', .pos = .zero }; - pub const newline: Whitespace = .{ .unit = '\n', .pos = .zero }; + const Whitespace = union(enum) { + source: struct { + pos: Pos, + unit: enc.unit(), + }, + new: enc.unit(), }; pub fn init(allocator: std.mem.Allocator, input: []const enc.unit()) @This() { @@ -320,6 +321,7 @@ pub fn Parser(comptime enc: Encoding) type { // .literal = null, .context = .init(allocator), .block_indents = .init(allocator), + .explicit_document_start_line = null, // .anchors = .{ .map = .init(allocator) }, .anchors = .init(allocator), // .aliases = .{ .list = .init(allocator) }, @@ -407,10 +409,10 @@ pub fn Parser(comptime enc: Encoding) type { try log.addError(source, e.pos.loc(), "Unexpected EOF"); }, .unexpected_token => |e| { - try log.addError(source, e.pos.loc(), "Expected token"); + try log.addError(source, e.pos.loc(), "Unexpected token"); }, .unexpected_character => |e| { - try log.addError(source, e.pos.loc(), "Expected character"); + try log.addError(source, e.pos.loc(), "Unexpected character"); }, .invalid_directive => |e| { try log.addError(source, e.pos.loc(), "Invalid directive"); @@ -486,6 +488,10 @@ pub fn Parser(comptime enc: Encoding) type { } }; + fn unexpectedToken() error{UnexpectedToken} { + return error.UnexpectedToken; + } + pub fn parse(self: *@This()) ParseError!Stream { try self.scan(.{ .first_scan = true }); @@ -693,31 +699,39 @@ pub fn Parser(comptime enc: Encoding) type { try self.scan(.{}); } + self.explicit_document_start_line = null; + if (self.token.data == .document_start) { + self.explicit_document_start_line = self.token.line; try self.scan(.{}); } else if (directives.items.len > 0) { // if there's directives they must end with '---' - return error.UnexpectedToken; + return unexpectedToken(); } const root = try self.parseNode(.{}); - // If document_start or document_end follows, consume it + // If document_start it needs to create a new document. + // If document_end, consume as many as possible. They should + // not create new documents. switch (self.token.data) { .eof => {}, - .document_start => { - try self.scan(.{}); - }, + .document_start => {}, .document_end => { const document_end_line = self.token.line; try self.scan(.{}); + // consume all bare documents + while (self.token.data == .document_end) { + try self.scan(.{}); + } + if (self.token.line == document_end_line) { - return error.UnexpectedToken; + return unexpectedToken(); } }, else => { - return error.UnexpectedToken; + return unexpectedToken(); }, } @@ -747,7 +761,7 @@ pub fn Parser(comptime enc: Encoding) type { } if (self.token.data != .collect_entry) { - return error.UnexpectedToken; + return unexpectedToken(); } try self.scan(.{}); @@ -803,7 +817,7 @@ pub fn Parser(comptime enc: Encoding) type { }, .mapping_value => {}, else => { - return error.UnexpectedToken; + return unexpectedToken(); }, } @@ -874,9 +888,6 @@ pub fn Parser(comptime enc: Encoding) type { const sequence_indent = self.token.indent; // const sequence_line = self.token.line; - // try self.context.set(.block_in); - // defer self.context.unset(.block_in); - try self.block_indents.push(sequence_indent); defer self.block_indents.pop(); @@ -934,6 +945,45 @@ pub fn Parser(comptime enc: Encoding) type { break :item try self.parseNode(.{}); }, + .tag, + .anchor, + => item: { + // consume anchor and/or tag, then decide if the next node + // should be parsed. + var has_tag: ?Token(enc) = null; + var has_anchor: ?Token(enc) = null; + + next: switch (self.token.data) { + .tag => { + if (has_tag != null) { + return unexpectedToken(); + } + has_tag = self.token; + + try self.scan(.{ .additional_parent_indent = entry_indent.add(1), .tag = self.token.data.tag }); + continue :next self.token.data; + }, + .anchor => |anchor| { + _ = anchor; + if (has_anchor != null) { + return unexpectedToken(); + } + has_anchor = self.token; + + const tag = if (has_tag) |tag| tag.data.tag else .none; + try self.scan(.{ .additional_parent_indent = entry_indent.add(1), .tag = tag }); + continue :next self.token.data; + }, + .sequence_entry => { + if (self.token.indent.isLessThanOrEqual(sequence_indent)) { + const tag = if (has_tag) |tag| tag.data.tag else .none; + break :item tag.resolveNull(entry_start.add(2).loc()); + } + break :item try self.parseNode(.{ .scanned_tag = has_tag, .scanned_anchor = has_anchor }); + }, + else => break :item try self.parseNode(.{ .scanned_tag = has_tag, .scanned_anchor = has_anchor }), + } + }, else => try self.parseNode(.{}), }; @@ -951,6 +1001,16 @@ pub fn Parser(comptime enc: Encoding) type { mapping_indent: Indent, mapping_line: Line, ) ParseError!Expr { + if (self.explicit_document_start_line) |explicit_document_start_line| { + if (mapping_line == explicit_document_start_line) { + // TODO: more specific error + return error.UnexpectedToken; + } + } + + try self.block_indents.push(mapping_indent); + defer self.block_indents.pop(); + var props: std.ArrayList(G.Property) = .init(self.allocator); { @@ -958,32 +1018,41 @@ pub fn Parser(comptime enc: Encoding) type { // defer self.context.unset(.block_in); // get the first value - try self.block_indents.push(mapping_indent); - defer self.block_indents.pop(); const mapping_value_start = self.token.start; const mapping_value_line = self.token.line; - try self.scan(.{}); - const value: Expr = switch (self.token.data) { - .sequence_entry => value: { - if (self.token.line == mapping_value_line) { - return error.UnexpectedToken; + // it's a !!set entry + .mapping_key => value: { + if (self.token.line == mapping_line) { + return unexpectedToken(); } - - if (self.token.indent.isLessThan(mapping_indent)) { - break :value .init(E.Null, .{}, mapping_value_start.loc()); - } - - break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + break :value .init(E.Null, .{}, mapping_value_start.loc()); }, else => value: { - if (self.token.line != mapping_value_line and self.token.indent.isLessThanOrEqual(mapping_indent)) { - break :value .init(E.Null, .{}, mapping_value_start.loc()); - } + try self.scan(.{}); - break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + switch (self.token.data) { + .sequence_entry => { + if (self.token.line == mapping_value_line) { + return unexpectedToken(); + } + + if (self.token.indent.isLessThan(mapping_indent)) { + break :value .init(E.Null, .{}, mapping_value_start.loc()); + } + + break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + }, + else => { + if (self.token.line != mapping_value_line and self.token.indent.isLessThanOrEqual(mapping_indent)) { + break :value .init(E.Null, .{}, mapping_value_start.loc()); + } + + break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + }, + } }, }; @@ -1028,14 +1097,17 @@ pub fn Parser(comptime enc: Encoding) type { try self.context.set(.block_in); defer self.context.unset(.block_in); + var previous_line = mapping_line; + while (switch (self.token.data) { .eof, .document_start, .document_end, => false, else => true, - } and self.token.indent == mapping_indent and self.token.line != mapping_line) { + } and self.token.indent == mapping_indent and self.token.line != previous_line) { const key_line = self.token.line; + previous_line = key_line; const explicit_key = self.token.data == .mapping_key; const key = try self.parseNode(.{ .current_mapping_indent = mapping_indent }); @@ -1051,44 +1123,53 @@ pub fn Parser(comptime enc: Encoding) type { }); continue; } - return error.UnexpectedToken; + return unexpectedToken(); }, .mapping_value => { if (key_line != self.token.line) { return error.MultilineImplicitKey; } }, + .mapping_key => {}, else => { - return error.UnexpectedToken; + return unexpectedToken(); }, } - try self.block_indents.push(mapping_indent); - defer self.block_indents.pop(); - const mapping_value_line = self.token.line; const mapping_value_start = self.token.start; - try self.scan(.{}); - const value: Expr = switch (self.token.data) { - .sequence_entry => value: { + // it's a !!set entry + .mapping_key => value: { if (self.token.line == key_line) { - return error.UnexpectedToken; + return unexpectedToken(); } - - if (self.token.indent.isLessThan(mapping_indent)) { - break :value .init(E.Null, .{}, mapping_value_start.loc()); - } - - break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + break :value .init(E.Null, .{}, mapping_value_start.loc()); }, else => value: { - if (self.token.line != mapping_value_line and self.token.indent.isLessThanOrEqual(mapping_indent)) { - break :value .init(E.Null, .{}, mapping_value_start.loc()); - } + try self.scan(.{}); - break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + switch (self.token.data) { + .sequence_entry => { + if (self.token.line == key_line) { + return unexpectedToken(); + } + + if (self.token.indent.isLessThan(mapping_indent)) { + break :value .init(E.Null, .{}, mapping_value_start.loc()); + } + + break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + }, + else => { + if (self.token.line != mapping_value_line and self.token.indent.isLessThanOrEqual(mapping_indent)) { + break :value .init(E.Null, .{}, mapping_value_start.loc()); + } + + break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + }, + } }, }; @@ -1237,6 +1318,8 @@ pub fn Parser(comptime enc: Encoding) type { const ParseNodeOptions = struct { current_mapping_indent: ?Indent = null, explicit_mapping_key: bool = false, + scanned_tag: ?Token(enc) = null, + scanned_anchor: ?Token(enc) = null, }; fn parseNode(self: *@This(), opts: ParseNodeOptions) ParseError!Expr { @@ -1247,6 +1330,14 @@ pub fn Parser(comptime enc: Encoding) type { // c-ns-properties var node_props: NodeProperties = .{}; + if (opts.scanned_tag) |tag| { + try node_props.setTag(tag); + } + + if (opts.scanned_anchor) |anchor| { + try node_props.setAnchor(anchor); + } + const node: Expr = node: switch (self.token.data) { .eof, .document_start, @@ -1273,8 +1364,19 @@ pub fn Parser(comptime enc: Encoding) type { }, .alias => |alias| { - if (node_props.hasAnchorOrTag()) { - return error.UnexpectedToken; + const alias_start = self.token.start; + const alias_indent = self.token.indent; + const alias_line = self.token.line; + + if (node_props.has_anchor) |anchor| { + if (anchor.line == alias_line) { + return unexpectedToken(); + } + } + if (node_props.has_tag) |tag| { + if (tag.line == alias_line) { + return unexpectedToken(); + } } var copy = self.anchors.get(alias.slice(self.input)) orelse { @@ -1289,10 +1391,35 @@ pub fn Parser(comptime enc: Encoding) type { }; // update position from the anchor node to the alias node. - copy.loc = self.token.start.loc(); + copy.loc = alias_start.loc(); try self.scan(.{}); + if (self.token.data == .mapping_value) { + if (alias_line != self.token.line and !opts.explicit_mapping_key) { + return error.MultilineImplicitKey; + } + + if (self.context.get() == .flow_key) { + return copy; + } + + if (opts.current_mapping_indent) |current_mapping_indent| { + if (current_mapping_indent == alias_indent) { + return copy; + } + } + + const map = try self.parseBlockMapping( + copy, + alias_start, + alias_indent, + alias_line, + ); + + return map; + } + break :node copy; }, @@ -1346,17 +1473,17 @@ pub fn Parser(comptime enc: Encoding) type { if (node_props.hasAnchorOrTag()) { break :node .init(E.Null, .{}, self.pos.loc()); } - return error.UnexpectedToken; + return unexpectedToken(); }, .sequence_entry => { if (node_props.anchorLine()) |anchor_line| { if (anchor_line == self.token.line) { - return error.UnexpectedToken; + return unexpectedToken(); } } if (node_props.tagLine()) |tag_line| { if (tag_line == self.token.line) { - return error.UnexpectedToken; + return unexpectedToken(); } } @@ -1400,6 +1527,8 @@ pub fn Parser(comptime enc: Encoding) type { if (implicit_key_anchors.mapping_anchor) |mapping_anchor| { try self.anchors.put(mapping_anchor.slice(self.input), parent_map); } + + break :node parent_map; } break :node map; }, @@ -1411,7 +1540,7 @@ pub fn Parser(comptime enc: Encoding) type { // if (node_props.anchorLine()) |anchor_line| { // if (anchor_line == self.token.line) { - // return error.UnexpectedToken; + // return unexpectedToken(); // } // } @@ -1441,11 +1570,11 @@ pub fn Parser(comptime enc: Encoding) type { }, .mapping_value => { if (self.context.get() == .flow_key) { - return .init(E.Null, .{}, self.token.start.loc()); + break :node .init(E.Null, .{}, self.token.start.loc()); } if (opts.current_mapping_indent) |current_mapping_indent| { if (current_mapping_indent == self.token.indent) { - return .init(E.Null, .{}, self.token.start.loc()); + break :node .init(E.Null, .{}, self.token.start.loc()); } } const first_key: Expr = .init(E.Null, .{}, self.token.start.loc()); @@ -1461,7 +1590,7 @@ pub fn Parser(comptime enc: Encoding) type { const scalar_indent = self.token.indent; const scalar_line = self.token.line; - try self.scan(.{ .tag = node_props.tag() }); + try self.scan(.{ .tag = node_props.tag(), .outside_context = true }); if (self.token.data == .mapping_value) { // this might be the start of a new object with an implicit key @@ -1541,10 +1670,10 @@ pub fn Parser(comptime enc: Encoding) type { break :node scalar.data.toExpr(scalar_start, self.input); }, .directive => { - return error.UnexpectedToken; + return unexpectedToken(); }, .reserved => { - return error.UnexpectedToken; + return unexpectedToken(); }, }; @@ -1558,11 +1687,16 @@ pub fn Parser(comptime enc: Encoding) type { return error.MultipleTags; } + const resolved = switch (node.data) { + .e_null => node_props.tag().resolveNull(node.loc), + else => node, + }; + if (node_props.anchor()) |anchor| { - try self.anchors.put(anchor.slice(self.input), node); + try self.anchors.put(anchor.slice(self.input), resolved); } - return node; + return resolved; } fn next(self: *const @This()) enc.unit() { @@ -1688,11 +1822,36 @@ pub fn Parser(comptime enc: Encoding) type { try ctx.str_builder.appendSourceSlice(off, end); } + // may or may not contain whitespace + pub fn appendUnknownSourceSlice(ctx: *@This(), off: Pos, end: Pos) OOM!void { + for (off.cast()..end.cast()) |_pos| { + const pos: Pos = .from(_pos); + const unit = ctx.parser.input[pos.cast()]; + switch (unit) { + ' ', + '\t', + '\r', + '\n', + => { + try ctx.str_builder.appendSourceWhitespace(unit, pos); + }, + else => { + ctx.checkAppend(); + try ctx.str_builder.appendSource(unit, pos); + }, + } + } + } + pub fn append(ctx: *@This(), unit: enc.unit()) OOM!void { ctx.checkAppend(); try ctx.str_builder.append(unit); } + pub fn appendWhitespace(ctx: *@This(), unit: enc.unit()) OOM!void { + try ctx.str_builder.appendWhitespace(unit); + } + pub fn appendSlice(ctx: *@This(), str: []const enc.unit()) OOM!void { ctx.checkAppend(); try ctx.str_builder.appendSlice(str); @@ -1706,6 +1865,14 @@ pub fn Parser(comptime enc: Encoding) type { try ctx.str_builder.appendNTimes(unit, n); } + pub fn appendWhitespaceNTimes(ctx: *@This(), unit: enc.unit(), n: usize) OOM!void { + if (n == 0) { + return; + } + + try ctx.str_builder.appendWhitespaceNTimes(unit, n); + } + const Keywords = enum { null, Null, @@ -1798,7 +1965,7 @@ pub fn Parser(comptime enc: Encoding) type { pub fn tryResolveNumber( ctx: *@This(), parser: *Parser(enc), - first_char: enum { positive, negative, dot, none }, + first_char: enum { positive, negative, dot, other }, ) ResolveError!void { const nan = std.math.nan(f64); const inf = std.math.inf(f64); @@ -1913,7 +2080,7 @@ pub fn Parser(comptime enc: Encoding) type { } } }, - .none => {}, + .other => {}, } const start = parser.pos; @@ -1926,7 +2093,9 @@ pub fn Parser(comptime enc: Encoding) type { var @"-" = false; var hex = false; - parser.inc(1); + if (first_char != .negative and first_char != .positive) { + parser.inc(1); + } var first = true; @@ -1945,7 +2114,12 @@ pub fn Parser(comptime enc: Encoding) type { '\n', '\r', ':', - => break :end .{ parser.pos, true }, + => { + if (first and (first_char == .positive or first_char == .negative)) { + break :end .{ parser.pos, false }; + } + break :end .{ parser.pos, true }; + }, ',', ']', @@ -1993,6 +2167,7 @@ pub fn Parser(comptime enc: Encoding) type { 'e', 'E', => { + first = false; if (e) { hex = true; } @@ -2008,12 +2183,12 @@ pub fn Parser(comptime enc: Encoding) type { => |c| { hex = true; - defer first = false; if (first) { if (c == 'b' or c == 'B') { break :end .{ parser.pos, false }; } } + first = false; parser.inc(1); continue :end parser.next(); @@ -2076,7 +2251,7 @@ pub fn Parser(comptime enc: Encoding) type { }, }; - try ctx.appendSourceSlice(start, end); + try ctx.appendUnknownSourceSlice(start, end); if (!valid) { return; @@ -2165,7 +2340,7 @@ pub fn Parser(comptime enc: Encoding) type { }, else => { - try ctx.tryResolveNumber(self, .none); + try ctx.tryResolveNumber(self, .other); continue :next self.next(); }, } @@ -2181,13 +2356,41 @@ pub fn Parser(comptime enc: Encoding) type { return ctx.done(); } + switch (self.context.get()) { + .block_out, + .block_in, + .flow_in, + => {}, + .flow_key => { + switch (self.peek(1)) { + ',', + '[', + ']', + '{', + '}', + => { + return ctx.done(); + }, + else => {}, + } + }, + } + try ctx.appendSource(':', self.pos); self.inc(1); continue :next self.next(); }, '#' => { - if (self.pos == .zero or self.input[self.pos.sub(1).cast()] == ' ') { + const prev = self.input[self.pos.sub(1).cast()]; + if (self.pos == .zero or switch (prev) { + ' ', + '\t', + '\r', + '\n', + => true, + else => false, + }) { return ctx.done(); } @@ -2253,11 +2456,14 @@ pub fn Parser(comptime enc: Encoding) type { } } + // clear the leading whitespace before the newline. + ctx.parser.whitespace_buf.clearRetainingCapacity(); + if (lines == 0 and !self.isEof()) { - try ctx.append(' '); + try ctx.appendWhitespace(' '); } - try ctx.appendNTimes('\n', lines); + try ctx.appendWhitespaceNTimes('\n', lines); continue :next self.next(); }, @@ -2461,7 +2667,7 @@ pub fn Parser(comptime enc: Encoding) type { }, '0'...'9' => { - try ctx.tryResolveNumber(self, .none); + try ctx.tryResolveNumber(self, .other); continue :next self.next(); }, @@ -2479,7 +2685,7 @@ pub fn Parser(comptime enc: Encoding) type { }, else => { - try ctx.tryResolveNumber(self, .none); + try ctx.tryResolveNumber(self, .other); continue :next self.next(); }, } @@ -2507,6 +2713,12 @@ pub fn Parser(comptime enc: Encoding) type { var chomp: ?Chomp = null; next: switch (self.next()) { + 0 => { + return .{ + indent_indicator orelse .default, + chomp orelse .default, + }; + }, '1'...'9' => |digit| { if (indent_indicator != null) { return error.UnexpectedCharacter; @@ -2564,6 +2776,11 @@ pub fn Parser(comptime enc: Encoding) type { // the first newline is always excluded from a literal self.inc(1); + if (self.next() == '\t') { + // tab for indentation + return error.UnexpectedCharacter; + } + return .{ indent_indicator orelse .default, chomp orelse .default, @@ -2582,10 +2799,102 @@ pub fn Parser(comptime enc: Encoding) type { }; fn scanAutoIndentedLiteralScalar(self: *@This(), chomp: Chomp, folded: bool, start: Pos, line: Line) ScanLiteralScalarError!Token(enc) { - var leading_newlines: usize = 0; - var text: std.ArrayList(enc.unit()) = .init(self.allocator); + const LiteralScalarCtx = struct { + chomp: Chomp, + leading_newlines: usize, + text: std.ArrayList(enc.unit()), + start: Pos, + content_indent: Indent, + previous_indent: Indent, + max_leading_indent: Indent, + line: Line, + folded: bool, - const content_indent: Indent, const first = next: switch (self.next()) { + pub fn done(ctx: *@This(), was_eof: bool) OOM!Token(enc) { + switch (ctx.chomp) { + .keep => { + if (was_eof) { + try ctx.text.appendNTimes('\n', ctx.leading_newlines + 1); + } else if (ctx.text.items.len != 0) { + try ctx.text.appendNTimes('\n', ctx.leading_newlines); + } + }, + .clip => { + if (was_eof or ctx.text.items.len != 0) { + try ctx.text.append('\n'); + } + }, + .strip => { + // no trailing newlines + }, + } + + return .scalar(.{ + .start = ctx.start, + .indent = ctx.content_indent, + .line = ctx.line, + .resolved = .{ + .data = .{ .string = .{ .list = ctx.text } }, + .multiline = true, + }, + }); + } + + const AppendError = OOM || error{UnexpectedCharacter}; + + pub fn append(ctx: *@This(), c: enc.unit()) AppendError!void { + if (ctx.text.items.len == 0) { + if (ctx.content_indent.isLessThan(ctx.max_leading_indent)) { + return error.UnexpectedCharacter; + } + } + switch (ctx.folded) { + true => { + switch (ctx.leading_newlines) { + 0 => { + try ctx.text.append(c); + }, + 1 => { + if (ctx.previous_indent == ctx.content_indent) { + try ctx.text.appendSlice(&.{ ' ', c }); + } else { + try ctx.text.appendSlice(&.{ '\n', c }); + } + ctx.leading_newlines = 0; + }, + else => { + // leading_newlines because -1 for '\n\n' and +1 for c + try ctx.text.ensureUnusedCapacity(ctx.leading_newlines); + ctx.text.appendNTimesAssumeCapacity('\n', ctx.leading_newlines - 1); + ctx.text.appendAssumeCapacity(c); + ctx.leading_newlines = 0; + }, + } + }, + false => { + try ctx.text.ensureUnusedCapacity(ctx.leading_newlines + 1); + ctx.text.appendNTimesAssumeCapacity('\n', ctx.leading_newlines); + ctx.text.appendAssumeCapacity(c); + ctx.leading_newlines = 0; + }, + } + } + }; + + var ctx: LiteralScalarCtx = .{ + .chomp = chomp, + .text = .init(self.allocator), + .folded = folded, + .start = start, + .line = line, + + .leading_newlines = 0, + .content_indent = .none, + .previous_indent = .none, + .max_leading_indent = .none, + }; + + ctx.content_indent, const first = next: switch (self.next()) { 0 => { return .scalar(.{ .start = start, @@ -2607,7 +2916,11 @@ pub fn Parser(comptime enc: Encoding) type { '\n' => { self.newline(); self.inc(1); - leading_newlines += 1; + if (self.next() == '\t') { + // tab for indentation + return error.UnexpectedCharacter; + } + ctx.leading_newlines += 1; continue :next self.next(); }, @@ -2619,6 +2932,10 @@ pub fn Parser(comptime enc: Encoding) type { self.inc(1); } + if (ctx.max_leading_indent.isLessThan(indent)) { + ctx.max_leading_indent = indent; + } + self.line_indent = indent; continue :next self.next(); @@ -2629,30 +2946,11 @@ pub fn Parser(comptime enc: Encoding) type { }, }; - var previous_indent = content_indent; + ctx.previous_indent = ctx.content_indent; next: switch (first) { 0 => { - switch (chomp) { - .keep => { - try text.appendNTimes('\n', leading_newlines + 1); - }, - .clip => { - try text.append('\n'); - }, - .strip => { - // no trailing newlines - }, - } - return .scalar(.{ - .start = start, - .indent = content_indent, - .line = line, - .resolved = .{ - .data = .{ .string = .{ .list = text } }, - .multiline = true, - }, - }); + return ctx.done(true); }, '\r' => { @@ -2662,7 +2960,7 @@ pub fn Parser(comptime enc: Encoding) type { continue :next '\n'; }, '\n' => { - leading_newlines += 1; + ctx.leading_newlines += 1; self.newline(); self.inc(1); newlines: switch (self.next()) { @@ -2673,44 +2971,47 @@ pub fn Parser(comptime enc: Encoding) type { continue :newlines '\n'; }, '\n' => { - leading_newlines += 1; + ctx.leading_newlines += 1; self.newline(); self.inc(1); + if (self.next() == '\t') { + // tab for indentation + return error.UnexpectedCharacter; + } continue :newlines self.next(); }, ' ' => { - var indent: Indent = .from(1); - self.inc(1); + var indent: Indent = .from(0); while (self.next() == ' ') { indent.inc(1); - if (content_indent.isLessThan(indent)) { + if (ctx.content_indent.isLessThan(indent)) { switch (folded) { true => { - switch (leading_newlines) { + switch (ctx.leading_newlines) { 0 => { - try text.append(' '); + try ctx.text.append(' '); }, else => { - try text.ensureUnusedCapacity(leading_newlines + 1); - text.appendNTimesAssumeCapacity('\n', leading_newlines); - text.appendAssumeCapacity(' '); - leading_newlines = 0; + try ctx.text.ensureUnusedCapacity(ctx.leading_newlines + 1); + ctx.text.appendNTimesAssumeCapacity('\n', ctx.leading_newlines); + ctx.text.appendAssumeCapacity(' '); + ctx.leading_newlines = 0; }, } }, else => { - try text.ensureUnusedCapacity(leading_newlines + 1); - text.appendNTimesAssumeCapacity('\n', leading_newlines); - leading_newlines = 0; - text.appendAssumeCapacity(' '); + try ctx.text.ensureUnusedCapacity(ctx.leading_newlines + 1); + ctx.text.appendNTimesAssumeCapacity('\n', ctx.leading_newlines); + ctx.leading_newlines = 0; + ctx.text.appendAssumeCapacity(' '); }, } } self.inc(1); } - if (content_indent.isLessThan(indent)) { - previous_indent = self.line_indent; + if (ctx.content_indent.isLessThan(indent)) { + ctx.previous_indent = self.line_indent; } self.line_indent = indent; @@ -2720,91 +3021,54 @@ pub fn Parser(comptime enc: Encoding) type { } }, + '-' => { + if (self.line_indent == .none and self.remainStartsWith("---") and self.isAnyOrEofAt(" \t\n\r", 3)) { + return ctx.done(false); + } + + if (self.block_indents.get()) |block_indent| { + if (self.line_indent.isLessThanOrEqual(block_indent)) { + return ctx.done(false); + } + } else if (self.line_indent.isLessThan(ctx.content_indent)) { + return ctx.done(false); + } + + try ctx.append('-'); + + self.inc(1); + continue :next self.next(); + }, + + '.' => { + if (self.line_indent == .none and self.remainStartsWith("...") and self.isAnyOrEofAt(" \t\n\r", 3)) { + return ctx.done(false); + } + + if (self.block_indents.get()) |block_indent| { + if (self.line_indent.isLessThanOrEqual(block_indent)) { + return ctx.done(false); + } + } else if (self.line_indent.isLessThan(ctx.content_indent)) { + return ctx.done(false); + } + + try ctx.append('.'); + + self.inc(1); + continue :next self.next(); + }, + else => |c| { if (self.block_indents.get()) |block_indent| { if (self.line_indent.isLessThanOrEqual(block_indent)) { - switch (chomp) { - .keep => { - if (text.items.len != 0) { - try text.appendNTimes('\n', leading_newlines); - } - }, - .clip => { - if (text.items.len != 0) { - try text.append('\n'); - } - }, - .strip => { - // no trailing newlines - }, - } - return .scalar(.{ - .start = start, - .indent = content_indent, - .line = line, - .resolved = .{ - .data = .{ .string = .{ .list = text } }, - .multiline = true, - }, - }); - } else if (self.line_indent.isLessThan(content_indent)) { - switch (chomp) { - .keep => { - if (text.items.len != 0) { - try text.appendNTimes('\n', leading_newlines); - } - }, - .clip => { - if (text.items.len != 0) { - try text.append('\n'); - } - }, - .strip => { - // no trailing newlines - }, - } - return .scalar(.{ - .start = start, - .indent = content_indent, - .line = line, - .resolved = .{ - .data = .{ .string = .{ .list = text } }, - .multiline = true, - }, - }); + return ctx.done(false); } + } else if (self.line_indent.isLessThan(ctx.content_indent)) { + return ctx.done(false); } - switch (folded) { - true => { - switch (leading_newlines) { - 0 => { - try text.append(c); - }, - 1 => { - if (previous_indent == content_indent) { - try text.appendSlice(&.{ ' ', c }); - } else { - try text.appendSlice(&.{ '\n', c }); - } - leading_newlines = 0; - }, - else => { - // leading_newlines because -1 for '\n\n' and +1 for c - try text.ensureUnusedCapacity(leading_newlines); - text.appendNTimesAssumeCapacity('\n', leading_newlines - 1); - text.appendAssumeCapacity(c); - leading_newlines = 0; - }, - } - }, - false => { - try text.ensureUnusedCapacity(leading_newlines + 1); - text.appendNTimesAssumeCapacity('\n', leading_newlines); - text.appendAssumeCapacity(c); - leading_newlines = 0; - }, - } + try ctx.append(c); self.inc(1); continue :next self.next(); @@ -2948,26 +3212,22 @@ pub fn Parser(comptime enc: Encoding) type { const scalar_indent = self.line_indent; var text: std.ArrayList(enc.unit()) = .init(self.allocator); - var nl = false; - next: switch (self.next()) { 0 => return error.UnexpectedCharacter, '.' => { - if (nl and self.remainStartsWith("...") and self.isSWhiteOrBCharAt(3)) { + if (self.line_indent == .none and self.remainStartsWith("...") and self.isSWhiteOrBCharAt(3)) { return error.UnexpectedDocumentEnd; } - nl = false; try text.append('.'); self.inc(1); continue :next self.next(); }, '-' => { - if (nl and self.remainStartsWith("---") and self.isSWhiteOrBCharAt(3)) { + if (self.line_indent == .none and self.remainStartsWith("---") and self.isSWhiteOrBCharAt(3)) { return error.UnexpectedDocumentStart; } - nl = false; try text.append('-'); self.inc(1); continue :next self.next(); @@ -2988,14 +3248,12 @@ pub fn Parser(comptime enc: Encoding) type { return error.UnexpectedCharacter; } } - nl = true; continue :next self.next(); }, ' ', '\t', => { - nl = false; const off = self.pos; self.inc(1); self.skipSWhite(); @@ -3006,7 +3264,6 @@ pub fn Parser(comptime enc: Encoding) type { }, '"' => { - nl = false; self.inc(1); return .scalar(.{ .start = start, @@ -3023,7 +3280,6 @@ pub fn Parser(comptime enc: Encoding) type { }, '\\' => { - nl = false; self.inc(1); switch (self.next()) { '\r', @@ -3094,7 +3350,6 @@ pub fn Parser(comptime enc: Encoding) type { }, else => |c| { - nl = false; try text.append(c); self.inc(1); continue :next self.next(); @@ -3246,6 +3501,38 @@ pub fn Parser(comptime enc: Encoding) type { self.inc(1); var range = self.stringRange(); try self.trySkipNsTagChars(); + + // s-separate + switch (self.next()) { + 0, + ' ', + '\t', + '\r', + '\n', + => {}, + + ',', + '[', + ']', + '{', + '}', + => { + switch (self.context.get()) { + .block_out, + .block_in, + => { + return error.UnexpectedCharacter; + }, + .flow_in, + .flow_key, + => {}, + } + }, + else => { + return error.UnexpectedCharacter; + }, + } + const shorthand = range.end(); const tag: NodeTag = tag: { @@ -3367,6 +3654,8 @@ pub fn Parser(comptime enc: Encoding) type { /// (or in compact collections). First scan needs to /// count indentation. first_scan: bool = false, + + outside_context: bool = false, }; fn scan(self: *@This(), opts: ScanOptions) ScanError!void { @@ -3477,7 +3766,7 @@ pub fn Parser(comptime enc: Encoding) type { .flow_key, => { self.token.start = start; - return error.UnexpectedToken; + return unexpectedToken(); }, } @@ -3507,7 +3796,7 @@ pub fn Parser(comptime enc: Encoding) type { .line = self.line, }); - return error.UnexpectedToken; + return unexpectedToken(); }, .block_in, .block_out, @@ -3641,10 +3930,12 @@ pub fn Parser(comptime enc: Encoding) type { switch (self.context.get()) { .block_in, .block_out, + .flow_in, => { // scanPlainScalar }, - .flow_in, .flow_key => { + .flow_key, + => { self.inc(1); break :next .mappingValue(.{ .start = start, @@ -3653,7 +3944,6 @@ pub fn Parser(comptime enc: Encoding) type { }); }, } - // scanPlainScalar }, } @@ -3861,7 +4151,7 @@ pub fn Parser(comptime enc: Encoding) type { => {}, } self.token.start = start; - return error.UnexpectedToken; + return unexpectedToken(); }, '>' => { const start = self.pos; @@ -3878,7 +4168,7 @@ pub fn Parser(comptime enc: Encoding) type { => {}, } self.token.start = start; - return error.UnexpectedToken; + return unexpectedToken(); }, '\'' => { self.inc(1); @@ -3907,7 +4197,7 @@ pub fn Parser(comptime enc: Encoding) type { .indent = self.line_indent, .line = self.line, }); - return error.UnexpectedToken; + return unexpectedToken(); }, inline '\r', @@ -3929,8 +4219,8 @@ pub fn Parser(comptime enc: Encoding) type { .flow_key, => { if (self.block_indents.get()) |block_indent| { - if (self.token.line != previous_token_line and self.token.indent.isLessThan(block_indent)) { - return error.UnexpectedToken; + if (!opts.outside_context and self.token.line != previous_token_line and self.token.indent.isLessThanOrEqual(block_indent)) { + return unexpectedToken(); } } }, @@ -4025,9 +4315,17 @@ pub fn Parser(comptime enc: Encoding) type { /// /// positions `pos` on the next newline, or eof. Errors fn trySkipToNewLine(self: *@This()) error{UnexpectedCharacter}!void { - self.skipSWhite(); + var whitespace = false; + + if (self.isSWhite()) { + whitespace = true; + self.skipSWhite(); + } if (self.isChar('#')) { + if (!whitespace) { + return error.UnexpectedCharacter; + } self.inc(1); while (!self.isChar('\n') and !self.isChar('\r')) { self.inc(1); @@ -4285,34 +4583,60 @@ pub fn Parser(comptime enc: Encoding) type { } fn drainWhitespace(self: *@This()) OOM!void { - for (self.parser.whitespace_buf.items) |ws| { - if (comptime Environment.ci_assert) { - const actual = self.parser.input[ws.pos.cast()]; - bun.assert(actual == ws.unit); - } + const parser = self.parser; + defer parser.whitespace_buf.clearRetainingCapacity(); - switch (self.str) { - .range => |*range| { - if (range.isEmpty()) { - range.off = ws.pos; - range.end = ws.pos; + for (parser.whitespace_buf.items) |ws| { + switch (ws) { + .source => |source| { + if (comptime Environment.ci_assert) { + const actual = self.parser.input[source.pos.cast()]; + bun.assert(actual == source.unit); } - bun.assert(range.end == ws.pos); + switch (self.str) { + .range => |*range| { + if (range.isEmpty()) { + range.off = source.pos; + range.end = source.pos; + } - range.end = ws.pos.add(1); + bun.assert(range.end == source.pos); + + range.end = source.pos.add(1); + }, + .list => |*list| { + try list.append(source.unit); + }, + } }, - .list => |*list| { - try list.append(ws.unit); + .new => |unit| { + switch (self.str) { + .range => |range| { + var list: std.ArrayList(enc.unit()) = try .initCapacity(parser.allocator, range.len() + 1); + list.appendSliceAssumeCapacity(range.slice(parser.input)); + list.appendAssumeCapacity(unit); + self.str = .{ .list = list }; + }, + .list => |*list| { + try list.append(unit); + }, + } }, } } - - self.parser.whitespace_buf.clearRetainingCapacity(); } pub fn appendSourceWhitespace(self: *@This(), unit: enc.unit(), pos: Pos) OOM!void { - try self.parser.whitespace_buf.append(.{ .unit = unit, .pos = pos }); + try self.parser.whitespace_buf.append(.{ .source = .{ .unit = unit, .pos = pos } }); + } + + pub fn appendWhitespace(self: *@This(), unit: enc.unit()) OOM!void { + try self.parser.whitespace_buf.append(.{ .new = unit }); + } + + pub fn appendWhitespaceNTimes(self: *@This(), unit: enc.unit(), n: usize) OOM!void { + try self.parser.whitespace_buf.appendNTimes(.{ .new = unit }, n); } pub fn appendSourceSlice(self: *@This(), off: Pos, end: Pos) OOM!void { @@ -4484,6 +4808,24 @@ pub fn Parser(comptime enc: Encoding) type { /// '!!unknown' unknown: String.Range, + + pub fn resolveNull(this: NodeTag, loc: logger.Loc) Expr { + return switch (this) { + .none, + .bool, + .int, + .float, + .null, + .verbatim, + .unknown, + => .init(E.Null, .{}, loc), + + // non-specific tags become seq, map, or str + .non_specific, + .str, + => .init(E.String, .{}, loc), + }; + } }; pub const NodeScalar = union(enum) { diff --git a/test/js/bun/import-attributes/import-attributes.test.ts b/test/js/bun/import-attributes/import-attributes.test.ts index da0e55d668..6931edcf55 100644 --- a/test/js/bun/import-attributes/import-attributes.test.ts +++ b/test/js/bun/import-attributes/import-attributes.test.ts @@ -313,7 +313,7 @@ test("jsonc", async () => { }, "yaml": { "default": { - "// my json ": null, + "// my json": null, "key": "👩‍👧‍👧value", }, "key": "👩‍👧‍👧value", diff --git a/test/js/bun/yaml/__snapshots__/yaml.test.ts.snap b/test/js/bun/yaml/__snapshots__/yaml.test.ts.snap new file mode 100644 index 0000000000..d09ae64a30 --- /dev/null +++ b/test/js/bun/yaml/__snapshots__/yaml.test.ts.snap @@ -0,0 +1,171 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`Bun.YAML parse issue 22286 2`] = ` +{ + "A Hat in Time": { + "ActPlando": { + "Dead Bird Studio Basement": "The Big Parade", + }, + "ActRandomizer": "insanity", + "BabyTrapWeight": 0, + "BadgeSellerMaxItems": 8, + "BadgeSellerMinItems": 5, + "BaseballBat": true, + "CTRLogic": { + "nothing": 0, + "scooter": 1, + "sprint": 0, + "time_stop_only": 0, + }, + "ChapterCostIncrement": 5, + "ChapterCostMinDifference": 5, + "CompassBadgeMode": "closest", + "DWAutoCompleteBonuses": true, + "DWEnableBonus": false, + "DWExcludeAnnoyingBonuses": true, + "DWExcludeAnnoyingContracts": true, + "DWExcludeCandles": true, + "DWShuffle": false, + "DWShuffleCountMax": 25, + "DWShuffleCountMin": 18, + "DWTimePieceRequirement": 15, + "DeathWishOnly": false, + "EnableDLC1": false, + "EnableDLC2": true, + "EnableDeathWish": false, + "EndGoal": { + "finale": 1, + "rush_hour": 0, + "seal_the_deal": 0, + }, + "ExcludeTour": false, + "FinalChapterMaxCost": 35, + "FinalChapterMinCost": 30, + "FinaleShuffle": false, + "HatItems": true, + "HighestChapterCost": 25, + "LaserTrapWeight": 0, + "LogicDifficulty": "moderate", + "LowestChapterCost": 5, + "MaxExtraTimePieces": "random-range-high-5-8", + "MaxPonCost": 80, + "MetroMaxPonCost": 50, + "MetroMinPonCost": 10, + "MinExtraYarn": "random-range-middle-5-15", + "MinPonCost": 20, + "NoPaintingSkips": true, + "NoTicketSkips": "rush_hour", + "NyakuzaThugMaxShopItems": 4, + "NyakuzaThugMinShopItems": 1, + "ParadeTrapWeight": 0, + "RandomizeHatOrder": "time_stop_last", + "ShipShapeCustomTaskGoal": 0, + "ShuffleActContracts": true, + "ShuffleAlpineZiplines": true, + "ShuffleStorybookPages": true, + "ShuffleSubconPaintings": true, + "StartWithCompassBadge": true, + "StartingChapter": { + "1": 1, + "2": 1, + "3": 1, + }, + "Tasksanity": false, + "TasksanityCheckCount": 18, + "TasksanityTaskStep": 1, + "TimePieceBalancePercent": "random-range-low-20-35", + "TrapChance": 0, + "UmbrellaLogic": true, + "YarnAvailable": "random-range-middle-40-50", + "YarnBalancePercent": 25, + "YarnCostMax": 8, + "YarnCostMin": 5, + "accessibility": { + "full": 1, + "minimal": 0, + }, + "death_link": false, + "exclude_locations": [ + "Queen Vanessa's Manor - Bedroom Chest", + "Queen Vanessa's Manor - Hall Chest", + "Act Completion (The Big Parade)", + ], + "non_local_items": [ + "Hookshot Badge", + "Umbrella", + "Dweller Mask", + ], + "priority_locations": [ + "Act Completion (Award Ceremony)", + "Badge Seller - Item 1", + "Badge Seller - Item 2", + "Mafia Boss Shop Item", + "Bluefin Tunnel Thug - Item 1", + "Green Clean Station Thug A - Item 1", + "Green Clean Station Thug B - Item 1", + "Main Station Thug A - Item 1", + "Main Station Thug B - Item 1", + "Main Station Thug C - Item 1", + "Pink Paw Station Thug - Item 1", + "Yellow Overpass Thug A - Item 1", + "Yellow Overpass Thug B - Item 1", + "Yellow Overpass Thug C - Item 1", + ], + "progression_balancing": "random-range-middle-40-50", + "start_inventory_from_pool": { + "Sprint Hat": 1, + }, + }, + "game": "A Hat in Time", + "name": "niyrme-AHiT{NUMBER}", + "requires": { + "version": "0.6.2", + }, + "x-options-async": { + "A Hat in Time": { + "+non_local_items": [ + "Brewing Hat", + "Ice Hat", + ], + "ChapterCostIncrement": 7, + "ChapterCostMinDifference": 7, + "EndGoal": { + "finale": 9, + "rush_hour": 1, + "seal_the_deal": 0, + }, + "FinalChapterMaxCost": 50, + "FinalChapterMinCost": 40, + "HighestChapterCost": 40, + "LowestChapterCost": 10, + "NoPaintingSkips": false, + "death_link": false, + "priority_locations": [], + "progression_balancing": "random-range-low-10-30", + }, + }, + "x-options-sync": { + "A Hat in Time": { + "+start_inventory_from_pool": { + "Badge Pin": 1, + }, + "+triggers": [ + { + "option_category": "A Hat in Time", + "option_name": "EndGoal", + "option_result": "finale", + "options": { + "A Hat in Time": { + "EnableDLC2": false, + "FinalChapterMaxCost": 35, + "FinalChapterMinCost": 25, + "MaxPonCost": 100, + "MinPonCost": 30, + }, + }, + }, + ], + }, + }, +} +`; diff --git a/test/js/bun/yaml/fixtures/AHatInTime.yaml b/test/js/bun/yaml/fixtures/AHatInTime.yaml new file mode 100644 index 0000000000..155a2491e4 --- /dev/null +++ b/test/js/bun/yaml/fixtures/AHatInTime.yaml @@ -0,0 +1,167 @@ +game: &AHiT "A Hat in Time" + +name: "niyrme-AHiT{NUMBER}" + +requires: + version: 0.6.2 + +*AHiT : + # game + progression_balancing: "random-range-middle-40-50" + accessibility: + "full": 1 + "minimal": 0 + death_link: false + + # general + &EndGoal EndGoal: + &EndGoal_Finale finale: 1 + &EndGoal_Rush rush_hour: 0 + &EndGoal_Seal seal_the_deal: 0 + ShuffleStorybookPages: true + ShuffleAlpineZiplines: true + ShuffleSubconPaintings: true + ShuffleActContracts: true + MinPonCost: 20 + MaxPonCost: 80 + BadgeSellerMinItems: 5 + BadgeSellerMaxItems: 8 + # https://docs.google.com/document/d/1x9VLSQ5davfx1KGamR9T0mD5h69_lDXJ6H7Gq7knJRI + LogicDifficulty: "moderate" + NoPaintingSkips: true + CTRLogic: + "time_stop_only": 0 + "scooter": 1 + "sprint": 0 + "nothing": 0 + + # acts + ActRandomizer: "insanity" + StartingChapter: + 1: 1 + 2: 1 + 3: 1 + LowestChapterCost: 5 + HighestChapterCost: 25 + ChapterCostIncrement: 5 + ChapterCostMinDifference: 5 + &GoalMinCost FinalChapterMinCost: 30 + &GoalMaxCost FinalChapterMaxCost: 35 + FinaleShuffle: false + + # items + StartWithCompassBadge: true + CompassBadgeMode: "closest" + RandomizeHatOrder: "time_stop_last" + YarnAvailable: "random-range-middle-40-50" + YarnCostMin: 5 + YarnCostMax: 8 + MinExtraYarn: "random-range-middle-5-15" + HatItems: true + UmbrellaLogic: true + MaxExtraTimePieces: "random-range-high-5-8" + YarnBalancePercent: 25 + TimePieceBalancePercent: "random-range-low-20-35" + + # DLC: Seal the Deal + EnableDLC1: false + Tasksanity: false + TasksanityTaskStep: 1 + TasksanityCheckCount: 18 + ShipShapeCustomTaskGoal: 0 + ExcludeTour: false + + # DLC: Nyakuza Metro + &DLCNyakuza EnableDLC2: true + MetroMinPonCost: 10 + MetroMaxPonCost: 50 + NyakuzaThugMinShopItems: 1 + NyakuzaThugMaxShopItems: 4 + BaseballBat: true + NoTicketSkips: "rush_hour" + + # Death Wish + EnableDeathWish: false + DWTimePieceRequirement: 15 + DWShuffle: false + DWShuffleCountMin: 18 + DWShuffleCountMax: 25 + DWEnableBonus: false + DWAutoCompleteBonuses: true + DWExcludeAnnoyingContracts: true + DWExcludeAnnoyingBonuses: true + DWExcludeCandles: true + DeathWishOnly: false + + # traps + TrapChance: 0 + BabyTrapWeight: 0 + LaserTrapWeight: 0 + ParadeTrapWeight: 0 + + # plando, item & location options + non_local_items: + - "Hookshot Badge" + - "Umbrella" + - "Dweller Mask" + start_inventory_from_pool: + "Sprint Hat": 1 + exclude_locations: + - "Queen Vanessa's Manor - Bedroom Chest" + - "Queen Vanessa's Manor - Hall Chest" + - "Act Completion (The Big Parade)" + priority_locations: + - "Act Completion (Award Ceremony)" + - "Badge Seller - Item 1" + - "Badge Seller - Item 2" + - "Mafia Boss Shop Item" + # Nyakuza DLC + - "Bluefin Tunnel Thug - Item 1" + - "Green Clean Station Thug A - Item 1" + - "Green Clean Station Thug B - Item 1" + - "Main Station Thug A - Item 1" + - "Main Station Thug B - Item 1" + - "Main Station Thug C - Item 1" + - "Pink Paw Station Thug - Item 1" + - "Yellow Overpass Thug A - Item 1" + - "Yellow Overpass Thug B - Item 1" + - "Yellow Overpass Thug C - Item 1" + + ActPlando: + "Dead Bird Studio Basement": "The Big Parade" + +x-options-sync: + *AHiT : + +start_inventory_from_pool: + "Badge Pin": 1 + +triggers: + - option_category: *AHiT + option_name: *EndGoal + option_result: *EndGoal_Finale + options: + *AHiT : + MinPonCost: 30 + MaxPonCost: 100 + *GoalMinCost : 25 + *GoalMaxCost : 35 + *DLCNyakuza : false + +x-options-async: + *AHiT : + progression_balancing: "random-range-low-10-30" + death_link: false + LowestChapterCost: 10 + HighestChapterCost: 40 + ChapterCostIncrement: 7 + ChapterCostMinDifference: 7 + *EndGoal : + *EndGoal_Finale : 9 + *EndGoal_Rush : 1 + *EndGoal_Seal : 0 + NoPaintingSkips: false + *GoalMinCost : 40 + *GoalMaxCost : 50 + +non_local_items: + - "Brewing Hat" + - "Ice Hat" + priority_locations: [] \ No newline at end of file diff --git a/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py b/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py new file mode 100644 index 0000000000..002af37c51 --- /dev/null +++ b/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py @@ -0,0 +1,1049 @@ +#!/usr/bin/env python3 + +import os +import json +import glob +import yaml +import subprocess +import sys +import re +import argparse + +def escape_js_string(s): + """Escape a string for use in JavaScript string literals.""" + result = [] + for char in s: + if char == '\\': + result.append('\\\\') + elif char == '"': + result.append('\\"') + elif char == '\n': + result.append('\\n') + elif char == '\t': + result.append('\\t') + elif char == '\r': + result.append('\\r') + elif char == '\b': + result.append('\\b') + elif char == '\f': + result.append('\\f') + elif ord(char) < 0x20 or ord(char) == 0x7F: + # Control characters - use \xNN notation + result.append(f'\\x{ord(char):02x}') + else: + result.append(char) + return ''.join(result) + +def format_js_string(content): + """Format content for JavaScript string literal.""" + # For JavaScript we'll use template literals for multiline strings + # unless they contain backticks or ${ + if '`' in content or '${' in content: + # Use regular string with escaping + escaped = escape_js_string(content) + return f'"{escaped}"' + elif '\n' in content: + # Use template literal for multiline + # But we still need to escape backslashes + escaped_content = content.replace('\\', '\\\\') # Escape backslashes + return f'`{escaped_content}`' + else: + # Short single line - use regular string + escaped = escape_js_string(content) + return f'"{escaped}"' + +def has_anchors_or_aliases(yaml_content): + """Check if YAML content has anchors (&) or aliases (*).""" + return '&' in yaml_content or '*' in yaml_content + +def stringify_map_keys(obj, from_yaml_package=False): + """Recursively stringify all map keys to match Bun's YAML behavior. + + Args: + obj: The object to process + from_yaml_package: If True, empty string keys came from yaml package converting null keys + and should be converted to "null". If False, empty strings are intentional. + """ + if isinstance(obj, dict): + new_dict = {} + for key, value in obj.items(): + # Convert key to string + if key is None: + # Actual None/null key should become "null" + str_key = "null" + elif key == "" and from_yaml_package: + # Empty string from yaml package (was originally a null key in YAML) + # should be converted to "null" to match Bun's behavior + str_key = "null" + elif key == "": + # Empty string from official test JSON or explicit empty string + # should stay as empty string + str_key = "" + elif isinstance(key, str) and from_yaml_package: + # Check if this is a stringified collection from yaml package + # yaml package converts [a, b] to "[ a, b ]" but Bun/JS uses "a,b" + if key.startswith('[ ') and key.endswith(' ]'): + # This looks like a stringified array from yaml package + # Extract the content and convert to JS array.toString() format + inner = key[2:-2] # Remove "[ " and " ]" + # Split by comma and space, then join with just comma + elements = [] + for elem in inner.split(','): + elem = elem.strip() + # Remove anchor notation (&name) from the element + # Anchors appear as "&name value" in the stringified form + if '&' in elem: + # Remove the anchor part (e.g., "&b b" becomes "b") + parts = elem.split() + if len(parts) > 1 and parts[0].startswith('&'): + elem = ' '.join(parts[1:]) + elements.append(elem) + str_key = ','.join(elements) + elif key.startswith('{ ') and key.endswith(' }'): + # This looks like a stringified object from yaml package + # JavaScript Object.toString() returns "[object Object]" + str_key = "[object Object]" + elif key.startswith('*'): + # This is an alias reference that wasn't resolved by yaml package + # This shouldn't happen in well-formed output, but handle it + # For now, keep it as-is but this might need special handling + str_key = key + else: + str_key = str(key) + else: + # All other keys get stringified + str_key = str(key) + # Recursively process value + new_dict[str_key] = stringify_map_keys(value, from_yaml_package) + return new_dict + elif isinstance(obj, list): + return [stringify_map_keys(item, from_yaml_package) for item in obj] + else: + return obj + +def json_to_js_literal(obj, indent_level=1, seen_objects=None, var_declarations=None): + """Convert JSON object to JavaScript literal, handling shared references.""" + if seen_objects is None: + seen_objects = {} + if var_declarations is None: + var_declarations = [] + + indent = " " * indent_level + + if obj is None: + return "null" + elif isinstance(obj, bool): + return "true" if obj else "false" + elif isinstance(obj, (int, float)): + # Handle special float values + if obj != obj: # NaN + return "NaN" + elif obj == float('inf'): + return "Infinity" + elif obj == float('-inf'): + return "-Infinity" + return str(obj) + elif isinstance(obj, str): + escaped = escape_js_string(obj) + return f'"{escaped}"' + elif isinstance(obj, list): + if len(obj) == 0: + return "[]" + + # Check for complex nested structures + if any(isinstance(item, (list, dict)) for item in obj): + items = [] + for item in obj: + item_str = json_to_js_literal(item, indent_level + 1, seen_objects, var_declarations) + items.append(f"{indent} {item_str}") + return "[\n" + ",\n".join(items) + f"\n{indent}]" + else: + # Simple array - inline + items = [json_to_js_literal(item, indent_level, seen_objects, var_declarations) for item in obj] + return "[" + ", ".join(items) + "]" + elif isinstance(obj, dict): + if len(obj) == 0: + return "{}" + + # Check if this is a simple object + is_simple = all(not isinstance(v, (list, dict)) for v in obj.values()) + + if is_simple and len(obj) <= 3: + # Simple object - inline + pairs = [] + for key, value in obj.items(): + if key.isidentifier() and not key.startswith('$'): + key_str = key + else: + key_str = f'"{escape_js_string(key)}"' + value_str = json_to_js_literal(value, indent_level, seen_objects, var_declarations) + pairs.append(f"{key_str}: {value_str}") + return "{ " + ", ".join(pairs) + " }" + else: + # Complex object - multiline + pairs = [] + for key, value in obj.items(): + if key.isidentifier() and not key.startswith('$'): + key_str = key + else: + key_str = f'"{escape_js_string(key)}"' + value_str = json_to_js_literal(value, indent_level + 1, seen_objects, var_declarations) + pairs.append(f"{indent} {key_str}: {value_str}") + return "{\n" + ",\n".join(pairs) + f"\n{indent}}}" + else: + # Fallback + return json.dumps(obj) + +def parse_test_events(event_file): + """Parse test.event file to infer expected JSON structure. + + Event format: + +STR - Stream start + +DOC - Document start + +MAP - Map start + +SEQ - Sequence start + =VAL - Value (scalar) + =ALI - Alias + -MAP - Map end + -SEQ - Sequence end + -DOC - Document end + -STR - Stream end + """ + if not os.path.exists(event_file): + return None + + with open(event_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + docs = [] + stack = [] + current_doc = None + in_key = False + pending_key = None + + for line in lines: + line = line.rstrip('\n') + if not line: + continue + + if line.startswith('+DOC'): + stack = [] + current_doc = None + in_key = False + pending_key = None + + elif line.startswith('+MAP'): + new_map = {} + if stack: + parent = stack[-1] + if isinstance(parent, list): + parent.append(new_map) + elif isinstance(parent, dict) and pending_key is not None: + parent[pending_key] = new_map + pending_key = None + in_key = False + else: + current_doc = new_map + stack.append(new_map) + + elif line.startswith('+SEQ'): + new_seq = [] + if stack: + parent = stack[-1] + if isinstance(parent, list): + parent.append(new_seq) + elif isinstance(parent, dict) and pending_key is not None: + parent[pending_key] = new_seq + pending_key = None + in_key = False + else: + current_doc = new_seq + stack.append(new_seq) + + elif line.startswith('=VAL'): + # Extract value after =VAL + value = line[4:].strip() + if value.startswith(':'): + value = value[1:].strip() if len(value) > 1 else '' + + # Convert special values + if value == '': + value = '' + elif value == '': + value = ' ' + + if stack: + parent = stack[-1] + if isinstance(parent, list): + parent.append(value) + elif isinstance(parent, dict): + if in_key or pending_key is None: + # This is a key + pending_key = value + in_key = False + else: + # This is a value for the pending key + parent[pending_key] = value + pending_key = None + else: + # Scalar document + current_doc = value + + elif line.startswith('-MAP') or line.startswith('-SEQ'): + if stack: + completed = stack.pop() + # If this was the last item and we have a pending key, it means empty value + if isinstance(stack[-1] if stack else None, dict) and pending_key is not None: + (stack[-1] if stack else {})[pending_key] = None + pending_key = None + + elif line.startswith('-DOC'): + if current_doc is not None: + docs.append(current_doc) + current_doc = None + stack = [] + pending_key = None + + return docs if docs else None + +def detect_shared_references(yaml_content): + """Detect anchors and their aliases in YAML to identify shared references.""" + # Find all anchors and their aliases + anchor_pattern = r'&(\w+)' + alias_pattern = r'\*(\w+)' + + anchors = re.findall(anchor_pattern, yaml_content) + aliases = re.findall(alias_pattern, yaml_content) + + # Return anchors that are referenced by aliases + shared_refs = [] + for anchor in set(anchors): + if anchor in aliases: + shared_refs.append(anchor) + + return shared_refs + +def generate_expected_with_shared_refs(json_data, yaml_content): + """Generate expected object with shared references for anchors/aliases.""" + shared_refs = detect_shared_references(yaml_content) + + if not shared_refs: + # No shared references, generate simple literal + return json_to_js_literal(json_data) + + # For simplicity, when there are anchors/aliases, we'll generate the expected + # object but note that some values might be shared references + # This is a simplified approach - in reality we'd need to track which values + # are aliased to generate exact shared references + + # Generate with a comment about shared refs + result = json_to_js_literal(json_data) + + # Add comment about shared references + comment = f" // Note: Original YAML has anchors/aliases: {', '.join(shared_refs)}\n" + comment += " // Some values in the parsed result may be shared object references\n" + + return comment + " const expected = " + result + ";" + +def get_expected_from_yaml_parser(yaml_content, use_eemeli_yaml=True): + """Use yaml package (eemeli/yaml) or js-yaml to get expected output.""" + # Create a temporary JavaScript file to parse the YAML + if use_eemeli_yaml: + # Use eemeli/yaml which is more spec-compliant + js_code = f''' +const YAML = require('/Users/dylan/yamlz-3/node_modules/yaml'); + +const input = {format_js_string(yaml_content)}; + +try {{ + const docs = YAML.parseAllDocuments(input); + const results = docs.map(doc => doc.toJSON()); + console.log(JSON.stringify(results)); +}} catch (e) {{ + console.log(JSON.stringify({{"error": e.message}})); +}} +''' + else: + # Fallback to js-yaml + js_code = f''' +const yaml = require('/Users/dylan/yamlz-3/node_modules/js-yaml'); + +const input = {format_js_string(yaml_content)}; + +try {{ + const docs = yaml.loadAll(input); + console.log(JSON.stringify(docs)); +}} catch (e) {{ + console.log(JSON.stringify({{"error": e.message}})); +}} +''' + + # Write to temp file and execute with node + temp_js = '/tmp/parse_yaml_temp.js' + with open(temp_js, 'w') as f: + f.write(js_code) + + try: + result = subprocess.run(['node', temp_js], capture_output=True, text=True, timeout=5) + if result.returncode == 0 and result.stdout.strip(): + output = json.loads(result.stdout.strip()) + if isinstance(output, dict) and 'error' in output: + return None, output['error'] + return output, None + else: + return None, result.stderr or "Failed to parse" + except subprocess.TimeoutExpired: + return None, "Timeout" + except Exception as e: + return None, str(e) + finally: + if os.path.exists(temp_js): + os.remove(temp_js) + +def generate_test(test_dir, test_name, check_ast=True, use_js_yaml=False, use_yaml_pkg=False): + """Generate a single Bun test case from a yaml-test-suite directory. + + Args: + test_dir: Directory containing the test files + test_name: Name for the test + check_ast: If True, validate parsed AST. If False, only check parse success/failure. + use_js_yaml: If True, generate test using js-yaml instead of Bun's YAML + use_yaml_pkg: If True, generate test using yaml package instead of Bun's YAML + """ + + yaml_file = os.path.join(test_dir, "in.yaml") + json_file = os.path.join(test_dir, "in.json") + desc_file = os.path.join(test_dir, "===") + + # Read YAML content + if not os.path.exists(yaml_file): + return None + + with open(yaml_file, 'r', encoding='utf-8') as f: + yaml_content = f.read() + + # Read test description + description = "" + if os.path.exists(desc_file): + with open(desc_file, 'r', encoding='utf-8') as f: + description = f.read().strip().replace('\n', ' ') + + # Check if this is an error test (has 'error' file) + error_file = os.path.join(test_dir, "error") + is_error_test = os.path.exists(error_file) + + # For js-yaml, check if it actually can parse this + js_yaml_fails = False + js_yaml_error_msg = None + if use_js_yaml and not is_error_test: + # Quick check if js-yaml will fail on this + yaml_js_docs, yaml_js_error = get_expected_from_yaml_parser(yaml_content, use_eemeli_yaml=False) + if yaml_js_error: + js_yaml_fails = True + js_yaml_error_msg = yaml_js_error + + # If js-yaml fails but spec says it should pass, generate a special test + if use_js_yaml and js_yaml_fails and not is_error_test: + formatted_content = format_js_string(yaml_content) + return f''' +test.skip("{test_name}", () => {{ + // {description} + // SKIPPED: js-yaml fails but spec says this should pass + // js-yaml error: {js_yaml_error_msg} + const input = {formatted_content}; + + // js-yaml is stricter than the YAML spec - it fails on this valid YAML + // The official test suite says this should parse successfully +}}); +''' + + if is_error_test: + # Generate error test + formatted_content = format_js_string(yaml_content) + if use_js_yaml: + return f''' +test("{test_name}", () => {{ + // {description} + // Error test - expecting parse to fail (using js-yaml) + const input = {formatted_content}; + + expect(() => {{ + return jsYaml.load(input); + }}).toThrow(); +}}); +''' + elif use_yaml_pkg: + return f''' +test("{test_name}", () => {{ + // {description} + // Error test - expecting parse to fail (using yaml package) + const input = {formatted_content}; + + expect(() => {{ + return yamlPkg.parse(input); + }}).toThrow(); +}}); +''' + else: + return f''' +test("{test_name}", () => {{ + // {description} + // Error test - expecting parse to fail + const input: string = {formatted_content}; + + expect(() => {{ + return YAML.parse(input); + }}).toThrow(); +}}); +''' + + # Special handling for known problematic tests + if test_name == "yaml-test-suite/X38W": + # X38W has alias key that creates duplicate - yaml package doesn't handle this correctly + # The correct output is just one key "a,b" with value ["c", "b", "d"] + test = f''' +test("{test_name}", () => {{ + // {description} + // Special case: *a references the same array as first key, creating duplicate key + const input: string = {format_js_string(yaml_content)}; + + const parsed = YAML.parse(input); + + const expected: any = {{ + "a,b": ["c", "b", "d"] + }}; + + expect(parsed).toEqual(expected); +}}); +''' + return test + + # Get expected data from official test suite JSON file if available + json_data = None + has_json = False + + if os.path.exists(json_file): + with open(json_file, 'r', encoding='utf-8') as f: + json_content = f.read().strip() + + if not json_content: + json_data = [None] # Empty file represents null document + has_json = True + else: + try: + # Try single document + single_doc = json.loads(json_content) + json_data = [single_doc] + has_json = True + except json.JSONDecodeError: + # Try to parse as multiple JSON objects concatenated + decoder = json.JSONDecoder() + idx = 0 + docs = [] + while idx < len(json_content): + json_content_from_idx = json_content[idx:].lstrip() + if not json_content_from_idx: + break + try: + obj, end_idx = decoder.raw_decode(json_content_from_idx) + docs.append(obj) + idx += len(json_content[idx:]) - len(json_content_from_idx) + end_idx + except json.JSONDecodeError: + break + + if docs: + json_data = docs + has_json = True + else: + # Last resort: Try multi-document (one JSON per line) + docs = [] + for line in json_content.split('\n'): + line = line.strip() + if line: + try: + docs.append(json.loads(line)) + except: + pass + + if docs: + json_data = docs + has_json = True + + # If no JSON from test suite, use yaml package as reference + # (Skip test.event parsing for now as it's too simplistic) + if not has_json: + yaml_docs, yaml_error = get_expected_from_yaml_parser(yaml_content, use_eemeli_yaml=True) + if yaml_error: + # yaml package couldn't parse it, but maybe Bun's YAML can + # Just check that it doesn't throw + formatted_content = format_js_string(yaml_content) + return f''' +test("{test_name}", () => {{ + // {description} + // Parse test - yaml package couldn't parse, checking YAML behavior + const input = {formatted_content}; + + // Test may pass or fail, we're just documenting behavior + try {{ + const parsed = YAML.parse(input); + // Successfully parsed + expect(parsed).toBeDefined(); + }} catch (e) {{ + // Failed to parse + expect(e).toBeDefined(); + }} +}}); +''' + else: + json_data = yaml_docs + + # If not checking AST, just verify parse success + if not check_ast: + formatted_content = format_js_string(yaml_content) + if use_js_yaml: + return f''' +test("{test_name}", () => {{ + // {description} + // Success test - expecting parse to succeed (AST checking disabled, using js-yaml) + const input = {formatted_content}; + + const parsed = jsYaml.load(input); + expect(parsed).toBeDefined(); +}}); +''' + elif use_yaml_pkg: + return f''' +test("{test_name}", () => {{ + // {description} + // Success test - expecting parse to succeed (AST checking disabled, using yaml package) + const input = {formatted_content}; + + const parsed = yamlPkg.parse(input); + expect(parsed).toBeDefined(); +}}); +''' + else: + return f''' +test("{test_name}", () => {{ + // {description} + // Success test - expecting parse to succeed (AST checking disabled) + const input: string = {formatted_content}; + + const parsed = YAML.parse(input); + expect(parsed).toBeDefined(); +}}); +''' + + # Format the YAML content for JavaScript + formatted_content = format_js_string(yaml_content) + + # Generate the test + comment = f"// {description}" + event_file = os.path.join(test_dir, "test.event") + if not os.path.exists(json_file) and os.path.exists(event_file): + comment += " (using test.event for expected values)" + elif not os.path.exists(json_file): + comment += " (using yaml package for expected values)" + + # Check if YAML has anchors/aliases + has_refs = has_anchors_or_aliases(yaml_content) + + # Handle multi-document YAML + # Only check the actual parsed data to determine if it's multi-document + # Document markers like --- and ... don't reliably indicate multiple documents + is_multi_doc = json_data and len(json_data) > 1 + + if is_multi_doc: + # Multi-document test - YAML.parse will return an array + if use_js_yaml: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = jsYaml.loadAll(input); +''' + elif use_yaml_pkg: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = yamlPkg.parseAllDocuments(input).map(doc => doc.toJSON()); +''' + else: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = YAML.parse(input); +''' + + # Generate expected array + if has_refs: + test += ''' + // Note: Original YAML may have anchors/aliases + // Some values in the parsed result may be shared object references +''' + + # Apply key stringification to match Bun's behavior + # from_yaml_package=!has_json: True if data came from yaml package, False if from official JSON + stringified_data = stringify_map_keys(json_data, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_data) + test += f''' + const expected: any = {expected_str}; + + expect(parsed).toEqual(expected); +}}); +''' + else: + # Single document test + expected_value = json_data[0] if json_data else None + + if use_js_yaml: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = jsYaml.load(input); +''' + elif use_yaml_pkg: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = yamlPkg.parse(input); +''' + else: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = YAML.parse(input); +''' + + # Generate expected value + if has_refs: + # For tests with anchors/aliases, we need to handle shared references + # Check specific patterns in YAML + if '*' in yaml_content and '&' in yaml_content: + # Has both anchors and aliases - need to create shared references + test += ''' + // This YAML has anchors and aliases - creating shared references +''' + # Try to identify simple cases + if 'bill-to: &' in yaml_content and 'ship-to: *' in yaml_content: + # Common pattern: bill-to/ship-to sharing + test += ''' + const sharedAddress: any = ''' + # Find the shared object from expected data + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + if isinstance(stringified_value, dict): + if 'bill-to' in stringified_value: + shared_obj = stringified_value.get('bill-to') + test += json_to_js_literal(shared_obj) + ';' + # Now create expected with shared ref + test += ''' + const expected = ''' + # Build object with shared reference + test += '{\n' + for key, value in stringified_value.items(): + if key == 'bill-to': + test += f' "bill-to": sharedAddress,\n' + elif key == 'ship-to' and value == shared_obj: + test += f' "ship-to": sharedAddress,\n' + else: + test += f' "{escape_js_string(key)}": {json_to_js_literal(value)},\n' + test = test.rstrip(',\n') + '\n };' + else: + # Fallback to regular generation + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + # Fallback to regular generation + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + # Generic anchor/alias case + # Look for patterns like "- &anchor value" and "- *anchor" + anchor_matches = re.findall(r'&(\w+)\s+(.+?)(?:\n|$)', yaml_content) + alias_matches = re.findall(r'\*(\w+)', yaml_content) + + if anchor_matches and alias_matches: + # Build shared values based on anchors + anchor_vars = {} + for anchor_name, _ in anchor_matches: + if anchor_name in [a for a in alias_matches]: + # This anchor is referenced + anchor_vars[anchor_name] = f'shared_{anchor_name}' + + if anchor_vars and isinstance(expected_value, (list, dict)): + # Try to detect which values are shared + test += f''' + // Detected anchors that are referenced: {', '.join(anchor_vars.keys())} +''' + # For now, just generate the expected normally with a note + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + # Has anchors but no aliases, or vice versa + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + # No anchors/aliases - simple case + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + + test += ''' + + expect(parsed).toEqual(expected); +}); +''' + + return test + +def main(): + # Parse command-line arguments + parser = argparse.ArgumentParser(description='Translate yaml-test-suite to Bun tests') + parser.add_argument('--no-ast-check', action='store_true', + help='Only check if parsing succeeds/fails, do not validate AST') + parser.add_argument('--with-js-yaml', action='store_true', + help='Also generate a companion test file using js-yaml for validation') + parser.add_argument('--with-yaml', action='store_true', + help='Also generate a companion test file using yaml package for validation') + args = parser.parse_args() + + check_ast = not args.no_ast_check + with_js_yaml = args.with_js_yaml + with_yaml = args.with_yaml + + # Check if yaml package is installed (for getting expected values) + yaml_pkg_found = False + try: + # Try local node_modules first + subprocess.run(['node', '-e', "require('./node_modules/yaml')"], capture_output=True, check=True, cwd='/Users/dylan/yamlz-3') + yaml_pkg_found = True + except: + try: + # Try global install + subprocess.run(['node', '-e', "require('yaml')"], capture_output=True, check=True) + yaml_pkg_found = True + except: + pass + + if not yaml_pkg_found and check_ast: + print("Error: yaml package is not installed. Please run: npm install yaml") + print("Note: yaml package is only required when checking AST. Use --no-ast-check to skip AST validation.") + sys.exit(1) + + # Get all test directories + test_dirs = [] + yaml_test_suite_path = '/Users/dylan/yamlz-3/yaml-test-suite' + + for entry in glob.glob(f'{yaml_test_suite_path}/*'): + if os.path.isdir(entry) and os.path.basename(entry) not in ['.git', 'name', 'tags']: + # Check if this is a test directory (has in.yaml) + if os.path.exists(os.path.join(entry, 'in.yaml')): + test_dirs.append(entry) + else: + # Check for subdirectories with in.yaml (multi-doc tests) + for subdir in glob.glob(os.path.join(entry, '*')): + if os.path.isdir(subdir) and os.path.exists(os.path.join(subdir, 'in.yaml')): + test_dirs.append(subdir) + + test_dirs = sorted(test_dirs) + + print(f"Found {len(test_dirs)} test directories in yaml-test-suite") + if not check_ast: + print("AST checking disabled - will only verify parse success/failure") + + # Generate a sample test first + if test_dirs: + print("\nGenerating sample test...") + # Look for a test with anchors/aliases + sample_dir = None + for td in test_dirs: + yaml_file = os.path.join(td, "in.yaml") + if os.path.exists(yaml_file): + with open(yaml_file, 'r') as f: + content = f.read() + if '&' in content and '*' in content: + sample_dir = td + break + + if not sample_dir: + sample_dir = test_dirs[0] + + test_id = sample_dir.replace(yaml_test_suite_path + '/', '') + test_name = f"yaml-test-suite/{test_id}" + + sample_test = generate_test(sample_dir, test_name, check_ast) + if sample_test: + print(f"Sample test for {test_id}:") + print(sample_test[:800] + "..." if len(sample_test) > 800 else sample_test) + + # Generate all tests + print("\nGenerating all tests...") + + mode_comment = "// AST validation disabled - only checking parse success/failure" if not check_ast else "// Using YAML.parse() with eemeli/yaml package as reference" + + output = f'''// Tests translated from official yaml-test-suite +{mode_comment} +// Total: {len(test_dirs)} test directories + +import {{ test, expect }} from "bun:test"; +import {{ YAML }} from "bun"; + +''' + + successful = 0 + failed = 0 + + for i, test_dir in enumerate(test_dirs): + test_id = test_dir.replace(yaml_test_suite_path + '/', '') + test_name = f"yaml-test-suite/{test_id}" + + if (i + 1) % 50 == 0: + print(f" Processing {i+1}/{len(test_dirs)}...") + + try: + test_case = generate_test(test_dir, test_name, check_ast) + if test_case: + output += test_case + successful += 1 + else: + print(f" Skipped {test_name}: returned None") + failed += 1 + except Exception as e: + print(f" Error with {test_name}: {e}") + failed += 1 + + # Write the output file to Bun's test directory + output_dir = '/Users/dylan/code/bun/test/js/bun/yaml' + os.makedirs(output_dir, exist_ok=True) + + filename = os.path.join(output_dir, 'yaml-test-suite.test.ts') + with open(filename, 'w', encoding='utf-8') as f: + f.write(output) + + print(f"\nGenerated {filename}") + print(f" Successful: {successful} tests") + print(f" Failed/Skipped: {failed} tests") + print(f" Total: {len(test_dirs)} directories processed") + + # Generate js-yaml companion tests if requested + if with_js_yaml: + print("\nGenerating js-yaml companion tests...") + + js_yaml_output = f'''// Tests translated from official yaml-test-suite +// Using js-yaml for validation of test translations +// Total: {len(test_dirs)} test directories + +import {{ test, expect }} from "bun:test"; +const jsYaml = require("js-yaml"); + +''' + + js_yaml_successful = 0 + js_yaml_failed = 0 + + for i, test_dir in enumerate(test_dirs): + test_id = test_dir.replace(yaml_test_suite_path + '/', '') + test_name = f"js-yaml/{test_id}" + + if (i + 1) % 50 == 0: + print(f" Processing js-yaml {i+1}/{len(test_dirs)}...") + + try: + test_case = generate_test(test_dir, test_name, check_ast, use_js_yaml=True) + if test_case: + js_yaml_output += test_case + js_yaml_successful += 1 + else: + js_yaml_failed += 1 + except Exception as e: + print(f" Error with {test_name}: {e}") + js_yaml_failed += 1 + + # Write js-yaml test file + js_yaml_filename = os.path.join(output_dir, 'yaml-test-suite-js-yaml.test.ts') + with open(js_yaml_filename, 'w', encoding='utf-8') as f: + f.write(js_yaml_output) + + print(f"\nGenerated js-yaml companion: {js_yaml_filename}") + print(f" Successful: {js_yaml_successful} tests") + print(f" Failed/Skipped: {js_yaml_failed} tests") + + + # Generate yaml package companion tests if requested + if with_yaml: + print("\nGenerating yaml package companion tests...") + + yaml_output = f'''// Tests translated from official yaml-test-suite +// Using yaml package (eemeli/yaml) for validation of test translations +// Total: {len(test_dirs)} test directories +// Note: Requires 'yaml' package to be installed: npm install yaml + +import {{ test, expect }} from "bun:test"; +import * as yamlPkg from "yaml"; + +''' + + yaml_successful = 0 + yaml_failed = 0 + + for i, test_dir in enumerate(test_dirs): + test_id = test_dir.replace(yaml_test_suite_path + '/', '') + test_name = f"yaml-pkg/{test_id}" + + if (i + 1) % 50 == 0: + print(f" Processing yaml package {i+1}/{len(test_dirs)}...") + + try: + test_case = generate_test(test_dir, test_name, check_ast, use_yaml_pkg=True) + if test_case: + yaml_output += test_case + yaml_successful += 1 + else: + yaml_failed += 1 + except Exception as e: + print(f" Error with {test_name}: {e}") + yaml_failed += 1 + + # Write yaml package test file + yaml_filename = os.path.join(output_dir, 'yaml-test-suite-yaml-pkg.test.ts') + with open(yaml_filename, 'w', encoding='utf-8') as f: + f.write(yaml_output) + + print(f"\nGenerated yaml package companion: {yaml_filename}") + print(f" Successful: {yaml_successful} tests") + print(f" Failed/Skipped: {yaml_failed} tests") + + print(f"\nTo run tests: cd {output_dir} && bun test") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/test/js/bun/yaml/yaml-test-suite.test.ts b/test/js/bun/yaml/yaml-test-suite.test.ts new file mode 100644 index 0000000000..ce10104203 --- /dev/null +++ b/test/js/bun/yaml/yaml-test-suite.test.ts @@ -0,0 +1,6405 @@ +// Tests translated from official yaml-test-suite (6e6c296) +// Using YAML.parse() with eemeli/yaml package as reference +// Total: 402 test directories + +import { YAML } from "bun"; +import { expect, test } from "bun:test"; + +test("yaml-test-suite/229Q", () => { + // Spec Example 2.4. Sequence of Mappings + const input: string = `- + name: Mark McGwire + hr: 65 + avg: 0.278 +- + name: Sammy Sosa + hr: 63 + avg: 0.288 +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { name: "Mark McGwire", hr: 65, avg: 0.278 }, + { name: "Sammy Sosa", hr: 63, avg: 0.288 }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/236B", () => { + // Invalid value after mapping + // Error test - expecting parse to fail + const input: string = `foo: + bar +invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/26DV", () => { + // Whitespace around colon in mappings + const input: string = `"top1" : + "key1" : &alias1 scalar1 +'top2' : + 'key2' : &alias2 scalar2 +top3: &node3 + *alias1 : scalar3 +top4: + *alias2 : scalar4 +top5 : + scalar5 +top6: + &anchor6 'key6' : scalar6 +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: alias1, alias2 + + const expected: any = { + top1: { key1: "scalar1" }, + top2: { key2: "scalar2" }, + top3: { scalar1: "scalar3" }, + top4: { scalar2: "scalar4" }, + top5: "scalar5", + top6: { key6: "scalar6" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/27NA", () => { + // Spec Example 5.9. Directive Indicator + const input: string = `%YAML 1.2 +--- text +`; + + const parsed = YAML.parse(input); + + const expected: any = "text"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2AUY", () => { + // Tags in Block Sequence + const input: string = ` - !!str a + - b + - !!int 42 + - d +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a", "b", 42, "d"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2CMS", () => { + // Invalid mapping in plain multiline + // Error test - expecting parse to fail + const input: string = `this + is + invalid: x +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/2EBW", () => { + // Allowed characters in keys + const input: string = + "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~: safe\n?foo: safe question mark\n:foo: safe colon\n-foo: safe dash\nthis is#not: a comment\n"; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const expected: any = { + "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~": "safe", + "?foo": "safe question mark", + ":foo": "safe colon", + "-foo": "safe dash", + "this is#not": "a comment", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2G84/00", () => { + // Literal modifers + // Error test - expecting parse to fail + const input: string = `--- |0 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/2G84/01", () => { + // Literal modifers + // Error test - expecting parse to fail + const input: string = `--- |10 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/2G84/02", () => { + // Literal modifers + const input: string = "--- |1-"; + + const parsed = YAML.parse(input); + + const expected: any = ""; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2G84/03", () => { + // Literal modifers + const input: string = "--- |1+"; + + const parsed = YAML.parse(input); + + const expected: any = ""; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2JQS", () => { + // Block Mapping with Missing Keys (using test.event for expected values) + const input: string = `: a +: b +`; + + const parsed = YAML.parse(input); + + const expected: any = { null: "b" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2LFX", () => { + // Spec Example 6.13. Reserved Directives [1.3] + const input: string = `%FOO bar baz # Should be ignored + # with a warning. +--- +"foo" +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2SXE", () => { + // Anchors With Colon in Name + const input: string = `&a: key: &a value +foo: + *a: +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a + + const expected: any = { key: "value", foo: "key" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2XXW", () => { + // Spec Example 2.25. Unordered Sets + const input: string = `# Sets are represented as a +# Mapping where each key is +# associated with a null value +--- !!set +? Mark McGwire +? Sammy Sosa +? Ken Griff +`; + + const parsed = YAML.parse(input); + + const expected: any = { "Mark McGwire": null, "Sammy Sosa": null, "Ken Griff": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/33X3", () => { + // Three explicit integers in a block sequence + const input: string = `--- +- !!int 1 +- !!int -2 +- !!int 33 +`; + + const parsed = YAML.parse(input); + + const expected: any = [1, -2, 33]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/35KP", () => { + // Tags for Root Objects + const input: string = `--- !!map +? a +: b +--- !!seq +- !!str c +--- !!str +d +e +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ a: "b" }, ["c"], "d e"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/36F6", () => { + // Multiline plain scalar with empty line + const input: string = `--- +plain: a + b + + c +`; + + const parsed = YAML.parse(input); + + const expected: any = { plain: "a b\nc" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3ALJ", () => { + // Block Sequence in Block Sequence + const input: string = `- - s1_i1 + - s1_i2 +- s2 +`; + + const parsed = YAML.parse(input); + + const expected: any = [["s1_i1", "s1_i2"], "s2"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3GZX", () => { + // Spec Example 7.1. Alias Nodes + const input: string = `First occurrence: &anchor Foo +Second occurrence: *anchor +Override anchor: &anchor Bar +Reuse anchor: *anchor +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = { + "First occurrence": "Foo", + "Second occurrence": "Foo", + "Override anchor": "Bar", + "Reuse anchor": "Bar", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3HFZ", () => { + // Invalid content after document end marker + // Error test - expecting parse to fail + const input: string = `--- +key: value +... invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/3MYT", () => { + // Plain Scalar looking like key, comment, anchor and tag + const input: string = `--- +k:#foo + &a !t s +`; + + const parsed = YAML.parse(input); + + const expected: any = "k:#foo &a !t s"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3R3P", () => { + // Single block sequence with anchor + const input: string = `&sequence +- a +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/00", () => { + // Leading tabs in double quoted + const input: string = `"1 leading + \\ttab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "1 leading \ttab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/01", () => { + // Leading tabs in double quoted + const input: string = `"2 leading + \\ tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "2 leading \ttab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/02", () => { + // Leading tabs in double quoted + const input: string = `"3 leading + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "3 leading tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/03", () => { + // Leading tabs in double quoted + const input: string = `"4 leading + \\t tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "4 leading \t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/04", () => { + // Leading tabs in double quoted + const input: string = `"5 leading + \\ tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "5 leading \t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/05", () => { + // Leading tabs in double quoted + const input: string = `"6 leading + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "6 leading tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3UYS", () => { + // Escaped slash in double quotes + const input: string = `escaped slash: "a\\/b" +`; + + const parsed = YAML.parse(input); + + const expected: any = { "escaped slash": "a/b" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4ABK", () => { + // Flow Mapping Separate Values (using test.event for expected values) + const input: string = `{ +unquoted : "separate", +http://foo.com, +omitted value:, +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { unquoted: "separate", "http://foo.com": null, "omitted value": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4CQQ", () => { + // Spec Example 2.18. Multi-line Flow Scalars + const input: string = `plain: + This unquoted scalar + spans many lines. + +quoted: "So does this + quoted scalar.\\n" +`; + + const parsed = YAML.parse(input); + + const expected: any = { plain: "This unquoted scalar spans many lines.", quoted: "So does this quoted scalar.\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4EJS", () => { + // Invalid tabs as indendation in a mapping + // Error test - expecting parse to fail + const input: string = `--- +a: + b: + c: value +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/4FJ6", () => { + // Nested implicit complex keys (using test.event for expected values) + const input: string = `--- +[ + [ a, [ [[b,c]]: d, e]]: 23 +] +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { "[\n a,\n [\n {\n ? [ [ b, c ] ]\n : d\n },\n e\n ]\n]": 23 }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4GC6", () => { + // Spec Example 7.7. Single Quoted Characters + const input: string = `'here''s to "quotes"' +`; + + const parsed = YAML.parse(input); + + const expected: any = 'here\'s to "quotes"'; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4H7K", () => { + // Flow sequence with invalid extra closing bracket + // Error test - expecting parse to fail + const input: string = `--- +[ a, b, c ] ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/4HVU", () => { + // Wrong indendation in Sequence + // Error test - expecting parse to fail + const input: string = `key: + - ok + - also ok + - wrong +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/4JVG", () => { + // Scalar value with two anchors + // Error test - expecting parse to fail + const input: string = `top1: &node1 + &k1 key1: val1 +top2: &node2 + &v2 val2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/4MUZ/00", () => { + // Flow mapping colon on line after key + const input: string = `{"foo" +: "bar"} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4MUZ/01", () => { + // Flow mapping colon on line after key + const input: string = `{"foo" +: bar} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4MUZ/02", () => { + // Flow mapping colon on line after key + const input: string = `{foo +: bar} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4Q9F", () => { + // Folded Block Scalar [1.3] + const input: string = `--- > + ab + cd + + ef + + + gh +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab cd\nef\n\ngh\n"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/4QFQ", () => { + // Spec Example 8.2. Block Indentation Indicator [1.3] + const input: string = `- | + detected +- > + + + # detected +- |1 + explicit +- > + detected +`; + + const parsed = YAML.parse(input); + + const expected: any = ["detected\n", "\n\n# detected\n", " explicit\n", "detected\n"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4RWC", () => { + // Trailing spaces after flow collection + const input: string = ` [1, 2, 3] + `; + + const parsed = YAML.parse(input); + + const expected: any = [1, 2, 3]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4UYU", () => { + // Colon in Double Quoted String + const input: string = `"foo: bar\\": baz" +`; + + const parsed = YAML.parse(input); + + const expected: any = 'foo: bar": baz'; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4V8U", () => { + // Plain scalar with backslashes + const input: string = `--- +plain\\value\\with\\backslashes +`; + + const parsed = YAML.parse(input); + + const expected: any = "plain\\value\\with\\backslashes"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4WA9", () => { + // Literal scalars + const input: string = `- aaa: |2 + xxx + bbb: | + xxx +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ aaa: "xxx\n", bbb: "xxx\n" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4ZYM", () => { + // Spec Example 6.4. Line Prefixes + const input: string = `plain: text + lines +quoted: "text + lines" +block: | + text + lines +`; + + const parsed = YAML.parse(input); + + const expected: any = { plain: "text lines", quoted: "text lines", block: "text\n \tlines\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/52DL", () => { + // Explicit Non-Specific Tag [1.3] + const input: string = `--- +! a +`; + + const parsed = YAML.parse(input); + + const expected: any = "a"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/54T7", () => { + // Flow Mapping + const input: string = `{foo: you, bar: far} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "you", bar: "far" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/55WF", () => { + // Invalid escape in double quoted string + // Error test - expecting parse to fail + const input: string = `--- +"\\." +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/565N", () => { + // Construct Binary + const input: string = `canonical: !!binary "\\ + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\\ + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\\ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\\ + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=" +generic: !!binary | + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5 + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs= +description: + The binary value above is a tiny arrow encoded as a gif image. +`; + + const parsed = YAML.parse(input); + + const expected: any = { + canonical: + "R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLCAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=", + generic: + "R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\nOTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\n+f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\nAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=\n", + description: "The binary value above is a tiny arrow encoded as a gif image.", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/57H4", () => { + // Spec Example 8.22. Block Collection Nodes + const input: string = `sequence: !!seq +- entry +- !!seq + - nested +mapping: !!map + foo: bar +`; + + const parsed = YAML.parse(input); + + const expected: any = { + sequence: ["entry", ["nested"]], + mapping: { foo: "bar" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/58MP", () => { + // Flow mapping edge cases + const input: string = `{x: :x} +`; + + const parsed = YAML.parse(input); + + const expected: any = { x: ":x" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5BVJ", () => { + // Spec Example 5.7. Block Scalar Indicators + const input: string = `literal: | + some + text +folded: > + some + text +`; + + const parsed = YAML.parse(input); + + const expected: any = { literal: "some\ntext\n", folded: "some text\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5C5M", () => { + // Spec Example 7.15. Flow Mappings + const input: string = `- { one : two , three: four , } +- {five: six,seven : eight} +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { one: "two", three: "four" }, + { five: "six", seven: "eight" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5GBF", () => { + // Spec Example 6.5. Empty Lines + const input: string = `Folding: + "Empty line + + as a line feed" +Chomping: | + Clipped empty lines + + +`; + + const parsed = YAML.parse(input); + + const expected: any = { Folding: "Empty line\nas a line feed", Chomping: "Clipped empty lines\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5KJE", () => { + // Spec Example 7.13. Flow Sequence + const input: string = `- [ one, two, ] +- [three ,four] +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + ["one", "two"], + ["three", "four"], + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5LLU", () => { + // Block scalar with wrong indented line after spaces only + // Error test - expecting parse to fail + const input: string = `block scalar: > + + + + invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/5MUD", () => { + // Colon and adjacent value on next line + const input: string = `--- +{ "foo" + :bar } +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5NYZ", () => { + // Spec Example 6.9. Separated Comment + const input: string = `key: # Comment + value +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5T43", () => { + // Colon at the beginning of adjacent flow scalar + const input: string = `- { "key":value } +- { "key"::value } +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ key: "value" }, { key: ":value" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5TRB", () => { + // Invalid document-start marker in doublequoted tring + // Error test - expecting parse to fail + const input: string = `--- +" +--- +" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/5TYM", () => { + // Spec Example 6.21. Local Tag Prefix + const input: string = `%TAG !m! !my- +--- # Bulb here +!m!light fluorescent +... +%TAG !m! !my- +--- # Color here +!m!light green +`; + + const parsed = YAML.parse(input); + + const expected: any = ["fluorescent", "green"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5U3A", () => { + // Sequence on same Line as Mapping Key + // Error test - expecting parse to fail + const input: string = `key: - a + - b +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/5WE3", () => { + // Spec Example 8.17. Explicit Block Mapping Entries + const input: string = `? explicit key # Empty value +? | + block key +: - one # Explicit compact + - two # block value +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "explicit key": null, + "block key\n": ["one", "two"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/62EZ", () => { + // Invalid block mapping key on same line as previous key + // Error test - expecting parse to fail + const input: string = `--- +x: { y: z }in: valid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/652Z", () => { + // Question mark at start of flow key + const input: string = `{ ?foo: bar, +bar: 42 +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { "?foo": "bar", bar: 42 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/65WH", () => { + // Single Entry Block Sequence + const input: string = `- foo +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6BCT", () => { + // Spec Example 6.3. Separation Spaces + const input: string = `- foo: bar +- - baz + - baz +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ foo: "bar" }, ["baz", "baz"]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6BFJ", () => { + // Mapping, key and flow sequence item anchors (using test.event for expected values) + const input: string = `--- +&mapping +&key [ &item a, b, c ]: value +`; + + const parsed = YAML.parse(input); + + const expected: any = { "a,b,c": "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6CA3", () => { + // Tab indented top flow + const input: string = ` [ + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = []; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6CK3", () => { + // Spec Example 6.26. Tag Shorthands + const input: string = `%TAG !e! tag:example.com,2000:app/ +--- +- !local foo +- !!str bar +- !e!tag%21 baz +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo", "bar", "baz"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6FWR", () => { + // Block Scalar Keep + const input: string = `--- |+ + ab + + +... +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab\n\n \n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6H3V", () => { + // Backslashes in singlequotes + const input: string = `'foo: bar\\': baz' +`; + + const parsed = YAML.parse(input); + + const expected: any = { "foo: bar\\": "baz'" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6HB6", () => { + // Spec Example 6.1. Indentation Spaces + const input: string = ` # Leading comment line spaces are + # neither content nor indentation. + +Not indented: + By one space: | + By four + spaces + Flow style: [ # Leading spaces + By two, # in flow style + Also by two, # are neither + Still by two # content nor + ] # indentation. +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "Not indented": { + "By one space": "By four\n spaces\n", + "Flow style": ["By two", "Also by two", "Still by two"], + }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6JQW", () => { + // Spec Example 2.13. In literals, newlines are preserved + const input: string = `# ASCII Art +--- | + \\//||\\/|| + // || ||__ +`; + + const parsed = YAML.parse(input); + + const expected: any = "\\//||\\/||\n// || ||__\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6JTT", () => { + // Flow sequence without closing bracket + // Error test - expecting parse to fail + const input: string = `--- +[ [ a, b, c ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/6JWB", () => { + // Tags for Block Objects + const input: string = `foo: !!seq + - !!str a + - !!map + key: !!str value +`; + + const parsed = YAML.parse(input); + + const expected: any = { + foo: ["a", { key: "value" }], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/6KGN", () => { + // Anchor for empty node + const input: string = `--- +a: &anchor +b: *anchor +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = { a: null, b: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6LVF", () => { + // Spec Example 6.13. Reserved Directives + const input: string = `%FOO bar baz # Should be ignored + # with a warning. +--- "foo" +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6M2F", () => { + // Aliases in Explicit Block Mapping (using test.event for expected values) + const input: string = `? &a a +: &b b +: *a +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a + + const expected: any = { a: "b", null: "a" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/6PBE", () => { + // Zero-indented sequences in explicit mapping keys (using test.event for expected values) + const input: string = `--- +? +- a +- b +: +- c +- d +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "a,b": ["c", "d"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6S55", () => { + // Invalid scalar at the end of sequence + // Error test - expecting parse to fail + const input: string = `key: + - bar + - baz + invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/6SLA", () => { + // Allowed characters in quoted mapping key + const input: string = `"foo\\nbar:baz\\tx \\\\$%^&*()x": 23 +'x\\ny:z\\tx $%^&*()x': 24 +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const expected: any = { "foo\nbar:baz\tx \\$%^&*()x": 23, "x\\ny:z\\tx $%^&*()x": 24 }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/6VJK", () => { + // Spec Example 2.15. Folded newlines are preserved for "more indented" and blank lines + const input: string = `> + Sammy Sosa completed another + fine season with great stats. + + 63 Home Runs + 0.288 Batting Average + + What a year! +`; + + const parsed = YAML.parse(input); + + const expected: any = + "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6WLZ", () => { + // Spec Example 6.18. Primary Tag Handle [1.3] + const input: string = `# Private +--- +!foo "bar" +... +# Global +%TAG ! tag:example.com,2000:app/ +--- +!foo "bar" +`; + + const parsed = YAML.parse(input); + + const expected: any = ["bar", "bar"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6WPF", () => { + // Spec Example 6.8. Flow Folding [1.3] + const input: string = `--- +" + foo + + bar + + baz +" +`; + + const parsed = YAML.parse(input); + + const expected: any = " foo\nbar\nbaz "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6XDY", () => { + // Two document start markers + const input: string = `--- +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = [null, null]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6ZKB", () => { + // Spec Example 9.6. Stream + const input: string = `Document +--- +# Empty +... +%YAML 1.2 +--- +matches %: 20 +`; + + const parsed = YAML.parse(input); + + const expected: any = ["Document", null, { "matches %": 20 }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/735Y", () => { + // Spec Example 8.20. Block Node Types + const input: string = `- + "flow in block" +- > + Block scalar +- !!map # Block collection + foo : bar +`; + + const parsed = YAML.parse(input); + + const expected: any = ["flow in block", "Block scalar\n", { foo: "bar" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/74H7", () => { + // Tags in Implicit Mapping + const input: string = `!!str a: b +c: !!int 42 +e: !!str f +g: h +!!str 23: !!bool false +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: "b", + c: 42, + e: "f", + g: "h", + "23": false, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/753E", () => { + // Block Scalar Strip [1.3] + const input: string = `--- |- + ab + + +... +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7A4E", () => { + // Spec Example 7.6. Double Quoted Lines + const input: string = `" 1st non-empty + + 2nd non-empty + 3rd non-empty " +`; + + const parsed = YAML.parse(input); + + const expected: any = " 1st non-empty\n2nd non-empty 3rd non-empty "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7BMT", () => { + // Node and Mapping Key Anchors [1.3] + const input: string = `--- +top1: &node1 + &k1 key1: one +top2: &node2 # comment + key2: two +top3: + &k3 key3: three +top4: &node4 + &k4 key4: four +top5: &node5 + key5: five +top6: &val6 + six +top7: + &val7 seven +`; + + const parsed = YAML.parse(input); + + const expected: any = { + top1: { key1: "one" }, + top2: { key2: "two" }, + top3: { key3: "three" }, + top4: { key4: "four" }, + top5: { key5: "five" }, + top6: "six", + top7: "seven", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7BUB", () => { + // Spec Example 2.10. Node for “Sammy Sosa” appears twice in this document + const input: string = `--- +hr: + - Mark McGwire + # Following node labeled SS + - &SS Sammy Sosa +rbi: + - *SS # Subsequent occurrence + - Ken Griffey +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: SS + + const expected: any = { + hr: ["Mark McGwire", "Sammy Sosa"], + rbi: ["Sammy Sosa", "Ken Griffey"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7FWL", () => { + // Spec Example 6.24. Verbatim Tags + const input: string = `! foo : + ! baz +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "baz" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7LBH", () => { + // Multiline double quoted implicit keys + // Error test - expecting parse to fail + const input: string = `"a\\nb": 1 +"c + d": 1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/7MNF", () => { + // Missing colon + // Error test - expecting parse to fail + const input: string = `top1: + key1: val1 +top2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/7T8X", () => { + // Spec Example 8.10. Folded Lines - 8.13. Final Empty Lines + const input: string = `> + + folded + line + + next + line + * bullet + + * list + * lines + + last + line + +# Comment +`; + + const parsed = YAML.parse(input); + + const expected: any = "\nfolded line\nnext line\n * bullet\n\n * list\n * lines\n\nlast line\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7TMG", () => { + // Comment in flow sequence before comma + const input: string = `--- +[ word1 +# comment +, word2] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["word1", "word2"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/7W2P", () => { + // Block Mapping with Missing Values + const input: string = `? a +? b +c: +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: null, b: null, c: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7Z25", () => { + // Bare document after document end marker + const input: string = `--- +scalar1 +... +key: value +`; + + const parsed = YAML.parse(input); + + const expected: any = ["scalar1", { key: "value" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7ZZ5", () => { + // Empty flow collections + const input: string = `--- +nested sequences: +- - - [] +- - - {} +key1: [] +key2: {} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "nested sequences": [[[[]]], [[{}]]], + key1: [], + key2: {}, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/82AN", () => { + // Three dashes and content without space + const input: string = `---word1 +word2 +`; + + const parsed = YAML.parse(input); + + const expected: any = "---word1 word2"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/87E4", () => { + // Spec Example 7.8. Single Quoted Implicit Keys + const input: string = `'implicit block key' : [ + 'implicit flow key' : value, + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "implicit block key": [{ "implicit flow key": "value" }], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8CWC", () => { + // Plain mapping key ending with colon + const input: string = `--- +key ends with two colons::: value +`; + + const parsed = YAML.parse(input); + + const expected: any = { "key ends with two colons::": "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8G76", () => { + // Spec Example 6.10. Comment Lines + const input: string = ` # Comment + + + +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8KB6", () => { + // Multiline plain flow mapping key without value + const input: string = `--- +- { single line, a: b} +- { multi + line, a: b} +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { "single line": null, a: "b" }, + { "multi line": null, a: "b" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8MK2", () => { + // Explicit Non-Specific Tag + const input: string = `! a +`; + + const parsed = YAML.parse(input); + + const expected: any = "a"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8QBE", () => { + // Block Sequence in Block Mapping + const input: string = `key: + - item1 + - item2 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + key: ["item1", "item2"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8UDB", () => { + // Spec Example 7.14. Flow Sequence Entries + const input: string = `[ +"double + quoted", 'single + quoted', +plain + text, [ nested ], +single: pair, +] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["double quoted", "single quoted", "plain text", ["nested"], { single: "pair" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8XDJ", () => { + // Comment in plain multiline value + // Error test - expecting parse to fail + const input: string = `key: word1 +# xxx + word2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/8XYN", () => { + // Anchor with unicode character + const input: string = `--- +- &😁 unicode anchor +`; + + const parsed = YAML.parse(input); + + const expected: any = ["unicode anchor"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/93JH", () => { + // Block Mappings in Block Sequence + const input: string = ` - key: value + key2: value2 + - + key3: value3 +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ key: "value", key2: "value2" }, { key3: "value3" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/93WF", () => { + // Spec Example 6.6. Line Folding [1.3] + const input: string = `--- >- + trimmed + + + + as + space +`; + + const parsed = YAML.parse(input); + + const expected: any = "trimmed\n\n\nas space"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/96L6", () => { + // Spec Example 2.14. In the folded scalars, newlines become spaces + const input: string = `--- > + Mark McGwire's + year was crippled + by a knee injury. +`; + + const parsed = YAML.parse(input); + + const expected: any = "Mark McGwire's year was crippled by a knee injury.\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/96NN/00", () => { + // Leading tab content in literals + const input: string = `foo: |- + bar +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "\tbar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/96NN/01", () => { + // Leading tab content in literals + const input: string = `foo: |- + bar`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "\tbar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/98YD", () => { + // Spec Example 5.5. Comment Indicator + const input: string = `# Comment only. +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9BXH", () => { + // Multiline doublequoted flow mapping key without value + const input: string = `--- +- { "single line", a: b} +- { "multi + line", a: b} +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { "single line": null, a: "b" }, + { "multi line": null, a: "b" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9C9N", () => { + // Wrong indented flow sequence + // Error test - expecting parse to fail + const input: string = `--- +flow: [a, +b, +c] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9CWY", () => { + // Invalid scalar at the end of mapping + // Error test - expecting parse to fail + const input: string = `key: + - item1 + - item2 +invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9DXL", () => { + // Spec Example 9.6. Stream [1.3] + const input: string = `Mapping: Document +--- +# Empty +... +%YAML 1.2 +--- +matches %: 20 +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ Mapping: "Document" }, null, { "matches %": 20 }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9FMG", () => { + // Multi-level Mapping Indent + const input: string = `a: + b: + c: d + e: + f: g +h: i +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: { + b: { c: "d" }, + e: { f: "g" }, + }, + h: "i", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9HCY", () => { + // Need document footer before directives + // Error test - expecting parse to fail + const input: string = `!foo "bar" +%TAG ! tag:example.com,2000:app/ +--- +!foo "bar" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9J7A", () => { + // Simple Mapping Indent + const input: string = `foo: + bar: baz +`; + + const parsed = YAML.parse(input); + + const expected: any = { + foo: { bar: "baz" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9JBA", () => { + // Invalid comment after end of flow sequence + // Error test - expecting parse to fail + const input: string = `--- +[ a, b, c, ]#invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9KAX", () => { + // Various combinations of tags and anchors + const input: string = `--- +&a1 +!!str +scalar1 +--- +!!str +&a2 +scalar2 +--- +&a3 +!!str scalar3 +--- +&a4 !!map +&a5 !!str key5: value4 +--- +a6: 1 +&anchor6 b6: 2 +--- +!!map +&a8 !!str key8: value7 +--- +!!map +!!str &a10 key10: value9 +--- +!!str &a11 +value11 +`; + + const parsed = YAML.parse(input); + + // Note: Original YAML may have anchors/aliases + // Some values in the parsed result may be shared object references + + const expected: any = [ + "scalar1", + "scalar2", + "scalar3", + { key5: "value4" }, + { a6: 1, b6: 2 }, + { key8: "value7" }, + { key10: "value9" }, + "value11", + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9KBC", () => { + // Mapping starting at --- line + // Error test - expecting parse to fail + const input: string = `--- key1: value1 + key2: value2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9MAG", () => { + // Flow sequence with invalid comma at the beginning + // Error test - expecting parse to fail + const input: string = `--- +[ , a, b, c ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9MMA", () => { + // Directive by itself with no document + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/9MMW", () => { + // Single Pair Implicit Entries (using test.event for expected values) + const input: string = `- [ YAML : separate ] +- [ "JSON like":adjacent ] +- [ {JSON: like}:adjacent ] +`; + + const parsed = YAML.parse(input); + + const expected: any = [[{ YAML: "separate" }], [{ "JSON like": "adjacent" }], [{ "[object Object]": "adjacent" }]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9MQT/00", () => { + // Scalar doc with '...' in content + const input: string = `--- "a +...x +b" +`; + + const parsed = YAML.parse(input); + + const expected: any = "a ...x b"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9MQT/01", () => { + // Scalar doc with '...' in content + // Error test - expecting parse to fail + const input: string = `--- "a +... x +b" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9SA2", () => { + // Multiline double quoted flow mapping key + const input: string = `--- +- { "single line": value} +- { "multi + line": value} +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "single line": "value" }, { "multi line": "value" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9SHH", () => { + // Spec Example 5.8. Quoted Scalar Indicators + const input: string = `single: 'text' +double: "text" +`; + + const parsed = YAML.parse(input); + + const expected: any = { single: "text", double: "text" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9TFX", () => { + // Spec Example 7.6. Double Quoted Lines [1.3] + const input: string = `--- +" 1st non-empty + + 2nd non-empty + 3rd non-empty " +`; + + const parsed = YAML.parse(input); + + const expected: any = " 1st non-empty\n2nd non-empty 3rd non-empty "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9U5K", () => { + // Spec Example 2.12. Compact Nested Mapping + const input: string = `--- +# Products purchased +- item : Super Hoop + quantity: 1 +- item : Basketball + quantity: 4 +- item : Big Shoes + quantity: 1 +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { item: "Super Hoop", quantity: 1 }, + { item: "Basketball", quantity: 4 }, + { item: "Big Shoes", quantity: 1 }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9WXW", () => { + // Spec Example 6.18. Primary Tag Handle + const input: string = `# Private +!foo "bar" +... +# Global +%TAG ! tag:example.com,2000:app/ +--- +!foo "bar" +`; + + const parsed = YAML.parse(input); + + const expected: any = ["bar", "bar"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9YRD", () => { + // Multiline Scalar at Top Level + const input: string = `a +b + c +d + +e +`; + + const parsed = YAML.parse(input); + + const expected: any = "a b c d\ne"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/A2M4", () => { + // Spec Example 6.2. Indentation Indicators + const input: string = `? a +: - b + - - c + - d +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: ["b", ["c", "d"]], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/A6F9", () => { + // Spec Example 8.4. Chomping Final Line Break + const input: string = `strip: |- + text +clip: | + text +keep: |+ + text +`; + + const parsed = YAML.parse(input); + + const expected: any = { strip: "text", clip: "text\n", keep: "text\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/A984", () => { + // Multiline Scalar in Mapping + const input: string = `a: b + c +d: + e + f +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: "b c", d: "e f" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/AB8U", () => { + // Sequence entry that looks like two with wrong indentation + const input: string = `- single multiline + - sequence entry +`; + + const parsed = YAML.parse(input); + + const expected: any = ["single multiline - sequence entry"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/AVM7", () => { + // Empty Stream + const input: string = ""; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/AZ63", () => { + // Sequence With Same Indentation as Parent Mapping + const input: string = `one: +- 2 +- 3 +four: 5 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + one: [2, 3], + four: 5, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/AZW3", () => { + // Lookahead test cases + const input: string = `- bla"keks: foo +- bla]keks: foo +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ 'bla"keks': "foo" }, { "bla]keks": "foo" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/B3HG", () => { + // Spec Example 8.9. Folded Scalar [1.3] + const input: string = `--- > + folded + text + + +`; + + const parsed = YAML.parse(input); + + const expected: any = "folded text\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/B63P", () => { + // Directive without document + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 +... +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/BD7L", () => { + // Invalid mapping after sequence + // Error test - expecting parse to fail + const input: string = `- item1 +- item2 +invalid: x +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/BEC7", () => { + // Spec Example 6.14. “YAML” directive + const input: string = `%YAML 1.3 # Attempt parsing + # with a warning +--- +"foo" +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/BF9H", () => { + // Trailing comment in multiline plain scalar + // Error test - expecting parse to fail + const input: string = `--- +plain: a + b # end of scalar + c +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/BS4K", () => { + // Comment between plain scalar lines + // Error test - expecting parse to fail + const input: string = `word1 # comment +word2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/BU8L", () => { + // Node Anchor and Tag on Seperate Lines + const input: string = `key: &anchor + !!map + a: b +`; + + const parsed = YAML.parse(input); + + const expected: any = { + key: { a: "b" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/C2DT", () => { + // Spec Example 7.18. Flow Mapping Adjacent Values + const input: string = `{ +"adjacent":value, +"readable": value, +"empty": +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { adjacent: "value", readable: "value", empty: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/C2SP", () => { + // Flow Mapping Key on two lines + // Error test - expecting parse to fail + const input: string = `[23 +]: 42 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/C4HZ", () => { + // Spec Example 2.24. Global Tags + const input: string = `%TAG ! tag:clarkevans.com,2002: +--- !shape + # Use the ! handle for presenting + # tag:clarkevans.com,2002:circle +- !circle + center: &ORIGIN {x: 73, y: 129} + radius: 7 +- !line + start: *ORIGIN + finish: { x: 89, y: 102 } +- !label + start: *ORIGIN + color: 0xFFEEBB + text: Pretty vector drawing. +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: ORIGIN + + const expected: any = [ + { + center: { x: 73, y: 129 }, + radius: 7, + }, + { + start: { x: 73, y: 129 }, + finish: { x: 89, y: 102 }, + }, + { + start: { x: 73, y: 129 }, + color: 16772795, + text: "Pretty vector drawing.", + }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CC74", () => { + // Spec Example 6.20. Tag Handles + const input: string = `%TAG !e! tag:example.com,2000:app/ +--- +!e!foo "bar" +`; + + const parsed = YAML.parse(input); + + const expected: any = "bar"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CFD4", () => { + // Empty implicit key in single pair flow sequences (using test.event for expected values) + const input: string = `- [ : empty key ] +- [: another empty key] +`; + + const parsed = YAML.parse(input); + + const expected: any = [[{ null: "empty key" }], [{ null: "another empty key" }]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CML9", () => { + // Missing comma in flow + // Error test - expecting parse to fail + const input: string = `key: [ word1 +# xxx + word2 ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/CN3R", () => { + // Various location of anchors in flow sequence + const input: string = `&flowseq [ + a: b, + &c c: d, + { &e e: f }, + &g { g: h } +] +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ a: "b" }, { c: "d" }, { e: "f" }, { g: "h" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CPZ3", () => { + // Doublequoted scalar starting with a tab + const input: string = `--- +tab: "\\tstring" +`; + + const parsed = YAML.parse(input); + + const expected: any = { tab: "\tstring" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CQ3W", () => { + // Double quoted string without closing quote + // Error test - expecting parse to fail + const input: string = `--- +key: "missing closing quote +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/CT4Q", () => { + // Spec Example 7.20. Single Pair Explicit Entry + const input: string = `[ +? foo + bar : baz +] +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "foo bar": "baz" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CTN5", () => { + // Flow sequence with invalid extra comma + // Error test - expecting parse to fail + const input: string = `--- +[ a, b, c, , ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/CUP7", () => { + // Spec Example 5.6. Node Property Indicators + const input: string = `anchored: !local &anchor value +alias: *anchor +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = { anchored: "value", alias: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CVW2", () => { + // Invalid comment after comma + // Error test - expecting parse to fail + const input: string = `--- +[ a, b, c,#invalid +] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/CXX2", () => { + // Mapping with anchor on document start line + // Error test - expecting parse to fail + const input: string = `--- &anchor a: b +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/D49Q", () => { + // Multiline single quoted implicit keys + // Error test - expecting parse to fail + const input: string = `'a\\nb': 1 +'c + d': 1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/D83L", () => { + // Block scalar indicator order + const input: string = `- |2- + explicit indent and chomp +- |-2 + chomp and explicit indent +`; + + const parsed = YAML.parse(input); + + const expected: any = ["explicit indent and chomp", "chomp and explicit indent"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/D88J", () => { + // Flow Sequence in Block Mapping + const input: string = `a: [b, c] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: ["b", "c"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/D9TU", () => { + // Single Pair Block Mapping + const input: string = `foo: bar +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DBG4", () => { + // Spec Example 7.10. Plain Characters + const input: string = `# Outside flow collection: +- ::vector +- ": - ()" +- Up, up, and away! +- -123 +- http://example.com/foo#bar +# Inside flow collection: +- [ ::vector, + ": - ()", + "Up, up and away!", + -123, + http://example.com/foo#bar ] +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + "::vector", + ": - ()", + "Up, up, and away!", + -123, + "http://example.com/foo#bar", + ["::vector", ": - ()", "Up, up and away!", -123, "http://example.com/foo#bar"], + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DC7X", () => { + // Various trailing tabs + const input: string = `a: b +seq: + - a +c: d #X +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: "b", + seq: ["a"], + c: "d", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/00", () => { + // Trailing tabs in double quoted + const input: string = `"1 trailing\\t + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "1 trailing\t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/01", () => { + // Trailing tabs in double quoted + const input: string = `"2 trailing\\t + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "2 trailing\t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/02", () => { + // Trailing tabs in double quoted + const input: string = `"3 trailing\\ + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "3 trailing\t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/03", () => { + // Trailing tabs in double quoted + const input: string = `"4 trailing\\ + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "4 trailing\t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/04", () => { + // Trailing tabs in double quoted + const input: string = `"5 trailing + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "5 trailing tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/05", () => { + // Trailing tabs in double quoted + const input: string = `"6 trailing + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "6 trailing tab"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/DFF7", () => { + // Spec Example 7.16. Flow Mapping Entries (using test.event for expected values) + const input: string = `{ +? explicit: entry, +implicit: entry, +? +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { explicit: "entry", implicit: "entry", null: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DHP8", () => { + // Flow Sequence + const input: string = `[foo, bar, 42] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo", "bar", 42]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK3J", () => { + // Zero indented block scalar with line that looks like a comment + const input: string = `--- > +line1 +# no comment +line3 +`; + + const parsed = YAML.parse(input); + + const expected: any = "line1 # no comment line3\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK4H", () => { + // Implicit key followed by newline + // Error test - expecting parse to fail + const input: string = `--- +[ key + : value ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/DK95/00", () => { + // Tabs that look like indentation + const input: string = `foo: + bar +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/01", () => { + // Tabs that look like indentation + // Error test - expecting parse to fail + const input: string = `foo: "bar + baz" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/DK95/02", () => { + // Tabs that look like indentation + const input: string = `foo: "bar + baz" +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar baz" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/03", () => { + // Tabs that look like indentation + const input: string = ` +foo: 1 +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: 1 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/04", () => { + // Tabs that look like indentation + const input: string = `foo: 1 + +bar: 2 +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: 1, bar: 2 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/05", () => { + // Tabs that look like indentation + const input: string = `foo: 1 + +bar: 2 +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: 1, bar: 2 }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/DK95/06", () => { + // Tabs that look like indentation + // Error test - expecting parse to fail + const input: string = `foo: + a: 1 + b: 2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/DK95/07", () => { + // Tabs that look like indentation + const input: string = `%YAML 1.2 + +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/08", () => { + // Tabs that look like indentation + const input: string = `foo: "bar + baz " +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar baz \t \t " }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DMG6", () => { + // Wrong indendation in Map + // Error test - expecting parse to fail + const input: string = `key: + ok: 1 + wrong: 2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/DWX9", () => { + // Spec Example 8.8. Literal Content + const input: string = `| + + + literal + + + text + + # Comment +`; + + const parsed = YAML.parse(input); + + const expected: any = "\n\nliteral\n \n\ntext\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/E76Z", () => { + // Aliases in Implicit Block Mapping + const input: string = `&a a: &b b +*b : *a +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a + + const expected: any = { a: "b", b: "a" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/EB22", () => { + // Missing document-end marker before directive + // Error test - expecting parse to fail + const input: string = `--- +scalar1 # comment +%YAML 1.2 +--- +scalar2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/EHF6", () => { + // Tags for Flow Objects + const input: string = `!!map { + k: !!seq + [ a, !!str b] +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + k: ["a", "b"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/EW3V", () => { + // Wrong indendation in mapping + // Error test - expecting parse to fail + const input: string = `k1: v1 + k2: v2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/EX5H", () => { + // Multiline Scalar at Top Level [1.3] + const input: string = `--- +a +b + c +d + +e +`; + + const parsed = YAML.parse(input); + + const expected: any = "a b c d\ne"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/EXG3", () => { + // Three dashes and content without space [1.3] + const input: string = `--- +---word1 +word2 +`; + + const parsed = YAML.parse(input); + + const expected: any = "---word1 word2"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/F2C7", () => { + // Anchors and Tags + const input: string = ` - &a !!str a + - !!int 2 + - !!int &c 4 + - &d d +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a", 2, 4, "d"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/F3CP", () => { + // Nested flow collections on one line + const input: string = `--- +{ a: [b, c, { d: [e, f] } ] } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: [ + "b", + "c", + { + d: ["e", "f"], + }, + ], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/F6MC", () => { + // More indented lines at the beginning of folded block scalars + const input: string = `--- +a: >2 + more indented + regular +b: >2 + + + more indented + regular +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: " more indented\nregular\n", b: "\n\n more indented\nregular\n" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/F8F9", () => { + // Spec Example 8.5. Chomping Trailing Lines + const input: string = ` # Strip + # Comments: +strip: |- + # text + + # Clip + # comments: + +clip: | + # text + + # Keep + # comments: + +keep: |+ + # text + + # Trail + # comments. +`; + + const parsed = YAML.parse(input); + + const expected: any = { strip: "# text", clip: "# text\n", keep: "# text\n\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FBC9", () => { + // Allowed characters in plain scalars + const input: string = + "safe: a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\n !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\nsafe question mark: ?foo\nsafe colon: :foo\nsafe dash: -foo\n"; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const expected: any = { + safe: "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~ !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~", + "safe question mark": "?foo", + "safe colon": ":foo", + "safe dash": "-foo", + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/FH7J", () => { + // Tags on Empty Scalars (using test.event for expected values) + const input: string = `- !!str +- + !!null : a + b: !!str +- !!str : !!null +`; + + const parsed = YAML.parse(input); + + const expected: any = ["", { null: "a", b: "" }, { null: null }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FP8R", () => { + // Zero indented block scalar + const input: string = `--- > +line1 +line2 +line3 +`; + + const parsed = YAML.parse(input); + + const expected: any = "line1 line2 line3\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FQ7F", () => { + // Spec Example 2.1. Sequence of Scalars + const input: string = `- Mark McGwire +- Sammy Sosa +- Ken Griffey +`; + + const parsed = YAML.parse(input); + + const expected: any = ["Mark McGwire", "Sammy Sosa", "Ken Griffey"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/FRK4", () => { + // Spec Example 7.3. Completely Empty Flow Nodes (using test.event for expected values) + const input: string = `{ + ? foo :, + : bar, +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: null, null: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FTA2", () => { + // Single block sequence with anchor and explicit document start + const input: string = `--- &sequence +- a +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FUP4", () => { + // Flow Sequence in Flow Sequence + const input: string = `[a, [b, c]] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a", ["b", "c"]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/G4RS", () => { + // Spec Example 2.17. Quoted Scalars + const input: string = `unicode: "Sosa did fine.\\u263A" +control: "\\b1998\\t1999\\t2000\\n" +hex esc: "\\x0d\\x0a is \\r\\n" + +single: '"Howdy!" he cried.' +quoted: ' # Not a ''comment''.' +tie-fighter: '|\\-*-/|' +`; + + const parsed = YAML.parse(input); + + const expected: any = { + unicode: "Sosa did fine.☺", + control: "\b1998\t1999\t2000\n", + "hex esc": "\r\n is \r\n", + single: '"Howdy!" he cried.', + quoted: " # Not a 'comment'.", + "tie-fighter": "|\\-*-/|", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/G5U8", () => { + // Plain dashes in flow sequence + // Error test - expecting parse to fail + const input: string = `--- +- [-, -] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/G7JE", () => { + // Multiline implicit keys + // Error test - expecting parse to fail + const input: string = `a\\nb: 1 +c + d: 1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/G992", () => { + // Spec Example 8.9. Folded Scalar + const input: string = `> + folded + text + + +`; + + const parsed = YAML.parse(input); + + const expected: any = "folded text\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/G9HC", () => { + // Invalid anchor in zero indented sequence + // Error test - expecting parse to fail + const input: string = `--- +seq: +&anchor +- a +- b +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/GDY7", () => { + // Comment that looks like a mapping key + // Error test - expecting parse to fail + const input: string = `key: value +this is #not a: key +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/GH63", () => { + // Mixed Block Mapping (explicit to implicit) + const input: string = `? a +: 1.3 +fifteen: d +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: 1.3, fifteen: "d" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/GT5M", () => { + // Node anchor in sequence + // Error test - expecting parse to fail + const input: string = `- item1 +&node +- item2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/H2RW", () => { + // Blank lines + const input: string = `foo: 1 + +bar: 2 + +text: | + a + + b + + c + + d +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: 1, bar: 2, text: "a\n \nb\n\nc\n\nd\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/H3Z8", () => { + // Literal unicode + const input: string = `--- +wanted: love ♥ and peace ☮ +`; + + const parsed = YAML.parse(input); + + const expected: any = { wanted: "love ♥ and peace ☮" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/H7J7", () => { + // Node anchor not indented + // Error test - expecting parse to fail + const input: string = `key: &x +!!map + a: b +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/H7TQ", () => { + // Extra words on %YAML directive + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 foo +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/HM87/00", () => { + // Scalars in flow start with syntax char + const input: string = `[:x] +`; + + const parsed = YAML.parse(input); + + const expected: any = [":x"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HM87/01", () => { + // Scalars in flow start with syntax char + const input: string = `[?x] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["?x"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HMK4", () => { + // Spec Example 2.16. Indentation determines scope + const input: string = `name: Mark McGwire +accomplishment: > + Mark set a major league + home run record in 1998. +stats: | + 65 Home Runs + 0.278 Batting Average +`; + + const parsed = YAML.parse(input); + + const expected: any = { + name: "Mark McGwire", + accomplishment: "Mark set a major league home run record in 1998.\n", + stats: "65 Home Runs\n0.278 Batting Average\n", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HMQ5", () => { + // Spec Example 6.23. Node Properties + const input: string = `!!str &a1 "foo": + !!str bar +&a2 baz : *a1 +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a1 + + const expected: any = { foo: "bar", baz: "foo" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HRE5", () => { + // Double quoted scalar with escaped single quote + // Error test - expecting parse to fail + const input: string = `--- +double: "quoted \\' scalar" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/HS5T", () => { + // Spec Example 7.12. Plain Lines + const input: string = `1st non-empty + + 2nd non-empty + 3rd non-empty +`; + + const parsed = YAML.parse(input); + + const expected: any = "1st non-empty\n2nd non-empty 3rd non-empty"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HU3P", () => { + // Invalid Mapping in plain scalar + // Error test - expecting parse to fail + const input: string = `key: + word1 word2 + no: key +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/HWV9", () => { + // Document-end marker + const input: string = `... +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J3BT", () => { + // Spec Example 5.12. Tabs and Spaces + const input: string = `# Tabs and spaces +quoted: "Quoted " +block: | + void main() { + printf("Hello, world!\\n"); + } +`; + + const parsed = YAML.parse(input); + + const expected: any = { quoted: "Quoted \t", block: 'void main() {\n\tprintf("Hello, world!\\n");\n}\n' }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J5UC", () => { + // Multiple Pair Block Mapping + const input: string = `foo: blue +bar: arrr +baz: jazz +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "blue", bar: "arrr", baz: "jazz" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J7PZ", () => { + // Spec Example 2.26. Ordered Mappings + const input: string = `# The !!omap tag is one of the optional types +# introduced for YAML 1.1. In 1.2, it is not +# part of the standard tags and should not be +# enabled by default. +# Ordered maps are represented as +# A sequence of mappings, with +# each mapping having one key +--- !!omap +- Mark McGwire: 65 +- Sammy Sosa: 63 +- Ken Griffy: 58 +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "Mark McGwire": 65 }, { "Sammy Sosa": 63 }, { "Ken Griffy": 58 }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J7VC", () => { + // Empty Lines Between Mapping Elements + const input: string = `one: 2 + + +three: 4 +`; + + const parsed = YAML.parse(input); + + const expected: any = { one: 2, three: 4 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J9HZ", () => { + // Spec Example 2.9. Single Document with Two Comments + const input: string = `--- +hr: # 1998 hr ranking + - Mark McGwire + - Sammy Sosa +rbi: + # 1998 rbi ranking + - Sammy Sosa + - Ken Griffey +`; + + const parsed = YAML.parse(input); + + const expected: any = { + hr: ["Mark McGwire", "Sammy Sosa"], + rbi: ["Sammy Sosa", "Ken Griffey"], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/JEF9/00", () => { + // Trailing whitespace in streams + const input: string = `- |+ + + +`; + + const parsed = YAML.parse(input); + + const expected: any = ["\n\n"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/JEF9/01", () => { + // Trailing whitespace in streams + const input: string = `- |+ + +`; + + const parsed = YAML.parse(input); + + const expected: any = ["\n"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/JEF9/02", () => { + // Trailing whitespace in streams + const input: string = `- |+ + `; + + const parsed = YAML.parse(input); + + const expected: any = ["\n"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JHB9", () => { + // Spec Example 2.7. Two Documents in a Stream + const input: string = `# Ranking of 1998 home runs +--- +- Mark McGwire +- Sammy Sosa +- Ken Griffey + +# Team ranking +--- +- Chicago Cubs +- St Louis Cardinals +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + ["Mark McGwire", "Sammy Sosa", "Ken Griffey"], + ["Chicago Cubs", "St Louis Cardinals"], + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JKF3", () => { + // Multiline unidented double quoted block key + // Error test - expecting parse to fail + const input: string = `- - "bar +bar": x +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/JQ4R", () => { + // Spec Example 8.14. Block Sequence + const input: string = `block sequence: + - one + - two : three +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "block sequence": ["one", { two: "three" }], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JR7V", () => { + // Question marks in scalars + const input: string = `- a?string +- another ? string +- key: value? +- [a?string] +- [another ? string] +- {key: value? } +- {key: value?} +- {key?: value } +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + "a?string", + "another ? string", + { key: "value?" }, + ["a?string"], + ["another ? string"], + { key: "value?" }, + { key: "value?" }, + { "key?": "value" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JS2J", () => { + // Spec Example 6.29. Node Anchors + const input: string = `First occurrence: &anchor Value +Second occurrence: *anchor +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = { "First occurrence": "Value", "Second occurrence": "Value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JTV5", () => { + // Block Mapping with Multiline Scalars + const input: string = `? a + true +: null + d +? e + 42 +`; + + const parsed = YAML.parse(input); + + const expected: any = { "a true": "null d", "e 42": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JY7Z", () => { + // Trailing content that looks like a mapping + // Error test - expecting parse to fail + const input: string = `key1: "quoted1" +key2: "quoted2" no key: nor value +key3: "quoted3" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/K3WX", () => { + // Colon and adjacent value after comment on next line + const input: string = `--- +{ "foo" # comment + :bar } +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/K4SU", () => { + // Multiple Entry Block Sequence + const input: string = `- foo +- bar +- 42 +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo", "bar", 42]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/K527", () => { + // Spec Example 6.6. Line Folding + const input: string = `>- + trimmed + + + + as + space +`; + + const parsed = YAML.parse(input); + + const expected: any = "trimmed\n\n\nas space"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/K54U", () => { + // Tab after document header + const input: string = `--- scalar +`; + + const parsed = YAML.parse(input); + + const expected: any = "scalar"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/K858", () => { + // Spec Example 8.6. Empty Scalar Chomping + const input: string = `strip: >- + +clip: > + +keep: |+ + +`; + + const parsed = YAML.parse(input); + + const expected: any = { strip: "", clip: "", keep: "\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KH5V/00", () => { + // Inline tabs in double quoted + const input: string = `"1 inline\\ttab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "1 inline\ttab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KH5V/01", () => { + // Inline tabs in double quoted + const input: string = `"2 inline\\ tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "2 inline\ttab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KH5V/02", () => { + // Inline tabs in double quoted + const input: string = `"3 inline tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "3 inline\ttab"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/KK5P", () => { + // Various combinations of explicit block mappings (using test.event for expected values) + const input: string = `complex1: + ? - a +complex2: + ? - a + : b +complex3: + ? - a + : > + b +complex4: + ? > + a + : +complex5: + ? - a + : - b +`; + + const parsed = YAML.parse(input); + + const expected: any = { + complex1: { a: null }, + complex2: { a: "b" }, + complex3: { a: "b\n" }, + complex4: { "a\n": null }, + complex5: { + a: ["b"], + }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KMK3", () => { + // Block Submapping + const input: string = `foo: + bar: 1 +baz: 2 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + foo: { bar: 1 }, + baz: 2, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KS4U", () => { + // Invalid item after end of flow sequence + // Error test - expecting parse to fail + const input: string = `--- +[ +sequence item +] +invalid item +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/KSS4", () => { + // Scalars on --- line + const input: string = `--- "quoted +string" +--- &node foo +`; + + const parsed = YAML.parse(input); + + // Note: Original YAML may have anchors/aliases + // Some values in the parsed result may be shared object references + + const expected: any = ["quoted string", "foo"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/L24T/00", () => { + // Trailing line of spaces + const input: string = `foo: | + x + +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "x\n \n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/L24T/01", () => { + // Trailing line of spaces + const input: string = `foo: | + x + `; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "x\n \n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/L383", () => { + // Two scalar docs with trailing comments + const input: string = `--- foo # comment +--- foo # comment +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo", "foo"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/L94M", () => { + // Tags in Explicit Mapping + const input: string = `? !!str a +: !!int 47 +? c +: !!str d +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: 47, c: "d" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/L9U5", () => { + // Spec Example 7.11. Plain Implicit Keys + const input: string = `implicit block key : [ + implicit flow key : value, + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "implicit block key": [{ "implicit flow key": "value" }], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/LE5A", () => { + // Spec Example 7.24. Flow Nodes + const input: string = `- !!str "a" +- 'b' +- &anchor "c" +- *anchor +- !!str +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = ["a", "b", "c", "c", ""]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/LHL4", () => { + // Invalid tag + // Error test - expecting parse to fail + const input: string = `--- +!invalid{}tag scalar +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/LP6E", () => { + // Whitespace After Scalars in Flow + const input: string = `- [a, b , c ] +- { "a" : b + , c : 'd' , + e : "f" + } +- [ ] +`; + + const parsed = YAML.parse(input); + + const expected: any = [["a", "b", "c"], { a: "b", c: "d", e: "f" }, []]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/LQZ7", () => { + // Spec Example 7.4. Double Quoted Implicit Keys + const input: string = `"implicit block key" : [ + "implicit flow key" : value, + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "implicit block key": [{ "implicit flow key": "value" }], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/LX3P", () => { + // Implicit Flow Mapping Key on one line (using test.event for expected values) + const input: string = `[flow]: block +`; + + const parsed = YAML.parse(input); + + const expected: any = { flow: "block" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M29M", () => { + // Literal Block Scalar + const input: string = `a: | + ab + + cd + ef + + +... +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: "ab\n\ncd\nef\n" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/M2N8/00", () => { + // Question mark edge cases (using test.event for expected values) + const input: string = `- ? : x +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "[object Object]": null }]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/M2N8/01", () => { + // Question mark edge cases (using test.event for expected values) + const input: string = `? []: x +`; + + const parsed = YAML.parse(input); + + const expected: any = { "{\n ? []\n : x\n}": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M5C3", () => { + // Spec Example 8.21. Block Scalar Nodes + const input: string = `literal: |2 + value +folded: + !foo + >1 + value +`; + + const parsed = YAML.parse(input); + + const expected: any = { literal: "value\n", folded: "value\n" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/M5DY", () => { + // Spec Example 2.11. Mapping between Sequences (using test.event for expected values) + const input: string = `? - Detroit Tigers + - Chicago cubs +: + - 2001-07-23 + +? [ New York Yankees, + Atlanta Braves ] +: [ 2001-07-02, 2001-08-12, + 2001-08-14 ] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "Detroit Tigers,Chicago cubs": ["2001-07-23"], + "New York Yankees,Atlanta Braves": ["2001-07-02", "2001-08-12", "2001-08-14"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M6YH", () => { + // Block sequence indentation + const input: string = `- | + x +- + foo: bar +- + - 42 +`; + + const parsed = YAML.parse(input); + + const expected: any = ["x\n", { foo: "bar" }, [42]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M7A3", () => { + // Spec Example 9.3. Bare Documents + const input: string = `Bare +document +... +# No document +... +| +%!PS-Adobe-2.0 # Not the first line +`; + + const parsed = YAML.parse(input); + + const expected: any = ["Bare document", "%!PS-Adobe-2.0 # Not the first line\n"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M7NX", () => { + // Nested flow collections + const input: string = `--- +{ + a: [ + b, c, { + d: [e, f] + } + ] +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: [ + "b", + "c", + { + d: ["e", "f"], + }, + ], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M9B4", () => { + // Spec Example 8.7. Literal Scalar + const input: string = `| + literal + text + + +`; + + const parsed = YAML.parse(input); + + const expected: any = "literal\n\ttext\n"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/MJS9", () => { + // Spec Example 6.7. Block Folding + const input: string = `> + foo + + bar + + baz +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo \n\n\t bar\n\nbaz\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/00", () => { + // Directive variants + // Error test - expecting parse to fail + const input: string = `%YAML 1.1#... +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/MUS6/01", () => { + // Directive variants + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 +--- +%YAML 1.2 +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/MUS6/02", () => { + // Directive variants + const input: string = `%YAML 1.1 +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/03", () => { + // Directive variants + const input: string = `%YAML 1.1 +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/04", () => { + // Directive variants + const input: string = `%YAML 1.1 # comment +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/05", () => { + // Directive variants + const input: string = `%YAM 1.1 +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/06", () => { + // Directive variants + const input: string = `%YAMLL 1.1 +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MXS3", () => { + // Flow Mapping in Block Sequence + const input: string = `- {a: b} +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ a: "b" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MYW6", () => { + // Block Scalar Strip + const input: string = `|- + ab + + +... +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MZX3", () => { + // Non-Specific Tags on Scalars + const input: string = `- plain +- "double quoted" +- 'single quoted' +- > + block +- plain again +`; + + const parsed = YAML.parse(input); + + const expected: any = ["plain", "double quoted", "single quoted", "block\n", "plain again"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/N4JP", () => { + // Bad indentation in mapping + // Error test - expecting parse to fail + const input: string = `map: + key1: "quoted1" + key2: "bad indentation" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/N782", () => { + // Invalid document markers in flow style + // Error test - expecting parse to fail + const input: string = `[ +--- , +... +] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/NAT4", () => { + // Various empty or newline only quoted strings + const input: string = `--- +a: ' + ' +b: ' + ' +c: " + " +d: " + " +e: ' + + ' +f: " + + " +g: ' + + + ' +h: " + + + " +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: " ", + b: " ", + c: " ", + d: " ", + e: "\n", + f: "\n", + g: "\n\n", + h: "\n\n", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NB6Z", () => { + // Multiline plain value with tabs on empty lines + const input: string = `key: + value + with + + tabs +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value with\ntabs" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NHX8", () => { + // Empty Lines at End of Document (using test.event for expected values) + const input: string = `: + + +`; + + const parsed = YAML.parse(input); + + const expected: any = { null: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NJ66", () => { + // Multiline plain flow mapping key + const input: string = `--- +- { single line: value} +- { multi + line: value} +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "single line": "value" }, { "multi line": "value" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NKF9", () => { + // Empty keys in block and flow mapping (using test.event for expected values) + const input: string = `--- +key: value +: empty key +--- +{ + key: value, : empty key +} +--- +# empty key and value +: +--- +# empty key and value +{ : } +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { key: "value", null: "empty key" }, + { key: "value", null: "empty key" }, + { null: null }, + { null: null }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NP9H", () => { + // Spec Example 7.5. Double Quoted Line Breaks + const input: string = `"folded +to a space, + +to a line feed, or \\ + \\ non-content" +`; + + const parsed = YAML.parse(input); + + const expected: any = "folded to a space,\nto a line feed, or \t \tnon-content"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/P2AD", () => { + // Spec Example 8.1. Block Scalar Header + const input: string = `- | # Empty header↓ + literal +- >1 # Indentation indicator↓ + folded +- |+ # Chomping indicator↓ + keep + +- >1- # Both indicators↓ + strip +`; + + const parsed = YAML.parse(input); + + const expected: any = ["literal\n", " folded\n", "keep\n\n", " strip"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/P2EQ", () => { + // Invalid sequene item on same line as previous item + // Error test - expecting parse to fail + const input: string = `--- +- { y: z }- invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/P76L", () => { + // Spec Example 6.19. Secondary Tag Handle + const input: string = `%TAG !! tag:example.com,2000:app/ +--- +!!int 1 - 3 # Interval, not integer +`; + + const parsed = YAML.parse(input); + + const expected: any = "1 - 3"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/P94K", () => { + // Spec Example 6.11. Multi-Line Comments + const input: string = `key: # Comment + # lines + value + + +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/PBJ2", () => { + // Spec Example 2.3. Mapping Scalars to Sequences + const input: string = `american: + - Boston Red Sox + - Detroit Tigers + - New York Yankees +national: + - New York Mets + - Chicago Cubs + - Atlanta Braves +`; + + const parsed = YAML.parse(input); + + const expected: any = { + american: ["Boston Red Sox", "Detroit Tigers", "New York Yankees"], + national: ["New York Mets", "Chicago Cubs", "Atlanta Braves"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/PRH3", () => { + // Spec Example 7.9. Single Quoted Lines + const input: string = `' 1st non-empty + + 2nd non-empty + 3rd non-empty ' +`; + + const parsed = YAML.parse(input); + + const expected: any = " 1st non-empty\n2nd non-empty 3rd non-empty "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/PUW8", () => { + // Document start on last line + const input: string = `--- +a: b +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ a: "b" }, null]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/PW8X", () => { + // Anchors on Empty Scalars (using test.event for expected values) + const input: string = `- &a +- a +- + &a : a + b: &b +- + &c : &a +- + ? &d +- + ? &e + : &a +`; + + const parsed = YAML.parse(input); + + const expected: any = [null, "a", { null: "a", b: null }, { null: null }, { null: null }, { null: null }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Q4CL", () => { + // Trailing content after quoted value + // Error test - expecting parse to fail + const input: string = `key1: "quoted1" +key2: "quoted2" trailing content +key3: "quoted3" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Q5MG", () => { + // Tab at beginning of line followed by a flow mapping + const input: string = ` {} +`; + + const parsed = YAML.parse(input); + + const expected: any = {}; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Q88A", () => { + // Spec Example 7.23. Flow Content + const input: string = `- [ a, b ] +- { a: b } +- "a" +- 'b' +- c +`; + + const parsed = YAML.parse(input); + + const expected: any = [["a", "b"], { a: "b" }, "a", "b", "c"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Q8AD", () => { + // Spec Example 7.5. Double Quoted Line Breaks [1.3] + const input: string = `--- +"folded +to a space, + +to a line feed, or \\ + \\ non-content" +`; + + const parsed = YAML.parse(input); + + const expected: any = "folded to a space,\nto a line feed, or \t \tnon-content"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Q9WF", () => { + // Spec Example 6.12. Separation Spaces (using test.event for expected values) + const input: string = `{ first: Sammy, last: Sosa }: +# Statistics: + hr: # Home runs + 65 + avg: # Average + 0.278 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "[object Object]": { hr: 65, avg: 0.278 }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/QB6E", () => { + // Wrong indented multiline quoted scalar + // Error test - expecting parse to fail + const input: string = `--- +quoted: "a +b +c" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/QF4Y", () => { + // Spec Example 7.19. Single Pair Flow Mappings + const input: string = `[ +foo: bar +] +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ foo: "bar" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/QLJ7", () => { + // Tag shorthand used in documents but only defined in the first + // Error test - expecting parse to fail + const input: string = `%TAG !prefix! tag:example.com,2011: +--- !prefix!A +a: b +--- !prefix!B +c: d +--- !prefix!C +e: f +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/QT73", () => { + // Comment and document-end marker + const input: string = `# comment +... +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/R4YG", () => { + // Spec Example 8.2. Block Indentation Indicator + const input: string = `- | + detected +- > + + + # detected +- |1 + explicit +- > + + detected +`; + + const parsed = YAML.parse(input); + + const expected: any = ["detected\n", "\n\n# detected\n", " explicit\n", "\t\ndetected\n"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/R52L", () => { + // Nested flow mapping sequence and mappings + const input: string = `--- +{ top1: [item1, {key2: value2}, item3], top2: value2 } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + top1: ["item1", { key2: "value2" }, "item3"], + top2: "value2", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/RHX7", () => { + // YAML directive without document end marker + // Error test - expecting parse to fail + const input: string = `--- +key: value +%YAML 1.2 +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/RLU9", () => { + // Sequence Indent + const input: string = `foo: +- 42 +bar: + - 44 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + foo: [42], + bar: [44], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/RR7F", () => { + // Mixed Block Mapping (implicit to explicit) + const input: string = `a: 4.2 +? d +: 23 +`; + + const parsed = YAML.parse(input); + + const expected: any = { d: 23, a: 4.2 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/RTP8", () => { + // Spec Example 9.2. Document Markers + const input: string = `%YAML 1.2 +--- +Document +... # Suffix +`; + + const parsed = YAML.parse(input); + + const expected: any = "Document"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/RXY3", () => { + // Invalid document-end marker in single quoted string + // Error test - expecting parse to fail + const input: string = `--- +' +... +' +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/RZP5", () => { + // Various Trailing Comments [1.3] (using test.event for expected values) + const input: string = `a: "double + quotes" # lala +b: plain + value # lala +c : #lala + d +? # lala + - seq1 +: # lala + - #lala + seq2 +e: &node # lala + - x: y +block: > # lala + abcde +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: "double quotes", + b: "plain value", + c: "d", + seq1: ["seq2"], + e: [{ x: "y" }], + block: "abcde\n", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/RZT7", () => { + // Spec Example 2.28. Log File + const input: string = `--- +Time: 2001-11-23 15:01:42 -5 +User: ed +Warning: + This is an error message + for the log file +--- +Time: 2001-11-23 15:02:31 -5 +User: ed +Warning: + A slightly different error + message. +--- +Date: 2001-11-23 15:03:17 -5 +User: ed +Fatal: + Unknown variable "bar" +Stack: + - file: TopClass.py + line: 23 + code: | + x = MoreObject("345\\n") + - file: MoreClass.py + line: 58 + code: |- + foo = bar +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { Time: "2001-11-23 15:01:42 -5", User: "ed", Warning: "This is an error message for the log file" }, + { Time: "2001-11-23 15:02:31 -5", User: "ed", Warning: "A slightly different error message." }, + { + Date: "2001-11-23 15:03:17 -5", + User: "ed", + Fatal: 'Unknown variable "bar"', + Stack: [ + { file: "TopClass.py", line: 23, code: 'x = MoreObject("345\\n")\n' }, + { file: "MoreClass.py", line: 58, code: "foo = bar" }, + ], + }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S3PD", () => { + // Spec Example 8.18. Implicit Block Mapping Entries (using test.event for expected values) + const input: string = `plain key: in-line value +: # Both empty +"quoted key": +- entry +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "plain key": "in-line value", + null: null, + "quoted key": ["entry"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S4GJ", () => { + // Invalid text after block scalar indicator + // Error test - expecting parse to fail + const input: string = `--- +folded: > first line + second line +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/S4JQ", () => { + // Spec Example 6.28. Non-Specific Tags + const input: string = `# Assuming conventional resolution: +- "12" +- 12 +- ! 12 +`; + + const parsed = YAML.parse(input); + + const expected: any = ["12", 12, "12"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S4T7", () => { + // Document with footer + const input: string = `aaa: bbb +... +`; + + const parsed = YAML.parse(input); + + const expected: any = { aaa: "bbb" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S7BG", () => { + // Colon followed by comma + const input: string = `--- +- :, +`; + + const parsed = YAML.parse(input); + + const expected: any = [":,"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S98Z", () => { + // Block scalar with more spaces than first content line + // Error test - expecting parse to fail + const input: string = `empty block scalar: > + + + + # comment +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/S9E8", () => { + // Spec Example 5.3. Block Structure Indicators + const input: string = `sequence: +- one +- two +mapping: + ? sky + : blue + sea : green +`; + + const parsed = YAML.parse(input); + + const expected: any = { + sequence: ["one", "two"], + mapping: { sky: "blue", sea: "green" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SBG9", () => { + // Flow Sequence in Flow Mapping (using test.event for expected values) + const input: string = `{a: [b, c], [d, e]: f} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: ["b", "c"], + "d,e": "f", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SF5V", () => { + // Duplicate YAML directive + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 +%YAML 1.2 +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SKE5", () => { + // Anchor before zero indented sequence + const input: string = `--- +seq: + &anchor +- a +- b +`; + + const parsed = YAML.parse(input); + + const expected: any = { + seq: ["a", "b"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SM9W/00", () => { + // Single character streams + const input: string = "-"; + + const parsed = YAML.parse(input); + + const expected: any = [null]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SM9W/01", () => { + // Single character streams (using test.event for expected values) + const input: string = ":"; + + const parsed = YAML.parse(input); + + const expected: any = { null: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SR86", () => { + // Anchor plus Alias + // Error test - expecting parse to fail + const input: string = `key1: &a value +key2: &b *a +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SSW6", () => { + // Spec Example 7.7. Single Quoted Characters [1.3] + const input: string = `--- +'here''s to "quotes"' +`; + + const parsed = YAML.parse(input); + + const expected: any = 'here\'s to "quotes"'; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SU5Z", () => { + // Comment without whitespace after doublequoted scalar + // Error test - expecting parse to fail + const input: string = `key: "value"# invalid comment +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SU74", () => { + // Anchor and alias as mapping key + // Error test - expecting parse to fail + const input: string = `key1: &alias value1 +&b *alias : value2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SY6V", () => { + // Anchor before sequence entry on same line + // Error test - expecting parse to fail + const input: string = `&anchor - sequence entry +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SYW4", () => { + // Spec Example 2.2. Mapping Scalars to Scalars + const input: string = `hr: 65 # Home runs +avg: 0.278 # Batting average +rbi: 147 # Runs Batted In +`; + + const parsed = YAML.parse(input); + + const expected: any = { hr: 65, avg: 0.278, rbi: 147 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/T26H", () => { + // Spec Example 8.8. Literal Content [1.3] + const input: string = `--- | + + + literal + + + text + + # Comment +`; + + const parsed = YAML.parse(input); + + const expected: any = "\n\nliteral\n \n\ntext\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/T4YY", () => { + // Spec Example 7.9. Single Quoted Lines [1.3] + const input: string = `--- +' 1st non-empty + + 2nd non-empty + 3rd non-empty ' +`; + + const parsed = YAML.parse(input); + + const expected: any = " 1st non-empty\n2nd non-empty 3rd non-empty "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/T5N4", () => { + // Spec Example 8.7. Literal Scalar [1.3] + const input: string = `--- | + literal + text + + +`; + + const parsed = YAML.parse(input); + + const expected: any = "literal\n\ttext\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/T833", () => { + // Flow mapping missing a separating comma + // Error test - expecting parse to fail + const input: string = `--- +{ + foo: 1 + bar: 2 } +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/TD5N", () => { + // Invalid scalar after sequence + // Error test - expecting parse to fail + const input: string = `- item1 +- item2 +invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/TE2A", () => { + // Spec Example 8.16. Block Mappings + const input: string = `block mapping: + key: value +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "block mapping": { key: "value" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/TL85", () => { + // Spec Example 6.8. Flow Folding + const input: string = `" + foo + + bar + + baz +" +`; + + const parsed = YAML.parse(input); + + const expected: any = " foo\nbar\nbaz "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/TS54", () => { + // Folded Block Scalar + const input: string = `> + ab + cd + + ef + + + gh +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab cd\nef\n\ngh\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/U3C3", () => { + // Spec Example 6.16. “TAG” directive + const input: string = `%TAG !yaml! tag:yaml.org,2002: +--- +!yaml!str "foo" +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/U3XV", () => { + // Node and Mapping Key Anchors + const input: string = `--- +top1: &node1 + &k1 key1: one +top2: &node2 # comment + key2: two +top3: + &k3 key3: three +top4: + &node4 + &k4 key4: four +top5: + &node5 + key5: five +top6: &val6 + six +top7: + &val7 seven +`; + + const parsed = YAML.parse(input); + + const expected: any = { + top1: { key1: "one" }, + top2: { key2: "two" }, + top3: { key3: "three" }, + top4: { key4: "four" }, + top5: { key5: "five" }, + top6: "six", + top7: "seven", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/U44R", () => { + // Bad indentation in mapping (2) + // Error test - expecting parse to fail + const input: string = `map: + key1: "quoted1" + key2: "bad indentation" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/U99R", () => { + // Invalid comma in tag + // Error test - expecting parse to fail + const input: string = `- !!str, xxx +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/U9NS", () => { + // Spec Example 2.8. Play by Play Feed from a Game + const input: string = `--- +time: 20:03:20 +player: Sammy Sosa +action: strike (miss) +... +--- +time: 20:03:47 +player: Sammy Sosa +action: grand slam +... +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { time: "20:03:20", player: "Sammy Sosa", action: "strike (miss)" }, + { time: "20:03:47", player: "Sammy Sosa", action: "grand slam" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UDM2", () => { + // Plain URL in flow mapping + const input: string = `- { url: http://example.org } +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ url: "http://example.org" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UDR7", () => { + // Spec Example 5.4. Flow Collection Indicators + const input: string = `sequence: [ one, two, ] +mapping: { sky: blue, sea: green } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + sequence: ["one", "two"], + mapping: { sky: "blue", sea: "green" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UGM3", () => { + // Spec Example 2.27. Invoice + const input: string = `--- ! +invoice: 34843 +date : 2001-01-23 +bill-to: &id001 + given : Chris + family : Dumars + address: + lines: | + 458 Walkman Dr. + Suite #292 + city : Royal Oak + state : MI + postal : 48046 +ship-to: *id001 +product: + - sku : BL394D + quantity : 4 + description : Basketball + price : 450.00 + - sku : BL4438H + quantity : 1 + description : Super Hoop + price : 2392.00 +tax : 251.42 +total: 4443.52 +comments: + Late afternoon is best. + Backup contact is Nancy + Billsmer @ 338-4338. +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const sharedAddress: any = { + given: "Chris", + family: "Dumars", + address: { + lines: "458 Walkman Dr.\nSuite #292\n", + city: "Royal Oak", + state: "MI", + postal: 48046, + }, + }; + const expected = { + "invoice": 34843, + "date": "2001-01-23", + "bill-to": sharedAddress, + "ship-to": sharedAddress, + "product": [ + { + sku: "BL394D", + quantity: 4, + description: "Basketball", + price: 450, + }, + { + sku: "BL4438H", + quantity: 1, + description: "Super Hoop", + price: 2392, + }, + ], + "tax": 251.42, + "total": 4443.52, + "comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UKK6/00", () => { + // Syntax character edge cases (using test.event for expected values) + const input: string = `- : +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ null: null }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UKK6/01", () => { + // Syntax character edge cases + const input: string = `:: +`; + + const parsed = YAML.parse(input); + + const expected: any = { ":": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UKK6/02", () => { + // Syntax character edge cases (using test.event for expected values) + const input: string = `! +`; + + const parsed = YAML.parse(input); + + const expected: any = ""; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UT92", () => { + // Spec Example 9.4. Explicit Documents + const input: string = `--- +{ matches +% : 20 } +... +--- +# Empty +... +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "matches %": 20 }, null]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UV7Q", () => { + // Legal tab after indentation + const input: string = `x: + - x + x +`; + + const parsed = YAML.parse(input); + + const expected: any = { + x: ["x x"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/V55R", () => { + // Aliases in Block Sequence + const input: string = `- &a a +- &b b +- *a +- *b +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a, b + + const expected: any = ["a", "b", "a", "b"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/V9D5", () => { + // Spec Example 8.19. Compact Block Mappings (using test.event for expected values) + const input: string = `- sun: yellow +- ? earth: blue + : moon: white +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { sun: "yellow" }, + { + "[object Object]": { moon: "white" }, + }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/VJP3/00", () => { + // Flow collections over many lines + // Error test - expecting parse to fail + const input: string = `k: { +k +: +v +} +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/VJP3/01", () => { + // Flow collections over many lines + const input: string = `k: { + k + : + v + } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + k: { k: "v" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/W42U", () => { + // Spec Example 8.15. Block Sequence Entry Types + const input: string = `- # Empty +- | + block node +- - one # Compact + - two # sequence +- one: two # Compact mapping +`; + + const parsed = YAML.parse(input); + + const expected: any = [null, "block node\n", ["one", "two"], { one: "two" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/W4TN", () => { + // Spec Example 9.5. Directives Documents + const input: string = `%YAML 1.2 +--- | +%!PS-Adobe-2.0 +... +%YAML 1.2 +--- +# Empty +... +`; + + const parsed = YAML.parse(input); + + const expected: any = ["%!PS-Adobe-2.0\n", null]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/W5VH", () => { + // Allowed characters in alias + const input: string = `a: &:@*!$": scalar a +b: *:@*!$": +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const expected: any = { a: "scalar a", b: "scalar a" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/W9L4", () => { + // Literal block scalar with more spaces in first line + // Error test - expecting parse to fail + const input: string = `--- +block scalar: | + + more spaces at the beginning + are invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/WZ62", () => { + // Spec Example 7.2. Empty Content + const input: string = `{ + foo : !!str, + !!str : bar, +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "", "": "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/X38W", () => { + // Aliases in Flow Objects + // Special case: *a references the same array as first key, creating duplicate key + const input: string = `{ &a [a, &b b]: *b, *a : [c, *b, d]} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "a,b": ["c", "b", "d"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/X4QW", () => { + // Comment without whitespace after block scalar indicator + // Error test - expecting parse to fail + const input: string = `block: ># comment + scalar +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/X8DW", () => { + // Explicit key and value seperated by comment + const input: string = `--- +? key +# comment +: value +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/XLQ9", () => { + // Multiline scalar that looks like a YAML directive + const input: string = `--- +scalar +%YAML 1.2 +`; + + const parsed = YAML.parse(input); + + const expected: any = "scalar %YAML 1.2"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/XV9V", () => { + // Spec Example 6.5. Empty Lines [1.3] + const input: string = `Folding: + "Empty line + + as a line feed" +Chomping: | + Clipped empty lines + + +`; + + const parsed = YAML.parse(input); + + const expected: any = { Folding: "Empty line\nas a line feed", Chomping: "Clipped empty lines\n" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/XW4D", () => { + // Various Trailing Comments (using test.event for expected values) + const input: string = `a: "double + quotes" # lala +b: plain + value # lala +c : #lala + d +? # lala + - seq1 +: # lala + - #lala + seq2 +e: + &node # lala + - x: y +block: > # lala + abcde +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: "double quotes", + b: "plain value", + c: "d", + seq1: ["seq2"], + e: [{ x: "y" }], + block: "abcde\n", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Y2GN", () => { + // Anchor with colon in the middle + const input: string = `--- +key: &an:chor value +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Y79Y/000", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `foo: | + +bar: 1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/001", () => { + // Tabs in various contexts + const input: string = `foo: | + +bar: 1 +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "\t\n", bar: 1 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Y79Y/002", () => { + // Tabs in various contexts + const input: string = `- [ + + foo + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = [["foo"]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Y79Y/003", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `- [ + foo, + foo + ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/004", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `- - +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/Y79Y/005", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `- - +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/Y79Y/006", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `? - +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/007", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `? - +: - +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/Y79Y/008", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `? key: +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/009", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `? key: +: key: +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/010", () => { + // Tabs in various contexts + const input: string = `- -1 +`; + + const parsed = YAML.parse(input); + + const expected: any = [-1]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/YD5X", () => { + // Spec Example 2.5. Sequence of Sequences + const input: string = `- [name , hr, avg ] +- [Mark McGwire, 65, 0.278] +- [Sammy Sosa , 63, 0.288] +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + ["name", "hr", "avg"], + ["Mark McGwire", 65, 0.278], + ["Sammy Sosa", 63, 0.288], + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/YJV2", () => { + // Dash in flow sequence + // Error test - expecting parse to fail + const input: string = `[-] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Z67P", () => { + // Spec Example 8.21. Block Scalar Nodes [1.3] + const input: string = `literal: |2 + value +folded: !foo >1 + value +`; + + const parsed = YAML.parse(input); + + const expected: any = { literal: "value\n", folded: "value\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Z9M4", () => { + // Spec Example 6.22. Global Tag Prefix + const input: string = `%TAG !e! tag:example.com,2000:app/ +--- +- !e!foo "bar" +`; + + const parsed = YAML.parse(input); + + const expected: any = ["bar"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZCZ6", () => { + // Invalid mapping in plain single line value + // Error test - expecting parse to fail + const input: string = `a: b: c: d +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/ZF4X", () => { + // Spec Example 2.6. Mapping of Mappings + const input: string = `Mark McGwire: {hr: 65, avg: 0.278} +Sammy Sosa: { + hr: 63, + avg: 0.288 + } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "Mark McGwire": { hr: 65, avg: 0.278 }, + "Sammy Sosa": { hr: 63, avg: 0.288 }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZH7C", () => { + // Anchors in Mapping + const input: string = `&a a: b +c: &d d +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: "b", c: "d" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZK9H", () => { + // Nested top level flow mapping + const input: string = `{ key: [[[ + value + ]]] +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + key: [[["value"]]], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZL4Z", () => { + // Invalid nested mapping + // Error test - expecting parse to fail + const input: string = `--- +a: 'b': c +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/ZVH3", () => { + // Wrong indented sequence item + // Error test - expecting parse to fail + const input: string = `- key: value + - item1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/ZWK4", () => { + // Key with anchor after missing explicit mapping value + const input: string = `--- +a: 1 +? b +&anchor c: 3 +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: 1, b: null, c: 3 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZXT5", () => { + // Implicit key followed by newline and adjacent value + // Error test - expecting parse to fail + const input: string = `[ "key" + :value ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index df904d64e9..f3e0d949ba 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -1,5 +1,6 @@ -import { YAML } from "bun"; +import { YAML, file } from "bun"; import { describe, expect, test } from "bun:test"; +import { join } from "path"; describe("Bun.YAML", () => { describe("parse", () => { @@ -702,6 +703,64 @@ production: }, }); }); + + test("issue 22659", () => { + const input1 = `- test2: next + test1: +`; + expect(YAML.parse(input1)).toMatchInlineSnapshot(` + [ + { + "test1": "+", + "test2": "next", + }, + ] + `); + const input2 = `- test1: + + test2: next`; + expect(YAML.parse(input2)).toMatchInlineSnapshot(` + [ + { + "test1": "+", + "test2": "next", + }, + ] + `); + }); + + test("issue 22392", () => { + const input = ` +foo: "some + ... + string" +`; + expect(YAML.parse(input)).toMatchInlineSnapshot(` + { + "foo": "some ... string", + } + `); + }); + + test("issue 22286", async () => { + const input1 = ` +my_anchor: &MyAnchor "MyAnchor" + +my_config: + *MyAnchor : + some_key: "some_value" +`; + expect(YAML.parse(input1)).toMatchInlineSnapshot(` + { + "my_anchor": "MyAnchor", + "my_config": { + "MyAnchor": { + "some_key": "some_value", + }, + }, + } + `); + const input2 = await file(join(import.meta.dir, "fixtures", "AHatInTime.yaml")).text(); + expect(YAML.parse(input2)).toMatchSnapshot(); + }); }); describe("stringify", () => { From bf26d725abaa93445240b7bd6152af0677ce6a8f Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sun, 5 Oct 2025 17:22:55 -0800 Subject: [PATCH 039/191] scripts/runner: pass TEST_SERIAL_ID for proper parallelism handling (#23031) adds environment variable for proper tmpdir setup actual fix for https://github.com/oven-sh/bun/commit/d2a4fb8124163b26cd9df65d838a73e6887d54c0 (which was reverted) this fixes flakyness in node:fs and node:cluster when using scripts/runner.node.mjs locally with the --parallel flag --- scripts/runner.node.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index fd9aee180b..cdf824ddfb 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -80,6 +80,7 @@ function getNodeParallelTestTimeout(testPath) { if (testPath.includes("test-dns")) { return 90_000; } + if (!isCI) return 60_000; // everything slower in debug mode return 20_000; } @@ -449,7 +450,7 @@ async function runTests() { if (parallelism > 1) { console.log(grouptitle); - result = await fn(); + result = await fn(index); } else { result = await startGroup(grouptitle, fn); } @@ -469,6 +470,7 @@ async function runTests() { const label = `${getAnsi(color)}[${index}/${total}] ${title} - ${error}${getAnsi("reset")}`; startGroup(label, () => { if (parallelism > 1) return; + if (!isCI) return; process.stderr.write(stdoutPreview); }); @@ -671,7 +673,9 @@ async function runTests() { const title = join(relative(cwd, vendorPath), testPath).replace(/\\/g, "/"); if (testRunner === "bun") { - await runTest(title, () => spawnBunTest(execPath, testPath, { cwd: vendorPath })); + await runTest(title, index => + spawnBunTest(execPath, testPath, { cwd: vendorPath, env: { TEST_SERIAL_ID: index } }), + ); } else { const testRunnerPath = join(cwd, "test", "runners", `${testRunner}.ts`); if (!existsSync(testRunnerPath)) { @@ -1298,6 +1302,7 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) { * @param {object} [opts] * @param {string} [opts.cwd] * @param {string[]} [opts.args] + * @param {object} [opts.env] * @returns {Promise} */ async function spawnBunTest(execPath, testPath, opts = { cwd }) { @@ -1331,6 +1336,7 @@ async function spawnBunTest(execPath, testPath, opts = { cwd }) { const env = { GITHUB_ACTIONS: "true", // always true so annotations are parsed + ...opts["env"], }; if ((basename(execPath).includes("asan") || !isCI) && shouldValidateExceptions(relative(cwd, absPath))) { env.BUN_JSC_validateExceptionChecks = "1"; From dd08a707e285bb0f7a5d965dad2dbeb69fe45366 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sun, 5 Oct 2025 18:58:26 -0700 Subject: [PATCH 040/191] update `yaml-test-suite` test generator script (#23277) ### What does this PR do? Adds `expect().toBe()` checks for anchors/aliases. Also adds git commit the tests were translated from. ### How did you verify your code works? Manually --- .../yaml/translate_yaml_test_suite_to_bun.py | 164 +++++++++++++----- test/js/bun/yaml/yaml-test-suite.test.ts | 69 +++++--- 2 files changed, 168 insertions(+), 65 deletions(-) diff --git a/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py b/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py index 002af37c51..7bf930b3d6 100644 --- a/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py +++ b/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py @@ -323,6 +323,40 @@ def detect_shared_references(yaml_content): return shared_refs +def generate_shared_reference_tests(yaml_content, parsed_path="parsed"): + """Generate toBe() tests for shared references based on anchors/aliases in YAML.""" + tests = [] + + # Common patterns for shared references + patterns = [ + # bill-to/ship-to pattern + (r'bill-to:\s*&(\w+)', r'ship-to:\s*\*\1', 'bill-to', 'ship-to'), + # Array items with anchors + (r'-\s*&(\w+)\s+', r'-\s*\*\1', None, None), + # Map values with anchors + (r':\s*&(\w+)\s+', r':\s*\*\1', None, None), + ] + + # Check for bill-to/ship-to pattern specifically + if 'bill-to:' in yaml_content and 'ship-to:' in yaml_content: + if re.search(r'bill-to:\s*&\w+', yaml_content) and re.search(r'ship-to:\s*\*\w+', yaml_content): + tests.append(f' // Shared reference check: bill-to and ship-to should be the same object') + tests.append(f' expect({parsed_path}["bill-to"]).toBe({parsed_path}["ship-to"]);') + + # Check for x-foo pattern (common in OpenAPI specs) + if re.search(r'x-\w+:\s*&\w+', yaml_content): + anchor_match = re.search(r'x-(\w+):\s*&(\w+)', yaml_content) + if anchor_match: + field_name = f'x-{anchor_match.group(1)}' + anchor_name = anchor_match.group(2) + # Find aliases to this anchor + alias_pattern = rf'\*{anchor_name}\b' + if re.search(alias_pattern, yaml_content): + tests.append(f' // Shared reference check: anchor {anchor_name}') + # This is generic - would need more context to generate specific tests + + return tests + def generate_expected_with_shared_refs(json_data, yaml_content): """Generate expected object with shared references for anchors/aliases.""" shared_refs = detect_shared_references(yaml_content) @@ -498,6 +532,23 @@ test("{test_name}", () => {{ ''' # Special handling for known problematic tests + if test_name == "yaml-test-suite/2SXE": + # 2SXE has complex anchor on key itself, not a shared reference case + test = f''' +test("{test_name}", () => {{ + // {description} + // Note: &a anchors the key "key" itself, *a references that string + const input: string = {format_js_string(yaml_content)}; + + const parsed = YAML.parse(input); + + const expected: any = {{ key: "value", foo: "key" }}; + + expect(parsed).toEqual(expected); +}}); +''' + return test + if test_name == "yaml-test-suite/X38W": # X38W has alias key that creates duplicate - yaml package doesn't handle this correctly # The correct output is just one key "a,b" with value ["c", "b", "d"] @@ -738,33 +789,20 @@ test("{test_name}", () => {{ # Try to identify simple cases if 'bill-to: &' in yaml_content and 'ship-to: *' in yaml_content: # Common pattern: bill-to/ship-to sharing - test += ''' - const sharedAddress: any = ''' - # Find the shared object from expected data stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) - if isinstance(stringified_value, dict): - if 'bill-to' in stringified_value: - shared_obj = stringified_value.get('bill-to') - test += json_to_js_literal(shared_obj) + ';' - # Now create expected with shared ref - test += ''' - const expected = ''' - # Build object with shared reference - test += '{\n' - for key, value in stringified_value.items(): - if key == 'bill-to': - test += f' "bill-to": sharedAddress,\n' - elif key == 'ship-to' and value == shared_obj: - test += f' "ship-to": sharedAddress,\n' - else: - test += f' "{escape_js_string(key)}": {json_to_js_literal(value)},\n' - test = test.rstrip(',\n') + '\n };' - else: - # Fallback to regular generation - stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) - expected_str = json_to_js_literal(stringified_value) - test += f''' - const expected: any = {expected_str};''' + if isinstance(stringified_value, dict) and 'bill-to' in stringified_value: + # Generate expected value normally with toBe check + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str}; + + expect(parsed).toEqual(expected); + + // Verify shared references - bill-to and ship-to should be the same object + expect((parsed as any)["bill-to"]).toBe((parsed as any)["ship-to"]); +}}); +''' + return test else: # Fallback to regular generation stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) @@ -774,28 +812,60 @@ test("{test_name}", () => {{ else: # Generic anchor/alias case # Look for patterns like "- &anchor value" and "- *anchor" - anchor_matches = re.findall(r'&(\w+)\s+(.+?)(?:\n|$)', yaml_content) + anchor_matches = re.findall(r'&(\w+)', yaml_content) alias_matches = re.findall(r'\*(\w+)', yaml_content) if anchor_matches and alias_matches: # Build shared values based on anchors - anchor_vars = {} - for anchor_name, _ in anchor_matches: - if anchor_name in [a for a in alias_matches]: - # This anchor is referenced - anchor_vars[anchor_name] = f'shared_{anchor_name}' + shared_anchors = [] + for anchor_name in set(anchor_matches): + if anchor_name in alias_matches: + # This anchor is referenced by an alias + shared_anchors.append(anchor_name) - if anchor_vars and isinstance(expected_value, (list, dict)): - # Try to detect which values are shared - test += f''' - // Detected anchors that are referenced: {', '.join(anchor_vars.keys())} -''' - # For now, just generate the expected normally with a note + if shared_anchors and isinstance(expected_value, (list, dict)): + # Generate the expected value stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) expected_str = json_to_js_literal(stringified_value) + + # Build toBe checks based on detected patterns + toBe_checks = [] + + # Try to detect specific patterns for toBe checks + # Pattern 1: Array with repeated elements (- &anchor value, - *anchor) + for anchor in shared_anchors: + # Check if it's in an array context + if re.search(rf'-\s+&{anchor}\s+', yaml_content) and re.search(rf'-\s+\*{anchor}', yaml_content): + # This might be array elements - but hard to know indices without parsing + pass + # Check if it's in mapping values (not keys) + # Pattern: "key: &anchor" not "&anchor:" (which anchors the key) + # Use [\w-]+ to match keys with hyphens like "bill-to" + anchor_key_match = re.search(rf'([\w-]+):\s*&{anchor}\s', yaml_content) + alias_key_matches = re.findall(rf'([\w-]+):\s*\*{anchor}(?:\s|$)', yaml_content) + if anchor_key_match and alias_key_matches: + anchor_key = anchor_key_match.group(1) + for alias_key in alias_key_matches: + if anchor_key != alias_key: + # Additional check: make sure the anchor is not on a key itself + if not re.search(rf'&{anchor}:', yaml_content): + toBe_checks.append(f' expect((parsed as any)["{anchor_key}"]).toBe((parsed as any)["{alias_key}"]);') + test += f''' - const expected: any = {expected_str};''' + // Detected anchors that are referenced: {', '.join(shared_anchors)} + + const expected: any = {expected_str}; + + expect(parsed).toEqual(expected);''' + + if toBe_checks: + test += '\n\n // Verify shared references\n' + test += '\n'.join(toBe_checks) + + test += '\n});' + return test else: + # No shared anchors or not a dict/list stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) expected_str = json_to_js_literal(stringified_value) test += f''' @@ -911,7 +981,21 @@ def main(): mode_comment = "// AST validation disabled - only checking parse success/failure" if not check_ast else "// Using YAML.parse() with eemeli/yaml package as reference" + # Get yaml-test-suite commit hash + yaml_test_suite_commit = None + try: + result = subprocess.run(['git', 'rev-parse', 'HEAD'], + capture_output=True, text=True, + cwd=yaml_test_suite_path) + if result.returncode == 0: + yaml_test_suite_commit = result.stdout.strip() + except: + pass + + commit_comment = f"// Generated from yaml-test-suite commit: {yaml_test_suite_commit}" if yaml_test_suite_commit else "" + output = f'''// Tests translated from official yaml-test-suite +{commit_comment} {mode_comment} // Total: {len(test_dirs)} test directories @@ -933,7 +1017,7 @@ import {{ YAML }} from "bun"; try: test_case = generate_test(test_dir, test_name, check_ast) if test_case: - output += test_case + output += test_case + '\n' # Add newline between tests successful += 1 else: print(f" Skipped {test_name}: returned None") diff --git a/test/js/bun/yaml/yaml-test-suite.test.ts b/test/js/bun/yaml/yaml-test-suite.test.ts index ce10104203..e526e035aa 100644 --- a/test/js/bun/yaml/yaml-test-suite.test.ts +++ b/test/js/bun/yaml/yaml-test-suite.test.ts @@ -1,4 +1,5 @@ -// Tests translated from official yaml-test-suite (6e6c296) +// Tests translated from official yaml-test-suite +// Generated from yaml-test-suite commit: 6e6c296ae9c9d2d5c4134b4b64d01b29ac19ff6f // Using YAML.parse() with eemeli/yaml package as reference // Total: 402 test directories @@ -60,7 +61,7 @@ top6: // This YAML has anchors and aliases - creating shared references - // Detected anchors that are referenced: alias1, alias2 + // Detected anchors that are referenced: alias2, alias1 const expected: any = { top1: { key1: "scalar1" }, @@ -209,6 +210,7 @@ test("yaml-test-suite/2LFX", () => { test("yaml-test-suite/2SXE", () => { // Anchors With Colon in Name + // Note: &a anchors the key "key" itself, *a references that string const input: string = `&a: key: &a value foo: *a: @@ -216,10 +218,6 @@ foo: const parsed = YAML.parse(input); - // This YAML has anchors and aliases - creating shared references - - // Detected anchors that are referenced: a - const expected: any = { key: "value", foo: "key" }; expect(parsed).toEqual(expected); @@ -329,6 +327,9 @@ Reuse anchor: *anchor }; expect(parsed).toEqual(expected); + + // Verify shared references + expect((parsed as any)["occurrence"]).toBe((parsed as any)["anchor"]); }); test("yaml-test-suite/3HFZ", () => { @@ -1221,6 +1222,9 @@ b: *anchor const expected: any = { a: null, b: null }; expect(parsed).toEqual(expected); + + // Verify shared references + expect((parsed as any)["a"]).toBe((parsed as any)["b"]); }); test("yaml-test-suite/6LVF", () => { @@ -2514,6 +2518,10 @@ test("yaml-test-suite/C4HZ", () => { ]; expect(parsed).toEqual(expected); + + // Verify shared references + expect((parsed as any)["center"]).toBe((parsed as any)["start"]); + expect((parsed as any)["center"]).toBe((parsed as any)["start"]); }); test("yaml-test-suite/CC74", () => { @@ -3065,7 +3073,7 @@ test("yaml-test-suite/E76Z", () => { // This YAML has anchors and aliases - creating shared references - // Detected anchors that are referenced: a + // Detected anchors that are referenced: a, b const expected: any = { a: "b", b: "a" }; @@ -5718,22 +5726,30 @@ comments: // This YAML has anchors and aliases - creating shared references - const sharedAddress: any = { - given: "Chris", - family: "Dumars", - address: { - lines: "458 Walkman Dr.\nSuite #292\n", - city: "Royal Oak", - state: "MI", - postal: 48046, + const expected: any = { + invoice: 34843, + date: "2001-01-23", + "bill-to": { + given: "Chris", + family: "Dumars", + address: { + lines: "458 Walkman Dr.\nSuite #292\n", + city: "Royal Oak", + state: "MI", + postal: 48046, + }, }, - }; - const expected = { - "invoice": 34843, - "date": "2001-01-23", - "bill-to": sharedAddress, - "ship-to": sharedAddress, - "product": [ + "ship-to": { + given: "Chris", + family: "Dumars", + address: { + lines: "458 Walkman Dr.\nSuite #292\n", + city: "Royal Oak", + state: "MI", + postal: 48046, + }, + }, + product: [ { sku: "BL394D", quantity: 4, @@ -5747,12 +5763,15 @@ comments: price: 2392, }, ], - "tax": 251.42, - "total": 4443.52, - "comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.", + tax: 251.42, + total: 4443.52, + comments: "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.", }; expect(parsed).toEqual(expected); + + // Verify shared references - bill-to and ship-to should be the same object + expect((parsed as any)["bill-to"]).toBe((parsed as any)["ship-to"]); }); test("yaml-test-suite/UKK6/00", () => { From a9c0ec63e84bb9234bec1a21d7f1d293e269fe60 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sun, 5 Oct 2025 19:28:32 -0800 Subject: [PATCH 041/191] node:net: removed explicit ebaf from writing to detached socket (#23278) supersedes https://github.com/oven-sh/bun/pull/23030 partial revert of https://github.com/oven-sh/bun/commit/354391a26331b90f9c64ee8eb629704167cc96a5 likely fixes https://github.com/oven-sh/bun/issues/21982 --- src/bun.js/api/bun/socket.zig | 9 +--- .../test-net-socket-write-after-close.js | 42 ------------------- 2 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 test/js/node/test/parallel/test-net-socket-write-after-close.js diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 8fa141fb59..3da90d798b 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -884,14 +884,7 @@ pub fn NewSocket(comptime ssl: bool) type { pub fn writeBuffered(this: *This, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { if (this.socket.isDetached()) { this.buffered_data_for_node_net.clearAndFree(bun.default_allocator); - // TODO: should we separate unattached and detached? unattached shouldn't throw here - const err: jsc.SystemError = .{ - .errno = @intFromEnum(bun.sys.SystemErrno.EBADF), - .code = .static("EBADF"), - .message = .static("write EBADF"), - .syscall = .static("write"), - }; - return globalObject.throwValue(err.toErrorInstance(globalObject)); + return .false; } const args = callframe.argumentsUndef(2); diff --git a/test/js/node/test/parallel/test-net-socket-write-after-close.js b/test/js/node/test/parallel/test-net-socket-write-after-close.js deleted file mode 100644 index 207f735fff..0000000000 --- a/test/js/node/test/parallel/test-net-socket-write-after-close.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; -const common = require('../common'); -const assert = require('assert'); -const net = require('net'); - -{ - const server = net.createServer(); - - server.listen(common.mustCall(() => { - const port = server.address().port; - const client = net.connect({ port }, common.mustCall(() => { - client.on('error', common.mustCall((err) => { - server.close(); - assert.strictEqual(err.constructor, Error); - assert.strictEqual(err.message, 'write EBADF'); - })); - client._handle.close(); - client.write('foo'); - })); - })); -} - -{ - const server = net.createServer(); - - server.listen(common.mustCall(() => { - const port = server.address().port; - const client = net.connect({ port }, common.mustCall(() => { - client.on('error', common.expectsError({ - code: 'ERR_SOCKET_CLOSED', - message: 'Socket is closed', - name: 'Error' - })); - - server.close(); - - client._handle.close(); - client._handle = null; - client.write('foo'); - })); - })); -} From d292dcad2622829835db25d2125847cab77e17f1 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 6 Oct 2025 00:37:29 -0700 Subject: [PATCH 042/191] fix(parser): typescript `module` parsing bug (#23284) ### What does this PR do? A bug in our typescript parser was causing `module.foo = foo` to parse as a typescript namespace. If it didn't end with a semicolon and there's a statement on the next line it would cause a syntax error. Example: ```ts module.foo = foo foo.foo = foo ``` fixes #22929 fixes #22883 ### How did you verify your code works? Added a regression test --- src/ast/parseStmt.zig | 5 +- .../issue/22929-module-extensions-asi.test.ts | 115 ++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 test/regression/issue/22929-module-extensions-asi.test.ts diff --git a/src/ast/parseStmt.zig b/src/ast/parseStmt.zig index b8bce67462..8f946e079b 100644 --- a/src/ast/parseStmt.zig +++ b/src/ast/parseStmt.zig @@ -1223,8 +1223,9 @@ pub fn ParseStmt( // "module Foo {}" // "declare module 'fs' {}" // "declare module 'fs';" - if (((opts.is_module_scope or opts.is_namespace_scope) and (p.lexer.token == .t_identifier or - (p.lexer.token == .t_string_literal and opts.is_typescript_declare)))) + if (!p.lexer.has_newline_before and + (opts.is_module_scope or opts.is_namespace_scope) and + (p.lexer.token == .t_identifier or (p.lexer.token == .t_string_literal and opts.is_typescript_declare))) { return p.parseTypeScriptNamespaceStmt(loc, opts); } diff --git a/test/regression/issue/22929-module-extensions-asi.test.ts b/test/regression/issue/22929-module-extensions-asi.test.ts new file mode 100644 index 0000000000..7c5ecf31d8 --- /dev/null +++ b/test/regression/issue/22929-module-extensions-asi.test.ts @@ -0,0 +1,115 @@ +import { expect, test } from "bun:test"; +import { mkdtempSync, writeFileSync } from "fs"; +import { bunEnv, bunExe } from "harness"; +import { tmpdir } from "os"; +import { join } from "path"; + +test("Module._extensions should not break ASI (automatic semicolon insertion)", async () => { + const dir = mkdtempSync(join(tmpdir(), "bun-module-extensions-asi-")); + + // Create a module without semicolons that relies on ASI + const moduleWithoutSemi = join(dir, "module-no-semi.js"); + writeFileSync( + moduleWithoutSemi, + `function f() {} +module.exports = f +f.f = f`, + ); + + // Create a test file that hooks Module._extensions + const testFile = join(dir, "test.js"); + writeFileSync( + testFile, + ` +const Module = require("module"); +const orig = Module._extensions[".js"]; + +// Hook Module._extensions[".js"] - commonly done by transpiler libraries +Module._extensions[".js"] = (m, f) => { + return orig(m, f); +}; + +// This should work without parse errors +const result = require("./module-no-semi.js"); +if (typeof result !== 'function') { + throw new Error('Expected function but got ' + typeof result); +} +if (result.f !== result) { + throw new Error('Expected result.f === result'); +} +console.log('SUCCESS'); +`, + ); + + // Run the test + const proc = Bun.spawn({ + cmd: [bunExe(), testFile], + cwd: dir, + env: bunEnv, + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should not have parse errors + expect(stderr).not.toContain("Expected '{'"); + expect(stderr).not.toContain("Unexpected end of file"); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("SUCCESS"); +}); + +test("Module._extensions works with modules that have semicolons", async () => { + const dir = mkdtempSync(join(tmpdir(), "bun-module-extensions-semi-")); + + // Create a module with semicolons + const moduleWithSemi = join(dir, "module-with-semi.js"); + writeFileSync( + moduleWithSemi, + `function g() { return 42; } +module.exports = g; +g.g = g;`, + ); + + // Create a test file that hooks Module._extensions + const testFile = join(dir, "test.js"); + writeFileSync( + testFile, + ` +const Module = require("module"); +const orig = Module._extensions[".js"]; + +Module._extensions[".js"] = (m, f) => { + return orig(m, f); +}; + +// This should also work with semicolons +const result = require("./module-with-semi.js"); +if (typeof result !== 'function') { + throw new Error('Expected function but got ' + typeof result); +} +if (result() !== 42) { + throw new Error('Expected result() === 42'); +} +if (result.g !== result) { + throw new Error('Expected result.g === result'); +} +console.log('SUCCESS'); +`, + ); + + // Run the test + const proc = Bun.spawn({ + cmd: [bunExe(), testFile], + cwd: dir, + env: bunEnv, + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should work correctly + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("SUCCESS"); +}); From 1c363f0ad0e725edabaedd0cf60a489914c18d4b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 6 Oct 2025 00:39:08 -0700 Subject: [PATCH 043/191] fix(parser): `typeof` minification regression (#23280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What does this PR do? In Bun v1.2.22 a minification for `typeof x === "undefined"` → `typeof x > "u"` was added. This introduced a regression causing `return (typeof x !== "undefined", false)` to minify to invalid syntax when `--minify-syntax` is enabled (this is also enabled for transpilation at runtime). This pr fixes the regression making sure `return (typeof x !== "undefined", false);` minifies correctly to `return !1;`. fixes #21137 ### How did you verify your code works? Added a regression test. --- src/ast/SideEffects.zig | 13 +- .../issue/21137-minify-typeof-comma.test.ts | 168 ++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 test/regression/issue/21137-minify-typeof-comma.test.ts diff --git a/src/ast/SideEffects.zig b/src/ast/SideEffects.zig index 1b0f023c3a..d7ce926198 100644 --- a/src/ast/SideEffects.zig +++ b/src/ast/SideEffects.zig @@ -205,9 +205,18 @@ pub const SideEffects = enum(u1) { .bin_ge, => { if (isPrimitiveWithSideEffects(bin.left.data) and isPrimitiveWithSideEffects(bin.right.data)) { + const left_simplified = simplifyUnusedExpr(p, bin.left); + const right_simplified = simplifyUnusedExpr(p, bin.right); + + // If both sides would be removed entirely, we can return null to remove the whole expression + if (left_simplified == null and right_simplified == null) { + return null; + } + + // Otherwise, preserve at least the structure return Expr.joinWithComma( - simplifyUnusedExpr(p, bin.left) orelse bin.left.toEmpty(), - simplifyUnusedExpr(p, bin.right) orelse bin.right.toEmpty(), + left_simplified orelse bin.left.toEmpty(), + right_simplified orelse bin.right.toEmpty(), p.allocator, ); } diff --git a/test/regression/issue/21137-minify-typeof-comma.test.ts b/test/regression/issue/21137-minify-typeof-comma.test.ts new file mode 100644 index 0000000000..1d5fe16727 --- /dev/null +++ b/test/regression/issue/21137-minify-typeof-comma.test.ts @@ -0,0 +1,168 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; +import path from "path"; + +// Regression test for minification bug where typeof comparison in comma operator +// produces invalid JavaScript output +test("issue #21137: minify typeof undefined in comma operator", async () => { + using dir = tempDir("issue-21137", {}); + + // This code pattern was producing invalid JavaScript: ", !1" instead of "!1" + const testCode = ` +function testFunc() { + return (typeof undefinedVar !== "undefined", false); +} + +// Test with other variations +function testFunc2() { + return (typeof someVar === "undefined", true); +} + +function testFunc3() { + // Nested comma operators + return ((typeof a !== "undefined", 1), (typeof b === "undefined", 2)); +} + +// Test in conditional +const result = typeof window !== "undefined" ? (typeof document !== "undefined", true) : false; + +console.log(testFunc()); +console.log(testFunc2()); +console.log(testFunc3()); +console.log(result); +`; + + const testFile = path.join(String(dir), "test.js"); + await Bun.write(testFile, testCode); + + // Build with minify-syntax flag + await using buildProc = Bun.spawn({ + cmd: [bunExe(), "build", "--minify-syntax", testFile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [buildOutput, buildStderr, buildExitCode] = await Promise.all([ + buildProc.stdout.text(), + buildProc.stderr.text(), + buildProc.exited, + ]); + + // Build should succeed + expect(buildExitCode).toBe(0); + + // The output should NOT contain invalid syntax like ", !" or ", false" or ", true" + // These patterns indicate the bug where the left side of comma was incorrectly removed + expect(buildOutput).not.toContain(", !"); + expect(buildOutput).not.toContain(", false"); + expect(buildOutput).not.toContain(", true"); + expect(buildOutput).not.toContain(", 1"); + expect(buildOutput).not.toContain(", 2"); + + // Verify the minified code runs without syntax errors + const minifiedFile = path.join(String(dir), "minified.js"); + await Bun.write(minifiedFile, buildOutput); + + await using runProc = Bun.spawn({ + cmd: [bunExe(), minifiedFile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [runOutput, runStderr, runExitCode] = await Promise.all([ + runProc.stdout.text(), + runProc.stderr.text(), + runProc.exited, + ]); + + // Should run without errors + expect(runExitCode).toBe(0); + + // Verify the output is correct + const lines = runOutput.trim().split("\n"); + expect(lines[0]).toBe("false"); // testFunc() returns false + expect(lines[1]).toBe("true"); // testFunc2() returns true + expect(lines[2]).toBe("2"); // testFunc3() returns 2 + expect(lines[3]).toBe("false"); // result is false (no window in Node/Bun) +}); + +// Additional test for the specific optimization that was causing the bug +test("issue #21137: typeof undefined optimization preserves valid syntax", async () => { + using dir = tempDir("issue-21137-opt", {}); + + // Test the specific optimization: typeof x !== "undefined" -> typeof x < "u" + const testCode = ` +// These should be optimized but remain valid +const a = typeof x !== "undefined"; +const b = typeof y === "undefined"; +const c = typeof z != "undefined"; +const d = typeof w == "undefined"; + +// In comma expressions +const e = (typeof foo !== "undefined", 42); +const f = (typeof bar === "undefined", "test"); + +// Should not break when left side is removed +function check() { + return (typeof missing !== "undefined", null); +} + +console.log(JSON.stringify({a, b, c, d, e, f, check: check()})); +`; + + const testFile = path.join(String(dir), "optimize.js"); + await Bun.write(testFile, testCode); + + await using buildProc = Bun.spawn({ + cmd: [bunExe(), "build", "--minify-syntax", testFile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [buildOutput, buildStderr, buildExitCode] = await Promise.all([ + buildProc.stdout.text(), + buildProc.stderr.text(), + buildProc.exited, + ]); + + expect(buildExitCode).toBe(0); + + // Check that the optimization is applied (should contain < or > comparisons with "u") + expect(buildOutput).toContain('"u"'); + + // But should not have invalid comma syntax + expect(buildOutput).not.toMatch(/,\s*[!<>]/); // No comma followed by operator + expect(buildOutput).not.toMatch(/,\s*"u"/); // No comma followed by "u" + + // Run the minified code to ensure it's valid + const minifiedFile = path.join(String(dir), "minified.js"); + await Bun.write(minifiedFile, buildOutput); + + await using runProc = Bun.spawn({ + cmd: [bunExe(), minifiedFile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [runOutput, runStderr, runExitCode] = await Promise.all([ + runProc.stdout.text(), + runProc.stderr.text(), + runProc.exited, + ]); + + expect(runExitCode).toBe(0); + + // Parse and verify the output + const result = JSON.parse(runOutput.trim()); + expect(result.a).toBe(false); + expect(result.b).toBe(true); + expect(result.c).toBe(false); + expect(result.d).toBe(true); + expect(result.e).toBe(42); + expect(result.f).toBe("test"); + expect(result.check).toBe(null); +}); From f7da0ac6fd24886d029dd6a9890ab119f2c5b946 Mon Sep 17 00:00:00 2001 From: Michael H Date: Mon, 6 Oct 2025 20:58:04 +1100 Subject: [PATCH 044/191] bun install: support for `minimumReleaseAge` (#22801) ### What does this PR do? fixes #22679 * includes a better error if a package cant be met because of the age (but would normally) * logs the resolved one in --verbose (which can be helpful in debugging to show it does know latest but couldn't use) * makes bun outdated show in the table when the package isn't true latest * includes a rudimentary "stability" check if a later version is in blacked out time (but only up to 7 days as it goes back to latest with min age) For extended security we could also Last-Modified header of the tgz download and then abort if too new (just like the hash) | install error with no recent version | bun outdated respecting the rule | | --- | --- | image | image | For stable release we will make it use `3d` type syntax instead of magic second numbers. ### How did you verify your code works? tests & manual --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway --- bunfig.toml | 1 + docs/cli/install.md | 36 + docs/runtime/bunfig.md | 14 + src/api/schema.zig | 169 +- src/bun.js/bindings/WTF.zig | 18 + src/bun.js/bindings/wtf-bindings.cpp | 6 + src/bunfig.zig | 34 + src/cli/outdated_command.zig | 79 +- src/cli/pm_view_command.zig | 1 + src/cli/update_interactive_command.zig | 7 +- src/install/NetworkTask.zig | 21 +- .../PackageManager/CommandLineArguments.zig | 15 + .../PackageManager/PackageManagerEnqueue.zig | 104 +- .../PackageManager/PackageManagerOptions.zig | 19 +- .../PackageManagerResolution.zig | 3 +- .../PackageManager/PopulateManifestCache.zig | 12 +- src/install/PackageManager/runTasks.zig | 3 + src/install/PackageManagerTask.zig | 1 + src/install/PackageManifestMap.zig | 28 +- src/install/lockfile.zig | 2 + src/install/npm.zig | 329 ++- test/cli/install/minimum-release-age.test.ts | 2306 +++++++++++++++++ test/no-validate-leaksan.txt | 1 + 23 files changed, 2995 insertions(+), 214 deletions(-) create mode 100644 test/cli/install/minimum-release-age.test.ts diff --git a/bunfig.toml b/bunfig.toml index 3eae059d7c..f1bba3259c 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -10,3 +10,4 @@ preload = "./test/preload.ts" [install] linker = "isolated" +minimumReleaseAge = 1 diff --git a/docs/cli/install.md b/docs/cli/install.md index 0ad692ac62..68578a1c22 100644 --- a/docs/cli/install.md +++ b/docs/cli/install.md @@ -221,6 +221,38 @@ Bun uses a global cache at `~/.bun/install/cache/` to minimize disk usage. Packa For complete documentation refer to [Package manager > Global cache](https://bun.com/docs/install/cache). +## Minimum release age + +To protect against supply chain attacks where malicious packages are quickly published, you can configure a minimum age requirement for npm packages. Package versions published more recently than the specified threshold (in seconds) will be filtered out during installation. + +```bash +# Only install package versions published at least 3 days ago +$ bun add @types/bun --minimum-release-age 259200 # seconds +``` + +You can also configure this in `bunfig.toml`: + +```toml +[install] +# Only install package versions published at least 3 days ago +minimumReleaseAge = 259200 # seconds + +# Exclude trusted packages from the age gate +minimumReleaseAgeExcludes = ["@types/node", "typescript"] +``` + +When the minimum age filter is active: + +- Only affects new package resolution - existing packages in `bun.lock` remain unchanged +- All dependencies (direct and transitive) are filtered to meet the age requirement when being resolved +- When versions are blocked by the age gate, a stability check detects rapid bugfix patterns + - If multiple versions were published close together just outside your age gate, it extends the filter to skip those potentially unstable versions and selects an older, more mature version + - Searches up to 7 days after the age gate, however if still finding rapid releases it ignores stability check + - Exact version requests (like `package@1.1.1`) still respect the age gate but bypass the stability check +- Versions without a `time` field are treated as passing the age check (npm registry should always provide timestamps) + +For more advanced security scanning, including integration with services & custom filtering, see [Package manager > Security Scanner API](https://bun.com/docs/install/security-scanner-api). + ## Configuration The default behavior of `bun install` can be configured in `bunfig.toml`. The default values are shown below. @@ -255,6 +287,10 @@ concurrentScripts = 16 # (cpu count or GOMAXPROCS) x2 # installation strategy: "hoisted" or "isolated" # default: "hoisted" linker = "hoisted" + +# minimum age config +minimumReleaseAge = 259200 # seconds +minimumReleaseAgeExcludes = ["@types/node", "typescript"] ``` ## CI/CD diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 11c47d814c..532908f9bc 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -570,6 +570,20 @@ Valid values are: {% /table %} +### `install.minimumReleaseAge` + +Configure a minimum age (in seconds) for npm package versions. Package versions published more recently than this threshold will be filtered out during installation. Default is `null` (disabled). + +```toml +[install] +# Only install package versions published at least 3 days ago +minimumReleaseAge = 259200 +# These packages will bypass the 3-day minimum age requirement +minimumReleaseAgeExcludes = ["@types/bun", "typescript"] +``` + +For more details see [Minimum release age](https://bun.com/docs/cli/install#minimum-release-age) in the install documentation. +