From da78103b1ca0d93f9c8bd9b177e4b463b7e0d28b Mon Sep 17 00:00:00 2001 From: Georgijs <48869301+gvilums@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:07:07 -0800 Subject: [PATCH 01/19] fix typo (#8929) --- src/js/node/child_process.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/node/child_process.js b/src/js/node/child_process.js index 22254214b4..f8b8ad9624 100644 --- a/src/js/node/child_process.js +++ b/src/js/node/child_process.js @@ -1334,7 +1334,7 @@ function nodeToBun(item, index) { // If not defined, use the default. // For stdin/stdout/stderr, it's pipe. For others, it's ignore. if (item == null) { - return index > 3 ? "ignore" : "pipe"; + return index > 2 ? "ignore" : "pipe"; } // If inherit and we are referencing stdin/stdout/stderr index, // we can get the fd from the ReadStream for the corresponding stdio From c920919c42f6421304c993839cb531a9aacb77c0 Mon Sep 17 00:00:00 2001 From: argosphil Date: Thu, 15 Feb 2024 21:09:13 +0000 Subject: [PATCH 02/19] fix: distinguish getters and setters in Bun.inspect() (#8858) * Bun.inspect: distinguish [Getter], [Setter], [Getter/Setter] fixes #8853 NOTE: this modifies files which were auto-generated at one point, but which are now maintained as part of the Bun sources. * test for #8853 --------- Co-authored-by: Georgijs <48869301+gvilums@users.noreply.github.com> --- src/bun.js/ConsoleObject.zig | 39 ++++++++++++++++++++++---- src/bun.js/bindings/bindings.cpp | 23 +++++++++++++++ src/bun.js/bindings/bindings.zig | 48 ++++++++++++++++++++++++++++++++ src/bun.js/bindings/headers.h | 12 ++++++++ src/bun.js/bindings/headers.zig | 8 ++++++ test/js/bun/util/inspect.test.js | 36 +++++++++++++++++++++++- 6 files changed, 159 insertions(+), 7 deletions(-) diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 840b05d350..e775e351d8 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -920,7 +920,8 @@ pub const Formatter = struct { JSX, Event, - Getter, + GetterSetter, + CustomGetterSetter, pub fn isPrimitive(this: Tag) bool { return switch (this) { @@ -973,7 +974,8 @@ pub const Formatter = struct { ArrayBuffer: void, JSX: void, Event: void, - Getter: void, + GetterSetter: void, + CustomGetterSetter: void, pub fn isPrimitive(this: @This()) bool { return @as(Tag, this).isPrimitive(); @@ -1178,7 +1180,8 @@ pub const Formatter = struct { .Event => .Event, - .GetterSetter, .CustomGetterSetter => .Getter, + .GetterSetter => .GetterSetter, + .CustomGetterSetter => .CustomGetterSetter, .JSAsJSONType => .toJSON, @@ -1888,8 +1891,31 @@ pub const Formatter = struct { writer.print(comptime Output.prettyFmt("[Function: {}]", enable_ansi_colors), .{printable}); } }, - .Getter => { - writer.print(comptime Output.prettyFmt("[Getter]", enable_ansi_colors), .{}); + .GetterSetter => { + const cell = value.asCell(); + const getterSetter = cell.getGetterSetter(); + const hasGetter = !getterSetter.isGetterNull(); + const hasSetter = !getterSetter.isSetterNull(); + if (hasGetter and hasSetter) { + writer.print(comptime Output.prettyFmt("[Getter/Setter]", enable_ansi_colors), .{}); + } else if (hasGetter) { + writer.print(comptime Output.prettyFmt("[Getter]", enable_ansi_colors), .{}); + } else if (hasSetter) { + writer.print(comptime Output.prettyFmt("[Setter]", enable_ansi_colors), .{}); + } + }, + .CustomGetterSetter => { + const cell = value.asCell(); + const getterSetter = cell.getCustomGetterSetter(); + const hasGetter = !getterSetter.isGetterNull(); + const hasSetter = !getterSetter.isSetterNull(); + if (hasGetter and hasSetter) { + writer.print(comptime Output.prettyFmt("[Getter/Setter]", enable_ansi_colors), .{}); + } else if (hasGetter) { + writer.print(comptime Output.prettyFmt("[Getter]", enable_ansi_colors), .{}); + } else if (hasSetter) { + writer.print(comptime Output.prettyFmt("[Setter]", enable_ansi_colors), .{}); + } }, .Array => { const len = @as(u32, @truncate(value.getLength(this.globalThis))); @@ -2926,7 +2952,8 @@ pub const Formatter = struct { .NativeCode => this.printAs(.NativeCode, Writer, writer, value, result.cell, enable_ansi_colors), .JSX => this.printAs(.JSX, Writer, writer, value, result.cell, enable_ansi_colors), .Event => this.printAs(.Event, Writer, writer, value, result.cell, enable_ansi_colors), - .Getter => this.printAs(.Getter, Writer, writer, value, result.cell, enable_ansi_colors), + .GetterSetter => this.printAs(.GetterSetter, Writer, writer, value, result.cell, enable_ansi_colors), + .CustomGetterSetter => this.printAs(.CustomGetterSetter, Writer, writer, value, result.cell, enable_ansi_colors), .CustomFormattedObject => |callback| { this.custom_formatted_object = callback; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 0f2d0f7e5f..32db72842a 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -100,6 +100,9 @@ #include "JavaScriptCore/InternalFieldTuple.h" #include "wtf/text/StringToIntegerConversion.h" +#include "JavaScriptCore/GetterSetter.h" +#include "JavaScriptCore/CustomGetterSetter.h" + static WTF::StringView StringView_slice(WTF::StringView sv, unsigned start, unsigned end) { return sv.substring(start, end - start); @@ -5406,3 +5409,23 @@ extern "C" bool JSGlobalObject__hasException(JSC::JSGlobalObject* globalObject) { return DECLARE_CATCH_SCOPE(globalObject->vm()).exception() != 0; } + +CPP_DECL bool JSC__GetterSetter__isGetterNull(JSC__GetterSetter *gettersetter) +{ + return gettersetter->isGetterNull(); +} + +CPP_DECL bool JSC__GetterSetter__isSetterNull(JSC__GetterSetter *gettersetter) +{ + return gettersetter->isSetterNull(); +} + +CPP_DECL bool JSC__CustomGetterSetter__isGetterNull(JSC__CustomGetterSetter *gettersetter) +{ + return gettersetter->getter() == nullptr; +} + +CPP_DECL bool JSC__CustomGetterSetter__isSetterNull(JSC__CustomGetterSetter *gettersetter) +{ + return gettersetter->setter() == nullptr; +} diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 5a6f45a45b..2ac7f25f60 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1682,6 +1682,20 @@ pub const JSCell = extern struct { } pub const Extern = [_][]const u8{ "getObject", "getType" }; + + pub fn getGetterSetter(this: *JSCell) *GetterSetter { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(JSValue.fromCell(this).isGetterSetter()); + } + return @as(*GetterSetter, @ptrCast(@alignCast(this))); + } + + pub fn getCustomGetterSetter(this: *JSCell) *CustomGetterSetter { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(JSValue.fromCell(this).isCustomGetterSetter()); + } + return @as(*CustomGetterSetter, @ptrCast(@alignCast(this))); + } }; pub const JSString = extern struct { @@ -1773,6 +1787,40 @@ pub const JSString = extern struct { pub const Extern = [_][]const u8{ "toZigString", "iterator", "toObject", "eql", "value", "length", "is8Bit", "createFromOwnedString", "createFromString" }; }; +pub const GetterSetter = extern struct { + pub const shim = Shimmer("JSC", "GetterSetter", @This()); + bytes: shim.Bytes, + const cppFn = shim.cppFn; + pub const include = "JavaScriptCore/GetterSetter.h"; + pub const name = "JSC::GetterSetter"; + pub const namespace = "JSC"; + + pub fn isGetterNull(this: *GetterSetter) bool { + return shim.cppFn("isGetterNull", .{this}); + } + + pub fn isSetterNull(this: *GetterSetter) bool { + return shim.cppFn("isSetterNull", .{this}); + } +}; + +pub const CustomGetterSetter = extern struct { + pub const shim = Shimmer("JSC", "CustomGetterSetter", @This()); + bytes: shim.Bytes, + const cppFn = shim.cppFn; + pub const include = "JavaScriptCore/CustomGetterSetter.h"; + pub const name = "JSC::CustomGetterSetter"; + pub const namespace = "JSC"; + + pub fn isGetterNull(this: *CustomGetterSetter) bool { + return shim.cppFn("isGetterNull", .{this}); + } + + pub fn isSetterNull(this: *CustomGetterSetter) bool { + return shim.cppFn("isSetterNull", .{this}); + } +}; + pub const JSPromiseRejectionOperation = enum(u32) { Reject = 0, Handle = 1, diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 355ec21ca5..baa5e74641 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -142,6 +142,8 @@ typedef void* JSClassRef; using WebCore__AbortSignal = WebCore::AbortSignal; using WebCore__DOMURL = WebCore::DOMURL; + using JSC__GetterSetter = JSC::GetterSetter; + using JSC__CustomGetterSetter = JSC::CustomGetterSetter; #endif @@ -860,3 +862,13 @@ ZIG_DECL JSC__JSValue Bun__TestScope__onReject(JSC__JSGlobalObject* arg0, JSC__C ZIG_DECL JSC__JSValue Bun__TestScope__onResolve(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); #endif + +#ifdef __cplusplus + +CPP_DECL bool JSC__GetterSetter__isGetterNull(JSC__GetterSetter *arg); +CPP_DECL bool JSC__GetterSetter__isSetterNull(JSC__GetterSetter *arg); + +CPP_DECL bool JSC__CustomGetterSetter__isGetterNull(JSC__CustomGetterSetter *arg); +CPP_DECL bool JSC__CustomGetterSetter__isSetterNull(JSC__CustomGetterSetter *arg); + +#endif diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 991ed68896..db2807ce2f 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -393,3 +393,11 @@ pub extern fn UVStreamSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC pub extern fn UVStreamSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; pub extern fn UVStreamSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; pub extern fn ZigException__fromException(arg0: [*c]bindings.Exception) ZigException; + +pub const JSC__GetterSetter = bindings.GetterSetter; +pub const JSC__CustomGetterSetter = bindings.CustomGetterSetter; + +pub extern fn JSC__GetterSetter__isGetterNull(arg: *JSC__GetterSetter) bool; +pub extern fn JSC__GetterSetter__isSetterNull(arg: *JSC__GetterSetter) bool; +pub extern fn JSC__CustomGetterSetter__isGetterNull(arg: *JSC__CustomGetterSetter) bool; +pub extern fn JSC__CustomGetterSetter__isSetterNull(arg: *JSC__CustomGetterSetter) bool; diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index c57faa6fe3..4c293d1059 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -43,10 +43,44 @@ it("getters", () => { }, }; - expect(Bun.inspect(objWithThrowingGetter)).toBe("{\n" + " foo: [Getter]," + "\n" + "}"); + expect(Bun.inspect(objWithThrowingGetter)).toBe("{\n" + " foo: [Getter/Setter]," + "\n" + "}"); expect(called).toBe(false); }); +it("setters", () => { + const obj = { + set foo(x) {}, + }; + + expect(Bun.inspect(obj)).toBe("{\n" + " foo: [Setter]," + "\n" + "}"); + var called = false; + const objWithThrowingGetter = { + get foo() { + called = true; + throw new Error("Test failed!"); + }, + set foo(v) { + called = true; + throw new Error("Test failed!"); + }, + }; + + expect(Bun.inspect(objWithThrowingGetter)).toBe("{\n" + " foo: [Getter/Setter]," + "\n" + "}"); + expect(called).toBe(false); +}); + +it("getter/setters", () => { + const obj = { + get foo() { + return 42; + }, + + set foo(x) {}, + }; + + expect(Bun.inspect(obj)).toBe("{\n" + " foo: [Getter/Setter]," + "\n" + "}"); +}); + it("Timeout", () => { const id = setTimeout(() => {}, 0); expect(Bun.inspect(id)).toBe(`Timeout (#${+id})`); From fc05cbfedcda406184eb1a7860b59fe357d22d40 Mon Sep 17 00:00:00 2001 From: Henrikh Kantuni Date: Thu, 15 Feb 2024 19:20:55 -0500 Subject: [PATCH 03/19] Fix typo (#8930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Buns supports" → "Bun supports" Co-authored-by: John-David Dalton --- docs/guides/test/migrate-from-jest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/test/migrate-from-jest.md b/docs/guides/test/migrate-from-jest.md index 5938ec00ac..7b0447867c 100644 --- a/docs/guides/test/migrate-from-jest.md +++ b/docs/guides/test/migrate-from-jest.md @@ -93,7 +93,7 @@ $ bun test --timeout 10000 Many other flags become irrelevant or obsolete when using `bun test`. -- `transform` — Buns supports TypeScript & JSX. Other file types can be configured with [Plugins](/docs/runtime/plugins). +- `transform` — Bun supports TypeScript & JSX. Other file types can be configured with [Plugins](/docs/runtime/plugins). - `extensionsToTreatAsEsm` - `haste` — Bun uses it's own internal source maps - `watchman`, `watchPlugins`, `watchPathIgnorePatterns` — use `--watch` to run tests in watch mode From 099825e5acf0addc32786749247d6c8a0d64fda2 Mon Sep 17 00:00:00 2001 From: Jake Gordon Date: Fri, 16 Feb 2024 00:30:59 +0000 Subject: [PATCH 04/19] Common HTTP server usage guide (#8732) Co-authored-by: Georgijs <48869301+gvilums@users.noreply.github.com> --- docs/guides/http/server.md | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/guides/http/server.md diff --git a/docs/guides/http/server.md b/docs/guides/http/server.md new file mode 100644 index 0000000000..1f1f9b48a6 --- /dev/null +++ b/docs/guides/http/server.md @@ -0,0 +1,46 @@ +--- +name: Common HTTP server usage +--- + +This starts an HTTP server listening on port `3000`. It demonstates basic routing with a number of common responses and also handles POST data from standard forms or as JSON. + +See [`Bun.serve`](/docs/api/http) for details. + +```ts +const server = Bun.serve({ + async fetch (req) { + const path = new URL(req.url).pathname; + + // respond with text/html + if (path === "/") return new Response("Welcome to Bun!"); + + // redirect + if (path === "/abc") return Response.redirect("/source", 301); + + // send back a file (in this case, *this* file) + if (path === "/source") return new Response(Bun.file(import.meta.file)); + + // respond with JSON + if (path === "/api") return Response.json({ some: "buns", for: "you" }); + + // receive JSON data to a POST request + if (req.method === "POST" && path === "/api/post") { + const data = await req.json(); + console.log("Received JSON:", data); + return Response.json({ success: true, data }); + } + + // receive POST data from a form + if (req.method === "POST" && path === "/form") { + const data = await req.formData(); + console.log(data.get("someField")); + return new Response("Success"); + } + + // 404s + return new Response("Page not found", { status: 404 }); + } +}) + +console.log(`Listening on ${server.url}`); +``` From bb31e768deaf7f3fd12609c42f9fe04aa3ad8d8e Mon Sep 17 00:00:00 2001 From: dave caruso Date: Thu, 15 Feb 2024 16:54:03 -0800 Subject: [PATCH 05/19] docs: update windows build instructions this removes the WSL codegen step as it is no longer supported, and some other notes i am aware of now --- docs/project/building-windows.md | 68 +++++++++++++++----------------- src/deps/tinycc | 2 +- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/docs/project/building-windows.md b/docs/project/building-windows.md index 9fe9208bb0..0ef360f057 100644 --- a/docs/project/building-windows.md +++ b/docs/project/building-windows.md @@ -1,6 +1,6 @@ This document describes the build process for Windows. If you run into problems, please join the [#windows channel on our Discord](http://bun.sh/discord) for help. -It is strongly recommended to use [PowerShell 7 (pwsh.exe)](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4) instead of the default `powershell.exe`. +It is strongly recommended to use [PowerShell 7 (`pwsh.exe`)](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4) instead of the default `powershell.exe`. ## Prerequisites @@ -44,6 +44,12 @@ Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted ### System Dependencies +- Bun 1.1 or later. We use Bun to run it's own code generators. + +```ps1 +irm bun.sh/install.ps1 | iex +``` + - [Visual Studio](https://visualstudio.microsoft.com) with the "Desktop Development with C++" workload. - Install Git and CMake from this installer, if not already installed. @@ -57,16 +63,20 @@ After Visual Studio, you need the following: - Ruby - Node.js -[Scoop](https://scoop.sh) can be used to install these easily: +{% callout %} +The Zig compiler is automatically downloaded, installed, and updated by the building process. +{% /callout %} + +[Scoop](https://scoop.sh) can be used to install these remaining tools easily: ```ps1 -Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression +irm https://get.scoop.sh | iex scoop install nodejs-lts go rust nasm ruby perl scoop llvm@16.0.4 # scoop bug if you install llvm and the rest at the same time ``` -If you intend on building WebKit locally (optional), you should install some more packages: +If you intend on building WebKit locally (optional), you should install these packages: ```ps1 scoop install make cygwin python @@ -88,65 +98,51 @@ Get-Command mt It is not recommended to install `ninja` / `cmake` into your global path, because you may run into a situation where you try to build bun without .\scripts\env.ps1 sourced. {% /callout %} -### Codegen - -On Unix platforms, we depend on an existing build of Bun to generate code for itself. Since the Windows build is not stable enough for this to run the code generators, you currently need to use another computer or WSL to generate this: - -```bash -$ wsl --install # run twice if it doesnt install -# in the linux environment -$ sudo apt install unzip -$ curl -fsSL https://bun.sh/install | bash -``` - -Whenever codegen-related things are updated, please re-run - -```ps1 -$ .\scripts\codegen.ps1 -``` - -(TODO: it probably is stable enough to use `bun.exe` for codegen, but the CMake configuration still has these disabled by default) - ## Building ```ps1 -bun install # or npm install +bun install .\scripts\env.ps1 .\scripts\update-submodules.ps1 # this syncs git submodule state -.\scripts\make-old-js.ps1 # runs some old code generators .\scripts\all-dependencies.ps1 # this builds all dependencies +.\scripts\make-old-js.ps1 # runs some old code generators -cd build # this was created by the codegen.ps1 script earlier +# Configure build environment +cmake -Bbuild -GNinja -DCMAKE_BUILD_TYPE=Debug -cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Debug -ninja +# Build bun +ninja -Cbuild ``` If this was successful, you should have a `bun-debug.exe` in the `build` folder. ```ps1 -.\build\bun-debug.exe --version +.\build\bun-debug.exe --revision ``` -You should add this to `$Env:PATH`. The simplest way to do so is to open the start menu, type "Path", and then navigate the environment variables menu to add `C:\.....\bun\build` to your path. +You should add this to `$Env:PATH`. The simplest way to do so is to open the start menu, type "Path", and then navigate the environment variables menu to add `C:\.....\bun\build` to the user environment variable `PATH`. You should then restart your editor (if it does not update still, log out and log back in). + +## Extra paths + +- WebKit is extracted to `build/bun-webkit` +- Zig is extracted to `.cache/zig/zig.exe` ## Tests -You can run the test suite by using `packages\bun-internal-test` +You can run the test suite either using `bun test`, or by using the wrapper script `packages\bun-internal-test`. The internal test package is a wrapper cli to run every test file in a separate instance of bun.exe, to prevent a crash in the test runner from stopping the entire suite. ```ps1 # Setup -cd packages\bun-internal-test -bun i -cd ..\.. +bun i --cwd packages\bun-internal-test # Run the entire test suite with reporter +# the package.json script "test" uses "build/bun-debug.exe" by default bun run test # Run an individual test file: -bun test node\fs -bun test "C:\bun\test\js\bun\resolve\import-meta.test.js" +bun-debug test node\fs +bun-debug test "C:\bun\test\js\bun\resolve\import-meta.test.js" ``` ## Troubleshooting diff --git a/src/deps/tinycc b/src/deps/tinycc index 2d3ad9e0d3..ab631362d8 160000 --- a/src/deps/tinycc +++ b/src/deps/tinycc @@ -1 +1 @@ -Subproject commit 2d3ad9e0d32194ad7fd867b66ebe218dcc8cb5cd +Subproject commit ab631362d839333660a265d3084d8ff060b96753 From d49cb0b98e4efb9e92ff6428dbd73441a3c690e1 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Thu, 15 Feb 2024 17:35:08 -0800 Subject: [PATCH 06/19] chore: Add PrivateRecursive type annotations (#8291) --- src/js/builtins.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index c0df84c58f..3604f8df6d 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -23,7 +23,7 @@ declare var $overriddenName: string; /** ??? */ declare var $linkTimeConstant: never; /** Assign to this directly above a function declaration (like a decorator) to set visibility */ -declare var $visibility: "Public" | "Private"; +declare var $visibility: "Public" | "Private" | "PrivateRecursive"; /** ??? */ declare var $nakedConstructor: never; /** Assign to this directly above a function declaration (like a decorator) to set intrinsic */ From dcda49a271ccb591559be1c0ffdfd76979a1b072 Mon Sep 17 00:00:00 2001 From: guest271314 Date: Thu, 15 Feb 2024 21:41:46 -0800 Subject: [PATCH 07/19] Missing toWeb (#8937) https://github.com/oven-sh/bun/issues/3927 --- docs/runtime/nodejs-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index ef3b8271a0..b95d0586dc 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -116,7 +116,7 @@ Some methods are not optimized yet. ### [`node:stream`](https://nodejs.org/api/stream.html) -🟡 Missing `getDefaultHighWaterMark` `setDefaultHighWaterMark` +🟡 Missing `getDefaultHighWaterMark` `setDefaultHighWaterMark` `toWeb` ### [`node:string_decoder`](https://nodejs.org/api/string_decoder.html) From 2b335d72e7f28b4e02f159df3d80d1cb968dafe3 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Fri, 16 Feb 2024 04:07:06 -0800 Subject: [PATCH 08/19] windows: make websocket-client.test.ts pass (#8935) --- test/js/web/websocket/websocket-client.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/web/websocket/websocket-client.test.ts b/test/js/web/websocket/websocket-client.test.ts index 89fea92e1d..0aa42a658e 100644 --- a/test/js/web/websocket/websocket-client.test.ts +++ b/test/js/web/websocket/websocket-client.test.ts @@ -1,8 +1,8 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import type { Subprocess } from "bun"; import { spawn } from "bun"; import { bunEnv, bunExe, nodeExe } from "harness"; +import * as path from "node:path"; const strings = [ { @@ -260,7 +260,7 @@ function test(label: string, fn: (ws: WebSocket, done: (err?: unknown) => void) } async function listen(): Promise { - const { pathname } = new URL("./websocket-server-echo.mjs", import.meta.url); + const pathname = path.join(import.meta.dir, "./websocket-server-echo.mjs"); const server = spawn({ cmd: [nodeExe() ?? bunExe(), pathname], cwd: import.meta.dir, From ebaeafbc89b8d86d4303a468d373792f8859bde3 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 16 Feb 2024 04:09:34 -0800 Subject: [PATCH 09/19] feat: More robust and faster shell escaping (#8904) * wip * Proper escaping algorithm * Don't use `$` for js obj/string referencs * [autofix.ci] apply automated fixes * Changes * Changes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner --- src/bun.js/api/BunObject.zig | 42 ++- src/shell/interpreter.zig | 38 ++- src/shell/shell.zig | 475 ++++++++++++++++++++++++----- src/string_immutable.zig | 2 +- test/js/bun/shell/bunshell.test.ts | 31 ++ 5 files changed, 489 insertions(+), 99 deletions(-) diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 76700e1cb4..a0266f8505 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -318,6 +318,17 @@ pub fn shellLex( defer arena.deinit(); const template_args = callframe.argumentsPtr()[1..callframe.argumentsCount()]; + var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); + var jsstrings = std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4) catch { + globalThis.throwOutOfMemory(); + return .undefined; + }; + defer { + for (jsstrings.items[0..]) |bunstr| { + bunstr.deref(); + } + jsstrings.deinit(); + } var jsobjs = std.ArrayList(JSValue).init(arena.allocator()); defer { for (jsobjs.items) |jsval| { @@ -326,7 +337,7 @@ pub fn shellLex( } var script = std.ArrayList(u8).init(arena.allocator()); - if (!(bun.shell.shellCmdFromJS(globalThis, string_args, template_args, &jsobjs, &script) catch { + if (!(bun.shell.shellCmdFromJS(globalThis, string_args, template_args, &jsobjs, &jsstrings, &script) catch { globalThis.throwOutOfMemory(); return JSValue.undefined; })) { @@ -335,14 +346,14 @@ pub fn shellLex( const lex_result = brk: { if (bun.strings.isAllASCII(script.items[0..])) { - var lexer = Shell.LexerAscii.new(arena.allocator(), script.items[0..]); + var lexer = Shell.LexerAscii.new(arena.allocator(), script.items[0..], jsstrings.items[0..]); lexer.lex() catch |err| { globalThis.throwError(err, "failed to lex shell"); return JSValue.undefined; }; break :brk lexer.get_result(); } - var lexer = Shell.LexerUnicode.new(arena.allocator(), script.items[0..]); + var lexer = Shell.LexerUnicode.new(arena.allocator(), script.items[0..], jsstrings.items[0..]); lexer.lex() catch |err| { globalThis.throwError(err, "failed to lex shell"); return JSValue.undefined; @@ -393,6 +404,17 @@ pub fn shellParse( defer arena.deinit(); const template_args = callframe.argumentsPtr()[1..callframe.argumentsCount()]; + var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); + var jsstrings = std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4) catch { + globalThis.throwOutOfMemory(); + return .undefined; + }; + defer { + for (jsstrings.items[0..]) |bunstr| { + bunstr.deref(); + } + jsstrings.deinit(); + } var jsobjs = std.ArrayList(JSValue).init(arena.allocator()); defer { for (jsobjs.items) |jsval| { @@ -400,7 +422,7 @@ pub fn shellParse( } } var script = std.ArrayList(u8).init(arena.allocator()); - if (!(bun.shell.shellCmdFromJS(globalThis, string_args, template_args, &jsobjs, &script) catch { + if (!(bun.shell.shellCmdFromJS(globalThis, string_args, template_args, &jsobjs, &jsstrings, &script) catch { globalThis.throwOutOfMemory(); return JSValue.undefined; })) { @@ -410,7 +432,7 @@ pub fn shellParse( var out_parser: ?bun.shell.Parser = null; var out_lex_result: ?bun.shell.LexResult = null; - const script_ast = bun.shell.Interpreter.parse(&arena, script.items[0..], jsobjs.items[0..], &out_parser, &out_lex_result) catch |err| { + const script_ast = bun.shell.Interpreter.parse(&arena, script.items[0..], jsobjs.items[0..], jsstrings.items[0..], &out_parser, &out_lex_result) catch |err| { if (err == bun.shell.ParseError.Lex) { std.debug.assert(out_lex_result != null); const str = out_lex_result.?.combineErrors(arena.allocator()); @@ -545,17 +567,21 @@ pub fn shellEscape( if (bunstr.isUTF16()) { if (bun.shell.needsEscapeUTF16(bunstr.utf16())) { - bun.shell.escapeUnicode(bunstr.byteSlice(), &outbuf) catch { + const has_invalid_utf16 = bun.shell.escapeUtf16(bunstr.utf16(), &outbuf, true) catch { globalThis.throwOutOfMemory(); return .undefined; }; + if (has_invalid_utf16) { + globalThis.throw("String has invalid utf-16: {s}", .{bunstr.byteSlice()}); + return .undefined; + } return bun.String.createUTF8(outbuf.items[0..]).toJS(globalThis); } return jsval; } - if (bun.shell.needsEscape(bunstr.latin1())) { - bun.shell.escape(bunstr.byteSlice(), &outbuf) catch { + if (bun.shell.needsEscapeUtf8AsciiLatin1(bunstr.latin1())) { + bun.shell.escape8Bit(bunstr.byteSlice(), &outbuf, true) catch { globalThis.throwOutOfMemory(); return .undefined; }; diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 37f8bd31b5..63c10ef418 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -842,9 +842,20 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { }; const template_args = callframe.argumentsPtr()[1..callframe.argumentsCount()]; + var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); + var jsstrings = std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4) catch { + globalThis.throwOutOfMemory(); + return null; + }; + defer { + for (jsstrings.items[0..]) |bunstr| { + bunstr.deref(); + } + jsstrings.deinit(); + } var jsobjs = std.ArrayList(JSValue).init(arena.allocator()); var script = std.ArrayList(u8).init(arena.allocator()); - if (!(bun.shell.shellCmdFromJS(globalThis, string_args, template_args, &jsobjs, &script) catch { + if (!(bun.shell.shellCmdFromJS(globalThis, string_args, template_args, &jsobjs, &jsstrings, &script) catch { globalThis.throwOutOfMemory(); return null; })) { @@ -857,6 +868,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { &arena, script.items[0..], jsobjs.items[0..], + jsstrings.items[0..], &parser, &lex_result, ) catch |err| { @@ -902,14 +914,21 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { return interpreter; } - pub fn parse(arena: *bun.ArenaAllocator, script: []const u8, jsobjs: []JSValue, out_parser: *?bun.shell.Parser, out_lex_result: *?shell.LexResult) !ast.Script { + pub fn parse( + arena: *bun.ArenaAllocator, + script: []const u8, + jsobjs: []JSValue, + jsstrings_to_escape: []bun.String, + out_parser: *?bun.shell.Parser, + out_lex_result: *?shell.LexResult, + ) !ast.Script { const lex_result = brk: { if (bun.strings.isAllASCII(script)) { - var lexer = bun.shell.LexerAscii.new(arena.allocator(), script); + var lexer = bun.shell.LexerAscii.new(arena.allocator(), script, jsstrings_to_escape); try lexer.lex(); break :brk lexer.get_result(); } - var lexer = bun.shell.LexerUnicode.new(arena.allocator(), script); + var lexer = bun.shell.LexerUnicode.new(arena.allocator(), script, jsstrings_to_escape); try lexer.lex(); break :brk lexer.get_result(); }; @@ -1029,7 +1048,14 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { const jsobjs: []JSValue = &[_]JSValue{}; var out_parser: ?bun.shell.Parser = null; var out_lex_result: ?bun.shell.LexResult = null; - const script = ThisInterpreter.parse(&arena, src, jsobjs, &out_parser, &out_lex_result) catch |err| { + const script = ThisInterpreter.parse( + &arena, + src, + jsobjs, + &[_]bun.String{}, + &out_parser, + &out_lex_result, + ) catch |err| { if (err == bun.shell.ParseError.Lex) { std.debug.assert(out_lex_result != null); const str = out_lex_result.?.combineErrors(arena.allocator()); @@ -1075,7 +1101,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { const jsobjs: []JSValue = &[_]JSValue{}; var out_parser: ?bun.shell.Parser = null; var out_lex_result: ?bun.shell.LexResult = null; - const script = ThisInterpreter.parse(&arena, src, jsobjs, &out_parser, &out_lex_result) catch |err| { + const script = ThisInterpreter.parse(&arena, src, jsobjs, &[_]bun.String{}, &out_parser, &out_lex_result) catch |err| { if (err == bun.shell.ParseError.Lex) { std.debug.assert(out_lex_result != null); const str = out_lex_result.?.combineErrors(arena.allocator()); diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 60c2b8ca9b..c6cd685544 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -1279,7 +1279,8 @@ pub const LexError = struct { /// Allocated with lexer arena msg: []const u8, }; -pub const LEX_JS_OBJREF_PREFIX = "$__bun_"; +pub const LEX_JS_OBJREF_PREFIX = "~__bun_"; +pub const LEX_JS_STRING_PREFIX = "~__bunstr_"; pub fn NewLexer(comptime encoding: StringEncoding) type { const Chars = ShellCharIter(encoding); @@ -1300,6 +1301,10 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { in_subshell: ?SubShellKind = null, errors: std.ArrayList(LexError), + /// Contains a list of strings we need to escape + /// Not owned by this struct + string_refs: []bun.String, + const SubShellKind = enum { /// (echo hi; echo hello) normal, @@ -1329,12 +1334,13 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { delimit_quote: bool, }; - pub fn new(alloc: Allocator, src: []const u8) @This() { + pub fn new(alloc: Allocator, src: []const u8, strings_to_escape: []bun.String) @This() { return .{ .chars = Chars.init(src), .tokens = ArrayList(Token).init(alloc), .strpool = ArrayList(u8).init(alloc), .errors = ArrayList(LexError).init(alloc), + .string_refs = strings_to_escape, }; } @@ -1364,6 +1370,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { .word_start = self.word_start, .j = self.j, + .string_refs = self.string_refs, }; sublexer.chars.state = .Normal; return sublexer; @@ -1411,11 +1418,31 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { const char = input.char; const escaped = input.escaped; + // Special token to denote substituted JS variables + if (char == '~') { + if (self.looksLikeJSStringRef()) { + if (self.eatJSStringRef()) |bunstr| { + try self.break_word(false); + try self.handleJSStringRef(bunstr); + continue; + } + } else if (self.looksLikeJSObjRef()) { + if (self.eatJSObjRef()) |tok| { + if (self.chars.state == .Double) { + self.add_error("JS object reference not allowed in double quotes"); + return; + } + try self.break_word(false); + try self.tokens.append(tok); + continue; + } + } + } // Handle non-escaped chars: // 1. special syntax (operators, etc.) // 2. lexing state switchers (quotes) // 3. word breakers (spaces, etc.) - if (!escaped) escaped: { + else if (!escaped) escaped: { switch (char) { '#' => { if (self.chars.state == .Single or self.chars.state == .Double) break :escaped; @@ -1506,21 +1533,13 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { // const snapshot = self.make_snapshot(); // Handle variable try self.break_word(false); - if (self.eat_js_obj_ref()) |ref| { - if (self.chars.state == .Double) { - try self.errors.append(.{ .msg = bun.default_allocator.dupe(u8, "JS object reference not allowed in double quotes") catch bun.outOfMemory() }); - return; - } - try self.tokens.append(ref); + const var_tok = try self.eat_var(); + // empty var + if (var_tok.start == var_tok.end) { + try self.appendCharToStrPool('$'); + try self.break_word(false); } else { - const var_tok = try self.eat_var(); - // empty var - if (var_tok.start == var_tok.end) { - try self.appendCharToStrPool('$'); - try self.break_word(false); - } else { - try self.tokens.append(.{ .Var = var_tok }); - } + try self.tokens.append(.{ .Var = var_tok }); } self.word_start = self.j; continue; @@ -1907,19 +1926,146 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { self.continue_from_sublexer(&sublexer); } - fn eat_js_obj_ref(self: *@This()) ?Token { - const snap = self.make_snapshot(); - if (self.eat_literal(u8, LEX_JS_OBJREF_PREFIX)) { - if (self.eat_number_word()) |num| { - if (num <= std.math.maxInt(u32)) { - return .{ .JSObjRef = @intCast(num) }; + fn appendStringToStrPool(self: *@This(), bunstr: bun.String) !void { + const start = self.strpool.items.len; + if (bunstr.is8Bit() or bunstr.isUTF8()) { + try self.strpool.appendSlice(bunstr.byteSlice()); + } else { + const utf16 = bunstr.utf16(); + const additional = bun.simdutf.simdutf__utf8_length_from_utf16le(utf16.ptr, utf16.len); + try self.strpool.ensureUnusedCapacity(additional); + try bun.strings.convertUTF16ToUTF8Append(&self.strpool, bunstr.utf16()); + } + const end = self.strpool.items.len; + self.j += @intCast(end - start); + } + + fn handleJSStringRef(self: *@This(), bunstr: bun.String) !void { + try self.appendStringToStrPool(bunstr); + } + + fn looksLikeJSObjRef(self: *@This()) bool { + const bytes = self.chars.srcBytesAtCursor(); + if (LEX_JS_OBJREF_PREFIX.len - 1 >= bytes.len) return false; + return std.mem.eql(u8, bytes[0 .. LEX_JS_OBJREF_PREFIX.len - 1], LEX_JS_OBJREF_PREFIX[1..]); + } + + fn looksLikeJSStringRef(self: *@This()) bool { + const bytes = self.chars.srcBytesAtCursor(); + if (LEX_JS_STRING_PREFIX.len - 1 >= bytes.len) return false; + return std.mem.eql(u8, bytes[0 .. LEX_JS_STRING_PREFIX.len - 1], LEX_JS_STRING_PREFIX[1..]); + } + + fn eatJSSubstitutionIdx(self: *@This(), comptime literal: []const u8, comptime name: []const u8, comptime validate: *const fn (*@This(), usize) bool) ?usize { + const bytes = self.chars.srcBytesAtCursor(); + if (literal.len - 1 >= bytes.len) return null; + if (std.mem.eql(u8, bytes[0 .. literal.len - 1], literal[1..])) { + var i: usize = 0; + var digit_buf: [32]u8 = undefined; + var digit_buf_count: u8 = 0; + + i += literal.len - 1; + + while (i < bytes.len) : (i += 1) { + switch (bytes[i]) { + '0'...'9' => { + if (digit_buf_count >= digit_buf.len) { + const ERROR_STR = "Invalid " ++ name ++ " (number too high): "; + var error_buf: [ERROR_STR.len + digit_buf.len + 1]u8 = undefined; + const error_msg = std.fmt.bufPrint(error_buf[0..], "{s} {s}{c}", .{ ERROR_STR, digit_buf[0..digit_buf_count], bytes[i] }) catch @panic("Should not happen"); + self.add_error(error_msg); + return null; + } + digit_buf[digit_buf_count] = bytes[i]; + digit_buf_count += 1; + }, + else => break, } } + + if (digit_buf_count == 0) { + self.add_error("Invalid " ++ name ++ " (no idx)"); + return null; + } + + const idx = std.fmt.parseInt(usize, digit_buf[0..digit_buf_count], 10) catch { + self.add_error("Invalid " ++ name ++ " ref "); + return null; + }; + + if (!validate(self, idx)) return null; + // if (idx >= self.string_refs.len) { + // self.add_error("Invalid " ++ name ++ " (out of bounds"); + // return null; + // } + + // Bump the cursor + brk: { + const new_idx = self.chars.cursorPos() + i; + const prev_ascii_char: ?u7 = if (digit_buf_count == 1) null else @truncate(digit_buf[digit_buf_count - 2]); + const cur_ascii_char: u7 = @truncate(digit_buf[digit_buf_count - 1]); + if (comptime encoding == .ascii) { + self.chars.src.i = new_idx; + if (prev_ascii_char) |pc| self.chars.prev = .{ .char = pc }; + self.chars.current = .{ .char = cur_ascii_char }; + break :brk; + } + self.chars.src.cursor = CodepointIterator.Cursor{ + .i = @intCast(new_idx), + .c = cur_ascii_char, + .width = 1, + }; + self.chars.src.next_cursor = self.chars.src.cursor; + SrcUnicode.nextCursor(&self.chars.src.iter, &self.chars.src.next_cursor); + if (prev_ascii_char) |pc| self.chars.prev = .{ .char = pc }; + self.chars.current = .{ .char = cur_ascii_char }; + } + + // return self.string_refs[idx]; + return idx; } - self.backtrack(snap); return null; } + /// __NOTE__: Do not store references to the returned bun.String, it does not have its ref count incremented + fn eatJSStringRef(self: *@This()) ?bun.String { + if (self.eatJSSubstitutionIdx( + LEX_JS_STRING_PREFIX, + "JS string ref", + validateJSStringRefIdx, + )) |idx| { + return self.string_refs[idx]; + } + return null; + } + + fn validateJSStringRefIdx(self: *@This(), idx: usize) bool { + if (idx >= self.string_refs.len) { + self.add_error("Invalid JS string ref (out of bounds"); + return false; + } + return true; + } + + fn eatJSObjRef(self: *@This()) ?Token { + if (self.eatJSSubstitutionIdx( + LEX_JS_OBJREF_PREFIX, + "JS object ref", + validateJSObjRefIdx, + )) |idx| { + return .{ .JSObjRef = @intCast(idx) }; + } + return null; + } + + fn validateJSObjRefIdx(self: *@This(), idx: usize) bool { + if (idx >= std.math.maxInt(u32)) { + self.add_error("Invalid JS object ref (out of bounds)"); + return false; + } + return true; + } + fn eat_var(self: *@This()) !Token.TextRange { const start = self.j; var i: usize = 0; @@ -2087,7 +2233,7 @@ const SrcUnicode = struct { inline fn indexNext(this: *const SrcUnicode) ?IndexValue { if (this.next_cursor.width + this.next_cursor.i > this.iter.bytes.len) return null; - return .{ .char = this.next_cursor.c, .width = this.next_cursor.width }; + return .{ .char = @intCast(this.next_cursor.c), .width = this.next_cursor.width }; } inline fn eat(this: *SrcUnicode, escaped: bool) void { @@ -2147,6 +2293,27 @@ pub fn ShellCharIter(comptime encoding: StringEncoding) type { }; } + pub fn srcBytes(self: *@This()) []const u8 { + if (comptime encoding == .ascii) return self.src.bytes; + return self.src.iter.bytes; + } + + pub fn srcBytesAtCursor(self: *@This()) []const u8 { + const bytes = self.srcBytes(); + if (comptime encoding == .ascii) { + if (self.src.i >= bytes.len) return ""; + return bytes[self.src.i..]; + } + + if (self.src.iter.i >= bytes.len) return ""; + return bytes[self.src.iter.i..]; + } + + pub fn cursorPos(self: *@This()) usize { + if (comptime encoding == .ascii) return self.src.i; + return self.src.iter.i; + } + pub fn eat(self: *@This()) ?InputChar { if (self.read_char()) |result| { self.prev = self.current; @@ -2451,8 +2618,10 @@ pub fn shellCmdFromJS( string_args: JSValue, template_args: []const JSValue, out_jsobjs: *std.ArrayList(JSValue), + jsstrings: *std.ArrayList(bun.String), out_script: *std.ArrayList(u8), ) !bool { + var builder = ShellSrcBuilder.init(globalThis, out_script, jsstrings); var jsobjref_buf: [128]u8 = [_]u8{0} ** 128; var string_iter = string_args.arrayIterator(globalThis); @@ -2460,7 +2629,7 @@ pub fn shellCmdFromJS( const last = string_iter.len -| 1; while (string_iter.next()) |js_value| { defer i += 1; - if (!try appendJSValueStr(globalThis, js_value, out_script, false)) { + if (!try builder.appendJSValueStr(js_value, false)) { globalThis.throw("Shell script string contains invalid UTF-16", .{}); return false; } @@ -2468,7 +2637,7 @@ pub fn shellCmdFromJS( // try script.appendSlice(str.full()); if (i < last) { const template_value = template_args[i]; - if (!(try handleTemplateValue(globalThis, template_value, out_jsobjs, out_script, jsobjref_buf[0..]))) return false; + if (!(try handleTemplateValue(globalThis, template_value, out_jsobjs, out_script, jsstrings, jsobjref_buf[0..]))) return false; } } return true; @@ -2479,8 +2648,10 @@ pub fn handleTemplateValue( template_value: JSValue, out_jsobjs: *std.ArrayList(JSValue), out_script: *std.ArrayList(u8), + jsstrings: *std.ArrayList(bun.String), jsobjref_buf: []u8, ) !bool { + var builder = ShellSrcBuilder.init(globalThis, out_script, jsstrings); if (!template_value.isEmpty()) { if (template_value.asArrayBuffer(globalThis)) |array_buffer| { _ = array_buffer; @@ -2497,7 +2668,7 @@ pub fn handleTemplateValue( if (store.data == .file) { if (store.data.file.pathlike == .path) { const path = store.data.file.pathlike.path.slice(); - if (!try appendUTF8Text(path, out_script, true)) { + if (!try builder.appendUTF8(path, true)) { globalThis.throw("Shell script string contains invalid UTF-16", .{}); return false; } @@ -2537,7 +2708,7 @@ pub fn handleTemplateValue( } if (template_value.isString()) { - if (!try appendJSValueStr(globalThis, template_value, out_script, true)) { + if (!try builder.appendJSValueStr(template_value, true)) { globalThis.throw("Shell script string contains invalid UTF-16", .{}); return false; } @@ -2549,10 +2720,10 @@ pub fn handleTemplateValue( const last = array.len -| 1; var i: u32 = 0; while (array.next()) |arr| : (i += 1) { - if (!(try handleTemplateValue(globalThis, arr, out_jsobjs, out_script, jsobjref_buf))) return false; + if (!(try handleTemplateValue(globalThis, arr, out_jsobjs, out_script, jsstrings, jsobjref_buf))) return false; if (i < last) { - const str = bun.String.init(" "); - if (!try appendBunStr(str, out_script, false)) return false; + const str = bun.String.static(" "); + if (!try builder.appendBunStr(str, false)) return false; } } return true; @@ -2562,7 +2733,7 @@ pub fn handleTemplateValue( if (template_value.getTruthy(globalThis, "raw")) |maybe_str| { const bunstr = maybe_str.toBunString(globalThis); defer bunstr.deref(); - if (!try appendBunStr(bunstr, out_script, false)) { + if (!try builder.appendBunStr(bunstr, false)) { globalThis.throw("Shell script string contains invalid UTF-16", .{}); return false; } @@ -2571,7 +2742,7 @@ pub fn handleTemplateValue( } if (template_value.isPrimitive()) { - if (!try appendJSValueStr(globalThis, template_value, out_script, true)) { + if (!try builder.appendJSValueStr(template_value, true)) { globalThis.throw("Shell script string contains invalid UTF-16", .{}); return false; } @@ -2579,7 +2750,7 @@ pub fn handleTemplateValue( } if (template_value.implementsToString(globalThis)) { - if (!try appendJSValueStr(globalThis, template_value, out_script, true)) { + if (!try builder.appendJSValueStr(template_value, true)) { globalThis.throw("Shell script string contains invalid UTF-16", .{}); return false; } @@ -2593,57 +2764,127 @@ pub fn handleTemplateValue( return true; } -/// This will disallow invalid surrogate pairs -pub fn appendJSValueStr(globalThis: *JSC.JSGlobalObject, jsval: JSValue, outbuf: *std.ArrayList(u8), comptime allow_escape: bool) !bool { - const bunstr = jsval.toBunString(globalThis); - defer bunstr.deref(); +pub const ShellSrcBuilder = struct { + globalThis: *JSC.JSGlobalObject, + outbuf: *std.ArrayList(u8), + jsstrs_to_escape: *std.ArrayList(bun.String), + jsstr_ref_buf: [128]u8 = [_]u8{0} ** 128, - return try appendBunStr(bunstr, outbuf, allow_escape); -} - -pub fn appendUTF8Text(slice: []const u8, outbuf: *std.ArrayList(u8), comptime allow_escape: bool) !bool { - if (!bun.simdutf.validate.utf8(slice)) { - return false; + pub fn init( + globalThis: *JSC.JSGlobalObject, + outbuf: *std.ArrayList(u8), + jsstrs_to_escape: *std.ArrayList(bun.String), + ) ShellSrcBuilder { + return .{ + .globalThis = globalThis, + .outbuf = outbuf, + .jsstrs_to_escape = jsstrs_to_escape, + }; } - if (allow_escape and needsEscape(slice)) { - try escape(slice, outbuf); - } else { - try outbuf.appendSlice(slice); + pub fn appendJSValueStr(this: *ShellSrcBuilder, jsval: JSValue, comptime allow_escape: bool) !bool { + const bunstr = jsval.toBunString(this.globalThis); + defer bunstr.deref(); + + return try this.appendBunStr(bunstr, allow_escape); } - return true; -} - -pub fn appendBunStr(bunstr: bun.String, outbuf: *std.ArrayList(u8), comptime allow_escape: bool) !bool { - const str = bunstr.toUTF8WithoutRef(bun.default_allocator); - defer str.deinit(); - - // TODO: toUTF8 already validates. We shouldn't have to do this twice! - const is_ascii = str.isAllocated(); - if (!is_ascii and !bun.simdutf.validate.utf8(str.slice())) { - return false; + pub fn appendBunStr( + this: *ShellSrcBuilder, + bunstr: bun.String, + comptime allow_escape: bool, + ) !bool { + const invalid = (bunstr.isUTF16() and !bun.simdutf.validate.utf16le(bunstr.utf16())) or (bunstr.isUTF8() and !bun.simdutf.validate.utf8(bunstr.byteSlice())); + if (invalid) return false; + if (allow_escape) { + if (needsEscapeBunstr(bunstr)) { + try this.appendJSStrRef(bunstr); + return true; + } + } + if (bunstr.isUTF16()) { + try this.appendUTF16Impl(bunstr.utf16()); + return true; + } + if (bunstr.isUTF8() or bun.strings.isAllASCII(bunstr.byteSlice())) { + try this.appendUTF8Impl(bunstr.byteSlice()); + return true; + } + try this.appendLatin1Impl(bunstr.byteSlice()); + return true; } - if (allow_escape and needsEscape(str.slice())) { - try escape(str.slice(), outbuf); - } else { - try outbuf.appendSlice(str.slice()); + pub fn appendUTF8(this: *ShellSrcBuilder, utf8: []const u8, comptime allow_escape: bool) !bool { + const invalid = bun.simdutf.validate.utf8(utf8); + if (!invalid) return false; + if (allow_escape) { + if (needsEscapeUtf8AsciiLatin1(utf8)) { + const bunstr = bun.String.createUTF8(utf8); + defer bunstr.deref(); + try this.appendJSStrRef(bunstr); + return true; + } + } + + try this.appendUTF8Impl(utf8); + return true; } - return true; -} + pub fn appendUTF16Impl(this: *ShellSrcBuilder, utf16: []const u16) !void { + const size = bun.simdutf.simdutf__utf8_length_from_utf16le(utf16.ptr, utf16.len); + try this.outbuf.ensureUnusedCapacity(size); + try bun.strings.convertUTF16ToUTF8Append(this.outbuf, utf16); + } + + pub fn appendUTF8Impl(this: *ShellSrcBuilder, utf8: []const u8) !void { + try this.outbuf.appendSlice(utf8); + } + + pub fn appendLatin1Impl(this: *ShellSrcBuilder, latin1: []const u8) !void { + const non_ascii_idx = bun.strings.firstNonASCII(latin1) orelse 0; + + if (non_ascii_idx > 0) { + try this.appendUTF8Impl(latin1[0..non_ascii_idx]); + } + + this.outbuf.* = try bun.strings.allocateLatin1IntoUTF8WithList(this.outbuf.*, this.outbuf.items.len, []const u8, latin1); + } + + pub fn appendJSStrRef(this: *ShellSrcBuilder, bunstr: bun.String) !void { + const idx = this.jsstrs_to_escape.items.len; + const str = std.fmt.bufPrint(this.jsstr_ref_buf[0..], "{s}{d}", .{ LEX_JS_STRING_PREFIX, idx }) catch { + @panic("Impossible"); + }; + try this.outbuf.appendSlice(str); + bunstr.ref(); + try this.jsstrs_to_escape.append(bunstr); + } +}; /// Characters that need to escaped -const SPECIAL_CHARS = [_]u8{ '$', '>', '&', '|', '=', ';', '\n', '{', '}', ',', '(', ')', '\\', '\"', ' ' }; +const SPECIAL_CHARS = [_]u8{ '$', '>', '&', '|', '=', ';', '\n', '{', '}', ',', '(', ')', '\\', '\"', ' ', '\'' }; /// Characters that need to be backslashed inside double quotes const BACKSLASHABLE_CHARS = [_]u8{ '$', '`', '"', '\\' }; -/// assumes WTF-8 -pub fn escape(str: []const u8, outbuf: *std.ArrayList(u8)) !void { +pub fn escapeBunStr(bunstr: bun.String, outbuf: *std.ArrayList(u8), comptime add_quotes: bool) !bool { + // latin-1 or ascii + if (bunstr.is8Bit()) { + try escape8Bit(bunstr.byteSlice(), outbuf, add_quotes); + return true; + } + if (bunstr.isUTF16()) { + return try escapeUtf16(bunstr.utf16(), outbuf, add_quotes); + } + // Otherwise is utf-8 + try escapeWTF8(bunstr.byteSlice(), outbuf, add_quotes); + return true; +} + +/// works for latin-1 and ascii +pub fn escape8Bit(str: []const u8, outbuf: *std.ArrayList(u8), comptime add_quotes: bool) !void { try outbuf.ensureUnusedCapacity(str.len); - try outbuf.append('\"'); + if (add_quotes) try outbuf.append('\"'); loop: for (str) |c| { inline for (BACKSLASHABLE_CHARS) |spc| { @@ -2658,15 +2899,15 @@ pub fn escape(str: []const u8, outbuf: *std.ArrayList(u8)) !void { try outbuf.append(c); } - try outbuf.append('\"'); + if (add_quotes) try outbuf.append('\"'); } -pub fn escapeUnicode(str: []const u8, outbuf: *std.ArrayList(u8)) !void { +pub fn escapeWTF8(str: []const u8, outbuf: *std.ArrayList(u8), comptime add_quotes: bool) !void { try outbuf.ensureUnusedCapacity(str.len); var bytes: [8]u8 = undefined; - var n = bun.strings.encodeWTF8Rune(bytes[0..4], '"'); - try outbuf.appendSlice(bytes[0..n]); + var n: u3 = if (add_quotes) bun.strings.encodeWTF8Rune(bytes[0..4], '"') else 0; + if (add_quotes) try outbuf.appendSlice(bytes[0..n]); loop: for (str) |c| { inline for (BACKSLASHABLE_CHARS) |spc| { @@ -2686,18 +2927,84 @@ pub fn escapeUnicode(str: []const u8, outbuf: *std.ArrayList(u8)) !void { try outbuf.appendSlice(bytes[0..n]); } - n = bun.strings.encodeWTF8Rune(bytes[0..4], '"'); - try outbuf.appendSlice(bytes[0..n]); + if (add_quotes) { + n = bun.strings.encodeWTF8Rune(bytes[0..4], '"'); + try outbuf.appendSlice(bytes[0..n]); + } +} + +pub fn escapeUtf16(str: []const u16, outbuf: *std.ArrayList(u8), comptime add_quotes: bool) !bool { + if (add_quotes) try outbuf.append('"'); + + const non_ascii = bun.strings.firstNonASCII16([]const u16, str) orelse 0; + var cp_buf: [4]u8 = undefined; + + var i: usize = 0; + loop: while (i < str.len) { + const char: u32 = brk: { + if (i < non_ascii) { + i += 1; + break :brk str[i]; + } + const ret = bun.strings.utf16Codepoint([]const u16, str[i..]); + if (ret.fail) return false; + i += ret.len; + break :brk ret.code_point; + }; + + inline for (BACKSLASHABLE_CHARS) |bchar| { + if (@as(u32, @intCast(bchar)) == char) { + try outbuf.appendSlice(&[_]u8{ '\\', @intCast(char) }); + continue :loop; + } + } + + const len = bun.strings.encodeWTF8RuneT(&cp_buf, u32, char); + try outbuf.appendSlice(cp_buf[0..len]); + } + if (add_quotes) try outbuf.append('"'); + return true; +} + +pub fn needsEscapeBunstr(bunstr: bun.String) bool { + if (bunstr.isUTF16()) return needsEscapeUTF16(bunstr.utf16()); + // Otherwise is utf-8, ascii, or latin-1 + return needsEscapeUtf8AsciiLatin1(bunstr.byteSlice()); +} + +pub fn needsEscapeUTF16Slow(str: []const u16) bool { + for (str) |codeunit| { + inline for (SPECIAL_CHARS) |spc| { + if (@as(u16, @intCast(spc)) == codeunit) return true; + } + } + + return false; } pub fn needsEscapeUTF16(str: []const u16) bool { - for (str) |char| { - switch (char) { - '$', '>', '&', '|', '=', ';', '\n', '{', '}', ',', '(', ')', '\\', '\"', ' ' => return true, - else => {}, + if (str.len < 64) return needsEscapeUTF16Slow(str); + + const needles = comptime brk: { + var needles: [SPECIAL_CHARS.len]@Vector(8, u16) = undefined; + for (SPECIAL_CHARS, 0..) |c, i| { + needles[i] = @splat(@as(u16, @intCast(c))); + } + break :brk needles; + }; + + var i: usize = 0; + while (i + 8 <= str.len) : (i += 8) { + const haystack: @Vector(8, u16) = str[i..][0..8].*; + + inline for (needles) |needle| { + const result = haystack == needle; + if (std.simd.firstTrue(result) != null) return true; } } + if (i < str.len) return needsEscapeUTF16Slow(str[i..]); + return false; } @@ -2705,8 +3012,8 @@ pub fn needsEscapeUTF16(str: []const u16) bool { /// indicates the *possibility* that the string must be escaped, so it can have /// false positives, but it is faster than running the shell lexer through the /// input string for a more correct implementation. -pub fn needsEscape(str: []const u8) bool { - if (str.len < 128) return needsEscapeSlow(str); +pub fn needsEscapeUtf8AsciiLatin1(str: []const u8) bool { + if (str.len < 128) return needsEscapeUtf8AsciiLatin1Slow(str); const needles = comptime brk: { var needles: [SPECIAL_CHARS.len]@Vector(16, u8) = undefined; @@ -2726,12 +3033,12 @@ pub fn needsEscape(str: []const u8) bool { } } - if (i < str.len) return needsEscapeSlow(str[i..]); + if (i < str.len) return needsEscapeUtf8AsciiLatin1Slow(str[i..]); return false; } -pub fn needsEscapeSlow(str: []const u8) bool { +pub fn needsEscapeUtf8AsciiLatin1Slow(str: []const u8) bool { for (str) |c| { inline for (SPECIAL_CHARS) |spc| { if (spc == c) return true; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 7c4d80a2f6..59fffedfde 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1884,7 +1884,7 @@ pub fn convertUTF16ToUTF8Append(list: *std.ArrayList(u8), utf16: []const u16) !v return; } - list.items.len = result.count; + list.items.len += result.count; } pub fn toUTF8AllocWithType(allocator: std.mem.Allocator, comptime Type: type, utf16: Type) ![]u8 { diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 55fdd78142..8bfb96d303 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -69,6 +69,37 @@ describe("bunshell", () => { `"hello" "lol" "nice"lkasjf;jdfla<>SKDJFLKSF`, `"\\"hello\\" \\"lol\\" \\"nice\\"lkasjf;jdfla<>SKDJFLKSF"`, ); + + test("wrapped in quotes", async () => { + const url = "http://www.example.com?candy_name=M&M"; + await TestBuilder.command`echo url="${url}"`.stdout(`url=${url}\n`).run(); + await TestBuilder.command`echo url='${url}'`.stdout(`url=${url}\n`).run(); + await TestBuilder.command`echo url=${url}`.stdout(`url=${url}\n`).run(); + }); + + test("escape var", async () => { + const shellvar = "$FOO"; + await TestBuilder.command`FOO=bar && echo "${shellvar}"`.stdout(`$FOO\n`).run(); + await TestBuilder.command`FOO=bar && echo '${shellvar}'`.stdout(`$FOO\n`).run(); + await TestBuilder.command`FOO=bar && echo ${shellvar}`.stdout(`$FOO\n`).run(); + }); + + test("can't escape a js string/obj ref", async () => { + const shellvar = "$FOO"; + await TestBuilder.command`FOO=bar && echo \\${shellvar}`.stdout(`$FOO\n`).run(); + const buf = new Uint8Array(1); + await TestBuilder.command`echo hi > \\${buf}`.run(); + }); + + test("in command position", async () => { + const x = "echo hi"; + await TestBuilder.command`${x}`.exitCode(1).stderr("bun: command not found: echo hi\n").run(); + }); + + test("arrays", async () => { + const x = ["echo", "hi"]; + await TestBuilder.command`${x}`.stdout("hi\n").run(); + }); }); describe("quiet", async () => { From 0e2a3a0197581c2fd0d56cc1e2ec9336037ab823 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 16 Feb 2024 04:12:12 -0800 Subject: [PATCH 10/19] fix(glob): fix patterns starting with * #8817 (#8847) * Fix #8817 * [autofix.ci] apply automated fixes * yoops * fix broken stuff from merge --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/glob.zig | 1528 +-------------------------------- test/js/bun/glob/scan.test.ts | 79 ++ 2 files changed, 121 insertions(+), 1486 deletions(-) diff --git a/src/glob.zig b/src/glob.zig index 0d91085a95..d7b18edff8 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -53,6 +53,8 @@ const ZigString = @import("./bun.js/bindings/bindings.zig").ZigString; // const Codepoint = u32; const Cursor = CodepointIterator.Cursor; +const log = bun.Output.scoped(.Glob, false); + const CursorState = struct { cursor: CodepointIterator.Cursor = .{}, /// The index in terms of codepoints @@ -111,8 +113,6 @@ const CursorState = struct { } }; -const log = bun.Output.scoped(.glob, false); - pub const BunGlobWalker = GlobWalker_(null, false); fn dummyFilterTrue(val: []const u8) bool { @@ -200,7 +200,8 @@ pub fn GlobWalker_( fds_open: if (bun.Environment.allow_assert) usize else u0 = 0, pub fn init(this: *Iterator) !Maybe(void) { - const path_buf: *[bun.MAX_PATH_BYTES]u8 = &this.walker.pathBuf; + log("Iterator init pattern={s}", .{this.walker.pattern}); + var path_buf: *[bun.MAX_PATH_BYTES]u8 = &this.walker.pathBuf; const root_path = this.walker.cwd; @memcpy(path_buf[0..root_path.len], root_path[0..root_path.len]); path_buf[root_path.len] = 0; @@ -338,7 +339,7 @@ pub fn GlobWalker_( }; }; - // std.fs.cwd().iterate(); + log("Transition(dirpath={s}, fd={d}, component_idx={d})", .{ dir_path, fd, component_idx }); this.iter_state.directory.fd = fd; const iterator = DirIterator.iterate(fd.asDir(), .u8); @@ -639,6 +640,25 @@ pub fn GlobWalker_( _ = bun.simdutf.convert.utf8.to.utf32.le(pattern, codepoints); } + pub fn debugPatternComopnents(this: *GlobWalker) void { + const pattern = this.pattern; + const components = &this.patternComponents; + const ptr = @intFromPtr(this); + log("GlobWalker(0x{x}) components:", .{ptr}); + for (components.items) |cmp| { + if (cmp.syntax_hint == .None) { + continue; + } + switch (cmp.syntax_hint) { + .Single => log(" *", .{}), + .Double => log(" **", .{}), + .Dot => log(" .", .{}), + .DotBack => log(" ../", .{}), + .Literal, .WildcardFilepath, .None => log(" hint={s} component_str={s}", .{ @tagName(cmp.syntax_hint), pattern[cmp.start .. cmp.start + cmp.len] }), + } + } + } + /// `cwd` should be allocated with the arena /// The arena parameter is dereferenced and copied if all allocations go well and nothing goes wrong pub fn initWithCwd( @@ -673,6 +693,10 @@ pub fn GlobWalker_( this.error_on_broken_symlinks = error_on_broken_symlinks; this.only_files = only_files; + if (bun.Environment.allow_assert) { + this.debugPatternComopnents(); + } + return Maybe(void).success; } @@ -1108,7 +1132,20 @@ pub fn GlobWalker_( { for (pattern[component.start + 2 ..]) |c| { switch (c) { - '[', '{', '!', '?' => break :out_of_check_wildcard_filepath, + // The fast path checks that path[1..] == pattern[1..], + // this will obviously not work if additional + // glob syntax is present in the pattern, so we + // must not apply this optimization if we see + // special glob syntax. + // + // This is not a complete check, there can be + // false negatives, but that's okay, it just + // means we don't apply the optimization. + // + // We also don't need to look for the `!` token, + // because that only applies negation if at the + // beginning of the string. + '[', '{', '?', '*' => break :out_of_check_wildcard_filepath, else => {}, } } @@ -1642,1484 +1679,3 @@ pub fn matchWildcardFilepath(glob: []const u8, path: []const u8) bool { pub fn matchWildcardLiteral(literal: []const u8, path: []const u8) bool { return std.mem.eql(u8, literal, path); } - -// test "basic" { -// try expect(match("abc", "abc")); -// try expect(match("*", "abc")); -// try expect(match("*", "")); -// try expect(match("**", "")); -// try expect(match("*c", "abc")); -// try expect(!match("*b", "abc")); -// try expect(match("a*", "abc")); -// try expect(!match("b*", "abc")); -// try expect(match("a*", "a")); -// try expect(match("*a", "a")); -// try expect(match("a*b*c*d*e*", "axbxcxdxe")); -// try expect(match("a*b*c*d*e*", "axbxcxdxexxx")); -// try expect(match("a*b?c*x", "abxbbxdbxebxczzx")); -// try expect(!match("a*b?c*x", "abxbbxdbxebxczzy")); - -// try expect(match("a/*/test", "a/foo/test")); -// try expect(!match("a/*/test", "a/foo/bar/test")); -// try expect(match("a/**/test", "a/foo/test")); -// try expect(match("a/**/test", "a/foo/bar/test")); -// try expect(match("a/**/b/c", "a/foo/bar/b/c")); -// try expect(match("a\\*b", "a*b")); -// try expect(!match("a\\*b", "axb")); - -// try expect(match("[abc]", "a")); -// try expect(match("[abc]", "b")); -// try expect(match("[abc]", "c")); -// try expect(!match("[abc]", "d")); -// try expect(match("x[abc]x", "xax")); -// try expect(match("x[abc]x", "xbx")); -// try expect(match("x[abc]x", "xcx")); -// try expect(!match("x[abc]x", "xdx")); -// try expect(!match("x[abc]x", "xay")); -// try expect(match("[?]", "?")); -// try expect(!match("[?]", "a")); -// try expect(match("[*]", "*")); -// try expect(!match("[*]", "a")); - -// try expect(match("[a-cx]", "a")); -// try expect(match("[a-cx]", "b")); -// try expect(match("[a-cx]", "c")); -// try expect(!match("[a-cx]", "d")); -// try expect(match("[a-cx]", "x")); - -// try expect(!match("[^abc]", "a")); -// try expect(!match("[^abc]", "b")); -// try expect(!match("[^abc]", "c")); -// try expect(match("[^abc]", "d")); -// try expect(!match("[!abc]", "a")); -// try expect(!match("[!abc]", "b")); -// try expect(!match("[!abc]", "c")); -// try expect(match("[!abc]", "d")); -// try expect(match("[\\!]", "!")); - -// try expect(match("a*b*[cy]*d*e*", "axbxcxdxexxx")); -// try expect(match("a*b*[cy]*d*e*", "axbxyxdxexxx")); -// try expect(match("a*b*[cy]*d*e*", "axbxxxyxdxexxx")); - -// try expect(match("test.{jpg,png}", "test.jpg")); -// try expect(match("test.{jpg,png}", "test.png")); -// try expect(match("test.{j*g,p*g}", "test.jpg")); -// try expect(match("test.{j*g,p*g}", "test.jpxxxg")); -// try expect(match("test.{j*g,p*g}", "test.jxg")); -// try expect(!match("test.{j*g,p*g}", "test.jnt")); -// try expect(match("test.{j*g,j*c}", "test.jnc")); -// try expect(match("test.{jpg,p*g}", "test.png")); -// try expect(match("test.{jpg,p*g}", "test.pxg")); -// try expect(!match("test.{jpg,p*g}", "test.pnt")); -// try expect(match("test.{jpeg,png}", "test.jpeg")); -// try expect(!match("test.{jpeg,png}", "test.jpg")); -// try expect(match("test.{jpeg,png}", "test.png")); -// try expect(match("test.{jp\\,g,png}", "test.jp,g")); -// try expect(!match("test.{jp\\,g,png}", "test.jxg")); -// try expect(match("test/{foo,bar}/baz", "test/foo/baz")); -// try expect(match("test/{foo,bar}/baz", "test/bar/baz")); -// try expect(!match("test/{foo,bar}/baz", "test/baz/baz")); -// try expect(match("test/{foo*,bar*}/baz", "test/foooooo/baz")); -// try expect(match("test/{foo*,bar*}/baz", "test/barrrrr/baz")); -// try expect(match("test/{*foo,*bar}/baz", "test/xxxxfoo/baz")); -// try expect(match("test/{*foo,*bar}/baz", "test/xxxxbar/baz")); -// try expect(match("test/{foo/**,bar}/baz", "test/bar/baz")); -// try expect(!match("test/{foo/**,bar}/baz", "test/bar/test/baz")); - -// try expect(!match("*.txt", "some/big/path/to/the/needle.txt")); -// try expect(match( -// "some/**/needle.{js,tsx,mdx,ts,jsx,txt}", -// "some/a/bigger/path/to/the/crazy/needle.txt", -// )); -// try expect(match( -// "some/**/{a,b,c}/**/needle.txt", -// "some/foo/a/bigger/path/to/the/crazy/needle.txt", -// )); -// try expect(!match( -// "some/**/{a,b,c}/**/needle.txt", -// "some/foo/d/bigger/path/to/the/crazy/needle.txt", -// )); -// try expect(match("a/{a{a,b},b}", "a/aa")); -// try expect(match("a/{a{a,b},b}", "a/ab")); -// try expect(!match("a/{a{a,b},b}", "a/ac")); -// try expect(match("a/{a{a,b},b}", "a/b")); -// try expect(!match("a/{a{a,b},b}", "a/c")); -// try expect(match("a/{b,c[}]*}", "a/b")); -// try expect(match("a/{b,c[}]*}", "a/c}xx")); -// } - -// // The below tests are based on Bash and micromatch. -// // https://github.com/micromatch/picomatch/blob/master/test/bash.js -// test "bash" { -// try expect(!match("a*", "*")); -// try expect(!match("a*", "**")); -// try expect(!match("a*", "\\*")); -// try expect(!match("a*", "a/*")); -// try expect(!match("a*", "b")); -// try expect(!match("a*", "bc")); -// try expect(!match("a*", "bcd")); -// try expect(!match("a*", "bdir/")); -// try expect(!match("a*", "Beware")); -// try expect(match("a*", "a")); -// try expect(match("a*", "ab")); -// try expect(match("a*", "abc")); - -// try expect(!match("\\a*", "*")); -// try expect(!match("\\a*", "**")); -// try expect(!match("\\a*", "\\*")); - -// try expect(match("\\a*", "a")); -// try expect(!match("\\a*", "a/*")); -// try expect(match("\\a*", "abc")); -// try expect(match("\\a*", "abd")); -// try expect(match("\\a*", "abe")); -// try expect(!match("\\a*", "b")); -// try expect(!match("\\a*", "bb")); -// try expect(!match("\\a*", "bcd")); -// try expect(!match("\\a*", "bdir/")); -// try expect(!match("\\a*", "Beware")); -// try expect(!match("\\a*", "c")); -// try expect(!match("\\a*", "ca")); -// try expect(!match("\\a*", "cb")); -// try expect(!match("\\a*", "d")); -// try expect(!match("\\a*", "dd")); -// try expect(!match("\\a*", "de")); -// } - -// test "bash directories" { -// try expect(!match("b*/", "*")); -// try expect(!match("b*/", "**")); -// try expect(!match("b*/", "\\*")); -// try expect(!match("b*/", "a")); -// try expect(!match("b*/", "a/*")); -// try expect(!match("b*/", "abc")); -// try expect(!match("b*/", "abd")); -// try expect(!match("b*/", "abe")); -// try expect(!match("b*/", "b")); -// try expect(!match("b*/", "bb")); -// try expect(!match("b*/", "bcd")); -// try expect(match("b*/", "bdir/")); -// try expect(!match("b*/", "Beware")); -// try expect(!match("b*/", "c")); -// try expect(!match("b*/", "ca")); -// try expect(!match("b*/", "cb")); -// try expect(!match("b*/", "d")); -// try expect(!match("b*/", "dd")); -// try expect(!match("b*/", "de")); -// } - -// test "bash escaping" { -// try expect(!match("\\^", "*")); -// try expect(!match("\\^", "**")); -// try expect(!match("\\^", "\\*")); -// try expect(!match("\\^", "a")); -// try expect(!match("\\^", "a/*")); -// try expect(!match("\\^", "abc")); -// try expect(!match("\\^", "abd")); -// try expect(!match("\\^", "abe")); -// try expect(!match("\\^", "b")); -// try expect(!match("\\^", "bb")); -// try expect(!match("\\^", "bcd")); -// try expect(!match("\\^", "bdir/")); -// try expect(!match("\\^", "Beware")); -// try expect(!match("\\^", "c")); -// try expect(!match("\\^", "ca")); -// try expect(!match("\\^", "cb")); -// try expect(!match("\\^", "d")); -// try expect(!match("\\^", "dd")); -// try expect(!match("\\^", "de")); - -// try expect(match("\\*", "*")); -// // try expect(match("\\*", "\\*")); -// try expect(!match("\\*", "**")); -// try expect(!match("\\*", "a")); -// try expect(!match("\\*", "a/*")); -// try expect(!match("\\*", "abc")); -// try expect(!match("\\*", "abd")); -// try expect(!match("\\*", "abe")); -// try expect(!match("\\*", "b")); -// try expect(!match("\\*", "bb")); -// try expect(!match("\\*", "bcd")); -// try expect(!match("\\*", "bdir/")); -// try expect(!match("\\*", "Beware")); -// try expect(!match("\\*", "c")); -// try expect(!match("\\*", "ca")); -// try expect(!match("\\*", "cb")); -// try expect(!match("\\*", "d")); -// try expect(!match("\\*", "dd")); -// try expect(!match("\\*", "de")); - -// try expect(!match("a\\*", "*")); -// try expect(!match("a\\*", "**")); -// try expect(!match("a\\*", "\\*")); -// try expect(!match("a\\*", "a")); -// try expect(!match("a\\*", "a/*")); -// try expect(!match("a\\*", "abc")); -// try expect(!match("a\\*", "abd")); -// try expect(!match("a\\*", "abe")); -// try expect(!match("a\\*", "b")); -// try expect(!match("a\\*", "bb")); -// try expect(!match("a\\*", "bcd")); -// try expect(!match("a\\*", "bdir/")); -// try expect(!match("a\\*", "Beware")); -// try expect(!match("a\\*", "c")); -// try expect(!match("a\\*", "ca")); -// try expect(!match("a\\*", "cb")); -// try expect(!match("a\\*", "d")); -// try expect(!match("a\\*", "dd")); -// try expect(!match("a\\*", "de")); - -// try expect(match("*q*", "aqa")); -// try expect(match("*q*", "aaqaa")); -// try expect(!match("*q*", "*")); -// try expect(!match("*q*", "**")); -// try expect(!match("*q*", "\\*")); -// try expect(!match("*q*", "a")); -// try expect(!match("*q*", "a/*")); -// try expect(!match("*q*", "abc")); -// try expect(!match("*q*", "abd")); -// try expect(!match("*q*", "abe")); -// try expect(!match("*q*", "b")); -// try expect(!match("*q*", "bb")); -// try expect(!match("*q*", "bcd")); -// try expect(!match("*q*", "bdir/")); -// try expect(!match("*q*", "Beware")); -// try expect(!match("*q*", "c")); -// try expect(!match("*q*", "ca")); -// try expect(!match("*q*", "cb")); -// try expect(!match("*q*", "d")); -// try expect(!match("*q*", "dd")); -// try expect(!match("*q*", "de")); - -// try expect(match("\\**", "*")); -// try expect(match("\\**", "**")); -// try expect(!match("\\**", "\\*")); -// try expect(!match("\\**", "a")); -// try expect(!match("\\**", "a/*")); -// try expect(!match("\\**", "abc")); -// try expect(!match("\\**", "abd")); -// try expect(!match("\\**", "abe")); -// try expect(!match("\\**", "b")); -// try expect(!match("\\**", "bb")); -// try expect(!match("\\**", "bcd")); -// try expect(!match("\\**", "bdir/")); -// try expect(!match("\\**", "Beware")); -// try expect(!match("\\**", "c")); -// try expect(!match("\\**", "ca")); -// try expect(!match("\\**", "cb")); -// try expect(!match("\\**", "d")); -// try expect(!match("\\**", "dd")); -// try expect(!match("\\**", "de")); -// } - -// test "bash classes" { -// try expect(!match("a*[^c]", "*")); -// try expect(!match("a*[^c]", "**")); -// try expect(!match("a*[^c]", "\\*")); -// try expect(!match("a*[^c]", "a")); -// try expect(!match("a*[^c]", "a/*")); -// try expect(!match("a*[^c]", "abc")); -// try expect(match("a*[^c]", "abd")); -// try expect(match("a*[^c]", "abe")); -// try expect(!match("a*[^c]", "b")); -// try expect(!match("a*[^c]", "bb")); -// try expect(!match("a*[^c]", "bcd")); -// try expect(!match("a*[^c]", "bdir/")); -// try expect(!match("a*[^c]", "Beware")); -// try expect(!match("a*[^c]", "c")); -// try expect(!match("a*[^c]", "ca")); -// try expect(!match("a*[^c]", "cb")); -// try expect(!match("a*[^c]", "d")); -// try expect(!match("a*[^c]", "dd")); -// try expect(!match("a*[^c]", "de")); -// try expect(!match("a*[^c]", "baz")); -// try expect(!match("a*[^c]", "bzz")); -// try expect(!match("a*[^c]", "BZZ")); -// try expect(!match("a*[^c]", "beware")); -// try expect(!match("a*[^c]", "BewAre")); - -// try expect(match("a[X-]b", "a-b")); -// try expect(match("a[X-]b", "aXb")); - -// try expect(!match("[a-y]*[^c]", "*")); -// try expect(match("[a-y]*[^c]", "a*")); -// try expect(!match("[a-y]*[^c]", "**")); -// try expect(!match("[a-y]*[^c]", "\\*")); -// try expect(!match("[a-y]*[^c]", "a")); -// try expect(match("[a-y]*[^c]", "a123b")); -// try expect(!match("[a-y]*[^c]", "a123c")); -// try expect(match("[a-y]*[^c]", "ab")); -// try expect(!match("[a-y]*[^c]", "a/*")); -// try expect(!match("[a-y]*[^c]", "abc")); -// try expect(match("[a-y]*[^c]", "abd")); -// try expect(match("[a-y]*[^c]", "abe")); -// try expect(!match("[a-y]*[^c]", "b")); -// try expect(match("[a-y]*[^c]", "bd")); -// try expect(match("[a-y]*[^c]", "bb")); -// try expect(match("[a-y]*[^c]", "bcd")); -// try expect(match("[a-y]*[^c]", "bdir/")); -// try expect(!match("[a-y]*[^c]", "Beware")); -// try expect(!match("[a-y]*[^c]", "c")); -// try expect(match("[a-y]*[^c]", "ca")); -// try expect(match("[a-y]*[^c]", "cb")); -// try expect(!match("[a-y]*[^c]", "d")); -// try expect(match("[a-y]*[^c]", "dd")); -// try expect(match("[a-y]*[^c]", "dd")); -// try expect(match("[a-y]*[^c]", "dd")); -// try expect(match("[a-y]*[^c]", "de")); -// try expect(match("[a-y]*[^c]", "baz")); -// try expect(match("[a-y]*[^c]", "bzz")); -// try expect(match("[a-y]*[^c]", "bzz")); -// // assert(!isMatch('bzz', '[a-y]*[^c]', { regex: true })); -// try expect(!match("[a-y]*[^c]", "BZZ")); -// try expect(match("[a-y]*[^c]", "beware")); -// try expect(!match("[a-y]*[^c]", "BewAre")); - -// try expect(match("a\\*b/*", "a*b/ooo")); -// try expect(match("a\\*?/*", "a*b/ooo")); - -// try expect(!match("a[b]c", "*")); -// try expect(!match("a[b]c", "**")); -// try expect(!match("a[b]c", "\\*")); -// try expect(!match("a[b]c", "a")); -// try expect(!match("a[b]c", "a/*")); -// try expect(match("a[b]c", "abc")); -// try expect(!match("a[b]c", "abd")); -// try expect(!match("a[b]c", "abe")); -// try expect(!match("a[b]c", "b")); -// try expect(!match("a[b]c", "bb")); -// try expect(!match("a[b]c", "bcd")); -// try expect(!match("a[b]c", "bdir/")); -// try expect(!match("a[b]c", "Beware")); -// try expect(!match("a[b]c", "c")); -// try expect(!match("a[b]c", "ca")); -// try expect(!match("a[b]c", "cb")); -// try expect(!match("a[b]c", "d")); -// try expect(!match("a[b]c", "dd")); -// try expect(!match("a[b]c", "de")); -// try expect(!match("a[b]c", "baz")); -// try expect(!match("a[b]c", "bzz")); -// try expect(!match("a[b]c", "BZZ")); -// try expect(!match("a[b]c", "beware")); -// try expect(!match("a[b]c", "BewAre")); - -// try expect(!match("a[\"b\"]c", "*")); -// try expect(!match("a[\"b\"]c", "**")); -// try expect(!match("a[\"b\"]c", "\\*")); -// try expect(!match("a[\"b\"]c", "a")); -// try expect(!match("a[\"b\"]c", "a/*")); -// try expect(match("a[\"b\"]c", "abc")); -// try expect(!match("a[\"b\"]c", "abd")); -// try expect(!match("a[\"b\"]c", "abe")); -// try expect(!match("a[\"b\"]c", "b")); -// try expect(!match("a[\"b\"]c", "bb")); -// try expect(!match("a[\"b\"]c", "bcd")); -// try expect(!match("a[\"b\"]c", "bdir/")); -// try expect(!match("a[\"b\"]c", "Beware")); -// try expect(!match("a[\"b\"]c", "c")); -// try expect(!match("a[\"b\"]c", "ca")); -// try expect(!match("a[\"b\"]c", "cb")); -// try expect(!match("a[\"b\"]c", "d")); -// try expect(!match("a[\"b\"]c", "dd")); -// try expect(!match("a[\"b\"]c", "de")); -// try expect(!match("a[\"b\"]c", "baz")); -// try expect(!match("a[\"b\"]c", "bzz")); -// try expect(!match("a[\"b\"]c", "BZZ")); -// try expect(!match("a[\"b\"]c", "beware")); -// try expect(!match("a[\"b\"]c", "BewAre")); - -// try expect(!match("a[\\\\b]c", "*")); -// try expect(!match("a[\\\\b]c", "**")); -// try expect(!match("a[\\\\b]c", "\\*")); -// try expect(!match("a[\\\\b]c", "a")); -// try expect(!match("a[\\\\b]c", "a/*")); -// try expect(match("a[\\\\b]c", "abc")); -// try expect(!match("a[\\\\b]c", "abd")); -// try expect(!match("a[\\\\b]c", "abe")); -// try expect(!match("a[\\\\b]c", "b")); -// try expect(!match("a[\\\\b]c", "bb")); -// try expect(!match("a[\\\\b]c", "bcd")); -// try expect(!match("a[\\\\b]c", "bdir/")); -// try expect(!match("a[\\\\b]c", "Beware")); -// try expect(!match("a[\\\\b]c", "c")); -// try expect(!match("a[\\\\b]c", "ca")); -// try expect(!match("a[\\\\b]c", "cb")); -// try expect(!match("a[\\\\b]c", "d")); -// try expect(!match("a[\\\\b]c", "dd")); -// try expect(!match("a[\\\\b]c", "de")); -// try expect(!match("a[\\\\b]c", "baz")); -// try expect(!match("a[\\\\b]c", "bzz")); -// try expect(!match("a[\\\\b]c", "BZZ")); -// try expect(!match("a[\\\\b]c", "beware")); -// try expect(!match("a[\\\\b]c", "BewAre")); - -// try expect(!match("a[\\b]c", "*")); -// try expect(!match("a[\\b]c", "**")); -// try expect(!match("a[\\b]c", "\\*")); -// try expect(!match("a[\\b]c", "a")); -// try expect(!match("a[\\b]c", "a/*")); -// try expect(!match("a[\\b]c", "abc")); -// try expect(!match("a[\\b]c", "abd")); -// try expect(!match("a[\\b]c", "abe")); -// try expect(!match("a[\\b]c", "b")); -// try expect(!match("a[\\b]c", "bb")); -// try expect(!match("a[\\b]c", "bcd")); -// try expect(!match("a[\\b]c", "bdir/")); -// try expect(!match("a[\\b]c", "Beware")); -// try expect(!match("a[\\b]c", "c")); -// try expect(!match("a[\\b]c", "ca")); -// try expect(!match("a[\\b]c", "cb")); -// try expect(!match("a[\\b]c", "d")); -// try expect(!match("a[\\b]c", "dd")); -// try expect(!match("a[\\b]c", "de")); -// try expect(!match("a[\\b]c", "baz")); -// try expect(!match("a[\\b]c", "bzz")); -// try expect(!match("a[\\b]c", "BZZ")); -// try expect(!match("a[\\b]c", "beware")); -// try expect(!match("a[\\b]c", "BewAre")); - -// try expect(!match("a[b-d]c", "*")); -// try expect(!match("a[b-d]c", "**")); -// try expect(!match("a[b-d]c", "\\*")); -// try expect(!match("a[b-d]c", "a")); -// try expect(!match("a[b-d]c", "a/*")); -// try expect(match("a[b-d]c", "abc")); -// try expect(!match("a[b-d]c", "abd")); -// try expect(!match("a[b-d]c", "abe")); -// try expect(!match("a[b-d]c", "b")); -// try expect(!match("a[b-d]c", "bb")); -// try expect(!match("a[b-d]c", "bcd")); -// try expect(!match("a[b-d]c", "bdir/")); -// try expect(!match("a[b-d]c", "Beware")); -// try expect(!match("a[b-d]c", "c")); -// try expect(!match("a[b-d]c", "ca")); -// try expect(!match("a[b-d]c", "cb")); -// try expect(!match("a[b-d]c", "d")); -// try expect(!match("a[b-d]c", "dd")); -// try expect(!match("a[b-d]c", "de")); -// try expect(!match("a[b-d]c", "baz")); -// try expect(!match("a[b-d]c", "bzz")); -// try expect(!match("a[b-d]c", "BZZ")); -// try expect(!match("a[b-d]c", "beware")); -// try expect(!match("a[b-d]c", "BewAre")); - -// try expect(!match("a?c", "*")); -// try expect(!match("a?c", "**")); -// try expect(!match("a?c", "\\*")); -// try expect(!match("a?c", "a")); -// try expect(!match("a?c", "a/*")); -// try expect(match("a?c", "abc")); -// try expect(!match("a?c", "abd")); -// try expect(!match("a?c", "abe")); -// try expect(!match("a?c", "b")); -// try expect(!match("a?c", "bb")); -// try expect(!match("a?c", "bcd")); -// try expect(!match("a?c", "bdir/")); -// try expect(!match("a?c", "Beware")); -// try expect(!match("a?c", "c")); -// try expect(!match("a?c", "ca")); -// try expect(!match("a?c", "cb")); -// try expect(!match("a?c", "d")); -// try expect(!match("a?c", "dd")); -// try expect(!match("a?c", "de")); -// try expect(!match("a?c", "baz")); -// try expect(!match("a?c", "bzz")); -// try expect(!match("a?c", "BZZ")); -// try expect(!match("a?c", "beware")); -// try expect(!match("a?c", "BewAre")); - -// try expect(match("*/man*/bash.*", "man/man1/bash.1")); - -// try expect(match("[^a-c]*", "*")); -// try expect(match("[^a-c]*", "**")); -// try expect(!match("[^a-c]*", "a")); -// try expect(!match("[^a-c]*", "a/*")); -// try expect(!match("[^a-c]*", "abc")); -// try expect(!match("[^a-c]*", "abd")); -// try expect(!match("[^a-c]*", "abe")); -// try expect(!match("[^a-c]*", "b")); -// try expect(!match("[^a-c]*", "bb")); -// try expect(!match("[^a-c]*", "bcd")); -// try expect(!match("[^a-c]*", "bdir/")); -// try expect(match("[^a-c]*", "Beware")); -// try expect(match("[^a-c]*", "Beware")); -// try expect(!match("[^a-c]*", "c")); -// try expect(!match("[^a-c]*", "ca")); -// try expect(!match("[^a-c]*", "cb")); -// try expect(match("[^a-c]*", "d")); -// try expect(match("[^a-c]*", "dd")); -// try expect(match("[^a-c]*", "de")); -// try expect(!match("[^a-c]*", "baz")); -// try expect(!match("[^a-c]*", "bzz")); -// try expect(match("[^a-c]*", "BZZ")); -// try expect(!match("[^a-c]*", "beware")); -// try expect(match("[^a-c]*", "BewAre")); -// } - -// test "bash wildmatch" { -// try expect(!match("a[]-]b", "aab")); -// try expect(!match("[ten]", "ten")); -// try expect(match("]", "]")); -// try expect(match("a[]-]b", "a-b")); -// try expect(match("a[]-]b", "a]b")); -// try expect(match("a[]]b", "a]b")); -// try expect(match("a[\\]a\\-]b", "aab")); -// try expect(match("t[a-g]n", "ten")); -// try expect(match("t[^a-g]n", "ton")); -// } - -// test "bash slashmatch" { -// // try expect(!match("f[^eiu][^eiu][^eiu][^eiu][^eiu]r", "foo/bar")); -// try expect(match("foo[/]bar", "foo/bar")); -// try expect(match("f[^eiu][^eiu][^eiu][^eiu][^eiu]r", "foo-bar")); -// } - -// test "bash extra_stars" { -// try expect(!match("a**c", "bbc")); -// try expect(match("a**c", "abc")); -// try expect(!match("a**c", "bbd")); - -// try expect(!match("a***c", "bbc")); -// try expect(match("a***c", "abc")); -// try expect(!match("a***c", "bbd")); - -// try expect(!match("a*****?c", "bbc")); -// try expect(match("a*****?c", "abc")); -// try expect(!match("a*****?c", "bbc")); - -// try expect(match("?*****??", "bbc")); -// try expect(match("?*****??", "abc")); - -// try expect(match("*****??", "bbc")); -// try expect(match("*****??", "abc")); - -// try expect(match("?*****?c", "bbc")); -// try expect(match("?*****?c", "abc")); - -// try expect(match("?***?****c", "bbc")); -// try expect(match("?***?****c", "abc")); -// try expect(!match("?***?****c", "bbd")); - -// try expect(match("?***?****?", "bbc")); -// try expect(match("?***?****?", "abc")); - -// try expect(match("?***?****", "bbc")); -// try expect(match("?***?****", "abc")); - -// try expect(match("*******c", "bbc")); -// try expect(match("*******c", "abc")); - -// try expect(match("*******?", "bbc")); -// try expect(match("*******?", "abc")); - -// try expect(match("a*cd**?**??k", "abcdecdhjk")); -// try expect(match("a**?**cd**?**??k", "abcdecdhjk")); -// try expect(match("a**?**cd**?**??k***", "abcdecdhjk")); -// try expect(match("a**?**cd**?**??***k", "abcdecdhjk")); -// try expect(match("a**?**cd**?**??***k**", "abcdecdhjk")); -// try expect(match("a****c**?**??*****", "abcdecdhjk")); -// } - -// test "stars" { -// try expect(!match("*.js", "a/b/c/z.js")); -// try expect(!match("*.js", "a/b/z.js")); -// try expect(!match("*.js", "a/z.js")); -// try expect(match("*.js", "z.js")); - -// // try expect(!match("*/*", "a/.ab")); -// // try expect(!match("*", ".ab")); - -// try expect(match("z*.js", "z.js")); -// try expect(match("*/*", "a/z")); -// try expect(match("*/z*.js", "a/z.js")); -// try expect(match("a/z*.js", "a/z.js")); - -// try expect(match("*", "ab")); -// try expect(match("*", "abc")); - -// try expect(!match("f*", "bar")); -// try expect(!match("*r", "foo")); -// try expect(!match("b*", "foo")); -// try expect(!match("*", "foo/bar")); -// try expect(match("*c", "abc")); -// try expect(match("a*", "abc")); -// try expect(match("a*c", "abc")); -// try expect(match("*r", "bar")); -// try expect(match("b*", "bar")); -// try expect(match("f*", "foo")); - -// try expect(match("*abc*", "one abc two")); -// try expect(match("a*b", "a b")); - -// try expect(!match("*a*", "foo")); -// try expect(match("*a*", "bar")); -// try expect(match("*abc*", "oneabctwo")); -// try expect(!match("*-bc-*", "a-b.c-d")); -// try expect(match("*-*.*-*", "a-b.c-d")); -// try expect(match("*-b*c-*", "a-b.c-d")); -// try expect(match("*-b.c-*", "a-b.c-d")); -// try expect(match("*.*", "a-b.c-d")); -// try expect(match("*.*-*", "a-b.c-d")); -// try expect(match("*.*-d", "a-b.c-d")); -// try expect(match("*.c-*", "a-b.c-d")); -// try expect(match("*b.*d", "a-b.c-d")); -// try expect(match("a*.c*", "a-b.c-d")); -// try expect(match("a-*.*-d", "a-b.c-d")); -// try expect(match("*.*", "a.b")); -// try expect(match("*.b", "a.b")); -// try expect(match("a.*", "a.b")); -// try expect(match("a.b", "a.b")); - -// try expect(!match("**-bc-**", "a-b.c-d")); -// try expect(match("**-**.**-**", "a-b.c-d")); -// try expect(match("**-b**c-**", "a-b.c-d")); -// try expect(match("**-b.c-**", "a-b.c-d")); -// try expect(match("**.**", "a-b.c-d")); -// try expect(match("**.**-**", "a-b.c-d")); -// try expect(match("**.**-d", "a-b.c-d")); -// try expect(match("**.c-**", "a-b.c-d")); -// try expect(match("**b.**d", "a-b.c-d")); -// try expect(match("a**.c**", "a-b.c-d")); -// try expect(match("a-**.**-d", "a-b.c-d")); -// try expect(match("**.**", "a.b")); -// try expect(match("**.b", "a.b")); -// try expect(match("a.**", "a.b")); -// try expect(match("a.b", "a.b")); - -// try expect(match("*/*", "/ab")); -// try expect(match(".", ".")); -// try expect(!match("a/", "a/.b")); -// try expect(match("/*", "/ab")); -// try expect(match("/??", "/ab")); -// try expect(match("/?b", "/ab")); -// try expect(match("/*", "/cd")); -// try expect(match("a", "a")); -// try expect(match("a/.*", "a/.b")); -// try expect(match("?/?", "a/b")); -// try expect(match("a/**/j/**/z/*.md", "a/b/c/d/e/j/n/p/o/z/c.md")); -// try expect(match("a/**/z/*.md", "a/b/c/d/e/z/c.md")); -// try expect(match("a/b/c/*.md", "a/b/c/xyz.md")); -// try expect(match("a/b/c/*.md", "a/b/c/xyz.md")); -// try expect(match("a/*/z/.a", "a/b/z/.a")); -// try expect(!match("bz", "a/b/z/.a")); -// try expect(match("a/**/c/*.md", "a/bb.bb/aa/b.b/aa/c/xyz.md")); -// try expect(match("a/**/c/*.md", "a/bb.bb/aa/bb/aa/c/xyz.md")); -// try expect(match("a/*/c/*.md", "a/bb.bb/c/xyz.md")); -// try expect(match("a/*/c/*.md", "a/bb/c/xyz.md")); -// try expect(match("a/*/c/*.md", "a/bbbb/c/xyz.md")); -// try expect(match("*", "aaa")); -// try expect(match("*", "ab")); -// try expect(match("ab", "ab")); - -// try expect(!match("*/*/*", "aaa")); -// try expect(!match("*/*/*", "aaa/bb/aa/rr")); -// try expect(!match("aaa*", "aaa/bba/ccc")); -// // try expect(!match("aaa**", "aaa/bba/ccc")); -// try expect(!match("aaa/*", "aaa/bba/ccc")); -// try expect(!match("aaa/*ccc", "aaa/bba/ccc")); -// try expect(!match("aaa/*z", "aaa/bba/ccc")); -// try expect(!match("*/*/*", "aaa/bbb")); -// try expect(!match("*/*jk*/*i", "ab/zzz/ejkl/hi")); -// try expect(match("*/*/*", "aaa/bba/ccc")); -// try expect(match("aaa/**", "aaa/bba/ccc")); -// try expect(match("aaa/*", "aaa/bbb")); -// try expect(match("*/*z*/*/*i", "ab/zzz/ejkl/hi")); -// try expect(match("*j*i", "abzzzejklhi")); - -// try expect(match("*", "a")); -// try expect(match("*", "b")); -// try expect(!match("*", "a/a")); -// try expect(!match("*", "a/a/a")); -// try expect(!match("*", "a/a/b")); -// try expect(!match("*", "a/a/a/a")); -// try expect(!match("*", "a/a/a/a/a")); - -// try expect(!match("*/*", "a")); -// try expect(match("*/*", "a/a")); -// try expect(!match("*/*", "a/a/a")); - -// try expect(!match("*/*/*", "a")); -// try expect(!match("*/*/*", "a/a")); -// try expect(match("*/*/*", "a/a/a")); -// try expect(!match("*/*/*", "a/a/a/a")); - -// try expect(!match("*/*/*/*", "a")); -// try expect(!match("*/*/*/*", "a/a")); -// try expect(!match("*/*/*/*", "a/a/a")); -// try expect(match("*/*/*/*", "a/a/a/a")); -// try expect(!match("*/*/*/*", "a/a/a/a/a")); - -// try expect(!match("*/*/*/*/*", "a")); -// try expect(!match("*/*/*/*/*", "a/a")); -// try expect(!match("*/*/*/*/*", "a/a/a")); -// try expect(!match("*/*/*/*/*", "a/a/b")); -// try expect(!match("*/*/*/*/*", "a/a/a/a")); -// try expect(match("*/*/*/*/*", "a/a/a/a/a")); -// try expect(!match("*/*/*/*/*", "a/a/a/a/a/a")); - -// try expect(!match("a/*", "a")); -// try expect(match("a/*", "a/a")); -// try expect(!match("a/*", "a/a/a")); -// try expect(!match("a/*", "a/a/a/a")); -// try expect(!match("a/*", "a/a/a/a/a")); - -// try expect(!match("a/*/*", "a")); -// try expect(!match("a/*/*", "a/a")); -// try expect(match("a/*/*", "a/a/a")); -// try expect(!match("a/*/*", "b/a/a")); -// try expect(!match("a/*/*", "a/a/a/a")); -// try expect(!match("a/*/*", "a/a/a/a/a")); - -// try expect(!match("a/*/*/*", "a")); -// try expect(!match("a/*/*/*", "a/a")); -// try expect(!match("a/*/*/*", "a/a/a")); -// try expect(match("a/*/*/*", "a/a/a/a")); -// try expect(!match("a/*/*/*", "a/a/a/a/a")); - -// try expect(!match("a/*/*/*/*", "a")); -// try expect(!match("a/*/*/*/*", "a/a")); -// try expect(!match("a/*/*/*/*", "a/a/a")); -// try expect(!match("a/*/*/*/*", "a/a/b")); -// try expect(!match("a/*/*/*/*", "a/a/a/a")); -// try expect(match("a/*/*/*/*", "a/a/a/a/a")); - -// try expect(!match("a/*/a", "a")); -// try expect(!match("a/*/a", "a/a")); -// try expect(match("a/*/a", "a/a/a")); -// try expect(!match("a/*/a", "a/a/b")); -// try expect(!match("a/*/a", "a/a/a/a")); -// try expect(!match("a/*/a", "a/a/a/a/a")); - -// try expect(!match("a/*/b", "a")); -// try expect(!match("a/*/b", "a/a")); -// try expect(!match("a/*/b", "a/a/a")); -// try expect(match("a/*/b", "a/a/b")); -// try expect(!match("a/*/b", "a/a/a/a")); -// try expect(!match("a/*/b", "a/a/a/a/a")); - -// try expect(!match("*/**/a", "a")); -// try expect(!match("*/**/a", "a/a/b")); -// try expect(match("*/**/a", "a/a")); -// try expect(match("*/**/a", "a/a/a")); -// try expect(match("*/**/a", "a/a/a/a")); -// try expect(match("*/**/a", "a/a/a/a/a")); - -// try expect(!match("*/", "a")); -// try expect(!match("*/*", "a")); -// try expect(!match("a/*", "a")); -// // try expect(!match("*/*", "a/")); -// // try expect(!match("a/*", "a/")); -// try expect(!match("*", "a/a")); -// try expect(!match("*/", "a/a")); -// try expect(!match("*/", "a/x/y")); -// try expect(!match("*/*", "a/x/y")); -// try expect(!match("a/*", "a/x/y")); -// // try expect(match("*", "a/")); -// try expect(match("*", "a")); -// try expect(match("*/", "a/")); -// try expect(match("*{,/}", "a/")); -// try expect(match("*/*", "a/a")); -// try expect(match("a/*", "a/a")); - -// try expect(!match("a/**/*.txt", "a.txt")); -// try expect(match("a/**/*.txt", "a/x/y.txt")); -// try expect(!match("a/**/*.txt", "a/x/y/z")); - -// try expect(!match("a/*.txt", "a.txt")); -// try expect(match("a/*.txt", "a/b.txt")); -// try expect(!match("a/*.txt", "a/x/y.txt")); -// try expect(!match("a/*.txt", "a/x/y/z")); - -// try expect(match("a*.txt", "a.txt")); -// try expect(!match("a*.txt", "a/b.txt")); -// try expect(!match("a*.txt", "a/x/y.txt")); -// try expect(!match("a*.txt", "a/x/y/z")); - -// try expect(match("*.txt", "a.txt")); -// try expect(!match("*.txt", "a/b.txt")); -// try expect(!match("*.txt", "a/x/y.txt")); -// try expect(!match("*.txt", "a/x/y/z")); - -// try expect(!match("a*", "a/b")); -// try expect(!match("a/**/b", "a/a/bb")); -// try expect(!match("a/**/b", "a/bb")); - -// try expect(!match("*/**", "foo")); -// try expect(!match("**/", "foo/bar")); -// try expect(!match("**/*/", "foo/bar")); -// try expect(!match("*/*/", "foo/bar")); - -// try expect(match("**/..", "/home/foo/..")); -// try expect(match("**/a", "a")); -// try expect(match("**", "a/a")); -// try expect(match("a/**", "a/a")); -// try expect(match("a/**", "a/")); -// // try expect(match("a/**", "a")); -// try expect(!match("**/", "a/a")); -// // try expect(match("**/a/**", "a")); -// // try expect(match("a/**", "a")); -// try expect(!match("**/", "a/a")); -// try expect(match("*/**/a", "a/a")); -// // try expect(match("a/**", "a")); -// try expect(match("*/**", "foo/")); -// try expect(match("**/*", "foo/bar")); -// try expect(match("*/*", "foo/bar")); -// try expect(match("*/**", "foo/bar")); -// try expect(match("**/", "foo/bar/")); -// // try expect(match("**/*", "foo/bar/")); -// try expect(match("**/*/", "foo/bar/")); -// try expect(match("*/**", "foo/bar/")); -// try expect(match("*/*/", "foo/bar/")); - -// try expect(!match("*/foo", "bar/baz/foo")); -// try expect(!match("**/bar/*", "deep/foo/bar")); -// try expect(!match("*/bar/**", "deep/foo/bar/baz/x")); -// try expect(!match("/*", "ef")); -// try expect(!match("foo?bar", "foo/bar")); -// try expect(!match("**/bar*", "foo/bar/baz")); -// // try expect(!match("**/bar**", "foo/bar/baz")); -// try expect(!match("foo**bar", "foo/baz/bar")); -// try expect(!match("foo*bar", "foo/baz/bar")); -// // try expect(match("foo/**", "foo")); -// try expect(match("/*", "/ab")); -// try expect(match("/*", "/cd")); -// try expect(match("/*", "/ef")); -// try expect(match("a/**/j/**/z/*.md", "a/b/j/c/z/x.md")); -// try expect(match("a/**/j/**/z/*.md", "a/j/z/x.md")); - -// try expect(match("**/foo", "bar/baz/foo")); -// try expect(match("**/bar/*", "deep/foo/bar/baz")); -// try expect(match("**/bar/**", "deep/foo/bar/baz/")); -// try expect(match("**/bar/*/*", "deep/foo/bar/baz/x")); -// try expect(match("foo/**/**/bar", "foo/b/a/z/bar")); -// try expect(match("foo/**/bar", "foo/b/a/z/bar")); -// try expect(match("foo/**/**/bar", "foo/bar")); -// try expect(match("foo/**/bar", "foo/bar")); -// try expect(match("*/bar/**", "foo/bar/baz/x")); -// try expect(match("foo/**/**/bar", "foo/baz/bar")); -// try expect(match("foo/**/bar", "foo/baz/bar")); -// try expect(match("**/foo", "XXX/foo")); -// } - -// test "globstars" { -// try expect(match("**/*.js", "a/b/c/d.js")); -// try expect(match("**/*.js", "a/b/c.js")); -// try expect(match("**/*.js", "a/b.js")); -// try expect(match("a/b/**/*.js", "a/b/c/d/e/f.js")); -// try expect(match("a/b/**/*.js", "a/b/c/d/e.js")); -// try expect(match("a/b/c/**/*.js", "a/b/c/d.js")); -// try expect(match("a/b/**/*.js", "a/b/c/d.js")); -// try expect(match("a/b/**/*.js", "a/b/d.js")); -// try expect(!match("a/b/**/*.js", "a/d.js")); -// try expect(!match("a/b/**/*.js", "d.js")); - -// try expect(!match("**c", "a/b/c")); -// try expect(!match("a/**c", "a/b/c")); -// try expect(!match("a/**z", "a/b/c")); -// try expect(!match("a/**b**/c", "a/b/c/b/c")); -// try expect(!match("a/b/c**/*.js", "a/b/c/d/e.js")); -// try expect(match("a/**/b/**/c", "a/b/c/b/c")); -// try expect(match("a/**b**/c", "a/aba/c")); -// try expect(match("a/**b**/c", "a/b/c")); -// try expect(match("a/b/c**/*.js", "a/b/c/d.js")); - -// try expect(!match("a/**/*", "a")); -// try expect(!match("a/**/**/*", "a")); -// try expect(!match("a/**/**/**/*", "a")); -// try expect(!match("**/a", "a/")); -// try expect(!match("a/**/*", "a/")); -// try expect(!match("a/**/**/*", "a/")); -// try expect(!match("a/**/**/**/*", "a/")); -// try expect(!match("**/a", "a/b")); -// try expect(!match("a/**/j/**/z/*.md", "a/b/c/j/e/z/c.txt")); -// try expect(!match("a/**/b", "a/bb")); -// try expect(!match("**/a", "a/c")); -// try expect(!match("**/a", "a/b")); -// try expect(!match("**/a", "a/x/y")); -// try expect(!match("**/a", "a/b/c/d")); -// try expect(match("**", "a")); -// try expect(match("**/a", "a")); -// // try expect(match("a/**", "a")); -// try expect(match("**", "a/")); -// try expect(match("**/a/**", "a/")); -// try expect(match("a/**", "a/")); -// try expect(match("a/**/**", "a/")); -// try expect(match("**/a", "a/a")); -// try expect(match("**", "a/b")); -// try expect(match("*/*", "a/b")); -// try expect(match("a/**", "a/b")); -// try expect(match("a/**/*", "a/b")); -// try expect(match("a/**/**/*", "a/b")); -// try expect(match("a/**/**/**/*", "a/b")); -// try expect(match("a/**/b", "a/b")); -// try expect(match("**", "a/b/c")); -// try expect(match("**/*", "a/b/c")); -// try expect(match("**/**", "a/b/c")); -// try expect(match("*/**", "a/b/c")); -// try expect(match("a/**", "a/b/c")); -// try expect(match("a/**/*", "a/b/c")); -// try expect(match("a/**/**/*", "a/b/c")); -// try expect(match("a/**/**/**/*", "a/b/c")); -// try expect(match("**", "a/b/c/d")); -// try expect(match("a/**", "a/b/c/d")); -// try expect(match("a/**/*", "a/b/c/d")); -// try expect(match("a/**/**/*", "a/b/c/d")); -// try expect(match("a/**/**/**/*", "a/b/c/d")); -// try expect(match("a/b/**/c/**/*.*", "a/b/c/d.e")); -// try expect(match("a/**/f/*.md", "a/b/c/d/e/f/g.md")); -// try expect(match("a/**/f/**/k/*.md", "a/b/c/d/e/f/g/h/i/j/k/l.md")); -// try expect(match("a/b/c/*.md", "a/b/c/def.md")); -// try expect(match("a/*/c/*.md", "a/bb.bb/c/ddd.md")); -// try expect(match("a/**/f/*.md", "a/bb.bb/cc/d.d/ee/f/ggg.md")); -// try expect(match("a/**/f/*.md", "a/bb.bb/cc/dd/ee/f/ggg.md")); -// try expect(match("a/*/c/*.md", "a/bb/c/ddd.md")); -// try expect(match("a/*/c/*.md", "a/bbbb/c/ddd.md")); - -// try expect(match("foo/bar/**/one/**/*.*", "foo/bar/baz/one/image.png")); -// try expect(match("foo/bar/**/one/**/*.*", "foo/bar/baz/one/two/image.png")); -// try expect(match("foo/bar/**/one/**/*.*", "foo/bar/baz/one/two/three/image.png")); -// try expect(!match("a/b/**/f", "a/b/c/d/")); -// // try expect(match("a/**", "a")); -// try expect(match("**", "a")); -// // try expect(match("a{,/**}", "a")); -// try expect(match("**", "a/")); -// try expect(match("a/**", "a/")); -// try expect(match("**", "a/b/c/d")); -// try expect(match("**", "a/b/c/d/")); -// try expect(match("**/**", "a/b/c/d/")); -// try expect(match("**/b/**", "a/b/c/d/")); -// try expect(match("a/b/**", "a/b/c/d/")); -// try expect(match("a/b/**/", "a/b/c/d/")); -// try expect(match("a/b/**/c/**/", "a/b/c/d/")); -// try expect(match("a/b/**/c/**/d/", "a/b/c/d/")); -// try expect(match("a/b/**/**/*.*", "a/b/c/d/e.f")); -// try expect(match("a/b/**/*.*", "a/b/c/d/e.f")); -// try expect(match("a/b/**/c/**/d/*.*", "a/b/c/d/e.f")); -// try expect(match("a/b/**/d/**/*.*", "a/b/c/d/e.f")); -// try expect(match("a/b/**/d/**/*.*", "a/b/c/d/g/e.f")); -// try expect(match("a/b/**/d/**/*.*", "a/b/c/d/g/g/e.f")); -// try expect(match("a/b-*/**/z.js", "a/b-c/z.js")); -// try expect(match("a/b-*/**/z.js", "a/b-c/d/e/z.js")); - -// try expect(match("*/*", "a/b")); -// try expect(match("a/b/c/*.md", "a/b/c/xyz.md")); -// try expect(match("a/*/c/*.md", "a/bb.bb/c/xyz.md")); -// try expect(match("a/*/c/*.md", "a/bb/c/xyz.md")); -// try expect(match("a/*/c/*.md", "a/bbbb/c/xyz.md")); - -// try expect(match("**/*", "a/b/c")); -// try expect(match("**/**", "a/b/c")); -// try expect(match("*/**", "a/b/c")); -// try expect(match("a/**/j/**/z/*.md", "a/b/c/d/e/j/n/p/o/z/c.md")); -// try expect(match("a/**/z/*.md", "a/b/c/d/e/z/c.md")); -// try expect(match("a/**/c/*.md", "a/bb.bb/aa/b.b/aa/c/xyz.md")); -// try expect(match("a/**/c/*.md", "a/bb.bb/aa/bb/aa/c/xyz.md")); -// try expect(!match("a/**/j/**/z/*.md", "a/b/c/j/e/z/c.txt")); -// try expect(!match("a/b/**/c{d,e}/**/xyz.md", "a/b/c/xyz.md")); -// try expect(!match("a/b/**/c{d,e}/**/xyz.md", "a/b/d/xyz.md")); -// try expect(!match("a/**/", "a/b")); -// // try expect(!match("**/*", "a/b/.js/c.txt")); -// try expect(!match("a/**/", "a/b/c/d")); -// try expect(!match("a/**/", "a/bb")); -// try expect(!match("a/**/", "a/cb")); -// try expect(match("/**", "/a/b")); -// try expect(match("**/*", "a.b")); -// try expect(match("**/*", "a.js")); -// try expect(match("**/*.js", "a.js")); -// // try expect(match("a/**/", "a/")); -// try expect(match("**/*.js", "a/a.js")); -// try expect(match("**/*.js", "a/a/b.js")); -// try expect(match("a/**/b", "a/b")); -// try expect(match("a/**b", "a/b")); -// try expect(match("**/*.md", "a/b.md")); -// try expect(match("**/*", "a/b/c.js")); -// try expect(match("**/*", "a/b/c.txt")); -// try expect(match("a/**/", "a/b/c/d/")); -// try expect(match("**/*", "a/b/c/d/a.js")); -// try expect(match("a/b/**/*.js", "a/b/c/z.js")); -// try expect(match("a/b/**/*.js", "a/b/z.js")); -// try expect(match("**/*", "ab")); -// try expect(match("**/*", "ab/c")); -// try expect(match("**/*", "ab/c/d")); -// try expect(match("**/*", "abc.js")); - -// try expect(!match("**/", "a")); -// try expect(!match("**/a/*", "a")); -// try expect(!match("**/a/*/*", "a")); -// try expect(!match("*/a/**", "a")); -// try expect(!match("a/**/*", "a")); -// try expect(!match("a/**/**/*", "a")); -// try expect(!match("**/", "a/b")); -// try expect(!match("**/b/*", "a/b")); -// try expect(!match("**/b/*/*", "a/b")); -// try expect(!match("b/**", "a/b")); -// try expect(!match("**/", "a/b/c")); -// try expect(!match("**/**/b", "a/b/c")); -// try expect(!match("**/b", "a/b/c")); -// try expect(!match("**/b/*/*", "a/b/c")); -// try expect(!match("b/**", "a/b/c")); -// try expect(!match("**/", "a/b/c/d")); -// try expect(!match("**/d/*", "a/b/c/d")); -// try expect(!match("b/**", "a/b/c/d")); -// try expect(match("**", "a")); -// try expect(match("**/**", "a")); -// try expect(match("**/**/*", "a")); -// try expect(match("**/**/a", "a")); -// try expect(match("**/a", "a")); -// // try expect(match("**/a/**", "a")); -// // try expect(match("a/**", "a")); -// try expect(match("**", "a/b")); -// try expect(match("**/**", "a/b")); -// try expect(match("**/**/*", "a/b")); -// try expect(match("**/**/b", "a/b")); -// try expect(match("**/b", "a/b")); -// // try expect(match("**/b/**", "a/b")); -// // try expect(match("*/b/**", "a/b")); -// try expect(match("a/**", "a/b")); -// try expect(match("a/**/*", "a/b")); -// try expect(match("a/**/**/*", "a/b")); -// try expect(match("**", "a/b/c")); -// try expect(match("**/**", "a/b/c")); -// try expect(match("**/**/*", "a/b/c")); -// try expect(match("**/b/*", "a/b/c")); -// try expect(match("**/b/**", "a/b/c")); -// try expect(match("*/b/**", "a/b/c")); -// try expect(match("a/**", "a/b/c")); -// try expect(match("a/**/*", "a/b/c")); -// try expect(match("a/**/**/*", "a/b/c")); -// try expect(match("**", "a/b/c/d")); -// try expect(match("**/**", "a/b/c/d")); -// try expect(match("**/**/*", "a/b/c/d")); -// try expect(match("**/**/d", "a/b/c/d")); -// try expect(match("**/b/**", "a/b/c/d")); -// try expect(match("**/b/*/*", "a/b/c/d")); -// try expect(match("**/d", "a/b/c/d")); -// try expect(match("*/b/**", "a/b/c/d")); -// try expect(match("a/**", "a/b/c/d")); -// try expect(match("a/**/*", "a/b/c/d")); -// try expect(match("a/**/**/*", "a/b/c/d")); -// } - -// test "utf8" { -// try expect(match("フ*/**/*", "フォルダ/aaa.js")); -// try expect(match("フォ*/**/*", "フォルダ/aaa.js")); -// try expect(match("フォル*/**/*", "フォルダ/aaa.js")); -// try expect(match("フ*ル*/**/*", "フォルダ/aaa.js")); -// try expect(match("フォルダ/**/*", "フォルダ/aaa.js")); -// } - -// test "negation" { -// try expect(!match("!*", "abc")); -// try expect(!match("!abc", "abc")); -// try expect(!match("*!.md", "bar.md")); -// try expect(!match("foo!.md", "bar.md")); -// try expect(!match("\\!*!*.md", "foo!.md")); -// try expect(!match("\\!*!*.md", "foo!bar.md")); -// try expect(match("*!*.md", "!foo!.md")); -// try expect(match("\\!*!*.md", "!foo!.md")); -// try expect(match("!*foo", "abc")); -// try expect(match("!foo*", "abc")); -// try expect(match("!xyz", "abc")); -// try expect(match("*!*.*", "ba!r.js")); -// try expect(match("*.md", "bar.md")); -// try expect(match("*!*.*", "foo!.md")); -// try expect(match("*!*.md", "foo!.md")); -// try expect(match("*!.md", "foo!.md")); -// try expect(match("*.md", "foo!.md")); -// try expect(match("foo!.md", "foo!.md")); -// try expect(match("*!*.md", "foo!bar.md")); -// try expect(match("*b*.md", "foobar.md")); - -// try expect(!match("a!!b", "a")); -// try expect(!match("a!!b", "aa")); -// try expect(!match("a!!b", "a/b")); -// try expect(!match("a!!b", "a!b")); -// try expect(match("a!!b", "a!!b")); -// try expect(!match("a!!b", "a/!!/b")); - -// try expect(!match("!a/b", "a/b")); -// try expect(match("!a/b", "a")); -// try expect(match("!a/b", "a.b")); -// try expect(match("!a/b", "a/a")); -// try expect(match("!a/b", "a/c")); -// try expect(match("!a/b", "b/a")); -// try expect(match("!a/b", "b/b")); -// try expect(match("!a/b", "b/c")); - -// try expect(!match("!abc", "abc")); -// try expect(match("!!abc", "abc")); -// try expect(!match("!!!abc", "abc")); -// try expect(match("!!!!abc", "abc")); -// try expect(!match("!!!!!abc", "abc")); -// try expect(match("!!!!!!abc", "abc")); -// try expect(!match("!!!!!!!abc", "abc")); -// try expect(match("!!!!!!!!abc", "abc")); - -// // try expect(!match("!(*/*)", "a/a")); -// // try expect(!match("!(*/*)", "a/b")); -// // try expect(!match("!(*/*)", "a/c")); -// // try expect(!match("!(*/*)", "b/a")); -// // try expect(!match("!(*/*)", "b/b")); -// // try expect(!match("!(*/*)", "b/c")); -// // try expect(!match("!(*/b)", "a/b")); -// // try expect(!match("!(*/b)", "b/b")); -// // try expect(!match("!(a/b)", "a/b")); -// try expect(!match("!*", "a")); -// try expect(!match("!*", "a.b")); -// try expect(!match("!*/*", "a/a")); -// try expect(!match("!*/*", "a/b")); -// try expect(!match("!*/*", "a/c")); -// try expect(!match("!*/*", "b/a")); -// try expect(!match("!*/*", "b/b")); -// try expect(!match("!*/*", "b/c")); -// try expect(!match("!*/b", "a/b")); -// try expect(!match("!*/b", "b/b")); -// try expect(!match("!*/c", "a/c")); -// try expect(!match("!*/c", "a/c")); -// try expect(!match("!*/c", "b/c")); -// try expect(!match("!*/c", "b/c")); -// try expect(!match("!*a*", "bar")); -// try expect(!match("!*a*", "fab")); -// // try expect(!match("!a/(*)", "a/a")); -// // try expect(!match("!a/(*)", "a/b")); -// // try expect(!match("!a/(*)", "a/c")); -// // try expect(!match("!a/(b)", "a/b")); -// try expect(!match("!a/*", "a/a")); -// try expect(!match("!a/*", "a/b")); -// try expect(!match("!a/*", "a/c")); -// try expect(!match("!f*b", "fab")); -// // try expect(match("!(*/*)", "a")); -// // try expect(match("!(*/*)", "a.b")); -// // try expect(match("!(*/b)", "a")); -// // try expect(match("!(*/b)", "a.b")); -// // try expect(match("!(*/b)", "a/a")); -// // try expect(match("!(*/b)", "a/c")); -// // try expect(match("!(*/b)", "b/a")); -// // try expect(match("!(*/b)", "b/c")); -// // try expect(match("!(a/b)", "a")); -// // try expect(match("!(a/b)", "a.b")); -// // try expect(match("!(a/b)", "a/a")); -// // try expect(match("!(a/b)", "a/c")); -// // try expect(match("!(a/b)", "b/a")); -// // try expect(match("!(a/b)", "b/b")); -// // try expect(match("!(a/b)", "b/c")); -// try expect(match("!*", "a/a")); -// try expect(match("!*", "a/b")); -// try expect(match("!*", "a/c")); -// try expect(match("!*", "b/a")); -// try expect(match("!*", "b/b")); -// try expect(match("!*", "b/c")); -// try expect(match("!*/*", "a")); -// try expect(match("!*/*", "a.b")); -// try expect(match("!*/b", "a")); -// try expect(match("!*/b", "a.b")); -// try expect(match("!*/b", "a/a")); -// try expect(match("!*/b", "a/c")); -// try expect(match("!*/b", "b/a")); -// try expect(match("!*/b", "b/c")); -// try expect(match("!*/c", "a")); -// try expect(match("!*/c", "a.b")); -// try expect(match("!*/c", "a/a")); -// try expect(match("!*/c", "a/b")); -// try expect(match("!*/c", "b/a")); -// try expect(match("!*/c", "b/b")); -// try expect(match("!*a*", "foo")); -// // try expect(match("!a/(*)", "a")); -// // try expect(match("!a/(*)", "a.b")); -// // try expect(match("!a/(*)", "b/a")); -// // try expect(match("!a/(*)", "b/b")); -// // try expect(match("!a/(*)", "b/c")); -// // try expect(match("!a/(b)", "a")); -// // try expect(match("!a/(b)", "a.b")); -// // try expect(match("!a/(b)", "a/a")); -// // try expect(match("!a/(b)", "a/c")); -// // try expect(match("!a/(b)", "b/a")); -// // try expect(match("!a/(b)", "b/b")); -// // try expect(match("!a/(b)", "b/c")); -// try expect(match("!a/*", "a")); -// try expect(match("!a/*", "a.b")); -// try expect(match("!a/*", "b/a")); -// try expect(match("!a/*", "b/b")); -// try expect(match("!a/*", "b/c")); -// try expect(match("!f*b", "bar")); -// try expect(match("!f*b", "foo")); - -// try expect(!match("!.md", ".md")); -// try expect(match("!**/*.md", "a.js")); -// // try expect(!match("!**/*.md", "b.md")); -// try expect(match("!**/*.md", "c.txt")); -// try expect(match("!*.md", "a.js")); -// try expect(!match("!*.md", "b.md")); -// try expect(match("!*.md", "c.txt")); -// try expect(!match("!*.md", "abc.md")); -// try expect(match("!*.md", "abc.txt")); -// try expect(!match("!*.md", "foo.md")); -// try expect(match("!.md", "foo.md")); - -// try expect(match("!*.md", "a.js")); -// try expect(match("!*.md", "b.txt")); -// try expect(!match("!*.md", "c.md")); -// try expect(!match("!a/*/a.js", "a/a/a.js")); -// try expect(!match("!a/*/a.js", "a/b/a.js")); -// try expect(!match("!a/*/a.js", "a/c/a.js")); -// try expect(!match("!a/*/*/a.js", "a/a/a/a.js")); -// try expect(match("!a/*/*/a.js", "b/a/b/a.js")); -// try expect(match("!a/*/*/a.js", "c/a/c/a.js")); -// try expect(!match("!a/a*.txt", "a/a.txt")); -// try expect(match("!a/a*.txt", "a/b.txt")); -// try expect(match("!a/a*.txt", "a/c.txt")); -// try expect(!match("!a.a*.txt", "a.a.txt")); -// try expect(match("!a.a*.txt", "a.b.txt")); -// try expect(match("!a.a*.txt", "a.c.txt")); -// try expect(!match("!a/*.txt", "a/a.txt")); -// try expect(!match("!a/*.txt", "a/b.txt")); -// try expect(!match("!a/*.txt", "a/c.txt")); - -// try expect(match("!*.md", "a.js")); -// try expect(match("!*.md", "b.txt")); -// try expect(!match("!*.md", "c.md")); -// // try expect(!match("!**/a.js", "a/a/a.js")); -// // try expect(!match("!**/a.js", "a/b/a.js")); -// // try expect(!match("!**/a.js", "a/c/a.js")); -// try expect(match("!**/a.js", "a/a/b.js")); -// try expect(!match("!a/**/a.js", "a/a/a/a.js")); -// try expect(match("!a/**/a.js", "b/a/b/a.js")); -// try expect(match("!a/**/a.js", "c/a/c/a.js")); -// try expect(match("!**/*.md", "a/b.js")); -// try expect(match("!**/*.md", "a.js")); -// try expect(!match("!**/*.md", "a/b.md")); -// // try expect(!match("!**/*.md", "a.md")); -// try expect(!match("**/*.md", "a/b.js")); -// try expect(!match("**/*.md", "a.js")); -// try expect(match("**/*.md", "a/b.md")); -// try expect(match("**/*.md", "a.md")); -// try expect(match("!**/*.md", "a/b.js")); -// try expect(match("!**/*.md", "a.js")); -// try expect(!match("!**/*.md", "a/b.md")); -// // try expect(!match("!**/*.md", "a.md")); -// try expect(match("!*.md", "a/b.js")); -// try expect(match("!*.md", "a.js")); -// try expect(match("!*.md", "a/b.md")); -// try expect(!match("!*.md", "a.md")); -// try expect(match("!**/*.md", "a.js")); -// // try expect(!match("!**/*.md", "b.md")); -// try expect(match("!**/*.md", "c.txt")); -// } - -// test "question_mark" { -// try expect(match("?", "a")); -// try expect(!match("?", "aa")); -// try expect(!match("?", "ab")); -// try expect(!match("?", "aaa")); -// try expect(!match("?", "abcdefg")); - -// try expect(!match("??", "a")); -// try expect(match("??", "aa")); -// try expect(match("??", "ab")); -// try expect(!match("??", "aaa")); -// try expect(!match("??", "abcdefg")); - -// try expect(!match("???", "a")); -// try expect(!match("???", "aa")); -// try expect(!match("???", "ab")); -// try expect(match("???", "aaa")); -// try expect(!match("???", "abcdefg")); - -// try expect(!match("a?c", "aaa")); -// try expect(match("a?c", "aac")); -// try expect(match("a?c", "abc")); -// try expect(!match("ab?", "a")); -// try expect(!match("ab?", "aa")); -// try expect(!match("ab?", "ab")); -// try expect(!match("ab?", "ac")); -// try expect(!match("ab?", "abcd")); -// try expect(!match("ab?", "abbb")); -// try expect(match("a?b", "acb")); - -// try expect(!match("a/?/c/?/e.md", "a/bb/c/dd/e.md")); -// try expect(match("a/??/c/??/e.md", "a/bb/c/dd/e.md")); -// try expect(!match("a/??/c.md", "a/bbb/c.md")); -// try expect(match("a/?/c.md", "a/b/c.md")); -// try expect(match("a/?/c/?/e.md", "a/b/c/d/e.md")); -// try expect(!match("a/?/c/???/e.md", "a/b/c/d/e.md")); -// try expect(match("a/?/c/???/e.md", "a/b/c/zzz/e.md")); -// try expect(!match("a/?/c.md", "a/bb/c.md")); -// try expect(match("a/??/c.md", "a/bb/c.md")); -// try expect(match("a/???/c.md", "a/bbb/c.md")); -// try expect(match("a/????/c.md", "a/bbbb/c.md")); -// } - -// test "braces" { -// try expect(match("{a,b,c}", "a")); -// try expect(match("{a,b,c}", "b")); -// try expect(match("{a,b,c}", "c")); -// try expect(!match("{a,b,c}", "aa")); -// try expect(!match("{a,b,c}", "bb")); -// try expect(!match("{a,b,c}", "cc")); - -// try expect(match("a/{a,b}", "a/a")); -// try expect(match("a/{a,b}", "a/b")); -// try expect(!match("a/{a,b}", "a/c")); -// try expect(!match("a/{a,b}", "b/b")); -// try expect(!match("a/{a,b,c}", "b/b")); -// try expect(match("a/{a,b,c}", "a/c")); -// try expect(match("a{b,bc}.txt", "abc.txt")); - -// try expect(match("foo[{a,b}]baz", "foo{baz")); - -// try expect(!match("a{,b}.txt", "abc.txt")); -// try expect(!match("a{a,b,}.txt", "abc.txt")); -// try expect(!match("a{b,}.txt", "abc.txt")); -// try expect(match("a{,b}.txt", "a.txt")); -// try expect(match("a{b,}.txt", "a.txt")); -// try expect(match("a{a,b,}.txt", "aa.txt")); -// try expect(match("a{a,b,}.txt", "aa.txt")); -// try expect(match("a{,b}.txt", "ab.txt")); -// try expect(match("a{b,}.txt", "ab.txt")); - -// // try expect(match("{a/,}a/**", "a")); -// try expect(match("a{a,b/}*.txt", "aa.txt")); -// try expect(match("a{a,b/}*.txt", "ab/.txt")); -// try expect(match("a{a,b/}*.txt", "ab/a.txt")); -// // try expect(match("{a/,}a/**", "a/")); -// try expect(match("{a/,}a/**", "a/a/")); -// // try expect(match("{a/,}a/**", "a/a")); -// try expect(match("{a/,}a/**", "a/a/a")); -// try expect(match("{a/,}a/**", "a/a/")); -// try expect(match("{a/,}a/**", "a/a/a/")); -// try expect(match("{a/,}b/**", "a/b/a/")); -// try expect(match("{a/,}b/**", "b/a/")); -// try expect(match("a{,/}*.txt", "a.txt")); -// try expect(match("a{,/}*.txt", "ab.txt")); -// try expect(match("a{,/}*.txt", "a/b.txt")); -// try expect(match("a{,/}*.txt", "a/ab.txt")); - -// try expect(match("a{,.*{foo,db},\\(bar\\)}.txt", "a.txt")); -// try expect(!match("a{,.*{foo,db},\\(bar\\)}.txt", "adb.txt")); -// try expect(match("a{,.*{foo,db},\\(bar\\)}.txt", "a.db.txt")); - -// try expect(match("a{,*.{foo,db},\\(bar\\)}.txt", "a.txt")); -// try expect(!match("a{,*.{foo,db},\\(bar\\)}.txt", "adb.txt")); -// try expect(match("a{,*.{foo,db},\\(bar\\)}.txt", "a.db.txt")); - -// // try expect(match("a{,.*{foo,db},\\(bar\\)}", "a")); -// try expect(!match("a{,.*{foo,db},\\(bar\\)}", "adb")); -// try expect(match("a{,.*{foo,db},\\(bar\\)}", "a.db")); - -// // try expect(match("a{,*.{foo,db},\\(bar\\)}", "a")); -// try expect(!match("a{,*.{foo,db},\\(bar\\)}", "adb")); -// try expect(match("a{,*.{foo,db},\\(bar\\)}", "a.db")); - -// try expect(!match("{,.*{foo,db},\\(bar\\)}", "a")); -// try expect(!match("{,.*{foo,db},\\(bar\\)}", "adb")); -// try expect(!match("{,.*{foo,db},\\(bar\\)}", "a.db")); -// try expect(match("{,.*{foo,db},\\(bar\\)}", ".db")); - -// try expect(!match("{,*.{foo,db},\\(bar\\)}", "a")); -// try expect(match("{*,*.{foo,db},\\(bar\\)}", "a")); -// try expect(!match("{,*.{foo,db},\\(bar\\)}", "adb")); -// try expect(match("{,*.{foo,db},\\(bar\\)}", "a.db")); - -// try expect(!match("a/b/**/c{d,e}/**/xyz.md", "a/b/c/xyz.md")); -// try expect(!match("a/b/**/c{d,e}/**/xyz.md", "a/b/d/xyz.md")); -// try expect(match("a/b/**/c{d,e}/**/xyz.md", "a/b/cd/xyz.md")); -// try expect(match("a/b/**/{c,d,e}/**/xyz.md", "a/b/c/xyz.md")); -// try expect(match("a/b/**/{c,d,e}/**/xyz.md", "a/b/d/xyz.md")); -// try expect(match("a/b/**/{c,d,e}/**/xyz.md", "a/b/e/xyz.md")); - -// try expect(match("*{a,b}*", "xax")); -// try expect(match("*{a,b}*", "xxax")); -// try expect(match("*{a,b}*", "xbx")); - -// try expect(match("*{*a,b}", "xba")); -// try expect(match("*{*a,b}", "xb")); - -// try expect(!match("*??", "a")); -// try expect(!match("*???", "aa")); -// try expect(match("*???", "aaa")); -// try expect(!match("*****??", "a")); -// try expect(!match("*****???", "aa")); -// try expect(match("*****???", "aaa")); - -// try expect(!match("a*?c", "aaa")); -// try expect(match("a*?c", "aac")); -// try expect(match("a*?c", "abc")); - -// try expect(match("a**?c", "abc")); -// try expect(!match("a**?c", "abb")); -// try expect(match("a**?c", "acc")); -// try expect(match("a*****?c", "abc")); - -// try expect(match("*****?", "a")); -// try expect(match("*****?", "aa")); -// try expect(match("*****?", "abc")); -// try expect(match("*****?", "zzz")); -// try expect(match("*****?", "bbb")); -// try expect(match("*****?", "aaaa")); - -// try expect(!match("*****??", "a")); -// try expect(match("*****??", "aa")); -// try expect(match("*****??", "abc")); -// try expect(match("*****??", "zzz")); -// try expect(match("*****??", "bbb")); -// try expect(match("*****??", "aaaa")); - -// try expect(!match("?*****??", "a")); -// try expect(!match("?*****??", "aa")); -// try expect(match("?*****??", "abc")); -// try expect(match("?*****??", "zzz")); -// try expect(match("?*****??", "bbb")); -// try expect(match("?*****??", "aaaa")); - -// try expect(match("?*****?c", "abc")); -// try expect(!match("?*****?c", "abb")); -// try expect(!match("?*****?c", "zzz")); - -// try expect(match("?***?****c", "abc")); -// try expect(!match("?***?****c", "bbb")); -// try expect(!match("?***?****c", "zzz")); - -// try expect(match("?***?****?", "abc")); -// try expect(match("?***?****?", "bbb")); -// try expect(match("?***?****?", "zzz")); - -// try expect(match("?***?****", "abc")); -// try expect(match("*******c", "abc")); -// try expect(match("*******?", "abc")); -// try expect(match("a*cd**?**??k", "abcdecdhjk")); -// try expect(match("a**?**cd**?**??k", "abcdecdhjk")); -// try expect(match("a**?**cd**?**??k***", "abcdecdhjk")); -// try expect(match("a**?**cd**?**??***k", "abcdecdhjk")); -// try expect(match("a**?**cd**?**??***k**", "abcdecdhjk")); -// try expect(match("a****c**?**??*****", "abcdecdhjk")); - -// try expect(!match("a/?/c/?/*/e.md", "a/b/c/d/e.md")); -// try expect(match("a/?/c/?/*/e.md", "a/b/c/d/e/e.md")); -// try expect(match("a/?/c/?/*/e.md", "a/b/c/d/efghijk/e.md")); -// try expect(match("a/?/**/e.md", "a/b/c/d/efghijk/e.md")); -// try expect(!match("a/?/e.md", "a/bb/e.md")); -// try expect(match("a/??/e.md", "a/bb/e.md")); -// try expect(!match("a/?/**/e.md", "a/bb/e.md")); -// try expect(match("a/?/**/e.md", "a/b/ccc/e.md")); -// try expect(match("a/*/?/**/e.md", "a/b/c/d/efghijk/e.md")); -// try expect(match("a/*/?/**/e.md", "a/b/c/d/efgh.ijk/e.md")); -// try expect(match("a/*/?/**/e.md", "a/b.bb/c/d/efgh.ijk/e.md")); -// try expect(match("a/*/?/**/e.md", "a/bbb/c/d/efgh.ijk/e.md")); - -// try expect(match("a/*/ab??.md", "a/bbb/abcd.md")); -// try expect(match("a/bbb/ab??.md", "a/bbb/abcd.md")); -// try expect(match("a/bbb/ab???md", "a/bbb/abcd.md")); -// } - -// fn matchSame(str: []const u8) bool { -// return match(str, str); -// } -// test "fuzz_tests" { -// // https://github.com/devongovett/glob-match/issues/1 -// try expect(!matchSame( -// "{*{??*{??**,Uz*zz}w**{*{**a,z***b*[!}w??*azzzzzzzz*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!z[za,z&zz}w**z*z*}", -// )); -// try expect(!matchSame( -// "**** *{*{??*{??***\x05 *{*{??*{??***0x5,\x00U\x00}]*****0x1,\x00***\x00,\x00\x00}w****,\x00U\x00}]*****0x1,\x00***\x00,\x00\x00}w*****0x1***{}*.*\x00\x00*\x00", -// )); -// } diff --git a/test/js/bun/glob/scan.test.ts b/test/js/bun/glob/scan.test.ts index 38a5d2ad35..a0d774ef7b 100644 --- a/test/js/bun/glob/scan.test.ts +++ b/test/js/bun/glob/scan.test.ts @@ -26,6 +26,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import fg from "fast-glob"; import * as path from "path"; import { tempFixturesDir, createTempDirectoryWithBrokenSymlinks, prepareEntries } from "./util"; +import { tempDirWithFiles } from "harness"; let origAggressiveGC = Bun.unsafe.gcAggressionLevel(); let tempBrokenSymlinksDir: string; @@ -443,3 +444,81 @@ test("glob.scan('.')", async () => { // bun root dir expect(entries).toContain("README.md"); }); + +describe("glob.scan wildcard fast path", async () => { + test("works", async () => { + const tempdir = tempDirWithFiles("glob-scan-wildcard-fast-path", { + "lol.md": "", + "lol2.md": "", + "shouldnt-show.md23243": "", + "shouldnt-show.ts": "", + }); + const glob = new Glob("*.md"); + const entries = await Array.fromAsync(glob.scan(tempdir)); + // bun root dir + expect(entries.sort()).toEqual(["lol.md", "lol2.md"].sort()); + }); + + // https://github.com/oven-sh/bun/issues/8817 + describe("fast-path detection edgecase", async () => { + function runTest(pattern: string, files: Record, expected: string[]) { + test(`pattern: ${pattern}`, async () => { + const tempdir = tempDirWithFiles("glob-scan-wildcard-fast-path", files); + const glob = new Glob(pattern); + const entries = await Array.fromAsync(glob.scan(tempdir)); + expect(entries.sort()).toEqual(expected.sort()); + }); + } + + runTest( + "*.test.*", + { + "example.test.ts": "", + "example.test.js": "", + "shouldnt-show.ts": "", + }, + ["example.test.ts", "example.test.js"], + ); + + runTest( + "*.test.ts", + { + "example.test.ts": "", + "example.test.ts.test.ts": "", + "shouldnt-show.ts": "", + }, + ["example.test.ts", "example.test.ts.test.ts"], + ); + + runTest( + "*.test.{js,ts}", + { + "example.test.ts": "", + "example.test.js": "", + "shouldnt-show.ts": "", + }, + ["example.test.ts", "example.test.js"], + ); + + runTest( + "*.test.ts?", + { + "example.test.tsx": "", + "example.test.tsz": "", + "shouldnt-show.ts": "", + }, + ["example.test.tsx", "example.test.tsz"], + ); + + // `!` only applies negation if at the start of the pattern + runTest( + "*.test!.*", + { + "hi.test!.js": "", + "hello.test!.ts": "", + "no.test.ts": "", + }, + ["hi.test!.js", "hello.test!.ts"], + ); + }); +}); From 2b56451a11bf881efe567010e82d5ec66e9818c8 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 16 Feb 2024 05:01:55 -0800 Subject: [PATCH 11/19] Shell changes/fixes (#8846) * Fix #8403 * Throw on error by default * Add the shell promise utilities to `ShellOutput` and `ShellError` * Fix tests * [autofix.ci] apply automated fixes * Fix memleak * [autofix.ci] apply automated fixes * Woops * `Bun.gc(true)` in fd leak test * fd leak test should check if `fd <= baseline` * wtf * oob check * [autofix.ci] apply automated fixes * Fix double free * Fix #8550 * increase mem threshold for linux * Requested changes and make not throw on by default * [autofix.ci] apply automated fixes * more requested changes * Do destructuring in function definition * delete * Change shell output test to enable throwing * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- packages/bun-types/bun.d.ts | 154 ++++++++++++++++++++++++++ src/js/builtins/shell.ts | 110 +++++++++++++++--- src/shell/interpreter.zig | 89 +++++++++++++-- src/shell/shell.zig | 27 ++++- test/js/bun/shell/bunshell.test.ts | 50 +++++++-- test/js/bun/shell/commands/rm.test.ts | 2 + test/js/bun/shell/leak.test.ts | 49 ++++---- test/js/bun/shell/lex.test.ts | 6 +- test/js/bun/shell/parse.test.ts | 68 ++++++++++++ test/js/bun/shell/shelloutput.test.ts | 38 +++++++ test/js/bun/shell/test_builder.ts | 20 +++- test/js/bun/shell/throw.test.ts | 50 +++++++++ 12 files changed, 598 insertions(+), 65 deletions(-) create mode 100644 test/js/bun/shell/shelloutput.test.ts create mode 100644 test/js/bun/shell/throw.test.ts diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index c9a2d1e638..7dfde9920b 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -50,6 +50,8 @@ declare module "bun" { export type ShellFunction = (input: Uint8Array) => Uint8Array; export type ShellExpression = + | { toString(): string } + | Array | string | { raw: string } | Subprocess @@ -57,6 +59,75 @@ declare module "bun" { | SpawnOptions.Writable | ReadableStream; + class ShellError extends Error implements ShellOutput { + readonly stdout: Buffer; + readonly stderr: Buffer; + readonly exitCode: number; + + /** + * Read from stdout as a string + * + * @param encoding - The encoding to use when decoding the output + * @returns Stdout as a string with the given encoding + * @example + * + * ## Read as UTF-8 string + * + * ```ts + * const output = await $`echo hello`; + * console.log(output.text()); // "hello\n" + * ``` + * + * ## Read as base64 string + * + * ```ts + * const output = await $`echo ${atob("hello")}`; + * console.log(output.text("base64")); // "hello\n" + * ``` + * + */ + text(encoding?: BufferEncoding): string; + + /** + * Read from stdout as a JSON object + * + * @returns Stdout as a JSON object + * @example + * + * ```ts + * const output = await $`echo '{"hello": 123}'`; + * console.log(output.json()); // { hello: 123 } + * ``` + * + */ + json(): any; + + /** + * Read from stdout as an ArrayBuffer + * + * @returns Stdout as an ArrayBuffer + * @example + * + * ```ts + * const output = await $`echo hello`; + * console.log(output.arrayBuffer()); // ArrayBuffer { byteLength: 6 } + * ``` + */ + arrayBuffer(): ArrayBuffer; + + /** + * Read from stdout as a Blob + * + * @returns Stdout as a blob + * @example + * ```ts + * const output = await $`echo hello`; + * console.log(output.blob()); // Blob { size: 6, type: "" } + * ``` + */ + blob(): Blob; + } + class ShellPromise extends Promise { get stdin(): WritableStream; /** @@ -156,6 +227,16 @@ declare module "bun" { * ``` */ blob(): Promise; + + /** + * Configure the shell to not throw an exception on non-zero exit codes. + */ + nothrow(): this; + + /** + * Configure whether or not the shell should throw an exception on non-zero exit codes. + */ + throws(shouldThrow: boolean): this; } interface ShellConstructor { @@ -207,6 +288,16 @@ declare module "bun" { */ cwd(newCwd?: string): this; + /** + * Configure the shell to not throw an exception on non-zero exit codes. + */ + nothrow(): this; + + /** + * Configure whether or not the shell should throw an exception on non-zero exit codes. + */ + throws(shouldThrow: boolean): this; + readonly ShellPromise: typeof ShellPromise; readonly Shell: ShellConstructor; } @@ -215,6 +306,69 @@ declare module "bun" { readonly stdout: Buffer; readonly stderr: Buffer; readonly exitCode: number; + + /** + * Read from stdout as a string + * + * @param encoding - The encoding to use when decoding the output + * @returns Stdout as a string with the given encoding + * @example + * + * ## Read as UTF-8 string + * + * ```ts + * const output = await $`echo hello`; + * console.log(output.text()); // "hello\n" + * ``` + * + * ## Read as base64 string + * + * ```ts + * const output = await $`echo ${atob("hello")}`; + * console.log(output.text("base64")); // "hello\n" + * ``` + * + */ + text(encoding?: BufferEncoding): string; + + /** + * Read from stdout as a JSON object + * + * @returns Stdout as a JSON object + * @example + * + * ```ts + * const output = await $`echo '{"hello": 123}'`; + * console.log(output.json()); // { hello: 123 } + * ``` + * + */ + json(): any; + + /** + * Read from stdout as an ArrayBuffer + * + * @returns Stdout as an ArrayBuffer + * @example + * + * ```ts + * const output = await $`echo hello`; + * console.log(output.arrayBuffer()); // ArrayBuffer { byteLength: 6 } + * ``` + */ + arrayBuffer(): ArrayBuffer; + + /** + * Read from stdout as a Blob + * + * @returns Stdout as a blob + * @example + * ```ts + * const output = await $`echo hello`; + * console.log(output.blob()); // Blob { size: 6, type: "" } + * ``` + */ + blob(): Blob; } export const $: Shell; diff --git a/src/js/builtins/shell.ts b/src/js/builtins/shell.ts index b49d9a6799..479867c2b8 100644 --- a/src/js/builtins/shell.ts +++ b/src/js/builtins/shell.ts @@ -2,6 +2,42 @@ type ShellInterpreter = any; type Resolve = (value: ShellOutput) => void; export function createBunShellTemplateFunction(ShellInterpreter) { + class ShellError extends Error { + #output: ShellOutput; + constructor(output: ShellOutput) { + super(`Failed with exit code: ${output}`); + this.#output = output; + } + + get exitCode() { + return this.#output.exitCode; + } + + get stdout() { + return this.#output.stdout; + } + + get stderr() { + return this.#output.stderr; + } + + text(encoding) { + return this.#output.text(encoding); + } + + json() { + return this.#output.json(); + } + + arrayBuffer() { + return this.#output.arrayBuffer(); + } + + blob() { + return this.#output.blob(); + } + } + class ShellOutput { stdout: Buffer; stderr: Buffer; @@ -11,6 +47,22 @@ export function createBunShellTemplateFunction(ShellInterpreter) { this.stderr = stderr; this.exitCode = exitCode; } + + text(encoding) { + return this.stdout.toString(encoding); + } + + json() { + return JSON.parse(this.stdout.toString()); + } + + arrayBuffer() { + return this.stdout.buffer; + } + + blob() { + return new Blob([this.stdout]); + } } function autoStartShell(shell) { @@ -20,15 +72,24 @@ export function createBunShellTemplateFunction(ShellInterpreter) { class ShellPromise extends Promise { #core: ShellInterpreter; #hasRun: boolean = false; + #throws: boolean = true; // #immediate; - constructor(core: ShellInterpreter) { - var resolve, reject; + constructor(core: ShellInterpreter, throws: boolean) { + let resolve, reject; super((res, rej) => { - resolve = code => res(new ShellOutput(core.getBufferedStdout(), core.getBufferedStderr(), code)); - reject = code => rej(new ShellOutput(core.getBufferedStdout(), core.getBufferedStderr(), code)); + resolve = code => { + const out = new ShellOutput(core.getBufferedStdout(), core.getBufferedStderr(), code); + if (this.#throws && code !== 0) { + rej(out); + } else { + res(out); + } + }; + reject = code => rej(new ShellError(new ShellOutput(core.getBufferedStdout(), core.getBufferedStderr(), code))); }); + this.#throws = throws; this.#core = core; this.#hasRun = false; @@ -93,6 +154,16 @@ export function createBunShellTemplateFunction(ShellInterpreter) { return this.#quiet(); } + nothrow(): this { + this.#throws = false; + return this; + } + + throws(doThrow: boolean | undefined): this { + this.#throws = !!doThrow; + return this; + } + async text(encoding) { const { stdout } = (await this.#quiet()) as ShellOutput; return stdout.toString(encoding); @@ -149,10 +220,12 @@ export function createBunShellTemplateFunction(ShellInterpreter) { const cwdSymbol = Symbol("cwd"); const envSymbol = Symbol("env"); + const throwsSymbol = Symbol("throws"); class ShellPrototype { [cwdSymbol]: string | undefined; [envSymbol]: Record | undefined; + [throwsSymbol]: boolean = false; env(newEnv: Record) { if (typeof newEnv === "undefined" || newEnv === originalDefaultEnv) { @@ -178,19 +251,29 @@ export function createBunShellTemplateFunction(ShellInterpreter) { return this; } + nothrow() { + this[throwsSymbol] = false; + return this; + } + throws(doThrow: boolean | undefined) { + this[throwsSymbol] = !!doThrow; + return this; + } } - var BunShell = function BunShell() { - const core = new ShellInterpreter(...arguments); + var BunShell = function BunShell(first, ...rest) { + if (first?.raw === undefined) throw new Error("Please use '$' as a tagged template function: $`cmd arg1 arg2`"); + const core = new ShellInterpreter(first.raw, ...rest); const cwd = BunShell[cwdSymbol]; const env = BunShell[envSymbol]; + const throws = BunShell[throwsSymbol]; // cwd must be set before env or else it will be injected into env as "PWD=/" if (cwd) core.setCwd(cwd); if (env) core.setEnv(env); - return new ShellPromise(core); + return new ShellPromise(core, throws); }; function Shell() { @@ -198,17 +281,19 @@ export function createBunShellTemplateFunction(ShellInterpreter) { throw new TypeError("Class constructor Shell cannot be invoked without 'new'"); } - var Shell = function Shell() { - const core = new ShellInterpreter(...arguments); + var Shell = function Shell(first, ...rest) { + if (first?.raw === undefined) throw new Error("Please use '$' as a tagged template function: $`cmd arg1 arg2`"); + const core = new ShellInterpreter(first.raw, ...rest); const cwd = Shell[cwdSymbol]; const env = Shell[envSymbol]; + const throws = Shell[throwsSymbol]; // cwd must be set before env or else it will be injected into env as "PWD=/" if (cwd) core.setCwd(cwd); if (env) core.setEnv(env); - return new ShellPromise(core); + return new ShellPromise(core, throws); }; Object.setPrototypeOf(Shell, ShellPrototype.prototype); @@ -223,19 +308,16 @@ export function createBunShellTemplateFunction(ShellInterpreter) { BunShell[cwdSymbol] = defaultCwd; BunShell[envSymbol] = defaultEnv; + BunShell[throwsSymbol] = false; Object.defineProperties(BunShell, { Shell: { value: Shell, - configurable: false, enumerable: true, - writable: false, }, ShellPromise: { value: ShellPromise, - configurable: false, enumerable: true, - writable: false, }, }); diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 63c10ef418..3224a653ed 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -207,7 +207,7 @@ pub const EnvStr = packed struct { tag: Tag, len: usize = 0, - const print = bun.Output.scoped(.EnvStr, false); + const print = bun.Output.scoped(.EnvStr, true); const Tag = enum(u16) { /// Dealloced by reference counting @@ -1183,6 +1183,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { fn finish(this: *ThisInterpreter, exit_code: ExitCode) void { log("finish", .{}); + defer decrPendingActivityFlag(&this.has_pending_activity); if (comptime EventLoopKind == .js) { // defer this.deinit(); // this.promise.resolve(this.global, JSValue.jsNumberFromInt32(@intCast(exit_code))); @@ -1196,6 +1197,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { fn errored(this: *ThisInterpreter, the_error: ShellError) void { _ = the_error; // autofix + defer decrPendingActivityFlag(&this.has_pending_activity); if (comptime EventLoopKind == .js) { // defer this.deinit(); @@ -1346,6 +1348,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { pub fn finalize( this: *ThisInterpreter, ) callconv(.C) void { + log("Interpreter finalize", .{}); this.deinit(); } @@ -1387,12 +1390,12 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { word_idx: u32, current_out: std.ArrayList(u8), - state: enum { + state: union(enum) { normal, braces, glob, done, - err, + err: bun.shell.ShellErr, }, child_state: union(enum) { idle, @@ -1606,6 +1609,12 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { this.parent.childDone(this, 0); return; } + + // Parent will inspect the `this.state.err` + if (this.state == .err) { + this.parent.childDone(this, 1); + return; + } } fn transitionToGlobState(this: *Expansion) void { @@ -1613,10 +1622,23 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { this.child_state = .{ .glob = .{ .walker = .{} } }; const pattern = this.current_out.items[0..]; - switch (GlobWalker.init(&this.child_state.glob.walker, &arena, pattern, false, false, false, false, false) catch bun.outOfMemory()) { + const cwd = this.base.shell.cwd(); + + switch (GlobWalker.initWithCwd( + &this.child_state.glob.walker, + &arena, + pattern, + cwd, + false, + false, + false, + false, + false, + ) catch bun.outOfMemory()) { .result => {}, .err => |e| { - global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(e)); + this.state = .{ .err = bun.shell.ShellErr.newSys(e) }; + this.next(); return; }, } @@ -1827,6 +1849,19 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { } } + if (task.result.items.len == 0) { + const msg = std.fmt.allocPrint(bun.default_allocator, "no matches found: {s}", .{this.child_state.glob.walker.pattern}) catch bun.outOfMemory(); + this.state = .{ + .err = bun.shell.ShellErr{ + .custom = msg, + }, + }; + this.child_state.glob.walker.deinit(true); + this.child_state = .idle; + this.next(); + return; + } + for (task.result.items) |sentinel_str| { // The string is allocated in the glob walker arena and will be freed, so needs to be duped here const duped = this.base.interpreter.allocator.dupeZ(u8, sentinel_str[0..sentinel_str.len]) catch bun.outOfMemory(); @@ -2196,10 +2231,13 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { } this.base.shell.deinit(); + bun.default_allocator.destroy(this); } pub fn deinitFromInterpreter(this: *Script) void { - this.base.shell.deinitImpl(false, false); + // Let the interpreter deinitialize the shell state + // this.base.shell.deinitImpl(false, false); + bun.default_allocator.destroy(this); } }; @@ -2217,6 +2255,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { current_expansion_result: std.ArrayList([:0]const u8), expansion: Expansion, }, + err: bun.shell.ShellErr, done, }, ctx: AssignCtx, @@ -2289,6 +2328,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { return; }, .done => unreachable, + .err => return this.parent.childDone(this, 1), } } @@ -2296,9 +2336,14 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { } pub fn childDone(this: *Assigns, child: ChildPtr, exit_code: ExitCode) void { - _ = exit_code; - if (child.ptr.is(Expansion)) { + const expansion = child.ptr.as(Expansion); + if (exit_code != 0) { + this.state = .{ + .err = expansion.state.err, + }; + return; + } var expanding = &this.state.expanding; const label = this.node[expanding.idx].label; @@ -3206,9 +3251,16 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { } pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) void { - _ = exit_code; // autofix - if (child.ptr.is(Assigns)) { + if (exit_code != 0) { + const err = this.state.expanding_assigns.state.err; + defer err.deinit(bun.default_allocator); + this.state.expanding_assigns.deinit(); + const buf = err.fmt(); + this.writeFailingError(buf, exit_code); + return; + } + this.state.expanding_assigns.deinit(); this.state = .{ .expanding_redirect = .{ @@ -3220,6 +3272,18 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { } if (child.ptr.is(Expansion)) { + child.deinit(); + if (exit_code != 0) { + const err = switch (this.state) { + .expanding_redirect => this.state.expanding_redirect.expansion.state.err, + .expanding_args => this.state.expanding_args.expansion.state.err, + else => @panic("Invalid state"), + }; + defer err.deinit(bun.default_allocator); + const buf = err.fmt(); + this.writeFailingError(buf, exit_code); + return; + } this.next(); return; } @@ -3558,7 +3622,10 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { this.exec = .none; } - if (!this.spawn_arena_freed) this.spawn_arena.deinit(); + if (!this.spawn_arena_freed) { + log("Spawn arena free", .{}); + this.spawn_arena.deinit(); + } this.freed = true; this.base.interpreter.allocator.destroy(this); } diff --git a/src/shell/shell.zig b/src/shell/shell.zig index c6cd685544..6bf8c6f80a 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -47,6 +47,27 @@ pub const ShellErr = union(enum) { }; } + pub fn fmt(this: @This()) []const u8 { + switch (this) { + .sys => { + const err = this.sys; + const str = std.fmt.allocPrint(bun.default_allocator, "bun: {s}: {}\n", .{ err.message, err.path }) catch bun.outOfMemory(); + return str; + }, + .custom => { + return std.fmt.allocPrint(bun.default_allocator, "bun: {s}\n", .{this.custom}) catch bun.outOfMemory(); + }, + .invalid_arguments => { + const str = std.fmt.allocPrint(bun.default_allocator, "bun: invalid arguments: {s}\n", .{this.invalid_arguments.val}) catch bun.outOfMemory(); + return str; + }, + .todo => { + const str = std.fmt.allocPrint(bun.default_allocator, "bun: TODO: {s}\n", .{this.invalid_arguments.val}) catch bun.outOfMemory(); + return str; + }, + } + } + pub fn throwJS(this: @This(), globalThis: *JSC.JSGlobalObject) void { switch (this) { .sys => { @@ -922,7 +943,7 @@ pub const Parser = struct { self.continue_from_subparser(&subparser); if (self.delimits(self.peek())) { _ = self.match(.Delimit); - if (should_break) break; + break; } }, .Text => |txtrng| { @@ -1797,6 +1818,9 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { switch (char) { '0'...'9' => { _ = self.eat(); + if (count >= 32) { + return null; + } buf[count] = @intCast(char); count += 1; continue; @@ -1882,6 +1906,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { const char = result.char; switch (char) { '0'...'9' => { + if (count >= 32) return null; // Safe to cast here because 0-8 is in ASCII range buf[count] = @intCast(char); count += 1; diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 8bfb96d303..71732b27d4 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -11,14 +11,16 @@ import { mkdir, mkdtemp, realpath, rm } from "fs/promises"; import { bunEnv, runWithErrorPromise, tempDirWithFiles } from "harness"; import { tmpdir } from "os"; import { join } from "path"; -import { TestBuilder } from "./util"; +import { TestBuilder, sortedShellOutput } from "./util"; $.env(bunEnv); $.cwd(process.cwd()); +$.nothrow(); let temp_dir: string; const temp_files = ["foo.txt", "lmao.ts"]; beforeAll(async () => { + $.nothrow(); temp_dir = await mkdtemp(join(await realpath(tmpdir()), "bun-add.test")); await mkdir(temp_dir, { recursive: true }); @@ -150,9 +152,12 @@ describe("bunshell", () => { test("empty_input", async () => { await TestBuilder.command``.run(); await TestBuilder.command` `.run(); - await TestBuilder.command`\n`.run(); - await TestBuilder.command`\n\n\n`.run(); - await TestBuilder.command` \n\n \n\n`.run(); + await TestBuilder.command` +`.run(); + await TestBuilder.command` + `.run(); + await TestBuilder.command` +`.run(); }); describe("echo+cmdsubst edgecases", async () => { @@ -179,8 +184,10 @@ describe("bunshell", () => { test("escape unicode", async () => { const { stdout } = await $`echo \\弟\\気`; - - expect(stdout.toString("utf8")).toEqual(`\弟\気\n`); + // TODO: Uncomment and replace after unicode in template tags is supported + // expect(stdout.toString("utf8")).toEqual(`\弟\気\n`); + // Set this here for now, because unicode in template tags while using .raw is broken, but should be fixed + expect(stdout.toString("utf8")).toEqual("\\u5F1F\\u6C17\n"); }); /** @@ -298,6 +305,25 @@ describe("bunshell", () => { expect(stdout.toString()).toEqual(`noice\n`); }); + describe("glob expansion", () => { + test("No matches should fail", async () => { + // Issue #8403: https://github.com/oven-sh/bun/issues/8403 + await TestBuilder.command`ls *.sdfljsfsdf`.exitCode(1).stderr("bun: no matches found: *.sdfljsfsdf\n").run(); + }); + + test("Should work with a different cwd", async () => { + // Calling `ensureTempDir()` changes the cwd here + await TestBuilder.command`ls *.js` + .ensureTempDir() + .file("foo.js", "foo") + .file("bar.js", "bar") + .stdout(out => { + expect(sortedShellOutput(out)).toEqual(sortedShellOutput("foo.js\nbar.js\n")); + }) + .run(); + }); + }); + describe("brace expansion", () => { function doTest(pattern: string, expected: string) { test(pattern, async () => { @@ -463,8 +489,8 @@ describe("deno_task", () => { await TestBuilder.command`echo 1`.stdout("1\n").run(); await TestBuilder.command`echo 1 2 3`.stdout("1 2 3\n").run(); await TestBuilder.command`echo "1 2 3"`.stdout("1 2 3\n").run(); - await TestBuilder.command`echo 1 2\\ \\ \\ 3`.stdout("1 2 3\n").run(); - await TestBuilder.command`echo "1 2\\ \\ \\ 3"`.stdout("1 2\\ \\ \\ 3\n").run(); + await TestBuilder.command`echo 1 2\ \ \ 3`.stdout("1 2 3\n").run(); + await TestBuilder.command`echo "1 2\ \ \ 3"`.stdout("1 2\\ \\ \\ 3\n").run(); await TestBuilder.command`echo test$(echo 1 2)`.stdout("test1 2\n").run(); await TestBuilder.command`echo test$(echo "1 2")`.stdout("test1 2\n").run(); await TestBuilder.command`echo "test$(echo "1 2")"`.stdout("test1 2\n").run(); @@ -475,15 +501,15 @@ describe("deno_task", () => { await TestBuilder.command`VAR=1 VAR2=2 BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR + process.env.VAR2)'` .stdout("12\n") .run(); - await TestBuilder.command`EMPTY= BUN_TEST_VAR=1 bun -e 'console.log(\`EMPTY: \${process.env.EMPTY}\`)'` + await TestBuilder.command`EMPTY= BUN_TEST_VAR=1 ${BUN} -e ${"console.log(`EMPTY: ${process.env.EMPTY}`)"}` .stdout("EMPTY: \n") .run(); await TestBuilder.command`"echo" "1"`.stdout("1\n").run(); await TestBuilder.command`echo test-dashes`.stdout("test-dashes\n").run(); await TestBuilder.command`echo 'a/b'/c`.stdout("a/b/c\n").run(); - await TestBuilder.command`echo 'a/b'ctest\"te st\"'asdf'`.stdout("a/bctestte stasdf\n").run(); + await TestBuilder.command`echo 'a/b'ctest\"te st\"'asdf'`.stdout('a/bctest"te st"asdf\n').run(); await TestBuilder.command`echo --test=\"2\" --test='2' test\"TEST\" TEST'test'TEST 'test''test' test'test'\"test\" \"test\"\"test\"'test'` - .stdout("--test=2 --test=2 testTEST TESTtestTEST testtest testtesttest testtesttest\n") + .stdout(`--test="2" --test=2 test"TEST" TESTtestTEST testtest testtest"test" "test""test"test\n`) .run(); }); @@ -512,7 +538,7 @@ describe("deno_task", () => { await TestBuilder.command`VAR=1 && echo $VAR$VAR`.stdout("11\n").run(); - await TestBuilder.command`VAR=1 && echo Test$VAR && echo $(echo "Test: $VAR") ; echo CommandSub$($VAR) ; echo $ ; echo \\$VAR` + await TestBuilder.command`VAR=1 && echo Test$VAR && echo $(echo "Test: $VAR") ; echo CommandSub$($VAR) ; echo $ ; echo \$VAR` .stdout("Test1\nTest: 1\nCommandSub\n$\n$VAR\n") .stderr("bun: command not found: 1\n") .run(); diff --git a/test/js/bun/shell/commands/rm.test.ts b/test/js/bun/shell/commands/rm.test.ts index 35642379bb..e9e38da2b9 100644 --- a/test/js/bun/shell/commands/rm.test.ts +++ b/test/js/bun/shell/commands/rm.test.ts @@ -16,6 +16,8 @@ import { TestBuilder, sortedShellOutput } from "../util"; const fileExists = async (path: string): Promise => $`ls -d ${path}`.then(o => o.stdout.toString() == `${path}\n`); +$.nothrow(); + describe("bunshell rm", () => { test("force", async () => { const files = { diff --git a/test/js/bun/shell/leak.test.ts b/test/js/bun/shell/leak.test.ts index 636764f2dd..802d14d4a0 100644 --- a/test/js/bun/shell/leak.test.ts +++ b/test/js/bun/shell/leak.test.ts @@ -10,6 +10,9 @@ import { TestBuilder } from "./util"; $.env(bunEnv); $.cwd(process.cwd()); +$.nothrow(); + +const DEFAULT_THRESHOLD = process.platform === "darwin" ? 100 * (1 << 20) : 150 * (1 << 20); const TESTS: [name: string, builder: () => TestBuilder, runs?: number][] = [ ["redirect_file", () => TestBuilder.command`echo hello > test.txt`.fileEquals("test.txt", "hello\n")], @@ -61,9 +64,12 @@ describe("fd leak", () => { for (let i = 0; i < runs; i++) { await builder().quiet().run(); } + // Run the GC, because the interpreter closes file descriptors when it + // deinitializes when its finalizer is called + Bun.gc(true); const fd = openSync(devNull, "r"); closeSync(fd); - expect(fd).toBe(baseline); + expect(fd).toBeLessThanOrEqual(baseline); }, 100_000); } @@ -71,7 +77,7 @@ describe("fd leak", () => { name: string, builder: () => TestBuilder, runs: number = 500, - threshold: number = 100 * (1 << 20), + threshold: number = DEFAULT_THRESHOLD, ) { test(`memleak_${name}`, async () => { const tempfile = join(tmpdir(), "script.ts"); @@ -82,24 +88,25 @@ describe("fd leak", () => { writeFileSync(tempfile, testcode); const impl = /* ts */ ` - test("${name}", async () => { - const hundredMb = ${threshold} - let prev: number | undefined = undefined; - for (let i = 0; i < ${runs}; i++) { - Bun.gc(true); - await (async function() { - await ${builder.toString().slice("() =>".length)}.quiet().run() - })() - Bun.gc(true); - const val = process.memoryUsage.rss(); - if (prev === undefined) { - prev = val; - } else { - expect(Math.abs(prev - val)).toBeLessThan(hundredMb) - } - } - }, 1_000_000) - `; + test("${name}", async () => { + const threshold = ${threshold} + let prev: number | undefined = undefined; + for (let i = 0; i < ${runs}; i++) { + Bun.gc(true); + await (async function() { + await ${builder.toString().slice("() =>".length)}.quiet().run() + })() + Bun.gc(true); + const val = process.memoryUsage.rss(); + if (prev === undefined) { + prev = val; + } else { + expect(Math.abs(prev - val)).toBeLessThan(threshold) + if (!(Math.abs(prev - val) < threshold)) process.exit(1); + } + } + }, 1_000_000) + `; appendFileSync(tempfile, impl); @@ -126,5 +133,5 @@ describe("fd leak", () => { 100, ); memLeakTest("Buffer", () => TestBuilder.command`cat ${import.meta.filename} > ${Buffer.alloc((1 << 20) * 100)}`, 100); - memLeakTest("String", () => TestBuilder.command`echo ${Array(4096).fill("a").join("")}`.stdout(() => {}), 100, 4096); + memLeakTest("String", () => TestBuilder.command`echo ${Array(4096).fill("a").join("")}`.stdout(() => {}), 100); }); diff --git a/test/js/bun/shell/lex.test.ts b/test/js/bun/shell/lex.test.ts index abcb31af88..ebc28125bb 100644 --- a/test/js/bun/shell/lex.test.ts +++ b/test/js/bun/shell/lex.test.ts @@ -5,6 +5,8 @@ import { TestBuilder, redirect } from "./util"; const BUN = process.argv0; +$.nothrow(); + describe("lex shell", () => { test("basic", () => { const expected = [{ "Text": "next" }, { "Delimit": {} }, { "Text": "dev" }, { "Delimit": {} }, { "Eof": {} }]; @@ -680,8 +682,8 @@ describe("lex shell", () => { .exitCode(1) .run(); - await TestBuilder.command`echo hi && \`echo uh oh`.error("Unclosed command substitution").run(); - await TestBuilder.command`echo hi && \`echo uh oh\`` + await TestBuilder.command`echo hi && ${{ raw: "`echo uh oh" }}`.error("Unclosed command substitution").run(); + await TestBuilder.command`echo hi && ${{ raw: "`echo uh oh`" }}` .stdout("hi\n") .stderr("bun: command not found: uh\n") .exitCode(1) diff --git a/test/js/bun/shell/parse.test.ts b/test/js/bun/shell/parse.test.ts index a147652828..6d75d56a91 100644 --- a/test/js/bun/shell/parse.test.ts +++ b/test/js/bun/shell/parse.test.ts @@ -388,6 +388,74 @@ describe("parse shell", () => { expect(result).toEqual(expected); }); + test("cmd subst edgecase", () => { + const expected = { + "stmts": [ + { + "exprs": [ + { + "cond": { + "op": "And", + "left": { + "cmd": { + "assigns": [], + "name_and_args": [ + { "simple": { "Text": "echo" } }, + { + "simple": { + "cmd_subst": { + "script": { + "stmts": [ + { + "exprs": [ + { + "cmd": { + "assigns": [], + "name_and_args": [ + { "simple": { "Text": "ls" } }, + { "simple": { "Text": "foo" } }, + ], + "redirect": { + "stdin": false, + "stdout": false, + "stderr": false, + "append": false, + "__unused": 0, + }, + "redirect_file": null, + }, + }, + ], + }, + ], + }, + "quoted": false, + }, + }, + }, + ], + "redirect": { "stdin": false, "stdout": false, "stderr": false, "append": false, "__unused": 0 }, + "redirect_file": null, + }, + }, + "right": { + "cmd": { + "assigns": [], + "name_and_args": [{ "simple": { "Text": "echo" } }, { "simple": { "Text": "nice" } }], + "redirect": { "stdin": false, "stdout": false, "stderr": false, "append": false, "__unused": 0 }, + "redirect_file": null, + }, + }, + }, + }, + ], + }, + ], + }; + const result = JSON.parse($.parse`echo $(ls foo) && echo nice`); + expect(result).toEqual(expected); + }); + describe("bad syntax", () => { test("cmd subst edgecase", () => { const expected = { diff --git a/test/js/bun/shell/shelloutput.test.ts b/test/js/bun/shell/shelloutput.test.ts new file mode 100644 index 0000000000..604be9b79b --- /dev/null +++ b/test/js/bun/shell/shelloutput.test.ts @@ -0,0 +1,38 @@ +import { $, ShellError, ShellPromise } from "bun"; +import { beforeAll, describe, test, expect } from "bun:test"; +import { runWithErrorPromise } from "harness"; + +describe("ShellOutput + ShellError", () => { + test("output", async () => { + let output = await $`echo hi`; + expect(output.text()).toBe("hi\n"); + output = await $`echo '{"hello": 123}'`; + expect(output.json()).toEqual({ hello: 123 }); + output = await $`echo hello`; + expect(output.blob()).toEqual(new Blob([new TextEncoder().encode("hello")])); + }); + + test("error", async () => { + $.throws(true); + let output = await withErr($`echo hi; ls oogabooga`); + expect(output.stderr.toString()).toEqual("ls: oogabooga: No such file or directory\n"); + expect(output.text()).toBe("hi\n"); + output = await withErr($`echo '{"hello": 123}'; ls oogabooga`); + expect(output.stderr.toString()).toEqual("ls: oogabooga: No such file or directory\n"); + expect(output.json()).toEqual({ hello: 123 }); + output = await withErr($`echo hello; ls oogabooga`); + expect(output.stderr.toString()).toEqual("ls: oogabooga: No such file or directory\n"); + expect(output.blob()).toEqual(new Blob([new TextEncoder().encode("hello")])); + }); +}); + +async function withErr(promise: ShellPromise): Promise { + let err: ShellError | undefined; + try { + await promise; + } catch (e) { + err = e as ShellError; + } + expect(err).toBeDefined(); + return err as ShellError; +} diff --git a/test/js/bun/shell/test_builder.ts b/test/js/bun/shell/test_builder.ts index 4553531961..2a39d61ad4 100644 --- a/test/js/bun/shell/test_builder.ts +++ b/test/js/bun/shell/test_builder.ts @@ -1,5 +1,5 @@ import { describe, test, afterAll, beforeAll, expect } from "bun:test"; -import { ShellOutput } from "bun"; +import { ShellError, ShellOutput } from "bun"; import { ShellPromise } from "bun"; // import { tempDirWithFiles } from "harness"; import { join } from "node:path"; @@ -13,10 +13,11 @@ export class TestBuilder { private expected_stdout: string | ((stdout: string, tempdir: string) => void) = ""; private expected_stderr: string = ""; private expected_exit_code: number = 0; - private expected_error: string | boolean | undefined = undefined; + private expected_error: ShellError | string | boolean | undefined = undefined; private file_equals: { [filename: string]: string } = {}; private tempdir: string | undefined = undefined; + private _env: { [key: string]: string } | undefined = undefined; static UNEXPECTED_SUBSHELL_ERROR_OPEN = "Unexpected `(`, subshells are currently not supported right now. Escape the `(` or open a GitHub issue."; @@ -52,6 +53,11 @@ export class TestBuilder { return this; } + env(env: { [key: string]: string }): this { + this._env = env; + return this; + } + quiet(): this { if (this.promise.type === "ok") { this.promise.val.quiet(); @@ -79,7 +85,7 @@ export class TestBuilder { return this; } - error(expected?: string | boolean): this { + error(expected?: ShellError | string | boolean): this { if (expected === undefined || expected === true) { this.expected_error = true; } else if (expected === false) { @@ -133,11 +139,17 @@ export class TestBuilder { if (this.expected_error === false) expect(err).toBeUndefined(); if (typeof this.expected_error === "string") { expect(err.message).toEqual(this.expected_error); + } else if (this.expected_error instanceof ShellError) { + expect(err).toBeInstanceOf(ShellError); + const e = err as ShellError; + expect(e.exitCode).toEqual(this.expected_error.exitCode); + expect(e.stdout.toString()).toEqual(this.expected_error.stdout.toString()); + expect(e.stderr.toString()).toEqual(this.expected_error.stderr.toString()); } return undefined; } - const output = await this.promise.val; + const output = await (this._env !== undefined ? this.promise.val.env(this._env) : this.promise.val); const { stdout, stderr, exitCode } = output!; const tempdir = this.tempdir || "NO_TEMP_DIR"; diff --git a/test/js/bun/shell/throw.test.ts b/test/js/bun/shell/throw.test.ts new file mode 100644 index 0000000000..e00351d39a --- /dev/null +++ b/test/js/bun/shell/throw.test.ts @@ -0,0 +1,50 @@ +import { $ } from "bun"; +import { beforeAll, describe, test, expect } from "bun:test"; + +beforeAll(() => { + $.nothrow(); +}); + +describe("throw", () => { + test("enabled globally", async () => { + $.throws(true); + let e; + try { + await $`ls ksjflkjfksjdflksdjflksdf`; + expect("Woops").toBe("Should have thrown"); + } catch (err) { + e = err; + } + expect(e).toBeDefined(); + }); + + test("enabled locally", async () => { + let e; + try { + await $`ls ksjflkjfksjdflksdjflksdf`.throws(true); + expect("Woops").toBe("Should have thrown"); + } catch (err) { + e = err; + } + expect(e).toBeDefined(); + }); + + test("disable globally", async () => { + $.throws(true); + $.nothrow(); + try { + await $`ls ksjflkjfksjdflksdjflksdf`; + } catch (err) { + expect("Woops").toBe("Should not have thrown"); + } + }); + + test("disable locally", async () => { + $.throws(true); + try { + await $`ls ksjflkjfksjdflksdjflksdf`.nothrow(); + } catch (err) { + expect("Woops").toBe("Should not have thrown"); + } + }); +}); From 17e01a284bfe44f6011aaf7f5bd9c1604ce4bbcd Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Fri, 16 Feb 2024 11:32:08 -0800 Subject: [PATCH 12/19] Add url unit tests with failing tests TODO commented-out (#8933) Co-authored-by: Georgijs <48869301+gvilums@users.noreply.github.com> --- test/js/node/url/url-canParse-whatwg.test.js | 26 + .../node/url/url-domain-ascii-unicode.test.js | 31 + test/js/node/url/url-fileurltopath.test.js | 156 +++ .../node/url/url-format-invalid-input.test.js | 28 + test/js/node/url/url-format-whatwg.test.js | 78 ++ test/js/node/url/url-format.test.js | 281 +++++ test/js/node/url/url-is-url.test.js | 22 + test/js/node/url/url-null-char.test.js | 15 + test/js/node/url/url-parse-format.test.js | 1077 +++++++++++++++++ .../node/url/url-parse-invalid-input.test.js | 121 ++ test/js/node/url/url-parse-query.test.js | 93 ++ test/js/node/url/url-pathtofileurl.test.js | 203 ++++ test/js/node/url/url-relative.test.js | 420 +++++++ test/js/node/url/url-revokeobjecturl.test.js | 19 + 14 files changed, 2570 insertions(+) create mode 100644 test/js/node/url/url-canParse-whatwg.test.js create mode 100644 test/js/node/url/url-domain-ascii-unicode.test.js create mode 100644 test/js/node/url/url-fileurltopath.test.js create mode 100644 test/js/node/url/url-format-invalid-input.test.js create mode 100644 test/js/node/url/url-format-whatwg.test.js create mode 100644 test/js/node/url/url-format.test.js create mode 100644 test/js/node/url/url-is-url.test.js create mode 100644 test/js/node/url/url-null-char.test.js create mode 100644 test/js/node/url/url-parse-format.test.js create mode 100644 test/js/node/url/url-parse-invalid-input.test.js create mode 100644 test/js/node/url/url-parse-query.test.js create mode 100644 test/js/node/url/url-pathtofileurl.test.js create mode 100644 test/js/node/url/url-relative.test.js create mode 100644 test/js/node/url/url-revokeobjecturl.test.js diff --git a/test/js/node/url/url-canParse-whatwg.test.js b/test/js/node/url/url-canParse-whatwg.test.js new file mode 100644 index 0000000000..9740e53896 --- /dev/null +++ b/test/js/node/url/url-canParse-whatwg.test.js @@ -0,0 +1,26 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import { URL } from "node:url"; + +describe("URL.canParse", () => { + // TODO: Support error code. + test.todo("invalid input", () => { + // One argument is required + assert.throws( + () => { + URL.canParse(); + }, + { + code: "ERR_MISSING_ARGS", + name: "TypeError", + }, + ); + }); + + test("repeatedly called produces same result", () => { + // This test is to ensure that the v8 fast api works. + for (let i = 0; i < 1e5; i++) { + assert(URL.canParse("https://www.example.com/path/?query=param#hash")); + } + }); +}); diff --git a/test/js/node/url/url-domain-ascii-unicode.test.js b/test/js/node/url/url-domain-ascii-unicode.test.js new file mode 100644 index 0000000000..8c3964d018 --- /dev/null +++ b/test/js/node/url/url-domain-ascii-unicode.test.js @@ -0,0 +1,31 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import url from "node:url"; + +const domainToASCII = url.domainToASCII; +const domainToUnicode = url.domainToUnicode; + +// TODO: Support url.domainToASCII and url.domainToUnicode. +describe.todo("url.domainToASCII and url.domainToUnicode", () => { + test("convert from unicode to ascii and back", () => { + const domainWithASCII = [ + ["ıíd", "xn--d-iga7r"], + ["يٴ", "xn--mhb8f"], + ["www.ϧƽəʐ.com", "www.xn--cja62apfr6c.com"], + ["новини.com", "xn--b1amarcd.com"], + ["名がドメイン.com", "xn--v8jxj3d1dzdz08w.com"], + ["افغانستا.icom.museum", "xn--mgbaal8b0b9b2b.icom.museum"], + ["الجزائر.icom.fake", "xn--lgbbat1ad8j.icom.fake"], + ["भारत.org", "xn--h2brj9c.org"], + ]; + + domainWithASCII.forEach(pair => { + const domain = pair[0]; + const ascii = pair[1]; + const domainConvertedToASCII = domainToASCII(domain); + assert.strictEqual(domainConvertedToASCII, ascii); + const asciiConvertedToUnicode = domainToUnicode(ascii); + assert.strictEqual(asciiConvertedToUnicode, domain); + }); + }); +}); diff --git a/test/js/node/url/url-fileurltopath.test.js b/test/js/node/url/url-fileurltopath.test.js new file mode 100644 index 0000000000..6e77b1d864 --- /dev/null +++ b/test/js/node/url/url-fileurltopath.test.js @@ -0,0 +1,156 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import url, { URL } from "node:url"; + +const isWindows = process.platform === "win32"; + +describe("url.fileURLToPath", () => { + function testInvalidArgs(...args) { + for (const arg of args) { + assert.throws(() => url.fileURLToPath(arg), { + code: "ERR_INVALID_ARG_TYPE", + }); + } + } + + // TODO: Support error code. + test.todo("invalid input", () => { + // Input must be string or URL + testInvalidArgs(null, undefined, 1, {}, true); + + // Input must be a file URL + assert.throws(() => url.fileURLToPath("https://a/b/c"), { + code: "ERR_INVALID_URL_SCHEME", + }); + + const withHost = new URL("file://host/a"); + + if (isWindows) { + assert.strictEqual(url.fileURLToPath(withHost), "\\\\host\\a"); + } else { + assert.throws(() => url.fileURLToPath(withHost), { + code: "ERR_INVALID_FILE_URL_HOST", + }); + } + + if (isWindows) { + assert.throws(() => url.fileURLToPath("file:///C:/a%2F/"), { + code: "ERR_INVALID_FILE_URL_PATH", + }); + assert.throws(() => url.fileURLToPath("file:///C:/a%5C/"), { + code: "ERR_INVALID_FILE_URL_PATH", + }); + assert.throws(() => url.fileURLToPath("file:///?:/"), { + code: "ERR_INVALID_FILE_URL_PATH", + }); + } else { + assert.throws(() => url.fileURLToPath("file:///a%2F/"), { + code: "ERR_INVALID_FILE_URL_PATH", + }); + } + }); + + test("general", () => { + let testCases; + if (isWindows) { + testCases = [ + // Lowercase ascii alpha + { path: "C:\\foo", fileURL: "file:///C:/foo" }, + // Uppercase ascii alpha + { path: "C:\\FOO", fileURL: "file:///C:/FOO" }, + // dir + { path: "C:\\dir\\foo", fileURL: "file:///C:/dir/foo" }, + // trailing separator + { path: "C:\\dir\\", fileURL: "file:///C:/dir/" }, + // dot + { path: "C:\\foo.mjs", fileURL: "file:///C:/foo.mjs" }, + // space + { path: "C:\\foo bar", fileURL: "file:///C:/foo%20bar" }, + // question mark + { path: "C:\\foo?bar", fileURL: "file:///C:/foo%3Fbar" }, + // number sign + { path: "C:\\foo#bar", fileURL: "file:///C:/foo%23bar" }, + // ampersand + { path: "C:\\foo&bar", fileURL: "file:///C:/foo&bar" }, + // equals + { path: "C:\\foo=bar", fileURL: "file:///C:/foo=bar" }, + // colon + { path: "C:\\foo:bar", fileURL: "file:///C:/foo:bar" }, + // semicolon + { path: "C:\\foo;bar", fileURL: "file:///C:/foo;bar" }, + // percent + { path: "C:\\foo%bar", fileURL: "file:///C:/foo%25bar" }, + // backslash + { path: "C:\\foo\\bar", fileURL: "file:///C:/foo/bar" }, + // backspace + { path: "C:\\foo\bbar", fileURL: "file:///C:/foo%08bar" }, + // tab + { path: "C:\\foo\tbar", fileURL: "file:///C:/foo%09bar" }, + // newline + { path: "C:\\foo\nbar", fileURL: "file:///C:/foo%0Abar" }, + // carriage return + { path: "C:\\foo\rbar", fileURL: "file:///C:/foo%0Dbar" }, + // latin1 + { path: "C:\\fóóbàr", fileURL: "file:///C:/f%C3%B3%C3%B3b%C3%A0r" }, + // Euro sign (BMP code point) + { path: "C:\\€", fileURL: "file:///C:/%E2%82%AC" }, + // Rocket emoji (non-BMP code point) + { path: "C:\\🚀", fileURL: "file:///C:/%F0%9F%9A%80" }, + // UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows) + { path: "\\\\nas\\My Docs\\File.doc", fileURL: "file://nas/My%20Docs/File.doc" }, + ]; + } else { + testCases = [ + // Lowercase ascii alpha + { path: "/foo", fileURL: "file:///foo" }, + // Uppercase ascii alpha + { path: "/FOO", fileURL: "file:///FOO" }, + // dir + { path: "/dir/foo", fileURL: "file:///dir/foo" }, + // trailing separator + { path: "/dir/", fileURL: "file:///dir/" }, + // dot + { path: "/foo.mjs", fileURL: "file:///foo.mjs" }, + // space + { path: "/foo bar", fileURL: "file:///foo%20bar" }, + // question mark + { path: "/foo?bar", fileURL: "file:///foo%3Fbar" }, + // number sign + { path: "/foo#bar", fileURL: "file:///foo%23bar" }, + // ampersand + { path: "/foo&bar", fileURL: "file:///foo&bar" }, + // equals + { path: "/foo=bar", fileURL: "file:///foo=bar" }, + // colon + { path: "/foo:bar", fileURL: "file:///foo:bar" }, + // semicolon + { path: "/foo;bar", fileURL: "file:///foo;bar" }, + // percent + { path: "/foo%bar", fileURL: "file:///foo%25bar" }, + // backslash + { path: "/foo\\bar", fileURL: "file:///foo%5Cbar" }, + // backspace + { path: "/foo\bbar", fileURL: "file:///foo%08bar" }, + // tab + { path: "/foo\tbar", fileURL: "file:///foo%09bar" }, + // newline + { path: "/foo\nbar", fileURL: "file:///foo%0Abar" }, + // carriage return + { path: "/foo\rbar", fileURL: "file:///foo%0Dbar" }, + // latin1 + { path: "/fóóbàr", fileURL: "file:///f%C3%B3%C3%B3b%C3%A0r" }, + // Euro sign (BMP code point) + { path: "/€", fileURL: "file:///%E2%82%AC" }, + // Rocket emoji (non-BMP code point) + { path: "/🚀", fileURL: "file:///%F0%9F%9A%80" }, + ]; + } + + for (const { path, fileURL } of testCases) { + const fromString = url.fileURLToPath(fileURL); + assert.strictEqual(fromString, path); + const fromURL = url.fileURLToPath(new URL(fileURL)); + assert.strictEqual(fromURL, path); + } + }); +}); diff --git a/test/js/node/url/url-format-invalid-input.test.js b/test/js/node/url/url-format-invalid-input.test.js new file mode 100644 index 0000000000..cb385583dd --- /dev/null +++ b/test/js/node/url/url-format-invalid-input.test.js @@ -0,0 +1,28 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import url from "node:url"; + +describe("url.format", () => { + // TODO: Support error code. + test.todo("invalid input", () => { + const throwsObjsAndReportTypes = [undefined, null, true, false, 0, function () {}, Symbol("foo")]; + + for (const urlObject of throwsObjsAndReportTypes) { + assert.throws( + () => { + url.format(urlObject); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + message: 'The "urlObject" argument must be one of type object or string.', + }, + ); + } + }); + + test("empty", () => { + assert.strictEqual(url.format(""), ""); + assert.strictEqual(url.format({}), ""); + }); +}); diff --git a/test/js/node/url/url-format-whatwg.test.js b/test/js/node/url/url-format-whatwg.test.js new file mode 100644 index 0000000000..91c6535ffb --- /dev/null +++ b/test/js/node/url/url-format-whatwg.test.js @@ -0,0 +1,78 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import url, { URL } from "node:url"; + +describe("url.format", () => { + test("WHATWG", () => { + const myURL = new URL("http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // TODO: Support these. + // + // assert.strictEqual(url.format(myURL), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, {}), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // TODO: Support this kind of assert.throws. + // { + // [true, 1, "test", Infinity].forEach(value => { + // assert.throws(() => url.format(myURL, value), { + // code: "ERR_INVALID_ARG_TYPE", + // name: "TypeError", + // message: 'The "options" argument must be of type object.', + // }); + // }); + // } + + // Any falsy value other than undefined will be treated as false. + // Any truthy value will be treated as true. + + assert.strictEqual(url.format(myURL, { auth: false }), "http://xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + assert.strictEqual(url.format(myURL, { auth: "" }), "http://xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + assert.strictEqual(url.format(myURL, { auth: 0 }), "http://xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // TODO: Support these. + // + // assert.strictEqual(url.format(myURL, { auth: 1 }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { auth: {} }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { fragment: false }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b"); + + // assert.strictEqual(url.format(myURL, { fragment: "" }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b"); + + // assert.strictEqual(url.format(myURL, { fragment: 0 }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b"); + + // assert.strictEqual(url.format(myURL, { fragment: 1 }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { fragment: {} }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { search: false }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a#c"); + + // assert.strictEqual(url.format(myURL, { search: "" }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a#c"); + + // assert.strictEqual(url.format(myURL, { search: 0 }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a#c"); + + // assert.strictEqual(url.format(myURL, { search: 1 }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { search: {} }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { unicode: true }), "http://user:pass@理容ナカムラ.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { unicode: 1 }), "http://user:pass@理容ナカムラ.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { unicode: {} }), "http://user:pass@理容ナカムラ.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { unicode: false }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual(url.format(myURL, { unicode: 0 }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c"); + + // assert.strictEqual( + // url.format(new URL("http://user:pass@xn--0zwm56d.com:8080/path"), { unicode: true }), + // "http://user:pass@测试.com:8080/path", + // ); + + assert.strictEqual(url.format(new URL("tel:123")), url.format(new URL("tel:123"), { unicode: true })); + }); +}); diff --git a/test/js/node/url/url-format.test.js b/test/js/node/url/url-format.test.js new file mode 100644 index 0000000000..ed8b8b8478 --- /dev/null +++ b/test/js/node/url/url-format.test.js @@ -0,0 +1,281 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import url from "node:url"; + +describe("url.format", () => { + test("slightly wonky content", () => { + // Formatting tests to verify that it'll format slightly wonky content to a + // valid URL. + const formatTests = { + "http://example.com?": { + href: "http://example.com/?", + protocol: "http:", + slashes: true, + host: "example.com", + hostname: "example.com", + search: "?", + query: {}, + pathname: "/", + }, + "http://example.com?foo=bar#frag": { + href: "http://example.com/?foo=bar#frag", + protocol: "http:", + host: "example.com", + hostname: "example.com", + hash: "#frag", + search: "?foo=bar", + query: "foo=bar", + pathname: "/", + }, + "http://example.com?foo=@bar#frag": { + href: "http://example.com/?foo=@bar#frag", + protocol: "http:", + host: "example.com", + hostname: "example.com", + hash: "#frag", + search: "?foo=@bar", + query: "foo=@bar", + pathname: "/", + }, + "http://example.com?foo=/bar/#frag": { + href: "http://example.com/?foo=/bar/#frag", + protocol: "http:", + host: "example.com", + hostname: "example.com", + hash: "#frag", + search: "?foo=/bar/", + query: "foo=/bar/", + pathname: "/", + }, + "http://example.com?foo=?bar/#frag": { + href: "http://example.com/?foo=?bar/#frag", + protocol: "http:", + host: "example.com", + hostname: "example.com", + hash: "#frag", + search: "?foo=?bar/", + query: "foo=?bar/", + pathname: "/", + }, + "http://example.com#frag=?bar/#frag": { + href: "http://example.com/#frag=?bar/#frag", + protocol: "http:", + host: "example.com", + hostname: "example.com", + hash: "#frag=?bar/#frag", + pathname: "/", + }, + 'http://google.com" onload="alert(42)/': { + href: "http://google.com/%22%20onload=%22alert(42)/", + protocol: "http:", + host: "google.com", + pathname: "/%22%20onload=%22alert(42)/", + }, + "http://a.com/a/b/c?s#h": { + href: "http://a.com/a/b/c?s#h", + protocol: "http", + host: "a.com", + pathname: "a/b/c", + hash: "h", + search: "s", + }, + "xmpp:isaacschlueter@jabber.org": { + href: "xmpp:isaacschlueter@jabber.org", + protocol: "xmpp:", + host: "jabber.org", + auth: "isaacschlueter", + hostname: "jabber.org", + }, + "http://atpass:foo%40bar@127.0.0.1/": { + href: "http://atpass:foo%40bar@127.0.0.1/", + auth: "atpass:foo@bar", + hostname: "127.0.0.1", + protocol: "http:", + pathname: "/", + }, + "http://atslash%2F%40:%2F%40@foo/": { + href: "http://atslash%2F%40:%2F%40@foo/", + auth: "atslash/@:/@", + hostname: "foo", + protocol: "http:", + pathname: "/", + }, + "svn+ssh://foo/bar": { + href: "svn+ssh://foo/bar", + hostname: "foo", + protocol: "svn+ssh:", + pathname: "/bar", + slashes: true, + }, + "dash-test://foo/bar": { + href: "dash-test://foo/bar", + hostname: "foo", + protocol: "dash-test:", + pathname: "/bar", + slashes: true, + }, + "dash-test:foo/bar": { + href: "dash-test:foo/bar", + hostname: "foo", + protocol: "dash-test:", + pathname: "/bar", + }, + "dot.test://foo/bar": { + href: "dot.test://foo/bar", + hostname: "foo", + protocol: "dot.test:", + pathname: "/bar", + slashes: true, + }, + "dot.test:foo/bar": { + href: "dot.test:foo/bar", + hostname: "foo", + protocol: "dot.test:", + pathname: "/bar", + }, + // IPv6 support + "coap:u:p@[::1]:61616/.well-known/r?n=Temperature": { + href: "coap:u:p@[::1]:61616/.well-known/r?n=Temperature", + protocol: "coap:", + auth: "u:p", + hostname: "::1", + port: "61616", + pathname: "/.well-known/r", + search: "n=Temperature", + }, + "coap:[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616/s/stopButton": { + href: "coap:[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616/s/stopButton", + protocol: "coap", + host: "[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616", + pathname: "/s/stopButton", + }, + // TODO: Support this. + // + // "http://[::]/": { + // href: "http://[::]/", + // protocol: "http:", + // hostname: "[::]", + // pathname: "/", + // }, + + // Encode context-specific delimiters in path and query, but do not touch + // other non-delimiter chars like `%`. + // + + // `#`,`?` in path + "/path/to/%%23%3F+=&.txt?foo=theA1#bar": { + href: "/path/to/%%23%3F+=&.txt?foo=theA1#bar", + pathname: "/path/to/%#?+=&.txt", + query: { + foo: "theA1", + }, + hash: "#bar", + }, + + // `#`,`?` in path + `#` in query + "/path/to/%%23%3F+=&.txt?foo=the%231#bar": { + href: "/path/to/%%23%3F+=&.txt?foo=the%231#bar", + pathname: "/path/to/%#?+=&.txt", + query: { + foo: "the#1", + }, + hash: "#bar", + }, + + // `#` in path end + `#` in query + "/path/to/%%23?foo=the%231#bar": { + href: "/path/to/%%23?foo=the%231#bar", + pathname: "/path/to/%#", + query: { + foo: "the#1", + }, + hash: "#bar", + }, + + // `?` and `#` in path and search + "http://ex.com/foo%3F100%m%23r?abc=the%231?&foo=bar#frag": { + href: "http://ex.com/foo%3F100%m%23r?abc=the%231?&foo=bar#frag", + protocol: "http:", + hostname: "ex.com", + hash: "#frag", + search: "?abc=the#1?&foo=bar", + pathname: "/foo?100%m#r", + }, + + // `?` and `#` in search only + "http://ex.com/fooA100%mBr?abc=the%231?&foo=bar#frag": { + href: "http://ex.com/fooA100%mBr?abc=the%231?&foo=bar#frag", + protocol: "http:", + hostname: "ex.com", + hash: "#frag", + search: "?abc=the#1?&foo=bar", + pathname: "/fooA100%mBr", + }, + // TODO: Support these. + // + // // Multiple `#` in search + // "http://example.com/?foo=bar%231%232%233&abc=%234%23%235#frag": { + // href: "http://example.com/?foo=bar%231%232%233&abc=%234%23%235#frag", + // protocol: "http:", + // slashes: true, + // host: "example.com", + // hostname: "example.com", + // hash: "#frag", + // search: "?foo=bar#1#2#3&abc=#4##5", + // query: {}, + // pathname: "/", + // }, + + // More than 255 characters in hostname which exceeds the limit + // [`http://${"a".repeat(255)}.com/node`]: { + // href: "http:///node", + // protocol: "http:", + // slashes: true, + // host: "", + // hostname: "", + // pathname: "/node", + // path: "/node", + // }, + + // Greater than or equal to 63 characters after `.` in hostname + // [`http://www.${"z".repeat(63)}example.com/node`]: { + // href: `http://www.${"z".repeat(63)}example.com/node`, + // protocol: "http:", + // slashes: true, + // host: `www.${"z".repeat(63)}example.com`, + // hostname: `www.${"z".repeat(63)}example.com`, + // pathname: "/node", + // path: "/node", + // }, + + // https://github.com/nodejs/node/issues/3361 + // "file:///home/user": { + // href: "file:///home/user", + // protocol: "file", + // pathname: "/home/user", + // path: "/home/user", + // }, + + // surrogate in auth + "http://%F0%9F%98%80@www.example.com/": { + href: "http://%F0%9F%98%80@www.example.com/", + protocol: "http:", + auth: "\uD83D\uDE00", + hostname: "www.example.com", + pathname: "/", + }, + }; + for (const u in formatTests) { + const expect = formatTests[u].href; + delete formatTests[u].href; + const actual = url.format(u); + const actualObj = url.format(formatTests[u]); + assert.strictEqual(actual, expect, `wonky format(${u}) == ${expect}\nactual:${actual}`); + assert.strictEqual( + actualObj, + expect, + `wonky format(${JSON.stringify(formatTests[u])}) == ${expect}\nactual: ${actualObj}`, + ); + } + }); +}); diff --git a/test/js/node/url/url-is-url.test.js b/test/js/node/url/url-is-url.test.js new file mode 100644 index 0000000000..be2f331eea --- /dev/null +++ b/test/js/node/url/url-is-url.test.js @@ -0,0 +1,22 @@ +// Flags: --expose-internals +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import { URL, parse } from "node:url"; + +describe("internal/url", () => { + test.skip("isURL", () => { + const { isURL } = require("internal/url"); + + assert.strictEqual(isURL("https://www.nodejs.org"), true); + assert.strictEqual(isURL(new URL("https://www.nodejs.org")), true); + assert.strictEqual(isURL(parse("https://www.nodejs.org")), false); + assert.strictEqual( + isURL({ + href: "https://www.nodejs.org", + protocol: "https:", + path: "/", + }), + false, + ); + }); +}); diff --git a/test/js/node/url/url-null-char.test.js b/test/js/node/url/url-null-char.test.js new file mode 100644 index 0000000000..418de47ea5 --- /dev/null +++ b/test/js/node/url/url-null-char.test.js @@ -0,0 +1,15 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import { URL } from "node:url"; + +describe("URL", () => { + // TODO: Fix error properties + test.skip("null character", () => { + assert.throws( + () => { + new URL("a\0b"); + }, + { code: "ERR_INVALID_URL", input: "a\0b" }, + ); + }); +}); diff --git a/test/js/node/url/url-parse-format.test.js b/test/js/node/url/url-parse-format.test.js new file mode 100644 index 0000000000..df8c5d776a --- /dev/null +++ b/test/js/node/url/url-parse-format.test.js @@ -0,0 +1,1077 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import { inspect } from "node:util"; +import url from "node:url"; + +describe("url.parse then url.format", () => { + // URLs to parse, and expected data + // { URL : parsed } + const parseTests = { + "//some_path": { + href: "//some_path", + pathname: "//some_path", + path: "//some_path", + }, + + "http:\\\\evil-phisher\\foo.html#h\\a\\s\\h": { + protocol: "http:", + slashes: true, + host: "evil-phisher", + hostname: "evil-phisher", + pathname: "/foo.html", + path: "/foo.html", + hash: "#h%5Ca%5Cs%5Ch", + href: "http://evil-phisher/foo.html#h%5Ca%5Cs%5Ch", + }, + + 'http:\\\\evil-phisher\\foo.html?json="\\"foo\\""#h\\a\\s\\h': { + protocol: "http:", + slashes: true, + host: "evil-phisher", + hostname: "evil-phisher", + pathname: "/foo.html", + search: "?json=%22%5C%22foo%5C%22%22", + query: "json=%22%5C%22foo%5C%22%22", + path: "/foo.html?json=%22%5C%22foo%5C%22%22", + hash: "#h%5Ca%5Cs%5Ch", + href: "http://evil-phisher/foo.html?json=%22%5C%22foo%5C%22%22#h%5Ca%5Cs%5Ch", + }, + + "http:\\\\evil-phisher\\foo.html#h\\a\\s\\h?blarg": { + protocol: "http:", + slashes: true, + host: "evil-phisher", + hostname: "evil-phisher", + pathname: "/foo.html", + path: "/foo.html", + hash: "#h%5Ca%5Cs%5Ch?blarg", + href: "http://evil-phisher/foo.html#h%5Ca%5Cs%5Ch?blarg", + }, + + "http:\\\\evil-phisher\\foo.html": { + protocol: "http:", + slashes: true, + host: "evil-phisher", + hostname: "evil-phisher", + pathname: "/foo.html", + path: "/foo.html", + href: "http://evil-phisher/foo.html", + }, + + "HTTP://www.example.com/": { + href: "http://www.example.com/", + protocol: "http:", + slashes: true, + host: "www.example.com", + hostname: "www.example.com", + pathname: "/", + path: "/", + }, + + "HTTP://www.example.com": { + href: "http://www.example.com/", + protocol: "http:", + slashes: true, + host: "www.example.com", + hostname: "www.example.com", + pathname: "/", + path: "/", + }, + + "http://www.ExAmPlE.com/": { + href: "http://www.example.com/", + protocol: "http:", + slashes: true, + host: "www.example.com", + hostname: "www.example.com", + pathname: "/", + path: "/", + }, + + "http://user:pw@www.ExAmPlE.com/": { + href: "http://user:pw@www.example.com/", + protocol: "http:", + slashes: true, + auth: "user:pw", + host: "www.example.com", + hostname: "www.example.com", + pathname: "/", + path: "/", + }, + + "http://USER:PW@www.ExAmPlE.com/": { + href: "http://USER:PW@www.example.com/", + protocol: "http:", + slashes: true, + auth: "USER:PW", + host: "www.example.com", + hostname: "www.example.com", + pathname: "/", + path: "/", + }, + + "http://user@www.example.com/": { + href: "http://user@www.example.com/", + protocol: "http:", + slashes: true, + auth: "user", + host: "www.example.com", + hostname: "www.example.com", + pathname: "/", + path: "/", + }, + + "http://user%3Apw@www.example.com/": { + href: "http://user:pw@www.example.com/", + protocol: "http:", + slashes: true, + auth: "user:pw", + host: "www.example.com", + hostname: "www.example.com", + pathname: "/", + path: "/", + }, + + "http://x.com/path?that's#all, folks": { + href: "http://x.com/path?that%27s#all,%20folks", + protocol: "http:", + slashes: true, + host: "x.com", + hostname: "x.com", + search: "?that%27s", + query: "that%27s", + pathname: "/path", + hash: "#all,%20folks", + path: "/path?that%27s", + }, + + "HTTP://X.COM/Y": { + href: "http://x.com/Y", + protocol: "http:", + slashes: true, + host: "x.com", + hostname: "x.com", + pathname: "/Y", + path: "/Y", + }, + + // Whitespace in the front + " http://www.example.com/": { + href: "http://www.example.com/", + protocol: "http:", + slashes: true, + host: "www.example.com", + hostname: "www.example.com", + pathname: "/", + path: "/", + }, + + // + not an invalid host character + // per https://URL.spec.whatwg.org/#host-parsing + "http://x.y.com+a/b/c": { + href: "http://x.y.com+a/b/c", + protocol: "http:", + slashes: true, + host: "x.y.com+a", + hostname: "x.y.com+a", + pathname: "/b/c", + path: "/b/c", + }, + + // An unexpected invalid char in the hostname. + "HtTp://x.y.cOm;a/b/c?d=e#f gi": { + href: "http://x.y.com/;a/b/c?d=e#f%20g%3Ch%3Ei", + protocol: "http:", + slashes: true, + host: "x.y.com", + hostname: "x.y.com", + pathname: ";a/b/c", + search: "?d=e", + query: "d=e", + hash: "#f%20g%3Ch%3Ei", + path: ";a/b/c?d=e", + }, + + // Make sure that we don't accidentally lcast the path parts. + "HtTp://x.y.cOm;A/b/c?d=e#f gi": { + href: "http://x.y.com/;A/b/c?d=e#f%20g%3Ch%3Ei", + protocol: "http:", + slashes: true, + host: "x.y.com", + hostname: "x.y.com", + pathname: ";A/b/c", + search: "?d=e", + query: "d=e", + hash: "#f%20g%3Ch%3Ei", + path: ";A/b/c?d=e", + }, + + "http://x...y...#p": { + href: "http://x...y.../#p", + protocol: "http:", + slashes: true, + host: "x...y...", + hostname: "x...y...", + hash: "#p", + pathname: "/", + path: "/", + }, + + 'http://x/p/"quoted"': { + href: "http://x/p/%22quoted%22", + protocol: "http:", + slashes: true, + host: "x", + hostname: "x", + pathname: "/p/%22quoted%22", + path: "/p/%22quoted%22", + }, + + " Is a URL!": { + href: "%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!", + pathname: "%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!", + path: "%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!", + }, + + "http://www.narwhaljs.org/blog/categories?id=news": { + href: "http://www.narwhaljs.org/blog/categories?id=news", + protocol: "http:", + slashes: true, + host: "www.narwhaljs.org", + hostname: "www.narwhaljs.org", + search: "?id=news", + query: "id=news", + pathname: "/blog/categories", + path: "/blog/categories?id=news", + }, + + "http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=": { + href: "http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=", + protocol: "http:", + slashes: true, + host: "mt0.google.com", + hostname: "mt0.google.com", + pathname: "/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=", + path: "/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=", + }, + + "http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=": { + href: "http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api" + "&x=2&y=2&z=3&s=", + protocol: "http:", + slashes: true, + host: "mt0.google.com", + hostname: "mt0.google.com", + search: "???&hl=en&src=api&x=2&y=2&z=3&s=", + query: "??&hl=en&src=api&x=2&y=2&z=3&s=", + pathname: "/vt/lyrs=m@114", + path: "/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=", + }, + + "http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=": { + href: "http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=", + protocol: "http:", + slashes: true, + host: "mt0.google.com", + auth: "user:pass", + hostname: "mt0.google.com", + search: "???&hl=en&src=api&x=2&y=2&z=3&s=", + query: "??&hl=en&src=api&x=2&y=2&z=3&s=", + pathname: "/vt/lyrs=m@114", + path: "/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=", + }, + + "file:///etc/passwd": { + href: "file:///etc/passwd", + slashes: true, + protocol: "file:", + pathname: "/etc/passwd", + hostname: "", + host: "", + path: "/etc/passwd", + }, + + "file://localhost/etc/passwd": { + href: "file://localhost/etc/passwd", + protocol: "file:", + slashes: true, + pathname: "/etc/passwd", + hostname: "localhost", + host: "localhost", + path: "/etc/passwd", + }, + + "file://foo/etc/passwd": { + href: "file://foo/etc/passwd", + protocol: "file:", + slashes: true, + pathname: "/etc/passwd", + hostname: "foo", + host: "foo", + path: "/etc/passwd", + }, + + "file:///etc/node/": { + href: "file:///etc/node/", + slashes: true, + protocol: "file:", + pathname: "/etc/node/", + hostname: "", + host: "", + path: "/etc/node/", + }, + + "file://localhost/etc/node/": { + href: "file://localhost/etc/node/", + protocol: "file:", + slashes: true, + pathname: "/etc/node/", + hostname: "localhost", + host: "localhost", + path: "/etc/node/", + }, + + "file://foo/etc/node/": { + href: "file://foo/etc/node/", + protocol: "file:", + slashes: true, + pathname: "/etc/node/", + hostname: "foo", + host: "foo", + path: "/etc/node/", + }, + + "http:/baz/../foo/bar": { + href: "http:/baz/../foo/bar", + protocol: "http:", + pathname: "/baz/../foo/bar", + path: "/baz/../foo/bar", + }, + + "http://user:pass@example.com:8000/foo/bar?baz=quux#frag": { + href: "http://user:pass@example.com:8000/foo/bar?baz=quux#frag", + protocol: "http:", + slashes: true, + host: "example.com:8000", + auth: "user:pass", + port: "8000", + hostname: "example.com", + hash: "#frag", + search: "?baz=quux", + query: "baz=quux", + pathname: "/foo/bar", + path: "/foo/bar?baz=quux", + }, + + "//user:pass@example.com:8000/foo/bar?baz=quux#frag": { + href: "//user:pass@example.com:8000/foo/bar?baz=quux#frag", + slashes: true, + host: "example.com:8000", + auth: "user:pass", + port: "8000", + hostname: "example.com", + hash: "#frag", + search: "?baz=quux", + query: "baz=quux", + pathname: "/foo/bar", + path: "/foo/bar?baz=quux", + }, + + "/foo/bar?baz=quux#frag": { + href: "/foo/bar?baz=quux#frag", + hash: "#frag", + search: "?baz=quux", + query: "baz=quux", + pathname: "/foo/bar", + path: "/foo/bar?baz=quux", + }, + + "http:/foo/bar?baz=quux#frag": { + href: "http:/foo/bar?baz=quux#frag", + protocol: "http:", + hash: "#frag", + search: "?baz=quux", + query: "baz=quux", + pathname: "/foo/bar", + path: "/foo/bar?baz=quux", + }, + + "mailto:foo@bar.com?subject=hello": { + href: "mailto:foo@bar.com?subject=hello", + protocol: "mailto:", + host: "bar.com", + auth: "foo", + hostname: "bar.com", + search: "?subject=hello", + query: "subject=hello", + path: "?subject=hello", + }, + + "javascript:alert('hello');": { + href: "javascript:alert('hello');", + protocol: "javascript:", + pathname: "alert('hello');", + path: "alert('hello');", + }, + + "xmpp:isaacschlueter@jabber.org": { + href: "xmpp:isaacschlueter@jabber.org", + protocol: "xmpp:", + host: "jabber.org", + auth: "isaacschlueter", + hostname: "jabber.org", + }, + + "http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar": { + href: "http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar", + protocol: "http:", + slashes: true, + host: "127.0.0.1:8080", + auth: "atpass:foo@bar", + hostname: "127.0.0.1", + port: "8080", + pathname: "/path", + search: "?search=foo", + query: "search=foo", + hash: "#bar", + path: "/path?search=foo", + }, + + "svn+ssh://foo/bar": { + href: "svn+ssh://foo/bar", + host: "foo", + hostname: "foo", + protocol: "svn+ssh:", + pathname: "/bar", + path: "/bar", + slashes: true, + }, + + "dash-test://foo/bar": { + href: "dash-test://foo/bar", + host: "foo", + hostname: "foo", + protocol: "dash-test:", + pathname: "/bar", + path: "/bar", + slashes: true, + }, + + "dash-test:foo/bar": { + href: "dash-test:foo/bar", + host: "foo", + hostname: "foo", + protocol: "dash-test:", + pathname: "/bar", + path: "/bar", + }, + + "dot.test://foo/bar": { + href: "dot.test://foo/bar", + host: "foo", + hostname: "foo", + protocol: "dot.test:", + pathname: "/bar", + path: "/bar", + slashes: true, + }, + + "dot.test:foo/bar": { + href: "dot.test:foo/bar", + host: "foo", + hostname: "foo", + protocol: "dot.test:", + pathname: "/bar", + path: "/bar", + }, + + // IDNA tests + "http://www.日本語.com/": { + href: "http://www.xn--wgv71a119e.com/", + protocol: "http:", + slashes: true, + host: "www.xn--wgv71a119e.com", + hostname: "www.xn--wgv71a119e.com", + pathname: "/", + path: "/", + }, + + "http://example.Bücher.com/": { + href: "http://example.xn--bcher-kva.com/", + protocol: "http:", + slashes: true, + host: "example.xn--bcher-kva.com", + hostname: "example.xn--bcher-kva.com", + pathname: "/", + path: "/", + }, + + "http://www.Äffchen.com/": { + href: "http://www.xn--ffchen-9ta.com/", + protocol: "http:", + slashes: true, + host: "www.xn--ffchen-9ta.com", + hostname: "www.xn--ffchen-9ta.com", + pathname: "/", + path: "/", + }, + + "http://www.Äffchen.cOm;A/b/c?d=e#f gi": { + href: "http://www.xn--ffchen-9ta.com/;A/b/c?d=e#f%20g%3Ch%3Ei", + protocol: "http:", + slashes: true, + host: "www.xn--ffchen-9ta.com", + hostname: "www.xn--ffchen-9ta.com", + pathname: ";A/b/c", + search: "?d=e", + query: "d=e", + hash: "#f%20g%3Ch%3Ei", + path: ";A/b/c?d=e", + }, + + "http://SÉLIER.COM/": { + href: "http://xn--slier-bsa.com/", + protocol: "http:", + slashes: true, + host: "xn--slier-bsa.com", + hostname: "xn--slier-bsa.com", + pathname: "/", + path: "/", + }, + + "http://ليهمابتكلموشعربي؟.ي؟/": { + href: "http://xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f/", + protocol: "http:", + slashes: true, + host: "xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f", + hostname: "xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f", + pathname: "/", + path: "/", + }, + + "http://➡.ws/➡": { + href: "http://xn--hgi.ws/➡", + protocol: "http:", + slashes: true, + host: "xn--hgi.ws", + hostname: "xn--hgi.ws", + pathname: "/➡", + path: "/➡", + }, + + "http://bucket_name.s3.amazonaws.com/image.jpg": { + protocol: "http:", + slashes: true, + host: "bucket_name.s3.amazonaws.com", + hostname: "bucket_name.s3.amazonaws.com", + pathname: "/image.jpg", + href: "http://bucket_name.s3.amazonaws.com/image.jpg", + path: "/image.jpg", + }, + + "git+http://github.com/joyent/node.git": { + protocol: "git+http:", + slashes: true, + host: "github.com", + hostname: "github.com", + pathname: "/joyent/node.git", + path: "/joyent/node.git", + href: "git+http://github.com/joyent/node.git", + }, + + // If local1@domain1 is uses as a relative URL it may + // be parse into auth@hostname, but here there is no + // way to make it work in URL.parse, I add the test to be explicit + "local1@domain1": { + pathname: "local1@domain1", + path: "local1@domain1", + href: "local1@domain1", + }, + + // While this may seem counter-intuitive, a browser will parse + // as a path. + "www.example.com": { + href: "www.example.com", + pathname: "www.example.com", + path: "www.example.com", + }, + + // ipv6 support + "[fe80::1]": { + href: "[fe80::1]", + pathname: "[fe80::1]", + path: "[fe80::1]", + }, + + "coap://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]": { + protocol: "coap:", + slashes: true, + host: "[fedc:ba98:7654:3210:fedc:ba98:7654:3210]", + hostname: "fedc:ba98:7654:3210:fedc:ba98:7654:3210", + href: "coap://[fedc:ba98:7654:3210:fedc:ba98:7654:3210]/", + pathname: "/", + path: "/", + }, + // TODO: Support parsing this. + // + // "coap://[1080:0:0:0:8:800:200C:417A]:61616/": { + // protocol: "coap:", + // slashes: true, + // host: "[1080:0:0:0:8:800:200c:417a]:61616", + // port: "61616", + // hostname: "1080:0:0:0:8:800:200c:417a", + // href: "coap://[1080:0:0:0:8:800:200c:417a]:61616/", + // pathname: "/", + // path: "/", + // }, + + "http://user:password@[3ffe:2a00:100:7031::1]:8080": { + protocol: "http:", + slashes: true, + auth: "user:password", + host: "[3ffe:2a00:100:7031::1]:8080", + port: "8080", + hostname: "3ffe:2a00:100:7031::1", + href: "http://user:password@[3ffe:2a00:100:7031::1]:8080/", + pathname: "/", + path: "/", + }, + // TODO: Support parsing this. + // + // "coap://u:p@[::192.9.5.5]:61616/.well-known/r?n=Temperature": { + // protocol: "coap:", + // slashes: true, + // auth: "u:p", + // host: "[::192.9.5.5]:61616", + // port: "61616", + // hostname: "::192.9.5.5", + // href: "coap://u:p@[::192.9.5.5]:61616/.well-known/r?n=Temperature", + // search: "?n=Temperature", + // query: "n=Temperature", + // pathname: "/.well-known/r", + // path: "/.well-known/r?n=Temperature", + // }, + + // empty port + "http://example.com:": { + protocol: "http:", + slashes: true, + host: "example.com", + hostname: "example.com", + href: "http://example.com/", + pathname: "/", + path: "/", + }, + + "http://example.com:/a/b.html": { + protocol: "http:", + slashes: true, + host: "example.com", + hostname: "example.com", + href: "http://example.com/a/b.html", + pathname: "/a/b.html", + path: "/a/b.html", + }, + + "http://example.com:?a=b": { + protocol: "http:", + slashes: true, + host: "example.com", + hostname: "example.com", + href: "http://example.com/?a=b", + search: "?a=b", + query: "a=b", + pathname: "/", + path: "/?a=b", + }, + + "http://example.com:#abc": { + protocol: "http:", + slashes: true, + host: "example.com", + hostname: "example.com", + href: "http://example.com/#abc", + hash: "#abc", + pathname: "/", + path: "/", + }, + + "http://[fe80::1]:/a/b?a=b#abc": { + protocol: "http:", + slashes: true, + host: "[fe80::1]", + hostname: "fe80::1", + href: "http://[fe80::1]/a/b?a=b#abc", + search: "?a=b", + query: "a=b", + hash: "#abc", + pathname: "/a/b", + path: "/a/b?a=b", + }, + + "http://-lovemonsterz.tumblr.com/rss": { + protocol: "http:", + slashes: true, + host: "-lovemonsterz.tumblr.com", + hostname: "-lovemonsterz.tumblr.com", + href: "http://-lovemonsterz.tumblr.com/rss", + pathname: "/rss", + path: "/rss", + }, + + "http://-lovemonsterz.tumblr.com:80/rss": { + protocol: "http:", + slashes: true, + port: "80", + host: "-lovemonsterz.tumblr.com:80", + hostname: "-lovemonsterz.tumblr.com", + href: "http://-lovemonsterz.tumblr.com:80/rss", + pathname: "/rss", + path: "/rss", + }, + + "http://user:pass@-lovemonsterz.tumblr.com/rss": { + protocol: "http:", + slashes: true, + auth: "user:pass", + host: "-lovemonsterz.tumblr.com", + hostname: "-lovemonsterz.tumblr.com", + href: "http://user:pass@-lovemonsterz.tumblr.com/rss", + pathname: "/rss", + path: "/rss", + }, + + "http://user:pass@-lovemonsterz.tumblr.com:80/rss": { + protocol: "http:", + slashes: true, + auth: "user:pass", + port: "80", + host: "-lovemonsterz.tumblr.com:80", + hostname: "-lovemonsterz.tumblr.com", + href: "http://user:pass@-lovemonsterz.tumblr.com:80/rss", + pathname: "/rss", + path: "/rss", + }, + + "http://_jabber._tcp.google.com/test": { + protocol: "http:", + slashes: true, + host: "_jabber._tcp.google.com", + hostname: "_jabber._tcp.google.com", + href: "http://_jabber._tcp.google.com/test", + pathname: "/test", + path: "/test", + }, + + "http://user:pass@_jabber._tcp.google.com/test": { + protocol: "http:", + slashes: true, + auth: "user:pass", + host: "_jabber._tcp.google.com", + hostname: "_jabber._tcp.google.com", + href: "http://user:pass@_jabber._tcp.google.com/test", + pathname: "/test", + path: "/test", + }, + + "http://_jabber._tcp.google.com:80/test": { + protocol: "http:", + slashes: true, + port: "80", + host: "_jabber._tcp.google.com:80", + hostname: "_jabber._tcp.google.com", + href: "http://_jabber._tcp.google.com:80/test", + pathname: "/test", + path: "/test", + }, + + "http://user:pass@_jabber._tcp.google.com:80/test": { + protocol: "http:", + slashes: true, + auth: "user:pass", + port: "80", + host: "_jabber._tcp.google.com:80", + hostname: "_jabber._tcp.google.com", + href: "http://user:pass@_jabber._tcp.google.com:80/test", + pathname: "/test", + path: "/test", + }, + + "http://x:1/' <>\"`/{}|\\^~`/": { + protocol: "http:", + slashes: true, + host: "x:1", + port: "1", + hostname: "x", + pathname: "/%27%20%3C%3E%22%60/%7B%7D%7C/%5E~%60/", + path: "/%27%20%3C%3E%22%60/%7B%7D%7C/%5E~%60/", + href: "http://x:1/%27%20%3C%3E%22%60/%7B%7D%7C/%5E~%60/", + }, + + "http://a@b@c/": { + protocol: "http:", + slashes: true, + auth: "a@b", + host: "c", + hostname: "c", + href: "http://a%40b@c/", + path: "/", + pathname: "/", + }, + + "http://a@b?@c": { + protocol: "http:", + slashes: true, + auth: "a", + host: "b", + hostname: "b", + href: "http://a@b/?@c", + path: "/?@c", + pathname: "/", + search: "?@c", + query: "@c", + }, + // TODO: Support parsing these. + // + // "http://a.b/\tbc\ndr\ref g\"hq'j?mn\\op^q=r`99{st|uv}wz": { + // protocol: "http:", + // slashes: true, + // host: "a.b", + // port: null, + // hostname: "a.b", + // hash: null, + // pathname: "/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E", + // path: "/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz", + // search: "?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz", + // query: "mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz", + // href: "http://a.b/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz", + // }, + + // "http://a\r\" \t\n<'b:b@c\r\nd/e?f": { + // protocol: "http:", + // slashes: true, + // auth: "a\" <'b:b", + // host: "cd", + // port: null, + // hostname: "cd", + // hash: null, + // search: "?f", + // query: "f", + // pathname: "/e", + // path: "/e?f", + // href: "http://a%22%20%3C'b:b@cd/e?f", + // }, + + // Git urls used by npm + "git+ssh://git@github.com:npm/npm": { + protocol: "git+ssh:", + slashes: true, + auth: "git", + host: "github.com", + port: null, + hostname: "github.com", + hash: null, + search: null, + query: null, + pathname: "/:npm/npm", + path: "/:npm/npm", + href: "git+ssh://git@github.com/:npm/npm", + }, + // TODO: Support parsing these. + // + // "https://*": { + // protocol: "https:", + // slashes: true, + // auth: null, + // host: "*", + // port: null, + // hostname: "*", + // hash: null, + // search: null, + // query: null, + // pathname: "/", + // path: "/", + // href: "https://*/", + // }, + + // The following two URLs are the same, but they differ for a capital A. + // Verify that the protocol is checked in a case-insensitive manner. + // "javascript:alert(1);a=\x27@white-listed.com\x27": { + // protocol: "javascript:", + // slashes: null, + // auth: null, + // host: null, + // port: null, + // hostname: null, + // hash: null, + // search: null, + // query: null, + // pathname: "alert(1);a='@white-listed.com'", + // path: "alert(1);a='@white-listed.com'", + // href: "javascript:alert(1);a='@white-listed.com'", + // }, + + // "javAscript:alert(1);a=\x27@white-listed.com\x27": { + // protocol: "javascript:", + // slashes: null, + // auth: null, + // host: null, + // port: null, + // hostname: null, + // hash: null, + // search: null, + // query: null, + // pathname: "alert(1);a='@white-listed.com'", + // path: "alert(1);a='@white-listed.com'", + // href: "javascript:alert(1);a='@white-listed.com'", + // }, + + // "ws://www.example.com": { + // protocol: "ws:", + // slashes: true, + // hostname: "www.example.com", + // host: "www.example.com", + // pathname: "/", + // path: "/", + // href: "ws://www.example.com/", + // }, + + // "wss://www.example.com": { + // protocol: "wss:", + // slashes: true, + // hostname: "www.example.com", + // host: "www.example.com", + // pathname: "/", + // path: "/", + // href: "wss://www.example.com/", + // }, + + // "//fhqwhgads@example.com/everybody-to-the-limit": { + // protocol: null, + // slashes: true, + // auth: "fhqwhgads", + // host: "example.com", + // port: null, + // hostname: "example.com", + // hash: null, + // search: null, + // query: null, + // pathname: "/everybody-to-the-limit", + // path: "/everybody-to-the-limit", + // href: "//fhqwhgads@example.com/everybody-to-the-limit", + // }, + + "//fhqwhgads@example.com/everybody#to-the-limit": { + protocol: null, + slashes: true, + auth: "fhqwhgads", + host: "example.com", + port: null, + hostname: "example.com", + hash: "#to-the-limit", + search: null, + query: null, + pathname: "/everybody", + path: "/everybody", + href: "//fhqwhgads@example.com/everybody#to-the-limit", + }, + // TODO: Support parsing these. + // + // "\bhttp://example.com/\b": { + // protocol: "http:", + // slashes: true, + // auth: null, + // host: "example.com", + // port: null, + // hostname: "example.com", + // hash: null, + // search: null, + // query: null, + // pathname: "/", + // path: "/", + // href: "http://example.com/", + // }, + + // "https://evil.com$.example.com": { + // protocol: "https:", + // slashes: true, + // auth: null, + // host: "evil.com$.example.com", + // port: null, + // hostname: "evil.com$.example.com", + // hash: null, + // search: null, + // query: null, + // pathname: "/", + // path: "/", + // href: "https://evil.com$.example.com/", + // }, + + // Validate the output of hostname with commas. + // "x://0.0,1.1/": { + // protocol: "x:", + // slashes: true, + // auth: null, + // host: "0.0,1.1", + // port: null, + // hostname: "0.0,1.1", + // hash: null, + // search: null, + // query: null, + // pathname: "/", + // path: "/", + // href: "x://0.0,1.1/", + // }, + }; + + test("url.parse", () => { + for (const u in parseTests) { + let actual = url.parse(u); + const spaced = url.parse(` \t ${u}\n\t`); + let expected = Object.assign(new url.Url(), parseTests[u]); + + Object.keys(actual).forEach(function (i) { + if (expected[i] === undefined && actual[i] === null) { + expected[i] = null; + } + }); + + assert.deepStrictEqual( + actual, + expected, + `parsing ${u} and expected ${inspect(expected)} but got ${inspect(actual)}`, + ); + assert.deepStrictEqual(spaced, expected, `expected ${inspect(expected)}, got ${inspect(spaced)}`); + } + }); + + test("url.format", () => { + for (const u in parseTests) { + const expected = parseTests[u].href; + const actual = url.format(parseTests[u]); + + assert.strictEqual(actual, expected, `format(${u}) == ${u}\nactual:${actual}`); + } + }); + + // TODO: Support parsing this. + test.todo("xss", () => { + const parsed = url.parse("http://nodejs.org/").resolveObject("jAvascript:alert(1);a=\x27@white-listed.com\x27"); + + const expected = Object.assign(new url.Url(), { + protocol: "javascript:", + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: null, + query: null, + pathname: "alert(1);a='@white-listed.com'", + path: "alert(1);a='@white-listed.com'", + href: "javascript:alert(1);a='@white-listed.com'", + }); + + assert.deepStrictEqual(parsed, expected); + }); +}); diff --git a/test/js/node/url/url-parse-invalid-input.test.js b/test/js/node/url/url-parse-invalid-input.test.js new file mode 100644 index 0000000000..e8b9b1831c --- /dev/null +++ b/test/js/node/url/url-parse-invalid-input.test.js @@ -0,0 +1,121 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import url from "node:url"; + +describe("url.parse", () => { + // TODO: Support error code. + test.todo("invalid input", () => { + // https://github.com/joyent/node/issues/568 + [ + [undefined, "undefined"], + [null, "object"], + [true, "boolean"], + [false, "boolean"], + [0.0, "number"], + [0, "number"], + [[], "object"], + [{}, "object"], + [() => {}, "function"], + [Symbol("foo"), "symbol"], + ].forEach(([val, type]) => { + assert.throws( + () => { + url.parse(val); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + message: 'The "url" argument must be of type string.', + }, + ); + }); + + assert.throws( + () => { + url.parse("http://%E0%A4%A@fail"); + }, + e => { + // The error should be a URIError. + if (!(e instanceof URIError)) return false; + + // The error should be from the JS engine and not from Node.js. + // JS engine errors do not have the `code` property. + return e.code === undefined; + }, + ); + + assert.throws( + () => { + url.parse("http://[127.0.0.1\x00c8763]:8000/"); + }, + { code: "ERR_INVALID_URL", input: "http://[127.0.0.1\x00c8763]:8000/" }, + ); + + if (common.hasIntl) { + // An array of Unicode code points whose Unicode NFKD contains a "bad + // character". + const badIDNA = (() => { + const BAD_CHARS = "#%/:?@[\\]^|"; + const out = []; + for (let i = 0x80; i < 0x110000; i++) { + const cp = String.fromCodePoint(i); + for (const badChar of BAD_CHARS) { + if (cp.normalize("NFKD").includes(badChar)) { + out.push(cp); + } + } + } + return out; + })(); + + // The generation logic above should at a minimum produce these two + // characters. + assert(badIDNA.includes("℀")); + assert(badIDNA.includes("@")); + + for (const badCodePoint of badIDNA) { + const badURL = `http://fail${badCodePoint}fail.com/`; + assert.throws( + () => { + url.parse(badURL); + }, + e => e.code === "ERR_INVALID_URL", + `parsing ${badURL}`, + ); + } + + assert.throws( + () => { + url.parse("http://\u00AD/bad.com/"); + }, + e => e.code === "ERR_INVALID_URL", + "parsing http://\u00AD/bad.com/", + ); + } + + { + const badURLs = ["https://evil.com:.example.com", "git+ssh://git@github.com:npm/npm"]; + badURLs.forEach(badURL => { + common.spawnPromisified(process.execPath, ["-e", `url.parse(${JSON.stringify(badURL)})`]).then( + common.mustCall(({ code, stdout, stderr }) => { + assert.strictEqual(code, 0); + assert.strictEqual(stdout, ""); + assert.match(stderr, /\[DEP0170\] DeprecationWarning:/); + }), + ); + }); + + // Warning should only happen once per process. + const expectedWarning = [ + `The URL ${badURLs[0]} is invalid. Future versions of Node.js will throw an error.`, + "DEP0170", + ]; + common.expectWarning({ + DeprecationWarning: expectedWarning, + }); + badURLs.forEach(badURL => { + url.parse(badURL); + }); + } + }); +}); diff --git a/test/js/node/url/url-parse-query.test.js b/test/js/node/url/url-parse-query.test.js new file mode 100644 index 0000000000..80f156e731 --- /dev/null +++ b/test/js/node/url/url-parse-query.test.js @@ -0,0 +1,93 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import url from "node:url"; + +describe("url.parse", () => { + // TODO: Support correct prototype and null values. + test.todo("with query string", () => { + function createWithNoPrototype(properties = []) { + const noProto = { __proto__: null }; + properties.forEach(property => { + noProto[property.key] = property.value; + }); + return noProto; + } + + function check(actual, expected) { + assert.notStrictEqual(Object.getPrototypeOf(actual), Object.prototype); + assert.deepStrictEqual(Object.keys(actual).sort(), Object.keys(expected).sort()); + Object.keys(expected).forEach(function (key) { + assert.deepStrictEqual(actual[key], expected[key]); + }); + } + + const parseTestsWithQueryString = { + "/foo/bar?baz=quux#frag": { + href: "/foo/bar?baz=quux#frag", + hash: "#frag", + search: "?baz=quux", + query: createWithNoPrototype([{ key: "baz", value: "quux" }]), + pathname: "/foo/bar", + path: "/foo/bar?baz=quux", + }, + "http://example.com": { + href: "http://example.com/", + protocol: "http:", + slashes: true, + host: "example.com", + hostname: "example.com", + query: createWithNoPrototype(), + search: null, + pathname: "/", + path: "/", + }, + "/example": { + protocol: null, + slashes: null, + auth: undefined, + host: null, + port: null, + hostname: null, + hash: null, + search: null, + query: createWithNoPrototype(), + pathname: "/example", + path: "/example", + href: "/example", + }, + "/example?query=value": { + protocol: null, + slashes: null, + auth: undefined, + host: null, + port: null, + hostname: null, + hash: null, + search: "?query=value", + query: createWithNoPrototype([{ key: "query", value: "value" }]), + pathname: "/example", + path: "/example?query=value", + href: "/example?query=value", + }, + }; + for (const u in parseTestsWithQueryString) { + const actual = url.parse(u, true); + const expected = Object.assign(new url.Url(), parseTestsWithQueryString[u]); + for (const i in actual) { + if (actual[i] === null && expected[i] === undefined) { + expected[i] = null; + } + } + + const properties = Object.keys(actual).sort(); + assert.deepStrictEqual(properties, Object.keys(expected).sort()); + properties.forEach(property => { + if (property === "query") { + check(actual[property], expected[property]); + } else { + assert.deepStrictEqual(actual[property], expected[property]); + } + }); + } + }); +}); diff --git a/test/js/node/url/url-pathtofileurl.test.js b/test/js/node/url/url-pathtofileurl.test.js new file mode 100644 index 0000000000..bdab051b4c --- /dev/null +++ b/test/js/node/url/url-pathtofileurl.test.js @@ -0,0 +1,203 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import url from "node:url"; + +const isWindows = process.platform === "win32"; + +describe("url.pathToFileURL", () => { + // TODO: Fix these asserts on Windows. + test.skipIf(isWindows)("dangling slashes and percent sign", () => { + { + const fileURL = url.pathToFileURL("test/").href; + assert.ok(fileURL.startsWith("file:///")); + assert.ok(fileURL.endsWith("/")); + } + + // TODO: Support these. + // + // { + // const fileURL = url.pathToFileURL("test\\").href; + // assert.ok(fileURL.startsWith("file:///")); + // if (isWindows) assert.ok(fileURL.endsWith("/")); + // else assert.ok(fileURL.endsWith("%5C")); + // } + + // { + // const fileURL = url.pathToFileURL("test/%").href; + // assert.ok(fileURL.includes("%25")); + // } + }); + + // TODO: Support UNC paths across platforms. + test.todo("UNC paths", () => { + if (isWindows) { + // UNC path: \\server\share\resource + + // Missing server: + assert.throws(() => url.pathToFileURL("\\\\\\no-server"), { + code: "ERR_INVALID_ARG_VALUE", + }); + + // Missing share or resource: + assert.throws(() => url.pathToFileURL("\\\\host"), { + code: "ERR_INVALID_ARG_VALUE", + }); + + // Regression test for direct String.prototype.startsWith call + assert.throws(() => url.pathToFileURL(["\\\\", { [Symbol.toPrimitive]: () => "blep\\blop" }]), { + code: "ERR_INVALID_ARG_TYPE", + }); + assert.throws(() => url.pathToFileURL(["\\\\", "blep\\blop"]), { + code: "ERR_INVALID_ARG_TYPE", + }); + assert.throws( + () => + url.pathToFileURL({ + [Symbol.toPrimitive]: () => "\\\\blep\\blop", + }), + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } else { + // UNC paths on posix are considered a single path that has backslashes: + const fileURL = url.pathToFileURL("\\\\nas\\share\\path.txt").href; + assert.match(fileURL, /file:\/\/.+%5C%5Cnas%5Cshare%5Cpath\.txt$/); + } + }); + + test("general", () => { + let testCases; + if (isWindows) { + testCases = [ + // Lowercase ascii alpha + { path: "C:\\foo", expected: "file:///C:/foo" }, + // Uppercase ascii alpha + { path: "C:\\FOO", expected: "file:///C:/FOO" }, + // dir + { path: "C:\\dir\\foo", expected: "file:///C:/dir/foo" }, + // trailing separator + { path: "C:\\dir\\", expected: "file:///C:/dir/" }, + // dot + { path: "C:\\foo.mjs", expected: "file:///C:/foo.mjs" }, + // space + { path: "C:\\foo bar", expected: "file:///C:/foo%20bar" }, + // question mark + { path: "C:\\foo?bar", expected: "file:///C:/foo%3Fbar" }, + // number sign + { path: "C:\\foo#bar", expected: "file:///C:/foo%23bar" }, + // ampersand + { path: "C:\\foo&bar", expected: "file:///C:/foo&bar" }, + // equals + { path: "C:\\foo=bar", expected: "file:///C:/foo=bar" }, + // colon + { path: "C:\\foo:bar", expected: "file:///C:/foo:bar" }, + // semicolon + { path: "C:\\foo;bar", expected: "file:///C:/foo;bar" }, + // TODO: Support these. + // + // percent + // { path: "C:\\foo%bar", expected: "file:///C:/foo%25bar" }, + // backslash + // { path: "C:\\foo\\bar", expected: "file:///C:/foo/bar" }, + // backspace + // { path: "C:\\foo\bbar", expected: "file:///C:/foo%08bar" }, + // tab + // { path: "C:\\foo\tbar", expected: "file:///C:/foo%09bar" }, + // newline + // { path: "C:\\foo\nbar", expected: "file:///C:/foo%0Abar" }, + // carriage return + // { path: "C:\\foo\rbar", expected: "file:///C:/foo%0Dbar" }, + // latin1 + { path: "C:\\fóóbàr", expected: "file:///C:/f%C3%B3%C3%B3b%C3%A0r" }, + // Euro sign (BMP code point) + { path: "C:\\€", expected: "file:///C:/%E2%82%AC" }, + // Rocket emoji (non-BMP code point) + { path: "C:\\🚀", expected: "file:///C:/%F0%9F%9A%80" }, + // UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows) + { path: "\\\\nas\\My Docs\\File.doc", expected: "file://nas/My%20Docs/File.doc" }, + ]; + } else { + testCases = [ + // Lowercase ascii alpha + { path: "/foo", expected: "file:///foo" }, + // Uppercase ascii alpha + { path: "/FOO", expected: "file:///FOO" }, + // dir + { path: "/dir/foo", expected: "file:///dir/foo" }, + // trailing separator + { path: "/dir/", expected: "file:///dir/" }, + // dot + { path: "/foo.mjs", expected: "file:///foo.mjs" }, + // space + { path: "/foo bar", expected: "file:///foo%20bar" }, + // question mark + { path: "/foo?bar", expected: "file:///foo%3Fbar" }, + // number sign + { path: "/foo#bar", expected: "file:///foo%23bar" }, + // ampersand + { path: "/foo&bar", expected: "file:///foo&bar" }, + // equals + { path: "/foo=bar", expected: "file:///foo=bar" }, + // colon + { path: "/foo:bar", expected: "file:///foo:bar" }, + // semicolon + { path: "/foo;bar", expected: "file:///foo;bar" }, + // TODO: Support these. + // + // percent + // { path: "/foo%bar", expected: "file:///foo%25bar" }, + // backslash + // { path: "/foo\\bar", expected: "file:///foo%5Cbar" }, + // backspace + //{ path: "/foo\bbar", expected: "file:///foo%08bar" }, + // tab + // { path: "/foo\tbar", expected: "file:///foo%09bar" }, + // newline + // { path: "/foo\nbar", expected: "file:///foo%0Abar" }, + // carriage return + // { path: "/foo\rbar", expected: "file:///foo%0Dbar" }, + // latin1 + { path: "/fóóbàr", expected: "file:///f%C3%B3%C3%B3b%C3%A0r" }, + // Euro sign (BMP code point) + { path: "/€", expected: "file:///%E2%82%AC" }, + // Rocket emoji (non-BMP code point) + { path: "/🚀", expected: "file:///%F0%9F%9A%80" }, + ]; + } + + for (const { path, expected } of testCases) { + const actual = url.pathToFileURL(path).href; + assert.strictEqual(actual, expected); + } + }); + + // TODO: Support throwing correct exception for non-string params. + test.todo("non-string parameter", () => { + for (const badPath of [ + undefined, + null, + true, + 42, + 42n, + Symbol("42"), + NaN, + {}, + [], + () => {}, + Promise.resolve("foo"), + new Date(), + new String("notPrimitive"), + { + toString() { + return "amObject"; + }, + }, + { [Symbol.toPrimitive]: hint => "amObject" }, + ]) { + assert.throws(() => url.pathToFileURL(badPath), { + code: "ERR_INVALID_ARG_TYPE", + }); + } + }); +}); diff --git a/test/js/node/url/url-relative.test.js b/test/js/node/url/url-relative.test.js new file mode 100644 index 0000000000..04bc0b6eaf --- /dev/null +++ b/test/js/node/url/url-relative.test.js @@ -0,0 +1,420 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import { inspect } from "node:util"; +import url from "node:url"; + +// [from, path, expected] +const relativeTests = [ + ["/foo/bar/baz", "quux", "/foo/bar/quux"], + ["/foo/bar/baz", "quux/asdf", "/foo/bar/quux/asdf"], + ["/foo/bar/baz", "quux/baz", "/foo/bar/quux/baz"], + ["/foo/bar/baz", "../quux/baz", "/foo/quux/baz"], + ["/foo/bar/baz", "/bar", "/bar"], + ["/foo/bar/baz/", "quux", "/foo/bar/baz/quux"], + ["/foo/bar/baz/", "quux/baz", "/foo/bar/baz/quux/baz"], + ["/foo/bar/baz", "../../../../../../../../quux/baz", "/quux/baz"], + ["/foo/bar/baz", "../../../../../../../quux/baz", "/quux/baz"], + ["/foo", ".", "/"], + ["/foo", "..", "/"], + ["/foo/", ".", "/foo/"], + ["/foo/", "..", "/"], + ["/foo/bar", ".", "/foo/"], + ["/foo/bar", "..", "/"], + ["/foo/bar/", ".", "/foo/bar/"], + ["/foo/bar/", "..", "/foo/"], + ["foo/bar", "../../../baz", "../../baz"], + ["foo/bar/", "../../../baz", "../baz"], + ["http://example.com/b//c//d;p?q#blarg", "https:#hash2", "https:///#hash2"], + ["http://example.com/b//c//d;p?q#blarg", "https:/p/a/t/h?s#hash2", "https://p/a/t/h?s#hash2"], + ["http://example.com/b//c//d;p?q#blarg", "https://u:p@h.com/p/a/t/h?s#hash2", "https://u:p@h.com/p/a/t/h?s#hash2"], + ["http://example.com/b//c//d;p?q#blarg", "https:/a/b/c/d", "https://a/b/c/d"], + ["http://example.com/b//c//d;p?q#blarg", "http:#hash2", "http://example.com/b//c//d;p?q#hash2"], + ["http://example.com/b//c//d;p?q#blarg", "http:/p/a/t/h?s#hash2", "http://example.com/p/a/t/h?s#hash2"], + ["http://example.com/b//c//d;p?q#blarg", "http://u:p@h.com/p/a/t/h?s#hash2", "http://u:p@h.com/p/a/t/h?s#hash2"], + ["http://example.com/b//c//d;p?q#blarg", "http:/a/b/c/d", "http://example.com/a/b/c/d"], + ["/foo/bar/baz", "/../etc/passwd", "/etc/passwd"], + // TODO: Support this. + // + // ["http://localhost", "file:///Users/foo", "file:///Users/foo"], + ["http://localhost", "file://foo/Users", "file://foo/Users"], + ["https://registry.npmjs.org", "@foo/bar", "https://registry.npmjs.org/@foo/bar"], +]; + +describe("url.resolveObject", () => { + test("source is false", () => { + // When source is false + assert.strictEqual(url.resolveObject("", "foo"), "foo"); + }); + + test("url.resolveObject and url.parse are inverse operations", () => { + // If format and parse are inverse operations then + // resolveObject(parse(x), y) == parse(resolve(x, y)) + + // format: [from, path, expected] + for (let i = 0; i < relativeTests.length; i++) { + const relativeTest = relativeTests[i]; + + let actual = url.resolveObject(url.parse(relativeTest[0]), relativeTest[1]); + let expected = url.parse(relativeTest[2]); + + assert.deepStrictEqual(actual, expected); + + expected = relativeTest[2]; + actual = url.format(actual); + + assert.strictEqual(actual, expected, `format(${actual}) == ${expected}\n` + `actual: ${actual}`); + } + }); +}); + +describe("url.resolve", () => { + test("relative paths", () => { + for (let i = 0; i < relativeTests.length; i++) { + const relativeTest = relativeTests[i]; + + const a = url.resolve(relativeTest[0], relativeTest[1]); + const e = relativeTest[2]; + assert.strictEqual(a, e, `resolve(${relativeTest[0]}, ${relativeTest[1]})` + ` == ${e}\n actual=${a}`); + } + }); +}); + +describe("chiron url.resolve rests", () => { + // + // Tests below taken from Chiron + // http://code.google.com/p/chironjs/source/browse/trunk/src/test/http/url.js + // + // Copyright (c) 2002-2008 Kris Kowal + // used with permission under MIT License + // + // Changes marked with @isaacs + const bases = [ + "http://a/b/c/d;p?q", + "http://a/b/c/d;p?q=1/2", + "http://a/b/c/d;p=1/2?q", + "fred:///s//a/b/c", + "http:///s//a/b/c", + ]; + + // [to, from, result] + const relativeTests2 = [ + // http://lists.w3.org/Archives/Public/uri/2004Feb/0114.html + ["../c", "foo:a/b", "foo:c"], + ["foo:.", "foo:a", "foo:"], + ["/foo/../../../bar", "zz:abc", "zz:/bar"], + ["/foo/../bar", "zz:abc", "zz:/bar"], + // @isaacs Disagree. Not how web browsers resolve this. + ["foo/../../../bar", "zz:abc", "zz:bar"], + // ['foo/../../../bar', 'zz:abc', 'zz:../../bar'], // @isaacs Added + ["foo/../bar", "zz:abc", "zz:bar"], + ["zz:.", "zz:abc", "zz:"], + ["/.", bases[0], "http://a/"], + ["/.foo", bases[0], "http://a/.foo"], + [".foo", bases[0], "http://a/b/c/.foo"], + + // http://gbiv.com/protocols/uri/test/rel_examples1.html + // examples from RFC 2396 + ["g:h", bases[0], "g:h"], + ["g", bases[0], "http://a/b/c/g"], + ["./g", bases[0], "http://a/b/c/g"], + ["g/", bases[0], "http://a/b/c/g/"], + ["/g", bases[0], "http://a/g"], + ["//g", bases[0], "http://g/"], + // Changed with RFC 2396bis + // ('?y', bases[0], 'http://a/b/c/d;p?y'], + ["?y", bases[0], "http://a/b/c/d;p?y"], + ["g?y", bases[0], "http://a/b/c/g?y"], + // Changed with RFC 2396bis + // ('#s', bases[0], CURRENT_DOC_URI + '#s'], + ["#s", bases[0], "http://a/b/c/d;p?q#s"], + ["g#s", bases[0], "http://a/b/c/g#s"], + ["g?y#s", bases[0], "http://a/b/c/g?y#s"], + [";x", bases[0], "http://a/b/c/;x"], + ["g;x", bases[0], "http://a/b/c/g;x"], + ["g;x?y#s", bases[0], "http://a/b/c/g;x?y#s"], + // Changed with RFC 2396bis + // ('', bases[0], CURRENT_DOC_URI], + ["", bases[0], "http://a/b/c/d;p?q"], + [".", bases[0], "http://a/b/c/"], + ["./", bases[0], "http://a/b/c/"], + ["..", bases[0], "http://a/b/"], + ["../", bases[0], "http://a/b/"], + ["../g", bases[0], "http://a/b/g"], + ["../..", bases[0], "http://a/"], + ["../../", bases[0], "http://a/"], + ["../../g", bases[0], "http://a/g"], + ["../../../g", bases[0], ("http://a/../g", "http://a/g")], + ["../../../../g", bases[0], ("http://a/../../g", "http://a/g")], + // Changed with RFC 2396bis + // ('/./g', bases[0], 'http://a/./g'], + ["/./g", bases[0], "http://a/g"], + // Changed with RFC 2396bis + // ('/../g', bases[0], 'http://a/../g'], + ["/../g", bases[0], "http://a/g"], + ["g.", bases[0], "http://a/b/c/g."], + [".g", bases[0], "http://a/b/c/.g"], + ["g..", bases[0], "http://a/b/c/g.."], + ["..g", bases[0], "http://a/b/c/..g"], + ["./../g", bases[0], "http://a/b/g"], + ["./g/.", bases[0], "http://a/b/c/g/"], + ["g/./h", bases[0], "http://a/b/c/g/h"], + ["g/../h", bases[0], "http://a/b/c/h"], + ["g;x=1/./y", bases[0], "http://a/b/c/g;x=1/y"], + ["g;x=1/../y", bases[0], "http://a/b/c/y"], + ["g?y/./x", bases[0], "http://a/b/c/g?y/./x"], + ["g?y/../x", bases[0], "http://a/b/c/g?y/../x"], + ["g#s/./x", bases[0], "http://a/b/c/g#s/./x"], + ["g#s/../x", bases[0], "http://a/b/c/g#s/../x"], + ["http:g", bases[0], ("http:g", "http://a/b/c/g")], + ["http:", bases[0], ("http:", bases[0])], + // Not sure where this one originated + ["/a/b/c/./../../g", bases[0], "http://a/a/g"], + + // http://gbiv.com/protocols/uri/test/rel_examples2.html + // slashes in base URI's query args + ["g", bases[1], "http://a/b/c/g"], + ["./g", bases[1], "http://a/b/c/g"], + ["g/", bases[1], "http://a/b/c/g/"], + ["/g", bases[1], "http://a/g"], + ["//g", bases[1], "http://g/"], + // Changed in RFC 2396bis + // ('?y', bases[1], 'http://a/b/c/?y'], + ["?y", bases[1], "http://a/b/c/d;p?y"], + ["g?y", bases[1], "http://a/b/c/g?y"], + ["g?y/./x", bases[1], "http://a/b/c/g?y/./x"], + ["g?y/../x", bases[1], "http://a/b/c/g?y/../x"], + ["g#s", bases[1], "http://a/b/c/g#s"], + ["g#s/./x", bases[1], "http://a/b/c/g#s/./x"], + ["g#s/../x", bases[1], "http://a/b/c/g#s/../x"], + ["./", bases[1], "http://a/b/c/"], + ["../", bases[1], "http://a/b/"], + ["../g", bases[1], "http://a/b/g"], + ["../../", bases[1], "http://a/"], + ["../../g", bases[1], "http://a/g"], + + // http://gbiv.com/protocols/uri/test/rel_examples3.html + // slashes in path params + // all of these changed in RFC 2396bis + ["g", bases[2], "http://a/b/c/d;p=1/g"], + ["./g", bases[2], "http://a/b/c/d;p=1/g"], + ["g/", bases[2], "http://a/b/c/d;p=1/g/"], + ["g?y", bases[2], "http://a/b/c/d;p=1/g?y"], + [";x", bases[2], "http://a/b/c/d;p=1/;x"], + ["g;x", bases[2], "http://a/b/c/d;p=1/g;x"], + ["g;x=1/./y", bases[2], "http://a/b/c/d;p=1/g;x=1/y"], + ["g;x=1/../y", bases[2], "http://a/b/c/d;p=1/y"], + ["./", bases[2], "http://a/b/c/d;p=1/"], + ["../", bases[2], "http://a/b/c/"], + ["../g", bases[2], "http://a/b/c/g"], + ["../../", bases[2], "http://a/b/"], + ["../../g", bases[2], "http://a/b/g"], + + // http://gbiv.com/protocols/uri/test/rel_examples4.html + // double and triple slash, unknown scheme + ["g:h", bases[3], "g:h"], + ["g", bases[3], "fred:///s//a/b/g"], + ["./g", bases[3], "fred:///s//a/b/g"], + ["g/", bases[3], "fred:///s//a/b/g/"], + ["/g", bases[3], "fred:///g"], // May change to fred:///s//a/g + ["//g", bases[3], "fred://g"], // May change to fred:///s//g + ["//g/x", bases[3], "fred://g/x"], // May change to fred:///s//g/x + ["///g", bases[3], "fred:///g"], + ["./", bases[3], "fred:///s//a/b/"], + ["../", bases[3], "fred:///s//a/"], + ["../g", bases[3], "fred:///s//a/g"], + + ["../../", bases[3], "fred:///s//"], + ["../../g", bases[3], "fred:///s//g"], + ["../../../g", bases[3], "fred:///s/g"], + // May change to fred:///s//a/../../../g + ["../../../../g", bases[3], "fred:///g"], + + // http://gbiv.com/protocols/uri/test/rel_examples5.html + // double and triple slash, well-known scheme + ["g:h", bases[4], "g:h"], + ["g", bases[4], "http:///s//a/b/g"], + ["./g", bases[4], "http:///s//a/b/g"], + ["g/", bases[4], "http:///s//a/b/g/"], + ["/g", bases[4], "http:///g"], // May change to http:///s//a/g + ["//g", bases[4], "http://g/"], // May change to http:///s//g + ["//g/x", bases[4], "http://g/x"], // May change to http:///s//g/x + ["///g", bases[4], "http:///g"], + ["./", bases[4], "http:///s//a/b/"], + ["../", bases[4], "http:///s//a/"], + ["../g", bases[4], "http:///s//a/g"], + ["../../", bases[4], "http:///s//"], + ["../../g", bases[4], "http:///s//g"], + // May change to http:///s//a/../../g + ["../../../g", bases[4], "http:///s/g"], + // May change to http:///s//a/../../../g + ["../../../../g", bases[4], "http:///g"], + + // From Dan Connelly's tests in http://www.w3.org/2000/10/swap/uripath.py + ["bar:abc", "foo:xyz", "bar:abc"], + ["../abc", "http://example/x/y/z", "http://example/x/abc"], + ["http://example/x/abc", "http://example2/x/y/z", "http://example/x/abc"], + ["../r", "http://ex/x/y/z", "http://ex/x/r"], + ["q/r", "http://ex/x/y", "http://ex/x/q/r"], + ["q/r#s", "http://ex/x/y", "http://ex/x/q/r#s"], + ["q/r#s/t", "http://ex/x/y", "http://ex/x/q/r#s/t"], + ["ftp://ex/x/q/r", "http://ex/x/y", "ftp://ex/x/q/r"], + ["", "http://ex/x/y", "http://ex/x/y"], + ["", "http://ex/x/y/", "http://ex/x/y/"], + ["", "http://ex/x/y/pdq", "http://ex/x/y/pdq"], + ["z/", "http://ex/x/y/", "http://ex/x/y/z/"], + ["#Animal", "file:/swap/test/animal.rdf", "file:/swap/test/animal.rdf#Animal"], + ["../abc", "file:/e/x/y/z", "file:/e/x/abc"], + ["/example/x/abc", "file:/example2/x/y/z", "file:/example/x/abc"], + ["../r", "file:/ex/x/y/z", "file:/ex/x/r"], + ["/r", "file:/ex/x/y/z", "file:/r"], + ["q/r", "file:/ex/x/y", "file:/ex/x/q/r"], + ["q/r#s", "file:/ex/x/y", "file:/ex/x/q/r#s"], + ["q/r#", "file:/ex/x/y", "file:/ex/x/q/r#"], + ["q/r#s/t", "file:/ex/x/y", "file:/ex/x/q/r#s/t"], + ["ftp://ex/x/q/r", "file:/ex/x/y", "ftp://ex/x/q/r"], + ["", "file:/ex/x/y", "file:/ex/x/y"], + ["", "file:/ex/x/y/", "file:/ex/x/y/"], + ["", "file:/ex/x/y/pdq", "file:/ex/x/y/pdq"], + ["z/", "file:/ex/x/y/", "file:/ex/x/y/z/"], + [ + "file://meetings.example.com/cal#m1", + "file:/devel/WWW/2000/10/swap/test/reluri-1.n3", + "file://meetings.example.com/cal#m1", + ], + [ + "file://meetings.example.com/cal#m1", + "file:/home/connolly/w3ccvs/WWW/2000/10/swap/test/reluri-1.n3", + "file://meetings.example.com/cal#m1", + ], + ["./#blort", "file:/some/dir/foo", "file:/some/dir/#blort"], + ["./#", "file:/some/dir/foo", "file:/some/dir/#"], + // Ryan Lee + ["./", "http://example/x/abc.efg", "http://example/x/"], + + // Graham Klyne's tests + // http://www.ninebynine.org/Software/HaskellUtils/Network/UriTest.xls + // 01-31 are from Connelly's cases + + // 32-49 + ["./q:r", "http://ex/x/y", "http://ex/x/q:r"], + ["./p=q:r", "http://ex/x/y", "http://ex/x/p=q:r"], + ["?pp/rr", "http://ex/x/y?pp/qq", "http://ex/x/y?pp/rr"], + ["y/z", "http://ex/x/y?pp/qq", "http://ex/x/y/z"], + ["local/qual@domain.org#frag", "mailto:local", "mailto:local/qual@domain.org#frag"], + ["more/qual2@domain2.org#frag", "mailto:local/qual1@domain1.org", "mailto:local/more/qual2@domain2.org#frag"], + ["y?q", "http://ex/x/y?q", "http://ex/x/y?q"], + ["/x/y?q", "http://ex?p", "http://ex/x/y?q"], + ["c/d", "foo:a/b", "foo:a/c/d"], + ["/c/d", "foo:a/b", "foo:/c/d"], + ["", "foo:a/b?c#d", "foo:a/b?c"], + ["b/c", "foo:a", "foo:b/c"], + ["../b/c", "foo:/a/y/z", "foo:/a/b/c"], + ["./b/c", "foo:a", "foo:b/c"], + ["/./b/c", "foo:a", "foo:/b/c"], + ["../../d", "foo://a//b/c", "foo://a/d"], + [".", "foo:a", "foo:"], + ["..", "foo:a", "foo:"], + + // 50-57[cf. TimBL comments -- + // http://lists.w3.org/Archives/Public/uri/2003Feb/0028.html, + // http://lists.w3.org/Archives/Public/uri/2003Jan/0008.html) + ["abc", "http://example/x/y%2Fz", "http://example/x/abc"], + ["../../x%2Fabc", "http://example/a/x/y/z", "http://example/a/x%2Fabc"], + ["../x%2Fabc", "http://example/a/x/y%2Fz", "http://example/a/x%2Fabc"], + ["abc", "http://example/x%2Fy/z", "http://example/x%2Fy/abc"], + ["q%3Ar", "http://ex/x/y", "http://ex/x/q%3Ar"], + ["/x%2Fabc", "http://example/x/y%2Fz", "http://example/x%2Fabc"], + ["/x%2Fabc", "http://example/x/y/z", "http://example/x%2Fabc"], + ["/x%2Fabc", "http://example/x/y%2Fz", "http://example/x%2Fabc"], + + // 70-77 + ["local2@domain2", "mailto:local1@domain1?query1", "mailto:local2@domain2"], + ["local2@domain2?query2", "mailto:local1@domain1", "mailto:local2@domain2?query2"], + ["local2@domain2?query2", "mailto:local1@domain1?query1", "mailto:local2@domain2?query2"], + ["?query2", "mailto:local@domain?query1", "mailto:local@domain?query2"], + ["local@domain?query2", "mailto:?query1", "mailto:local@domain?query2"], + ["?query2", "mailto:local@domain?query1", "mailto:local@domain?query2"], + ["http://example/a/b?c/../d", "foo:bar", "http://example/a/b?c/../d"], + ["http://example/a/b#c/../d", "foo:bar", "http://example/a/b#c/../d"], + + // 82-88 + // @isaacs Disagree. Not how browsers do it. + // ['http:this', 'http://example.org/base/uri', 'http:this'], + // @isaacs Added + ["http:this", "http://example.org/base/uri", "http://example.org/base/this"], + ["http:this", "http:base", "http:this"], + [".//g", "f:/a", "f://g"], + ["b/c//d/e", "f://example.org/base/a", "f://example.org/base/b/c//d/e"], + [ + "m2@example.ord/c2@example.org", + "mid:m@example.ord/c@example.org", + "mid:m@example.ord/m2@example.ord/c2@example.org", + ], + [ + "mini1.xml", + "file:///C:/DEV/Haskell/lib/HXmlToolbox-3.01/examples/", + "file:///C:/DEV/Haskell/lib/HXmlToolbox-3.01/examples/mini1.xml", + ], + ["../b/c", "foo:a/y/z", "foo:a/b/c"], + + // changeing auth + ["http://diff:auth@www.example.com", "http://asdf:qwer@www.example.com", "http://diff:auth@www.example.com/"], + + // TODO: Support this. + // + // changing port + //["https://example.com:81/", "https://example.com:82/", "https://example.com:81/"], + + // TODO: Support these. + // + // https://github.com/nodejs/node/issues/1435 + // ["https://another.host.com/", "https://user:password@example.org/", "https://another.host.com/"], + ["//another.host.com/", "https://user:password@example.org/", "https://another.host.com/"], + ["http://another.host.com/", "https://user:password@example.org/", "http://another.host.com/"], + // TODO: Support this. + // + // ["mailto:another.host.com", "mailto:user@example.org", "mailto:another.host.com"], + ["https://example.com/foo", "https://user:password@example.com", "https://user:password@example.com/foo"], + + // No path at all + ["#hash1", "#hash2", "#hash1"], + ]; + + test("relative paths", () => { + for (let i = 0; i < relativeTests2.length; i++) { + const relativeTest = relativeTests2[i]; + + const a = url.resolve(relativeTest[1], relativeTest[0]); + const e = url.format(relativeTest[2]); + assert.strictEqual(a, e, `resolve(${relativeTest[0]}, ${relativeTest[1]})` + ` == ${e}\n actual=${a}`); + } + }); + + test("special case relative paths", () => { + // format: [to, from, result] + // the test: ['.//g', 'f:/a', 'f://g'] is a fundamental problem + // url.parse('f:/a') does not have a host + // url.resolve('f:/a', './/g') does not have a host because you have moved + // down to the g directory. i.e. f: //g, however when this url is parsed + // f:// will indicate that the host is g which is not the case. + // it is unclear to me how to keep this information from being lost + // it may be that a pathname of ////g should collapse to /g but this seems + // to be a lot of work for an edge case. Right now I remove the test + if (relativeTests2[181][0] === ".//g" && relativeTests2[181][1] === "f:/a" && relativeTests2[181][2] === "f://g") { + relativeTests2.splice(181, 1); + } + for (let i = 0; i < relativeTests2.length; i++) { + const relativeTest = relativeTests2[i]; + + let actual = url.resolveObject(url.parse(relativeTest[1]), relativeTest[0]); + let expected = url.parse(relativeTest[2]); + + assert.deepStrictEqual(actual, expected, `expected ${inspect(expected)} but got ${inspect(actual)}`); + + expected = url.format(relativeTest[2]); + actual = url.format(actual); + + assert.strictEqual(actual, expected, `format(${relativeTest[1]}) == ${expected}\n` + `actual: ${actual}`); + } + }); +}); diff --git a/test/js/node/url/url-revokeobjecturl.test.js b/test/js/node/url/url-revokeobjecturl.test.js new file mode 100644 index 0000000000..1c09aa71a1 --- /dev/null +++ b/test/js/node/url/url-revokeobjecturl.test.js @@ -0,0 +1,19 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert"; +import { URL } from "node:url"; + +// TODO: Support throwing appropriate error. +describe.todo("URL.revokeObjectURL", () => { + test("invalid input", () => { + // Test ensures that the function receives the url argument. + assert.throws( + () => { + URL.revokeObjectURL(); + }, + { + code: "ERR_MISSING_ARGS", + name: "TypeError", + }, + ); + }); +}); From 27eed543efbe9315b5e7c8d401316860e21aa068 Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Fri, 16 Feb 2024 14:13:19 -0800 Subject: [PATCH 13/19] Add .env to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8373c489d1..65284a94ae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ packages/*/*.wasm *.a profile.json +.env node_modules .envrc .swcrc From fe8ec29f1fb459b459b58359261ebea51c6b1aad Mon Sep 17 00:00:00 2001 From: Dmitri Date: Sat, 17 Feb 2024 04:24:04 +0200 Subject: [PATCH 14/19] Add fs.exists[util.promisify.custom] (#8936) * Add fs.exists[util.promisify.custom] fs.exists doesn't follow the error-first-callback convention, so it needs a custom implementation for util.promisify. * Simplify --------- Co-authored-by: John-David Dalton --- src/js/node/fs.js | 2 ++ test/js/node/fs/fs.test.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/js/node/fs.js b/src/js/node/fs.js index 889f2becf2..94009c8df0 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -365,6 +365,8 @@ var access = function access(...args) { // TODO: make symbols a separate export somewhere var kCustomPromisifiedSymbol = Symbol.for("nodejs.util.promisify.custom"); +exists[kCustomPromisifiedSymbol] = path => new Promise(resolve => exists(path, resolve)); + read[kCustomPromisifiedSymbol] = async function (fd, bufferOrOptions, ...rest) { const { isArrayBufferView } = require("node:util/types"); let buffer; diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index acd8b81f75..4968f2468d 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1310,6 +1310,14 @@ describe("fs.exists", () => { } }); }); + it("should work with util.promisify when path exists", async () => { + const fsexists = promisify(fs.exists); + expect(await fsexists(import.meta.path)).toBe(true); + }); + it("should work with util.promisify when path doesn't exist", async () => { + const fsexists = promisify(fs.exists); + expect(await fsexists(`${tmpdir()}/test-fs-exists-${Date.now()}`)).toBe(false); + }); }); describe("rm", () => { From 135de4dff7bf778b4dde855ce50b3cd6844914cf Mon Sep 17 00:00:00 2001 From: Tony Zhang <49156174+ZTL-UwU@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:24:55 +0800 Subject: [PATCH 15/19] fix(sqlite): enable math functions (#8944) * fix(sqlite): enable math functions * fix(sqlite): enable math function flag in CMakeLists * test: add math function tests --------- Co-authored-by: Georgijs <48869301+gvilums@users.noreply.github.com> Co-authored-by: John-David Dalton --- CMakeLists.txt | 1 + Makefile | 2 +- test/js/bun/sqlite/sqlite.test.js | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e86a3f364..f3c5ebee29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1279,6 +1279,7 @@ if(USE_STATIC_SQLITE) "SQLITE_ENABLE_FTS3_PARENTHESIS=1" "SQLITE_ENABLE_FTS5=1" "SQLITE_ENABLE_JSON1=1" + "SQLITE_ENABLE_MATH_FUNCTIONS=1" ) target_link_libraries(${bun} PRIVATE sqlite3) message(STATUS "Using static sqlite3") diff --git a/Makefile b/Makefile index 8a6142c4ff..d60b70a72c 100644 --- a/Makefile +++ b/Makefile @@ -1722,7 +1722,7 @@ sizegen: # Linux uses bundled SQLite3 ifeq ($(OS_NAME),linux) sqlite: - $(CC) $(EMIT_LLVM_FOR_RELEASE) $(CFLAGS) $(INCLUDE_DIRS) -DSQLITE_ENABLE_COLUMN_METADATA= -DSQLITE_MAX_VARIABLE_NUMBER=250000 -DSQLITE_ENABLE_RTREE=1 -DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS=1 -DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_JSON1=1 $(SRC_DIR)/sqlite/sqlite3.c -c -o $(SQLITE_OBJECT) + $(CC) $(EMIT_LLVM_FOR_RELEASE) $(CFLAGS) $(INCLUDE_DIRS) -DSQLITE_ENABLE_COLUMN_METADATA= -DSQLITE_MAX_VARIABLE_NUMBER=250000 -DSQLITE_ENABLE_RTREE=1 -DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS=1 -DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_JSON1=1 -DSQLITE_ENABLE_MATH_FUNCTIONS=1 $(SRC_DIR)/sqlite/sqlite3.c -c -o $(SQLITE_OBJECT) endif picohttp: diff --git a/test/js/bun/sqlite/sqlite.test.js b/test/js/bun/sqlite/sqlite.test.js index 79a8514f71..1d95590f8b 100644 --- a/test/js/bun/sqlite/sqlite.test.js +++ b/test/js/bun/sqlite/sqlite.test.js @@ -743,3 +743,34 @@ it("multiple statements", () => { } } }); + +it("math functions", () => { + const db = new Database(":memory:"); + + expect(db.prepare("SELECT ABS(-243.5)").all()).toEqual([{ "ABS(-243.5)": 243.5 }]); + expect(db.prepare("SELECT ACOS(0.25)").all()).toEqual([{ "ACOS(0.25)": 1.318116071652818 }]); + expect(db.prepare("SELECT ASIN(0.25)").all()).toEqual([{ "ASIN(0.25)": 0.25268025514207865 }]); + expect(db.prepare("SELECT ATAN(0.25)").all()).toEqual([{ "ATAN(0.25)": 0.24497866312686414 }]); + db.exec( + ` + CREATE TABLE num_table (value TEXT NOT NULL); + INSERT INTO num_table values (1), (2), (6); + `, + ); + expect(db.prepare(`SELECT AVG(value) as value FROM num_table`).all()).toEqual([{ value: 3 }]); + expect(db.prepare("SELECT CEILING(0.25)").all()).toEqual([{ "CEILING(0.25)": 1 }]); + expect(db.prepare("SELECT COUNT(*) FROM num_table").all()).toEqual([{ "COUNT(*)": 3 }]); + expect(db.prepare("SELECT COS(0.25)").all()).toEqual([{ "COS(0.25)": 0.9689124217106447 }]); + expect(db.prepare("SELECT DEGREES(0.25)").all()).toEqual([{ "DEGREES(0.25)": 14.32394487827058 }]); + expect(db.prepare("SELECT EXP(0.25)").all()).toEqual([{ "EXP(0.25)": 1.2840254166877414 }]); + expect(db.prepare("SELECT FLOOR(0.25)").all()).toEqual([{ "FLOOR(0.25)": 0 }]); + expect(db.prepare("SELECT LOG10(0.25)").all()).toEqual([{ "LOG10(0.25)": -0.6020599913279624 }]); + expect(db.prepare("SELECT PI()").all()).toEqual([{ "PI()": 3.141592653589793 }]); + expect(db.prepare("SELECT POWER(0.25, 3)").all()).toEqual([{ "POWER(0.25, 3)": 0.015625 }]); + expect(db.prepare("SELECT RADIANS(0.25)").all()).toEqual([{ "RADIANS(0.25)": 0.004363323129985824 }]); + expect(db.prepare("SELECT ROUND(0.25)").all()).toEqual([{ "ROUND(0.25)": 0 }]); + expect(db.prepare("SELECT SIGN(0.25)").all()).toEqual([{ "SIGN(0.25)": 1 }]); + expect(db.prepare("SELECT SIN(0.25)").all()).toEqual([{ "SIN(0.25)": 0.24740395925452294 }]); + expect(db.prepare("SELECT SQRT(0.25)").all()).toEqual([{ "SQRT(0.25)": 0.5 }]); + expect(db.prepare("SELECT TAN(0.25)").all()).toEqual([{ "TAN(0.25)": 0.25534192122103627 }]); +}); From e2c92c69b5710241df9c7fb8aa1a20caa166f366 Mon Sep 17 00:00:00 2001 From: argosphil Date: Sat, 17 Feb 2024 02:32:37 +0000 Subject: [PATCH 16/19] fix: make sure Bun.sleep(Date) doesn't resolve prematurely (#8950) * fix: make sure Bun.sleep(Date) doesn't return prematurely Fixes #8834. This makes Bun.sleep(new Date(x)) fulfill its promise only when Date.now() >= x. * resolve test now #8834 is fixed 11 ms is in fact the right limit. --------- Co-authored-by: John-David Dalton --- src/bun.js/bindings/BunObject.cpp | 4 ++-- test/js/web/timers/setTimeout.test.js | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index e94a00bbbd..6145e3d2b0 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -352,8 +352,8 @@ JSC_DEFINE_HOST_FUNCTION(functionBunSleep, if (millisecondsValue.inherits()) { auto now = MonotonicTime::now(); - auto milliseconds = jsCast(millisecondsValue)->internalNumber() - now.approximateWallTime().secondsSinceEpoch().milliseconds(); - millisecondsValue = JSC::jsNumber(milliseconds > 0 ? milliseconds : 0); + double milliseconds = jsCast(millisecondsValue)->internalNumber() - now.approximateWallTime().secondsSinceEpoch().milliseconds(); + millisecondsValue = JSC::jsNumber(milliseconds > 0 ? std::ceil(milliseconds) : 0); } if (!millisecondsValue.isNumber()) { diff --git a/test/js/web/timers/setTimeout.test.js b/test/js/web/timers/setTimeout.test.js index 8dcc1aa67d..5ef9970e18 100644 --- a/test/js/web/timers/setTimeout.test.js +++ b/test/js/web/timers/setTimeout.test.js @@ -152,9 +152,15 @@ it("Bun.sleep works with a Date object", async () => { var ten_ms = new Date(); ten_ms.setMilliseconds(ten_ms.getMilliseconds() + 12); await Bun.sleep(ten_ms); - // TODO: Fix https://github.com/oven-sh/bun/issues/8834 - // This should be .toBeGreaterThan(11), or maybe even 12 - expect(performance.now() - now).toBeGreaterThan(10); + expect(performance.now() - now).toBeGreaterThan(11); +}); + +it("Bun.sleep(Date) fulfills after Date", async () => { + let ten_ms = new Date(); + ten_ms.setMilliseconds(ten_ms.getMilliseconds() + 12); + await Bun.sleep(ten_ms); + let now = new Date(); + expect(+now).toBeGreaterThanOrEqual(+ten_ms); }); it("node.js timers/promises setTimeout propagates exceptions", async () => { From abf12399767559b7295741e8126bd64b6424aafd Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 16 Feb 2024 20:02:22 -0800 Subject: [PATCH 17/19] feat: Support async generator functions in `Response` and `Request` for bodies (#8941) --- docs/api/streams.md | 39 +++ src/bun.js/bindings/ZigGlobalObject.cpp | 67 +++++- src/bun.js/bindings/bindings.cpp | 12 +- src/bun.js/bindings/bindings.zig | 1 + src/bun.js/webcore/streams.zig | 17 +- src/js/builtins/ReadableStream.ts | 6 +- src/js/builtins/ReadableStreamInternals.ts | 126 ++++++++-- .../js/bun/http/async-iterator-stream.test.ts | 227 ++++++++++++++++++ 8 files changed, 453 insertions(+), 42 deletions(-) create mode 100644 test/js/bun/http/async-iterator-stream.test.ts diff --git a/docs/api/streams.md b/docs/api/streams.md index cf404ed691..816b94dc8d 100644 --- a/docs/api/streams.md +++ b/docs/api/streams.md @@ -56,6 +56,45 @@ const stream = new ReadableStream({ When using a direct `ReadableStream`, all chunk queueing is handled by the destination. The consumer of the stream receives exactly what is passed to `controller.write()`, without any encoding or modification. +## Async generator streams + +Bun also supports async generator functions as a source for `Response` and `Request`. This is an easy way to create a `ReadableStream` that fetches data from an asynchronous source. + +```ts +const response = new Response(async function* () { + yield "hello"; + yield "world"; +}()); + +await response.text(); // "helloworld" +``` + +You can also use `[Symbol.asyncIterator]` directly. + +```ts +const response = new Response({ + [Symbol.asyncIterator]: async function* () { + yield "hello"; + yield "world"; + }, +}); + +await response.text(); // "helloworld" +``` + +If you need more granular control over the stream, `yield` will return the direct ReadableStream controller. + +```ts +const response = new Response({ + [Symbol.asyncIterator]: async function* () { + const controller = yield "hello"; + await controller.end(); + }, +}); + +await response.text(); // "hello" +``` + ## `Bun.ArrayBufferSink` The `Bun.ArrayBufferSink` class is a fast incremental writer for constructing an `ArrayBuffer` of unknown size. diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index a58b0194e9..7fa7546ae1 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1,10 +1,10 @@ + #include "root.h" #include "ZigGlobalObject.h" #include #include "helpers.h" #include "BunClientData.h" - -#include "JavaScriptCore/AggregateError.h" +#include "JavaScriptCore/JSObjectInlines.h" #include "JavaScriptCore/InternalFieldTuple.h" #include "JavaScriptCore/BytecodeIndex.h" #include "JavaScriptCore/CallFrameInlines.h" @@ -24,7 +24,6 @@ #include "JavaScriptCore/IteratorOperations.h" #include "JavaScriptCore/JSArray.h" #include "JavaScriptCore/JSGlobalProxyInlines.h" - #include "JavaScriptCore/JSCallbackConstructor.h" #include "JavaScriptCore/JSCallbackObject.h" #include "JavaScriptCore/JSCast.h" @@ -2311,19 +2310,69 @@ extern "C" bool ReadableStream__isLocked(JSC__JSValue possibleReadableStream, Zi return stream != nullptr && ReadableStream::isLocked(globalObject, stream); } -extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JSC__JSValue possibleReadableStream, JSValue* ptr); -extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JSC__JSValue possibleReadableStream, JSValue* ptr) +extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JSC__JSValue* possibleReadableStream, JSValue* ptr) { ASSERT(globalObject); - JSC::JSObject* object = JSValue::decode(possibleReadableStream).getObject(); - if (!object || !object->inherits()) { + JSC::JSObject* object = JSValue::decode(*possibleReadableStream).getObject(); + if (!object) { *ptr = JSC::JSValue(); return -1; } - auto* readableStream = jsCast(object); auto& vm = globalObject->vm(); - auto& builtinNames = WebCore::clientData(vm)->builtinNames(); + const auto& builtinNames = WebCore::builtinNames(vm); + + if (!object->inherits()) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSValue target = object; + JSValue fn = JSValue(); + auto* function = jsDynamicCast(object); + if (function && function->jsExecutable() && function->jsExecutable()->isAsyncGenerator()) { + fn = object; + target = jsUndefined(); + } else if (auto iterable = object->getIfPropertyExists(globalObject, vm.propertyNames->asyncIteratorSymbol)) { + if (iterable.isCallable()) { + fn = iterable; + } + } + + if (UNLIKELY(throwScope.exception())) { + *ptr = JSC::JSValue(); + return -1; + } + + if (fn.isEmpty()) { + *ptr = JSC::JSValue(); + return -1; + } + + auto* createIterator = globalObject->builtinInternalFunctions().readableStreamInternals().m_readableStreamFromAsyncIteratorFunction.get(); + + JSC::MarkedArgumentBuffer arguments; + arguments.append(target); + arguments.append(fn); + + JSC::JSValue result = profiledCall(globalObject, JSC::ProfilingReason::API, createIterator, JSC::getCallData(createIterator), JSC::jsUndefined(), arguments); + + if (UNLIKELY(throwScope.exception())) { + return -1; + } + + if (!result.isObject()) { + *ptr = JSC::JSValue(); + return -1; + } + + object = result.getObject(); + + ASSERT(object->inherits()); + *possibleReadableStream = JSValue::encode(object); + *ptr = JSValue(); + ensureStillAliveHere(object); + return 0; + } + + auto* readableStream = jsCast(object); int32_t num = 0; if (JSValue numberValue = readableStream->getDirect(vm, builtinNames.bunNativeTypePrivateName())) { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 32db72842a..da91128e75 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4716,6 +4716,7 @@ enum class BuiltinNamesMap : uint8_t { toString, redirect, inspectCustom, + asyncIterator, }; static JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, unsigned char name) @@ -4753,6 +4754,9 @@ static JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, unsigne case BuiltinNamesMap::inspectCustom: { return Identifier::fromUid(vm.symbolRegistry().symbolForKey("nodejs.util.inspect.custom"_s)); } + case BuiltinNamesMap::asyncIterator: { + return vm.propertyNames->asyncIteratorSymbol; + } } } @@ -5410,22 +5414,22 @@ extern "C" bool JSGlobalObject__hasException(JSC::JSGlobalObject* globalObject) return DECLARE_CATCH_SCOPE(globalObject->vm()).exception() != 0; } -CPP_DECL bool JSC__GetterSetter__isGetterNull(JSC__GetterSetter *gettersetter) +CPP_DECL bool JSC__GetterSetter__isGetterNull(JSC__GetterSetter* gettersetter) { return gettersetter->isGetterNull(); } -CPP_DECL bool JSC__GetterSetter__isSetterNull(JSC__GetterSetter *gettersetter) +CPP_DECL bool JSC__GetterSetter__isSetterNull(JSC__GetterSetter* gettersetter) { return gettersetter->isSetterNull(); } -CPP_DECL bool JSC__CustomGetterSetter__isGetterNull(JSC__CustomGetterSetter *gettersetter) +CPP_DECL bool JSC__CustomGetterSetter__isGetterNull(JSC__CustomGetterSetter* gettersetter) { return gettersetter->getter() == nullptr; } -CPP_DECL bool JSC__CustomGetterSetter__isSetterNull(JSC__CustomGetterSetter *gettersetter) +CPP_DECL bool JSC__CustomGetterSetter__isSetterNull(JSC__CustomGetterSetter* gettersetter) { return gettersetter->setter() == nullptr; } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 2ac7f25f60..43039ddff6 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4541,6 +4541,7 @@ pub const JSValue = enum(JSValueReprInt) { toString, redirect, inspectCustom, + asyncIterator, }; // intended to be more lightweight than ZigString diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index e7fa8b4d2e..1ed29b1382 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -241,7 +241,7 @@ pub const ReadableStream = struct { Bytes: *ByteStream, }; - extern fn ReadableStreamTag__tagged(globalObject: *JSGlobalObject, possibleReadableStream: JSValue, ptr: *JSValue) Tag; + extern fn ReadableStreamTag__tagged(globalObject: *JSGlobalObject, possibleReadableStream: *JSValue, ptr: *JSValue) Tag; extern fn ReadableStream__isDisturbed(possibleReadableStream: JSValue, globalObject: *JSGlobalObject) bool; extern fn ReadableStream__isLocked(possibleReadableStream: JSValue, globalObject: *JSGlobalObject) bool; extern fn ReadableStream__empty(*JSGlobalObject) JSC.JSValue; @@ -269,41 +269,42 @@ pub const ReadableStream = struct { pub fn fromJS(value: JSValue, globalThis: *JSGlobalObject) ?ReadableStream { JSC.markBinding(@src()); var ptr = JSValue.zero; - return switch (ReadableStreamTag__tagged(globalThis, value, &ptr)) { + var out = value; + return switch (ReadableStreamTag__tagged(globalThis, &out, &ptr)) { .JavaScript => ReadableStream{ - .value = value, + .value = out, .ptr = .{ .JavaScript = {}, }, }, .Blob => ReadableStream{ - .value = value, + .value = out, .ptr = .{ .Blob = ptr.asPtr(ByteBlobLoader), }, }, .File => ReadableStream{ - .value = value, + .value = out, .ptr = .{ .File = ptr.asPtr(FileReader), }, }, .Bytes => ReadableStream{ - .value = value, + .value = out, .ptr = .{ .Bytes = ptr.asPtr(ByteStream), }, }, // .HTTPRequest => ReadableStream{ - // .value = value, + // .value = out, // .ptr = .{ // .HTTPRequest = ptr.asPtr(HTTPRequest), // }, // }, // .HTTPSRequest => ReadableStream{ - // .value = value, + // .value = out, // .ptr = .{ // .HTTPSRequest = ptr.asPtr(HTTPSRequest), // }, diff --git a/src/js/builtins/ReadableStream.ts b/src/js/builtins/ReadableStream.ts index c1d43a4305..079fc129f2 100644 --- a/src/js/builtins/ReadableStream.ts +++ b/src/js/builtins/ReadableStream.ts @@ -140,7 +140,7 @@ export function readableStreamToArrayBuffer(stream: ReadableStream) var result = Bun.readableStreamToArray(stream); if ($isPromise(result)) { - // `result` is an InternalPromise, which doesn't have a `.$then` method + // `result` is an InternalPromise, which doesn't have a `.then` method // but `.then` isn't user-overridable, so we can use it safely. return result.then(Bun.concatArrayBuffers); } @@ -160,12 +160,12 @@ export function readableStreamToFormData( $linkTimeConstant; export function readableStreamToJSON(stream: ReadableStream): unknown { - return Bun.readableStreamToText(stream).$then(globalThis.JSON.parse); + return Promise.resolve(Bun.readableStreamToText(stream)).then(globalThis.JSON.parse); } $linkTimeConstant; export function readableStreamToBlob(stream: ReadableStream): Promise { - return Promise.resolve(Bun.readableStreamToArray(stream)).$then(array => new Blob(array)); + return Promise.resolve(Bun.readableStreamToArray(stream)).then(array => new Blob(array)); } $linkTimeConstant; diff --git a/src/js/builtins/ReadableStreamInternals.ts b/src/js/builtins/ReadableStreamInternals.ts index ca050ef7df..f3559892f4 100644 --- a/src/js/builtins/ReadableStreamInternals.ts +++ b/src/js/builtins/ReadableStreamInternals.ts @@ -613,7 +613,6 @@ export function isReadableStreamDefaultController(controller) { export function readDirectStream(stream, sink, underlyingSource) { $putByIdDirectPrivate(stream, "underlyingSource", undefined); $putByIdDirectPrivate(stream, "start", undefined); - function close(stream, reason) { if (reason && underlyingSource?.cancel) { try { @@ -647,21 +646,24 @@ export function readDirectStream(stream, sink, underlyingSource) { $throwTypeError("pull is not a function"); return; } - $putByIdDirectPrivate(stream, "readableStreamController", sink); const highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark"); - sink.start({ highWaterMark: !highWaterMark || highWaterMark < 64 ? 64 : highWaterMark, }); $startDirectStream.$call(sink, stream, underlyingSource.pull, close, stream.$asyncContext); + $putByIdDirectPrivate(stream, "reader", {}); var maybePromise = underlyingSource.pull(sink); sink = undefined; if (maybePromise && $isPromise(maybePromise)) { - return maybePromise.$then(() => {}); + if (maybePromise.$then) { + return maybePromise.$then(() => {}); + } + + return maybePromise.then(() => {}); } } @@ -1245,7 +1247,6 @@ export function readableStreamError(stream, error) { $assert($getByIdDirectPrivate(stream, "state") === $streamReadable); $putByIdDirectPrivate(stream, "state", $streamErrored); $putByIdDirectPrivate(stream, "storedError", error); - const reader = $getByIdDirectPrivate(stream, "reader"); if (!reader) return; @@ -1515,6 +1516,88 @@ export function readableStreamDefaultControllerCanCloseOrEnqueue(controller) { return $getByIdDirectPrivate(controlledReadableStream, "state") === $streamReadable; } +export function readableStreamFromAsyncIterator(target, fn) { + var cancelled = false, + iter: AsyncIterator; + return new ReadableStream({ + type: "direct", + + cancel(reason) { + $debug("readableStreamFromAsyncIterator.cancel", reason); + cancelled = true; + + if (iter) { + iter.throw?.((reason ||= new DOMException("ReadableStream has been cancelled", "AbortError"))); + } + }, + + async pull(controller) { + // we deliberately want to throw on error + iter = fn.$call(target, controller); + fn = target = undefined; + + if (!$isAsyncGenerator(iter) && typeof iter.next !== "function") { + iter = undefined; + throw new TypeError("Expected an async generator"); + } + + var closingError, value, done, immediateTask; + + try { + while (!cancelled && !done) { + const promise = iter.next(controller); + if (cancelled) { + return; + } + + if ( + $isPromise(promise) && + ($getPromiseInternalField(promise, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled + ) { + clearImmediate(immediateTask); + ({ value, done } = $getPromiseInternalField(promise, $promiseFieldReactionsOrResult)); + $assert(!$isPromise(value), "Expected a value, not a promise"); + } else { + immediateTask = setImmediate(() => immediateTask && controller?.flush?.(true)); + ({ value, done } = await promise); + + if (cancelled) { + return; + } + } + + if (!$isUndefinedOrNull(value)) { + controller.write(value); + } + } + } catch (e) { + closingError = e; + } finally { + clearImmediate(immediateTask); + immediateTask = undefined; + + // Stream was closed before we tried writing to it. + if (closingError?.code === "ERR_INVALID_THIS") { + await iter.return?.(); + return; + } + + if (closingError) { + try { + await iter.throw?.(closingError); + } finally { + throw closingError; + } + } else { + await controller.end(); + await iter.return?.(); + } + iter = undefined; + } + }, + }); +} + export function lazyLoadStream(stream, autoAllocateChunkSize) { $debug("lazyLoadStream", stream, autoAllocateChunkSize); var nativeType = $getByIdDirectPrivate(stream, "bunNativeType"); @@ -1764,25 +1847,32 @@ export function readableStreamToArrayBufferDirect(stream, underlyingSource) { var didError = false; try { - const firstPull = pull(controller); - if (firstPull && $isObject(firstPull) && $isPromise(firstPull)) { - return (async function (controller, promise, pull) { - while (!ended) { - await pull(controller); - } - return await promise; - })(controller, promise, pull); - } - - return capability.promise; + var firstPull = pull(controller); } catch (e) { didError = true; $readableStreamError(stream, e); return Promise.$reject(e); } finally { - if (!didError && stream) $readableStreamClose(stream); - controller = close = sink = pull = stream = undefined; + if (!$isPromise(firstPull)) { + if (!didError && stream) $readableStreamClose(stream); + controller = close = sink = pull = stream = undefined; + return capability.promise; + } } + + $assert($isPromise(firstPull)); + return firstPull.then( + () => { + if (!didError && stream) $readableStreamClose(stream); + controller = close = sink = pull = stream = undefined; + return capability.promise; + }, + e => { + didError = true; + if ($getByIdDirectPrivate(stream, "state") === $streamReadable) $readableStreamError(stream, e); + return Promise.$reject(e); + }, + ); } export async function readableStreamToTextDirect(stream, underlyingSource) { diff --git a/test/js/bun/http/async-iterator-stream.test.ts b/test/js/bun/http/async-iterator-stream.test.ts new file mode 100644 index 0000000000..0befeefda7 --- /dev/null +++ b/test/js/bun/http/async-iterator-stream.test.ts @@ -0,0 +1,227 @@ +import { describe, expect, test } from "bun:test"; +import { bunExe, bunEnv } from "harness"; +import path from "path"; + +describe("Streaming body via", () => { + test("async generator function", async () => { + const server = Bun.serve({ + port: 0, + + async fetch(req) { + return new Response(async function* yo() { + yield "Hello, "; + await Bun.sleep(30); + yield Buffer.from("world!"); + return "!"; + }); + }, + }); + + const res = await fetch(`${server.url}/`); + const chunks = []; + for await (const chunk of res.body) { + chunks.push(chunk); + } + + expect(Buffer.concat(chunks).toString()).toBe("Hello, world!!"); + expect(chunks).toHaveLength(2); + server.stop(true); + }); + + test("async generator function throws an error but continues to send the headers", async () => { + const server = Bun.serve({ + port: 0, + + async fetch(req) { + return new Response( + async function* () { + throw new Error("Oops"); + }, + { + headers: { + "X-Hey": "123", + }, + }, + ); + }, + }); + + const res = await fetch(server.url); + expect(res.headers.get("X-Hey")).toBe("123"); + server.stop(true); + }); + + test("async generator aborted doesn't crash", async () => { + var aborter = new AbortController(); + const server = Bun.serve({ + port: 0, + + async fetch(req) { + return new Response( + async function* yo() { + queueMicrotask(() => aborter.abort()); + yield "123"; + await Bun.sleep(0); + }, + { + headers: { + "X-Hey": "123", + }, + }, + ); + }, + }); + try { + const res = await fetch(`${server.url}/`, { signal: aborter.signal }); + } catch (e) { + expect(e).toBeInstanceOf(DOMException); + expect(e.name).toBe("AbortError"); + } finally { + server.stop(true); + } + }); + + test("[Symbol.asyncIterator]", async () => { + const server = Bun.serve({ + port: 0, + + async fetch(req) { + return new Response({ + async *[Symbol.asyncIterator]() { + var controller = yield "my string goes here\n"; + var controller2 = yield Buffer.from("my buffer goes here\n"); + await Bun.sleep(30); + yield Buffer.from("end!\n"); + if (controller !== controller2 || typeof controller.sinkId !== "number") { + throw new Error("Controller mismatch"); + } + return "!"; + }, + }); + }, + }); + + const res = await fetch(`${server.url}/`); + const chunks = []; + for await (const chunk of res.body) { + chunks.push(chunk); + } + + expect(Buffer.concat(chunks).toString()).toBe("my string goes here\nmy buffer goes here\nend!\n!"); + expect(chunks).toHaveLength(2); + server.stop(true); + }); + + test("[Symbol.asyncIterator] with a custom iterator", async () => { + const server = Bun.serve({ + port: 0, + + async fetch(req) { + var hasRun = false; + return new Response({ + [Symbol.asyncIterator]() { + return { + async next() { + await Bun.sleep(30); + + if (hasRun) { + return { value: Buffer.from("world!"), done: true }; + } + + hasRun = true; + return { value: "Hello, ", done: false }; + }, + }; + }, + }); + }, + }); + + const res = await fetch(server.url); + const chunks = []; + for await (const chunk of res.body) { + chunks.push(chunk); + } + + expect(Buffer.concat(chunks).toString()).toBe("Hello, world!"); + // TODO: + // expect(chunks).toHaveLength(2); + server.stop(true); + }); + + test("yield", async () => { + const response = new Response({ + [Symbol.asyncIterator]: async function* () { + const controller = yield "hello"; + await controller.end(); + }, + }); + + expect(await response.text()).toBe("hello"); + }); + + const callbacks = [ + { + fn: async function* () { + yield '"Hello, '; + yield Buffer.from('world! #1"'); + return; + }, + expected: '"Hello, world! #1"', + }, + { + fn: async function* () { + yield '"Hello, '; + await Bun.sleep(30); + yield Buffer.from('world! #2"'); + return; + }, + expected: '"Hello, world! #2"', + }, + { + fn: async function* () { + yield '"Hello, '; + await 42; + yield Buffer.from('world! #3"'); + return; + }, + expected: '"Hello, world! #3"', + }, + { + fn: async function* () { + yield '"Hello, '; + await 42; + return Buffer.from('world! #4"'); + }, + expected: '"Hello, world! #4"', + }, + ]; + + for (let { fn, expected } of callbacks) { + describe(expected, () => { + for (let bodyInit of [fn, { [Symbol.asyncIterator]: fn }] as const) { + for (let [label, constructFn] of [ + ["Response", () => new Response(bodyInit)], + ["Request", () => new Request({ "url": "https://example.com", body: bodyInit })], + ]) { + for (let method of ["arrayBuffer", "text"]) { + test(`${label}(${method})`, async () => { + const result = await constructFn()[method](); + expect(Buffer.from(result)).toEqual(Buffer.from(expected)); + }); + } + + test(`${label}(json)`, async () => { + const result = await constructFn().json(); + expect(result).toEqual(JSON.parse(expected)); + }); + + test(`${label}(blob)`, async () => { + const result = await constructFn().blob(); + expect(await result.arrayBuffer()).toEqual(await new Blob([expected]).arrayBuffer()); + }); + } + } + }); + } +}); From f9b12300d473987ea6473ef00c15459ff4e227c7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 16 Feb 2024 20:43:42 -0800 Subject: [PATCH 18/19] Make shell errors slightly better (#8945) * Make shell errors slightly better * Update shell.ts * Fix the failing tests --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/js/builtins/shell.ts | 40 +++++++++++++++++----------- test/js/bun/shell/bunshell.test.ts | 6 +++-- test/js/node/process/process.test.js | 2 +- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/js/builtins/shell.ts b/src/js/builtins/shell.ts index 479867c2b8..f79aa16c52 100644 --- a/src/js/builtins/shell.ts +++ b/src/js/builtins/shell.ts @@ -2,24 +2,33 @@ type ShellInterpreter = any; type Resolve = (value: ShellOutput) => void; export function createBunShellTemplateFunction(ShellInterpreter) { + function lazyBufferToHumanReadableString() { + return this.toString(); + } class ShellError extends Error { #output: ShellOutput; - constructor(output: ShellOutput) { - super(`Failed with exit code: ${output}`); + constructor(output: ShellOutput, code: number) { + super(`Failed with exit code ${code}`); this.#output = output; + this.name = "ShellError"; + + // Maybe we should just print all the properties on the Error instance + // instead of speical ones + this.info = { + stderr: output.stderr, + exitCode: code, + stdout: output.stdout, + }; + + this.info.stdout.toJSON = lazyBufferToHumanReadableString; + this.info.stderr.toJSON = lazyBufferToHumanReadableString; + + Object.assign(this, this.info); } - get exitCode() { - return this.#output.exitCode; - } - - get stdout() { - return this.#output.stdout; - } - - get stderr() { - return this.#output.stderr; - } + exitCode; + stdout; + stderr; text(encoding) { return this.#output.text(encoding); @@ -81,12 +90,13 @@ export function createBunShellTemplateFunction(ShellInterpreter) { resolve = code => { const out = new ShellOutput(core.getBufferedStdout(), core.getBufferedStderr(), code); if (this.#throws && code !== 0) { - rej(out); + rej(new ShellError(out, code)); } else { res(out); } }; - reject = code => rej(new ShellError(new ShellOutput(core.getBufferedStdout(), core.getBufferedStderr(), code))); + reject = code => + rej(new ShellError(new ShellOutput(core.getBufferedStdout(), core.getBufferedStderr(), code), code)); }); this.#throws = throws; diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 71732b27d4..7d945dfd76 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -88,9 +88,11 @@ describe("bunshell", () => { test("can't escape a js string/obj ref", async () => { const shellvar = "$FOO"; - await TestBuilder.command`FOO=bar && echo \\${shellvar}`.stdout(`$FOO\n`).run(); + await TestBuilder.command`FOO=bar && echo \\${shellvar}`.stdout(`\\$FOO\n`).run(); const buf = new Uint8Array(1); - await TestBuilder.command`echo hi > \\${buf}`.run(); + expect(async () => { + await TestBuilder.command`echo hi > \\${buf}`.run(); + }).toThrow("Redirection with no file"); }); test("in command position", async () => { diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index ab70485302..8ec69839b4 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -71,7 +71,7 @@ it("process.release", () => { expect(process.release.name).toBe("node"); const platform = process.platform == "win32" ? "windows" : process.platform; expect(process.release.sourceUrl).toContain( - `https://github.com/oven-sh/bun/release/bun-v${process.versions.bun}/bun-${platform}-${ + `https://github.com/oven-sh/bun/releases/download/bun-v${process.versions.bun}/bun-${platform}-${ { arm64: "aarch64", x64: "x64" }[process.arch] || process.arch }`, ); From c34bbb2e3fcaaadd8dfb5e75d545acee1c3cfd1b Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:19:31 +0900 Subject: [PATCH 19/19] fix: organize tsconfig (#8654) * fix: organize tsconfig * fix: remove unnecessary comments in tsconfig * docs: revert changes on comments and md text * docs: fix case --- docs/guides/runtime/typescript.md | 7 +++---- docs/typescript.md | 7 +++---- src/cli/tsconfig-for-init.json | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/guides/runtime/typescript.md b/docs/guides/runtime/typescript.md index bb0ce14092..91f8af403a 100644 --- a/docs/guides/runtime/typescript.md +++ b/docs/guides/runtime/typescript.md @@ -15,7 +15,7 @@ Below is the full set of recommended `compilerOptions` for a Bun project. With t ```jsonc { "compilerOptions": { - // enable latest features + // Enable latest features "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", @@ -32,12 +32,11 @@ Below is the full set of recommended `compilerOptions` for a Bun project. With t // Best practices "strict": true, "skipLibCheck": true, - "noUnusedLocals": true, - "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, // Some stricter flags - "useUnknownInCatchVariables": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "noPropertyAccessFromIndexSignature": true } } diff --git a/docs/typescript.md b/docs/typescript.md index 92d93d0a24..65c8f68985 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -17,7 +17,7 @@ Bun supports things like top-level await, JSX, and extensioned `.ts` imports, wh ```jsonc { "compilerOptions": { - // enable latest features + // Enable latest features "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", @@ -34,12 +34,11 @@ Bun supports things like top-level await, JSX, and extensioned `.ts` imports, wh // Best practices "strict": true, "skipLibCheck": true, - "noUnusedLocals": true, - "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, // Some stricter flags - "useUnknownInCatchVariables": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "noPropertyAccessFromIndexSignature": true } } diff --git a/src/cli/tsconfig-for-init.json b/src/cli/tsconfig-for-init.json index dcd8fc5119..e7130782d0 100644 --- a/src/cli/tsconfig-for-init.json +++ b/src/cli/tsconfig-for-init.json @@ -14,9 +14,8 @@ "noEmit": true, /* Linting */ - "skipLibCheck": true, "strict": true, + "skipLibCheck": true, "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true } }