From def454d859e04220fcc11b178b0816c25d5c8e7b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 9 Oct 2024 18:20:19 -0700 Subject: [PATCH 01/15] Bump --- LATEST | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LATEST b/LATEST index fae04a2a19450..321b7ce4c0f41 100644 --- a/LATEST +++ b/LATEST @@ -1 +1 @@ -1.1.29 \ No newline at end of file +1.1.30 \ No newline at end of file diff --git a/package.json b/package.json index da6cb1394fb4c..38cb4c6cdfb69 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "bun", - "version": "1.1.30", + "version": "1.1.31", "workspaces": [ "./packages/bun-types" ], From ff476313a8b62480b4acece512cfa57f807a9e59 Mon Sep 17 00:00:00 2001 From: snwy Date: Wed, 9 Oct 2024 19:14:22 -0700 Subject: [PATCH 02/15] 'let' statements before using statements are now properly converted into 'var' statements (#14260) --- src/js_parser.zig | 23 ++++++++++++++++------- test/bundler/bundler_edgecase.test.ts | 3 ++- test/bundler/bundler_npm.test.ts | 14 +++++++------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/js_parser.zig b/src/js_parser.zig index 57fb249dd4a98..4c9ce810bd6f3 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -19430,6 +19430,7 @@ fn NewParser_( } } + data.kind = kind; try stmts.append(stmt.*); if (p.options.features.react_fast_refresh and p.current_scope == p.module_scope) { @@ -22155,29 +22156,37 @@ fn NewParser_( switch (stmt.data) { .s_empty, .s_comment, .s_directive, .s_debugger, .s_type_script => continue, .s_local => |local| { - if (!local.is_export and local.kind == .k_const and !local.was_commonjs_export) { + if (!local.is_export and !local.was_commonjs_export) { var decls: []Decl = local.decls.slice(); var end: usize = 0; + var any_decl_in_const_values = local.kind == .k_const; for (decls) |decl| { if (decl.binding.data == .b_identifier) { - const symbol = p.symbols.items[decl.binding.data.b_identifier.ref.innerIndex()]; - if (p.const_values.contains(decl.binding.data.b_identifier.ref) and symbol.use_count_estimate == 0) { - continue; + if (p.const_values.contains(decl.binding.data.b_identifier.ref)) { + any_decl_in_const_values = true; + const symbol = p.symbols.items[decl.binding.data.b_identifier.ref.innerIndex()]; + if (symbol.use_count_estimate == 0) { + // Skip declarations that are constants with zero usage + continue; + } } } decls[end] = decl; end += 1; } local.decls.len = @as(u32, @truncate(end)); - if (end == 0) { - stmt.* = stmt.*.toEmpty(); + if (any_decl_in_const_values) { + if (end == 0) { + stmt.* = stmt.*.toEmpty(); + } + continue; } - continue; } }, else => {}, } + // Break after processing relevant statements break; } } diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 6755ba65d1c66..fc0116cf23af8 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -1121,7 +1121,7 @@ describe("bundler", () => { snapshotSourceMap: { "entry.js.map": { files: ["../node_modules/react/index.js", "../entry.js"], - mappingsExactMatch: "uYACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK", + mappingsExactMatch: "qYACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK", }, }, }); @@ -1883,6 +1883,7 @@ describe("bundler", () => { target: "browser", run: { stdout: `123` }, }); + itBundled("edgecase/UninitializedVariablesMoved", { files: { "/entry.ts": ` diff --git a/test/bundler/bundler_npm.test.ts b/test/bundler/bundler_npm.test.ts index b765e58598072..73d4b1556ef70 100644 --- a/test/bundler/bundler_npm.test.ts +++ b/test/bundler/bundler_npm.test.ts @@ -57,17 +57,17 @@ describe("bundler", () => { "../entry.tsx", ], mappings: [ - ["react.development.js:524:'getContextName'", "1:5428:Y1"], - ["react.development.js:2495:'actScopeDepth'", "1:26053:GJ++"], - ["react.development.js:696:''Component'", '1:7490:\'Component "%s"'], - ["entry.tsx:6:'\"Content-Type\"'", '1:221655:"Content-Type"'], - ["entry.tsx:11:''", "1:221911:void"], - ["entry.tsx:23:'await'", "1:222013:await"], + ["react.development.js:524:'getContextName'", "1:5426:Y1"], + ["react.development.js:2495:'actScopeDepth'", "1:26051:GJ++"], + ["react.development.js:696:''Component'", '1:7488:\'Component "%s"'], + ["entry.tsx:6:'\"Content-Type\"'", '1:221651:"Content-Type"'], + ["entry.tsx:11:''", "1:221905:void"], + ["entry.tsx:23:'await'", "1:222005:await"], ], }, }, expectExactFilesize: { - "out/entry.js": 222283, + "out/entry.js": 222273, }, run: { stdout: "

Hello World

This is an example.

", From 3452f50c969dc52160846f09f2c9349a1be5f723 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Thu, 10 Oct 2024 02:35:23 -0700 Subject: [PATCH 03/15] update webkit (#14449) --- cmake/tools/SetupWebKit.cmake | 2 +- test/bun.lockb | Bin 370290 -> 371754 bytes .../sass/__snapshots__/sass.test.ts.snap | 40 ++++++++++++++++++ test/integration/sass/sass.test.ts | 15 +++++++ test/package.json | 3 +- 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 test/integration/sass/__snapshots__/sass.test.ts.snap create mode 100644 test/integration/sass/sass.test.ts diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index 22f4ed8cfe834..ff750a96317dc 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION 0a0a3838e5fab36b579df26620237bb62ed6d950) + set(WEBKIT_VERSION 019ff6e1e879ff4533f2a857cab5028b6b95ab53) endif() if(WEBKIT_LOCAL) diff --git a/test/bun.lockb b/test/bun.lockb index c6449d11b362221940df96be17313e21482c2be6..37e3ded110d6bb27bcba2c0c37fdba983de8eaf5 100755 GIT binary patch delta 39394 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 diff --git a/test/integration/sass/__snapshots__/sass.test.ts.snap b/test/integration/sass/__snapshots__/sass.test.ts.snap new file mode 100644 index 0000000000000..51609653050a9 --- /dev/null +++ b/test/integration/sass/__snapshots__/sass.test.ts.snap @@ -0,0 +1,40 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`sass source maps 1`] = ` +{ + "css": +".ruleGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; + border-width: 1px; +}" +, + "loadedUrls": [], +} +`; + +exports[`sass source maps 2`] = ` +{ + "css": +".ruleGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; + border-width: 1px; +}" +, + "loadedUrls": [], + "sourceMap": { + "mappings": "AAAA;EACI;EACA;EACA;EACA;EACA", + "names": [], + "sourceRoot": "", + "sources": [ + "data:;charset=utf-8,.ruleGroup%20%7B%0A%20%20%20%20display:%20flex;%0A%20%20%20%20flex-direction:%20column;%0A%20%20%20%20gap:%200.5rem;%0A%20%20%20%20padding:%200.5rem;%0A%20%20%20%20border-width:%201px;%0A%20%20%7D%0A%20%20", + ], + "version": 3, + }, +} +`; diff --git a/test/integration/sass/sass.test.ts b/test/integration/sass/sass.test.ts new file mode 100644 index 0000000000000..f5520a6f22bf0 --- /dev/null +++ b/test/integration/sass/sass.test.ts @@ -0,0 +1,15 @@ +import { compileString } from "sass"; + +test("sass source maps", () => { + const scssString = `.ruleGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; + border-width: 1px; + } + `; + + expect(compileString(scssString, { sourceMap: false })).toMatchSnapshot(); + expect(compileString(scssString, { sourceMap: true })).toMatchSnapshot(); +}); diff --git a/test/package.json b/test/package.json index 7406bf448608e..e7d73be1efb17 100644 --- a/test/package.json +++ b/test/package.json @@ -19,13 +19,13 @@ "@types/ws": "8.5.10", "aws-cdk-lib": "2.148.0", "axios": "1.6.8", - "https-proxy-agent": "7.0.5", "body-parser": "1.20.2", "comlink": "4.4.1", "es-module-lexer": "1.3.0", "esbuild": "0.18.6", "express": "4.18.2", "fast-glob": "3.3.1", + "https-proxy-agent": "7.0.5", "iconv-lite": "0.6.3", "isbot": "5.1.13", "jest-extended": "4.0.0", @@ -47,6 +47,7 @@ "prompts": "2.4.2", "reflect-metadata": "0.1.13", "rollup": "4.4.1", + "sass": "1.79.4", "sharp": "0.33.0", "sinon": "6.0.0", "socket.io": "4.7.1", From 50bb5fa1f65c55de9e258c441b3bb4e611c3c7e1 Mon Sep 17 00:00:00 2001 From: 190n Date: Thu, 10 Oct 2024 02:35:38 -0700 Subject: [PATCH 04/15] Fix napi_throw_*/napi_create_*_error (#14446) --- src/bun.js/bindings/napi.cpp | 178 ++++++++++------------------------- test/napi/napi-app/main.cpp | 127 ++++++++++++++++++++++--- test/napi/napi-app/main.js | 2 +- test/napi/napi-app/module.js | 37 ++++++++ test/napi/napi.test.ts | 21 ++++- 5 files changed, 223 insertions(+), 142 deletions(-) diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 3a70970619c27..8f60db62928d1 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -1300,52 +1300,65 @@ napi_define_properties(napi_env env, napi_value object, size_t property_count, return napi_ok; } -static void throwErrorWithCode(JSC::JSGlobalObject* globalObject, const char* msg_utf8, const char* code_utf8, const WTF::Function& createError) +static JSC::ErrorInstance* createErrorWithCode(JSC::JSGlobalObject* globalObject, const WTF::String& code, const WTF::String& message, JSC::ErrorType type) { - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); + // no napi functions permit a null message, they must check before calling this function and + // return the right error code + ASSERT(!message.isNull()); - auto message = msg_utf8 ? WTF::String::fromUTF8(msg_utf8) : String(); - auto code = msg_utf8 ? WTF::String::fromUTF8(code_utf8) : String(); + auto& vm = globalObject->vm(); - auto* error = createError(globalObject, message); - if (!code.isEmpty()) { + // we don't call JSC::createError() as it asserts the message is not an empty string "" + auto* error = JSC::ErrorInstance::create(globalObject->vm(), globalObject->errorStructure(type), message, JSValue(), nullptr, RuntimeType::TypeNothing, type); + if (!code.isNull()) { error->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), JSC::jsString(vm, code), 0); } - scope.throwException(globalObject, Exception::create(vm, error)); + return error; } -static JSValue createErrorForNapi(napi_env env, napi_value code, napi_value msg, const WTF::Function& constructor) +// used to implement napi_throw_*_error +static napi_status throwErrorWithCStrings(napi_env env, const char* code_utf8, const char* msg_utf8, JSC::ErrorType type) { auto* globalObject = toJS(env); - JSC::VM& vm = globalObject->vm(); - auto catchScope = DECLARE_CATCH_SCOPE(vm); - - JSValue codeValue = toJS(code); - WTF::String message; + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); - if (msg) { - JSValue messageValue = toJS(msg); - message = messageValue.toWTFString(globalObject); - if (catchScope.exception()) { - catchScope.clearException(); - return {}; - } + if (!msg_utf8) { + return napi_invalid_arg; } - auto* error = constructor(globalObject, message); + WTF::String code = code_utf8 ? WTF::String::fromUTF8(code_utf8) : WTF::String(); + WTF::String message = WTF::String::fromUTF8(msg_utf8); - if (codeValue && error) { - error->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), codeValue, 0); - } + auto* error = createErrorWithCode(globalObject, code, message, type); + scope.throwException(globalObject, error); + return napi_ok; +} - if (catchScope.exception()) { - catchScope.clearException(); - return {}; +// code must be a string or nullptr (no code) +// msg must be a string +// never calls toString, never throws +static napi_status createErrorWithNapiValues(napi_env env, napi_value code, napi_value message, JSC::ErrorType type, napi_value* result) +{ + if (!result || !message) { + return napi_invalid_arg; + } + JSValue js_code = toJS(code); + JSValue js_message = toJS(message); + if (!js_message.isString() || !(js_code.isEmpty() || js_code.isString())) { + return napi_string_expected; } - return error; + auto* globalObject = toJS(env); + + auto wtf_code = js_code.isEmpty() ? WTF::String() : js_code.getString(globalObject); + auto wtf_message = js_message.getString(globalObject); + + *result = toNapi( + createErrorWithCode(globalObject, wtf_code, wtf_message, type), + globalObject); + return napi_ok; } extern "C" napi_status napi_throw_error(napi_env env, @@ -1353,13 +1366,7 @@ extern "C" napi_status napi_throw_error(napi_env env, const char* msg) { NAPI_PREMABLE - Zig::GlobalObject* globalObject = toJS(env); - - throwErrorWithCode(globalObject, msg, code, [](JSC::JSGlobalObject* globalObject, const WTF::String& message) { - return JSC::createError(globalObject, message); - }); - - return napi_ok; + return throwErrorWithCStrings(env, code, msg, JSC::ErrorType::Error); } extern "C" napi_status napi_create_reference(napi_env env, napi_value value, @@ -1650,20 +1657,7 @@ extern "C" napi_status node_api_create_syntax_error(napi_env env, napi_value* result) { NAPI_PREMABLE - if (UNLIKELY(!result)) { - return napi_invalid_arg; - } - - auto err = createErrorForNapi(env, code, msg, [](JSC::JSGlobalObject* globalObject, const WTF::String& message) { - return JSC::createSyntaxError(globalObject, message); - }); - - if (UNLIKELY(!err)) { - return napi_generic_failure; - } - - *result = toNapi(err, toJS(env)); - return napi_ok; + return createErrorWithNapiValues(env, code, msg, JSC::ErrorType::SyntaxError, result); } extern "C" napi_status node_api_throw_syntax_error(napi_env env, @@ -1671,51 +1665,22 @@ extern "C" napi_status node_api_throw_syntax_error(napi_env env, const char* msg) { NAPI_PREMABLE - - auto globalObject = toJS(env); - - throwErrorWithCode(globalObject, msg, code, [](JSC::JSGlobalObject* globalObject, const WTF::String& message) { - return JSC::createSyntaxError(globalObject, message); - }); - - return napi_ok; + return throwErrorWithCStrings(env, code, msg, JSC::ErrorType::SyntaxError); } extern "C" napi_status napi_throw_type_error(napi_env env, const char* code, const char* msg) { NAPI_PREMABLE - Zig::GlobalObject* globalObject = toJS(env); - - throwErrorWithCode(globalObject, msg, code, [](JSC::JSGlobalObject* globalObject, const WTF::String& message) { - return JSC::createTypeError(globalObject, message); - }); - - return napi_ok; + return throwErrorWithCStrings(env, code, msg, JSC::ErrorType::TypeError); } extern "C" napi_status napi_create_type_error(napi_env env, napi_value code, napi_value msg, napi_value* result) { - if (UNLIKELY(!result || !env)) { - return napi_invalid_arg; - } - - auto err = createErrorForNapi(env, code, msg, [](JSC::JSGlobalObject* globalObject, const WTF::String& message) { - if (message.isEmpty()) { - return JSC::createTypeError(globalObject); - } - - return JSC::createTypeError(globalObject, message); - }); - - if (UNLIKELY(!err)) { - return napi_generic_failure; - } - - *result = toNapi(err, toJS(env)); - return napi_ok; + NAPI_PREMABLE + return createErrorWithNapiValues(env, code, msg, JSC::ErrorType::TypeError, result); } extern "C" napi_status napi_create_error(napi_env env, napi_value code, @@ -1723,37 +1688,13 @@ extern "C" napi_status napi_create_error(napi_env env, napi_value code, napi_value* result) { NAPI_PREMABLE - - if (UNLIKELY(!result)) { - return napi_invalid_arg; - } - - auto err = createErrorForNapi(env, code, msg, [](JSC::JSGlobalObject* globalObject, const WTF::String& message) { - if (message.isEmpty()) { - return JSC::createError(globalObject, String("Error"_s)); - } - - return JSC::createError(globalObject, message); - }); - - if (UNLIKELY(!err)) { - return napi_generic_failure; - } - - *result = toNapi(err, toJS(env)); - return napi_ok; + return createErrorWithNapiValues(env, code, msg, JSC::ErrorType::Error, result); } extern "C" napi_status napi_throw_range_error(napi_env env, const char* code, const char* msg) { NAPI_PREMABLE - Zig::GlobalObject* globalObject = toJS(env); - - throwErrorWithCode(globalObject, msg, code, [](JSC::JSGlobalObject* globalObject, const WTF::String& message) { - return JSC::createRangeError(globalObject, message); - }); - - return napi_ok; + return throwErrorWithCStrings(env, code, msg, JSC::ErrorType::RangeError); } extern "C" napi_status napi_object_freeze(napi_env env, napi_value object_value) @@ -1818,24 +1759,7 @@ extern "C" napi_status napi_create_range_error(napi_env env, napi_value code, napi_value* result) { NAPI_PREMABLE - - if (UNLIKELY(!result)) { - return napi_invalid_arg; - } - - auto err = createErrorForNapi(env, code, msg, [](JSC::JSGlobalObject* globalObject, const WTF::String& message) { - if (message.isEmpty()) { - return JSC::createRangeError(globalObject, String("Range error"_s)); - } - - return JSC::createRangeError(globalObject, message); - }); - - if (UNLIKELY(!err)) { - return napi_generic_failure; - } - *result = toNapi(err, toJS(env)); - return napi_ok; + return createErrorWithNapiValues(env, code, msg, JSC::ErrorType::RangeError, result); } extern "C" napi_status napi_get_new_target(napi_env env, diff --git a/test/napi/napi-app/main.cpp b/test/napi/napi-app/main.cpp index e07d8b773d1f0..1e91e2ba9c90f 100644 --- a/test/napi/napi-app/main.cpp +++ b/test/napi/napi-app/main.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include napi_value fail(napi_env env, const char *msg) { @@ -35,6 +37,13 @@ static void run_gc(const Napi::CallbackInfo &info) { info[0].As().Call(0, nullptr); } +// calls napi_typeof and asserts it returns napi_ok +static napi_valuetype get_typeof(napi_env env, napi_value value) { + napi_valuetype result; + assert(napi_typeof(env, value, &result) == napi_ok); + return result; +} + napi_value test_issue_7685(const Napi::CallbackInfo &info) { Napi::Env env(info.Env()); Napi::HandleScope scope(env); @@ -229,8 +238,7 @@ napi_value test_napi_delete_property(const Napi::CallbackInfo &info) { // info[0] is a function to run the GC napi_value object = info[1]; - napi_valuetype type; - assert(napi_typeof(env, object, &type) == napi_ok); + napi_valuetype type = get_typeof(env, object); assert(type == napi_object); napi_value key; @@ -540,8 +548,7 @@ napi_value test_napi_ref(const Napi::CallbackInfo &info) { napi_value from_ref; assert(napi_get_reference_value(env, ref, &from_ref) == napi_ok); assert(from_ref != nullptr); - napi_valuetype typeof_result; - assert(napi_typeof(env, from_ref, &typeof_result) == napi_ok); + napi_valuetype typeof_result = get_typeof(env, from_ref); assert(typeof_result == napi_object); return ok(env); } @@ -629,8 +636,7 @@ napi_value call_and_get_exception(const Napi::CallbackInfo &info) { napi_value exception; assert(napi_get_and_clear_last_exception(env, &exception) == napi_ok); - napi_valuetype type; - assert(napi_typeof(env, exception, &type) == napi_ok); + napi_valuetype type = get_typeof(env, exception); printf("typeof thrown exception = %s\n", napi_valuetype_to_string(type)); assert(napi_is_exception_pending(env, &is_pending) == napi_ok); @@ -639,6 +645,103 @@ napi_value call_and_get_exception(const Napi::CallbackInfo &info) { return exception; } +// throw_error(code: string|undefined, msg: string|undefined, +// error_kind: 'error'|'type_error'|'range_error'|'syntax_error') +// if code and msg are JS undefined then change them to nullptr +napi_value throw_error(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value js_code = info[0]; + napi_value js_msg = info[1]; + napi_value js_error_kind = info[2]; + const char *code = nullptr; + const char *msg = nullptr; + char code_buf[256] = {0}, msg_buf[256] = {0}, error_kind_buf[256] = {0}; + + if (get_typeof(env, js_code) == napi_string) { + assert(napi_get_value_string_utf8(env, js_code, code_buf, sizeof code_buf, + nullptr) == napi_ok); + code = code_buf; + } + if (get_typeof(env, js_msg) == napi_string) { + assert(napi_get_value_string_utf8(env, js_msg, msg_buf, sizeof msg_buf, + nullptr) == napi_ok); + msg = msg_buf; + } + assert(napi_get_value_string_utf8(env, js_error_kind, error_kind_buf, + sizeof error_kind_buf, nullptr) == napi_ok); + + std::map + functions{{"error", napi_throw_error}, + {"type_error", napi_throw_type_error}, + {"range_error", napi_throw_range_error}, + {"syntax_error", node_api_throw_syntax_error}}; + + auto throw_function = functions[error_kind_buf]; + + if (msg == nullptr) { + assert(throw_function(env, code, msg) == napi_invalid_arg); + return ok(env); + } else { + assert(throw_function(env, code, msg) == napi_ok); + return nullptr; + } +} + +// create_and_throw_error(code: any, msg: any, +// error_kind: 'error'|'type_error'|'range_error'|'syntax_error') +// if code and msg are JS null then change them to nullptr +napi_value create_and_throw_error(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value js_code = info[0]; + napi_value js_msg = info[1]; + napi_value js_error_kind = info[2]; + char error_kind_buf[256] = {0}; + + if (get_typeof(env, js_code) == napi_null) { + js_code = nullptr; + } + if (get_typeof(env, js_msg) == napi_null) { + js_msg = nullptr; + } + + assert(napi_get_value_string_utf8(env, js_error_kind, error_kind_buf, + sizeof error_kind_buf, nullptr) == napi_ok); + + std::map + functions{{"error", napi_create_error}, + {"type_error", napi_create_type_error}, + {"range_error", napi_create_range_error}, + {"syntax_error", node_api_create_syntax_error}}; + + auto create_error_function = functions[error_kind_buf]; + + napi_value err; + napi_status create_status = create_error_function(env, js_code, js_msg, &err); + // cases that should fail: + // - js_msg is nullptr + // - js_msg is not a string + // - js_code is not nullptr and not a string + // also we need to make sure not to call get_typeof with nullptr, since it + // asserts that napi_typeof succeeded + if (!js_msg || get_typeof(env, js_msg) != napi_string || + (js_code && get_typeof(env, js_code) != napi_string)) { + // bun and node may return different errors here depending on in what order + // the parameters are checked, but what's important is that there is an + // error + assert(create_status == napi_string_expected || + create_status == napi_invalid_arg); + return ok(env); + } else { + assert(create_status == napi_ok); + assert(napi_throw(env, err) == napi_ok); + return nullptr; + } +} + napi_value eval_wrapper(const Napi::CallbackInfo &info) { napi_value ret = nullptr; // info[0] is the GC callback @@ -655,8 +758,7 @@ napi_value perform_get(const Napi::CallbackInfo &info) { napi_value value; // if key is a string, try napi_get_named_property - napi_valuetype type; - assert(napi_typeof(env, key, &type) == napi_ok); + napi_valuetype type = get_typeof(env, key); if (type == napi_string) { char buf[1024]; assert(napi_get_value_string_utf8(env, key, buf, 1024, nullptr) == napi_ok); @@ -666,8 +768,7 @@ napi_value perform_get(const Napi::CallbackInfo &info) { status == napi_pending_exception || status == napi_generic_failure); if (status == napi_ok) { assert(value != nullptr); - assert(napi_typeof(env, value, &type) == napi_ok); - printf("value type = %d\n", type); + printf("value type = %d\n", get_typeof(env, value)); } else { return ok(env); } @@ -678,8 +779,7 @@ napi_value perform_get(const Napi::CallbackInfo &info) { status == napi_pending_exception || status == napi_generic_failure); if (status == napi_ok) { assert(value != nullptr); - assert(napi_typeof(env, value, &type) == napi_ok); - printf("value type = %d\n", type); + printf("value type = %d\n", get_typeof(env, value)); return value; } else { return ok(env); @@ -740,6 +840,9 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) { Napi::Function::New(env, call_and_get_exception)); exports.Set("eval_wrapper", Napi::Function::New(env, eval_wrapper)); exports.Set("perform_get", Napi::Function::New(env, perform_get)); + exports.Set("throw_error", Napi::Function::New(env, throw_error)); + exports.Set("create_and_throw_error", + Napi::Function::New(env, create_and_throw_error)); return exports; } diff --git a/test/napi/napi-app/main.js b/test/napi/napi-app/main.js index bf7d66d9a4c11..d37ba09171f01 100644 --- a/test/napi/napi-app/main.js +++ b/test/napi/napi-app/main.js @@ -47,5 +47,5 @@ try { throw new Error(result); } } catch (e) { - console.log("synchronously threw:", e.name); + console.log(`synchronously threw ${e.name}: message ${JSON.stringify(e.message)}, code ${JSON.stringify(e.code)}`); } diff --git a/test/napi/napi-app/module.js b/test/napi/napi-app/module.js index 79903ed5c7d0c..60ce6c4aac683 100644 --- a/test/napi/napi-app/module.js +++ b/test/napi/napi-app/module.js @@ -91,4 +91,41 @@ nativeTests.test_get_property = () => { } }; +nativeTests.test_throw_functions_exhaustive = () => { + for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) { + for (const code of [undefined, "", "error code"]) { + for (const msg of [undefined, "", "error message"]) { + try { + nativeTests.throw_error(code, msg, errorKind); + console.log(`napi_throw_${errorKind}(${code ?? "nullptr"}, ${msg ?? "nullptr"}) did not throw`); + } catch (e) { + console.log( + `napi_throw_${errorKind} threw ${e.name}: message ${JSON.stringify(e.message)}, code ${JSON.stringify(e.code)}`, + ); + } + } + } + } +}; + +nativeTests.test_create_error_functions_exhaustive = () => { + for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) { + // null (JavaScript null) is changed to nullptr by the native function + for (const code of [undefined, null, "", 42, "error code"]) { + for (const msg of [undefined, null, "", 42, "error message"]) { + try { + nativeTests.create_and_throw_error(code, msg, errorKind); + console.log( + `napi_create_${errorKind}(${code === null ? "nullptr" : code}, ${msg === null ? "nullptr" : msg}) did not make an error`, + ); + } catch (e) { + console.log( + `create_and_throw_error(${errorKind}) threw ${e.name}: message ${JSON.stringify(e.message)}, code ${JSON.stringify(e.code)}`, + ); + } + } + } + } +}; + module.exports = nativeTests; diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index b9e0e23da2671..bbe2836ea632a 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -271,8 +271,14 @@ describe("napi", () => { checkSameOutput("eval_wrapper", ["(()=>{ throw new TypeError('oops'); })()"]); }); it("cannot see locals from around its invocation", () => { - // variable is declared on main.js:18, but it should not be in scope for the eval'd code - checkSameOutput("eval_wrapper", ["shouldNotExist"]); + // variable should_not_exist is declared on main.js:18, but it should not be in scope for the eval'd code + // this doesn't use checkSameOutput because V8 and JSC use different error messages for a missing variable + let bunResult = runOn(bunExe(), "eval_wrapper", ["shouldNotExist"]); + // remove all debug logs + bunResult = bunResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); + expect(bunResult).toBe( + `synchronously threw ReferenceError: message "Can't find variable: shouldNotExist", code undefined`, + ); }); }); @@ -281,6 +287,17 @@ describe("napi", () => { checkSameOutput("test_get_property", []); }); }); + + describe("napi_throw functions", () => { + it("has the right code and message", () => { + checkSameOutput("test_throw_functions_exhaustive", []); + }); + }); + describe("napi_create_error functions", () => { + it("has the right code and message", () => { + checkSameOutput("test_create_error_functions_exhaustive", []); + }); + }); }); function checkSameOutput(test: string, args: any[] | string) { From e650ee79671ca8d4bdc547df18986cfb6fa64adb Mon Sep 17 00:00:00 2001 From: huseeiin <122984423+huseeiin@users.noreply.github.com> Date: Thu, 10 Oct 2024 05:35:53 -0400 Subject: [PATCH 05/15] Update bun.d.ts (#14429) --- packages/bun-types/bun.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 6e7ed1cdb8f29..209efa034ecc1 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3961,7 +3961,7 @@ declare module "bun" { * * In a future version of Bun, this will be used in error messages. */ - name?: string; + name: string; /** * The target JavaScript environment the plugin should be applied to. From 05f68d79c8ea5b40aaee609638a2824b41bbee9b Mon Sep 17 00:00:00 2001 From: Michael H Date: Thu, 10 Oct 2024 22:04:58 +1100 Subject: [PATCH 06/15] docs: `--conditions` flag (#14463) --- docs/runtime/modules.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/runtime/modules.md b/docs/runtime/modules.md index 2d50ac6cdd2c4..526446751ea29 100644 --- a/docs/runtime/modules.md +++ b/docs/runtime/modules.md @@ -238,6 +238,30 @@ If `exports` is not defined, Bun falls back to `"module"` (ESM imports only) the } ``` +### Custom conditions + +The `--conditions` flag allows you to specify a list of conditions to use when resolving packages from package.json `"exports"`. + +This flag is supported in both `bun build` and Bun's runtime. + +```sh +# Use it with bun build: +$ bun build --conditions="react-server" --target=bun ./app/foo/route.js + +# Use it with bun's runtime: +$ bun --conditions="react-server" ./app/foo/route.js +``` + +You can also use `conditions` programmatically with `Bun.build`: + +```js +await Bun.build({ + conditions: ["react-server"], + target: "bun", + entryPoints: ["./app/foo/route.js"], +}); +``` + ## Path re-mapping In the spirit of treating TypeScript as a first-class citizen, the Bun runtime will re-map import paths according to the [`compilerOptions.paths`](https://www.typescriptlang.org/tsconfig#paths) field in `tsconfig.json`. This is a major divergence from Node.js, which doesn't support any form of import path re-mapping. From 584a8ceb8466e240a76e80b91f3a511735f32301 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 10 Oct 2024 15:47:59 -0700 Subject: [PATCH 07/15] enable iterator-helpers in webkit (#14455) --- src/bun.js/bindings/ZigGlobalObject.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 7da7d3e3badac..61da2ab06ffc1 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -245,6 +245,7 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c JSC::Options::evalMode() = evalMode; JSC::Options::usePromiseTryMethod() = true; JSC::Options::useRegExpEscape() = true; + JSC::Options::useIteratorHelpers() = true; JSC::dangerouslyOverrideJSCBytecodeCacheVersion(getWebKitBytecodeCacheVersion()); #ifdef BUN_DEBUG From 05f53dc70fa970d05771b2bda14547713582d7fd Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 10 Oct 2024 21:50:03 -0700 Subject: [PATCH 08/15] Fixes #14464 (#14473) --- src/bun.js/bindings/bindings.zig | 4 ++++ src/css/values/color_js.zig | 6 +++--- test/js/bun/css/color.test.ts | 10 ++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index ed9adccbe245f..0623c96d34631 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -6510,6 +6510,10 @@ pub const CallFrame = opaque { pub inline fn slice(self: *const @This()) []const JSValue { return self.ptr[0..self.len]; } + + pub inline fn all(self: *const @This()) []const JSValue { + return self.ptr[0..]; + } }; } diff --git a/src/css/values/color_js.zig b/src/css/values/color_js.zig index 95d195a283768..2ca8565133291 100644 --- a/src/css/values/color_js.zig +++ b/src/css/values/color_js.zig @@ -146,9 +146,9 @@ pub const Ansi256 = struct { }; pub fn jsFunctionColor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { - const args = callFrame.arguments(2).slice(); - if (args.len < 1 or args[0].isUndefined()) { - globalThis.throwNotEnoughArguments("Bun.color", 2, args.len); + const args = callFrame.argumentsUndef(2).all(); + if (args[0].isUndefined()) { + globalThis.throwInvalidArgumentType("color", "input", "string, number, or object"); return JSC.JSValue.jsUndefined(); } diff --git a/test/js/bun/css/color.test.ts b/test/js/bun/css/color.test.ts index defbd795cc63f..1d0ec0f292f2f 100644 --- a/test/js/bun/css/color.test.ts +++ b/test/js/bun/css/color.test.ts @@ -180,6 +180,7 @@ const bad = [ ]; test.each(bad)("color(%s, 'css') === null", input => { expect(color(input, "css")).toBeNull(); + expect(color(input)).toBeNull(); }); const weird = [ @@ -189,9 +190,18 @@ const weird = [ describe("weird", () => { test.each(weird)("color(%s, 'css') === %s", (input, expected) => { expect(color(input, "css")).toEqual(expected); + expect(color(input)).toEqual(expected); }); }); +test("0 args", () => { + expect(() => color()).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + }), + ); +}); + test("fuzz ansi256", () => { withoutAggressiveGC(() => { for (let i = 0; i < 256; i++) { From 874c9dbb243eed0970abc0d37beeb7beb4bb85ad Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 10 Oct 2024 22:04:33 -0700 Subject: [PATCH 09/15] fix fs-open.test.js (#14311) --- src/bun.js/node/types.zig | 21 ++--- src/bun.js/node/util/validators.zig | 8 +- test/js/node/test/parallel/fs-open.test.js | 102 +++++++++++++++++++++ 3 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 test/js/node/test/parallel/fs-open.test.js diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 6ff1448c06d61..5f9034610548b 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -23,6 +23,7 @@ const Shimmer = @import("../bindings/shimmer.zig").Shimmer; const Syscall = bun.sys; const URL = @import("../../url.zig").URL; const Value = std.json.Value; +const validators = @import("./util/validators.zig"); pub const Path = @import("./path.zig"); @@ -1210,14 +1211,16 @@ pub fn timeLikeFromJS(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue, _: pub fn modeFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?Mode { const mode_int = if (value.isNumber()) brk: { - if (!value.isUInt32AsAnyInt()) { - exception.* = ctx.ERR_OUT_OF_RANGE("The value of \"mode\" is out of range. It must be an integer. Received {d}", .{value.asNumber()}).toJS().asObjectRef(); - return null; - } - break :brk @as(Mode, @truncate(value.to(Mode))); + const m = validators.validateUint32(ctx, value, "mode", .{}, false) catch return null; + break :brk @as(Mode, @as(u24, @truncate(m))); } else brk: { if (value.isUndefinedOrNull()) return null; + if (!value.isString()) { + _ = ctx.throwInvalidArgumentTypeValue("mode", "number", value); + return null; + } + // An easier method of constructing the mode is to use a sequence of // three octal digits (e.g. 765). The left-most digit (7 in the example), // specifies the permissions for the file owner. The middle digit (6 in @@ -1232,16 +1235,12 @@ pub fn modeFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue, exception: JSC.C. } break :brk std.fmt.parseInt(Mode, slice, 8) catch { - JSC.throwInvalidArguments("Invalid mode string: must be an octal number", .{}, ctx, exception); + var formatter = bun.JSC.ConsoleObject.Formatter{ .globalThis = ctx }; + exception.* = ctx.ERR_INVALID_ARG_VALUE("The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received {}", .{value.toFmt(&formatter)}).toJS().asObjectRef(); return null; }; }; - if (mode_int < 0) { - JSC.throwInvalidArguments("Invalid mode: must be greater than or equal to 0.", .{}, ctx, exception); - return null; - } - return mode_int & 0o777; } diff --git a/src/bun.js/node/util/validators.zig b/src/bun.js/node/util/validators.zig index b7f56555c0c65..554f62ddbffc8 100644 --- a/src/bun.js/node/util/validators.zig +++ b/src/bun.js/node/util/validators.zig @@ -97,15 +97,17 @@ pub fn validateUint32(globalThis: *JSGlobalObject, value: JSValue, comptime name try throwErrInvalidArgType(globalThis, name_fmt, name_args, "number", value); } if (!value.isAnyInt()) { - try throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {s}", name_args ++ .{value}); + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; + try throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {}", name_args ++ .{value.toFmt(&formatter)}); } const num: i64 = value.asInt52(); const min: i64 = if (greater_than_zero) 1 else 0; const max: i64 = @intCast(std.math.maxInt(u32)); if (num < min or num > max) { - try throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {s}", name_args ++ .{ min, max, value }); + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; + try throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); } - return @truncate(num); + return @truncate(@as(u63, @intCast(num))); } pub fn validateString(globalThis: *JSGlobalObject, value: JSValue, comptime name_fmt: string, name_args: anytype) !void { diff --git a/test/js/node/test/parallel/fs-open.test.js b/test/js/node/test/parallel/fs-open.test.js new file mode 100644 index 0000000000000..c8c102d7a3605 --- /dev/null +++ b/test/js/node/test/parallel/fs-open.test.js @@ -0,0 +1,102 @@ +//#FILE: test-fs-open.js +//#SHA1: 0466ad8882a3256fdd8da5fc8da3167f6dde4fd6 +//----------------- +'use strict'; +const fs = require('fs'); +const path = require('path'); + +test('fs.openSync throws ENOENT for non-existent file', () => { + expect(() => { + fs.openSync('/8hvftyuncxrt/path/to/file/that/does/not/exist', 'r'); + }).toThrow(expect.objectContaining({ + code: 'ENOENT', + message: expect.any(String) + })); +}); + +test('fs.openSync succeeds for existing file', () => { + expect(() => fs.openSync(__filename)).not.toThrow(); +}); + +test('fs.open succeeds with various valid arguments', async () => { + await expect(fs.promises.open(__filename)).resolves.toBeDefined(); + await expect(fs.promises.open(__filename, 'r')).resolves.toBeDefined(); + await expect(fs.promises.open(__filename, 'rs')).resolves.toBeDefined(); + await expect(fs.promises.open(__filename, 'r', 0)).resolves.toBeDefined(); + await expect(fs.promises.open(__filename, 'r', null)).resolves.toBeDefined(); +}); + +test('fs.open throws for invalid mode argument', () => { + expect(() => fs.open(__filename, 'r', 'boom', () => {})).toThrow(({ + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: `The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received boom` + })); + expect(() => fs.open(__filename, 'r', 5.5, () => {})).toThrow(({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: `The value of "mode" is out of range. It must be an integer. Received 5.5` + })); + expect(() => fs.open(__filename, 'r', -7, () => {})).toThrow(({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: `The value of "mode" is out of range. It must be >= 0 and <= 4294967295. Received -7` + })); + expect(() => fs.open(__filename, 'r', 4304967295, () => {})).toThrow(({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: `The value of "mode" is out of range. It must be >= 0 and <= 4294967295. Received 4304967295` + })); +}); + +test('fs.open throws for invalid argument combinations', () => { + const invalidArgs = [[], ['r'], ['r', 0], ['r', 0, 'bad callback']]; + invalidArgs.forEach(args => { + expect(() => fs.open(__filename, ...args)).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); + }); +}); + +test('fs functions throw for invalid path types', () => { + const invalidPaths = [false, 1, [], {}, null, undefined]; + invalidPaths.forEach(path => { + expect(() => fs.open(path, 'r', () => {})).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); + expect(() => fs.openSync(path, 'r')).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); + expect(fs.promises.open(path, 'r')).rejects.toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); + }); +}); + +test('fs functions throw for invalid modes', () => { + const invalidModes = [false, [], {}]; + invalidModes.forEach(mode => { + expect(() => fs.open(__filename, 'r', mode, () => {})).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + message: expect.any(String) + })); + expect(() => fs.openSync(__filename, 'r', mode)).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + message: expect.any(String) + })); + expect(fs.promises.open(__filename, 'r', mode)).rejects.toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + message: expect.any(String) + })); + }); +}); + +//<#END_FILE: test-fs-open.js From 170fafbca981dcd35bd3f37e11754d233fb2376d Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 10 Oct 2024 22:07:41 -0700 Subject: [PATCH 10/15] fix fs-non-number-arguments-throw.test.js (#14312) --- src/bun.js/bindings/ErrorCode.cpp | 2 +- src/js/node/fs.ts | 20 ++++++ .../fs-non-number-arguments-throw.test.js | 65 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 test/js/node/test/parallel/fs-non-number-arguments-throw.test.js diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 43c5d3300408e..f7464b91dc159 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -300,7 +300,7 @@ WTF::String ERR_OUT_OF_RANGE(JSC::ThrowScope& scope, JSC::JSGlobalObject* global auto input = JSValueToStringSafe(globalObject, val_input); RETURN_IF_EXCEPTION(scope, {}); - return makeString("The value of \""_s, arg_name, "\" is out of range. It must be "_s, range, ". Received: \""_s, input, '"'); + return makeString("The value of \""_s, arg_name, "\" is out of range. It must be "_s, range, ". Received: "_s, input); } } diff --git a/src/js/node/fs.ts b/src/js/node/fs.ts index b0b7905d7def2..a9126aa871155 100644 --- a/src/js/node/fs.ts +++ b/src/js/node/fs.ts @@ -5,6 +5,9 @@ const promises = require("node:fs/promises"); const Stream = require("node:stream"); const types = require("node:util/types"); +const { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } = require("internal/errors"); +const { validateInteger } = require("internal/validators"); + const NumberIsFinite = Number.isFinite; const DateNow = Date.now; const DatePrototypeGetTime = Date.prototype.getTime; @@ -830,6 +833,18 @@ function ReadStream(this: typeof ReadStream, pathOrFd, options) { // Get the stream controller // We need the pointer to the underlying stream controller for the NativeReadable + if (start !== undefined) { + validateInteger(start, "start", 0); + } + if (end === undefined) { + end = Infinity; + } else if (end !== Infinity) { + validateInteger(end, "end", 0); + if (start !== undefined && start > end) { + throw new ERR_OUT_OF_RANGE("start", `<= "end" (here: ${end})`, start); + } + } + const stream = blobToStreamWithOffset.$apply(fileRef, [start]); var ptr = stream.$bunNativePtr; if (!ptr) { @@ -1068,6 +1083,11 @@ var WriteStreamClass = (WriteStream = function WriteStream(path, options = defau pos = defaultWriteStreamOptions.pos, } = options; + if (start !== undefined) { + validateInteger(start, "start", 0); + options.pos = start; + } + var tempThis = {}; var handle = null; if (fd != null) { diff --git a/test/js/node/test/parallel/fs-non-number-arguments-throw.test.js b/test/js/node/test/parallel/fs-non-number-arguments-throw.test.js new file mode 100644 index 0000000000000..fa7ff3127d963 --- /dev/null +++ b/test/js/node/test/parallel/fs-non-number-arguments-throw.test.js @@ -0,0 +1,65 @@ +//#FILE: test-fs-non-number-arguments-throw.js +//#SHA1: 65db5c653216831bc16d38c5d659fbffa296d3d8 +//----------------- +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const tmpdir = path.join(os.tmpdir(), 'test-fs-non-number-arguments-throw'); +const tempFile = path.join(tmpdir, 'fs-non-number-arguments-throw'); + +beforeAll(() => { + if (fs.existsSync(tmpdir)) { + fs.rmSync(tmpdir, { recursive: true, force: true }); + } + fs.mkdirSync(tmpdir, { recursive: true }); + fs.writeFileSync(tempFile, 'abc\ndef'); +}); + +afterAll(() => { + fs.rmSync(tmpdir, { recursive: true, force: true }); +}); + +test('createReadStream with valid number arguments', (done) => { + const sanity = 'def'; + const saneEmitter = fs.createReadStream(tempFile, { start: 4, end: 6 }); + + saneEmitter.on('data', (data) => { + expect(data.toString('utf8')).toBe(sanity); + done(); + }); +}); + +test('createReadStream throws with string start argument', () => { + expect(() => { + fs.createReadStream(tempFile, { start: '4', end: 6 }); + }).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); +}); + +test('createReadStream throws with string end argument', () => { + expect(() => { + fs.createReadStream(tempFile, { start: 4, end: '6' }); + }).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); +}); + +test('createWriteStream throws with string start argument', () => { + expect(() => { + fs.createWriteStream(tempFile, { start: '4' }); + }).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); +}); + +//<#END_FILE: test-fs-non-number-arguments-throw.js From 25fcbed8d187742faedfe2ff7ce413c059a3c774 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 10 Oct 2024 22:08:16 -0700 Subject: [PATCH 11/15] enhance Buffer.from to support (de)serialization roundtrip (#14201) Co-authored-by: Jarred Sumner --- src/js/builtins/JSBufferConstructor.ts | 6 ++++++ test/js/node/buffer.test.js | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/js/builtins/JSBufferConstructor.ts b/src/js/builtins/JSBufferConstructor.ts index 69615d8dcc8b8..2c0c09e982ba7 100644 --- a/src/js/builtins/JSBufferConstructor.ts +++ b/src/js/builtins/JSBufferConstructor.ts @@ -29,6 +29,12 @@ export function from(items) { } } } + if (typeof items === "object") { + const data = items.data; + if (items.type === "Buffer" && Array.isArray(data)) { + return new $Buffer(data); + } + } var arrayLike = $toObject( items, diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 9eec90f9ed29a..32402af3d273c 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -2921,3 +2921,27 @@ export function fillRepeating(dstBuffer, start, end) { sLen <<= 1; // double length for next segment } } + +describe("serialization", () => { + it("json", () => { + expect(JSON.stringify(Buffer.alloc(0))).toBe('{"type":"Buffer","data":[]}'); + expect(JSON.stringify(Buffer.from([1, 2, 3, 4]))).toBe('{"type":"Buffer","data":[1,2,3,4]}'); + }); + + it("and deserialization", () => { + const buf = Buffer.from("test"); + const json = JSON.stringify(buf); + const obj = JSON.parse(json); + const copy = Buffer.from(obj); + expect(copy).toEqual(buf); + }); + + it("custom", () => { + const buffer = Buffer.from("test"); + const string = JSON.stringify(buffer); + expect(string).toBe('{"type":"Buffer","data":[116,101,115,116]}'); + + const receiver = (key, value) => (value && value.type === "Buffer" ? Buffer.from(value.data) : value); + expect(JSON.parse(string, receiver)).toEqual(buffer); + }); +}); From ba9db6cdb6e408f135ea32a6485d380b5ffb50b2 Mon Sep 17 00:00:00 2001 From: pfg Date: Fri, 11 Oct 2024 01:50:02 -0500 Subject: [PATCH 12/15] Fix console.table for numeric keys (#14484) --- src/bun.js/ConsoleObject.zig | 4 ++-- src/bun.js/bindings/bindings.cpp | 17 ----------------- src/bun.js/bindings/bindings.zig | 8 -------- .../__snapshots__/console-table.test.ts.snap | 9 +++++++++ test/js/bun/console/console-table.test.ts | 8 ++++++++ 5 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 8ad8ec525a471..25b9df0ae0ed0 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -342,7 +342,7 @@ const TablePrinter = struct { // - otherwise: iterate the object properties, and create the columns on-demand if (!this.properties.isUndefined()) { for (columns.items[1..]) |*column| { - if (row_value.getWithString(this.globalObject, column.name)) |value| { + if (row_value.getOwn(this.globalObject, column.name)) |value| { column.width = @max(column.width, this.getWidthForValue(value)); } } @@ -436,7 +436,7 @@ const TablePrinter = struct { value = row_value; } } else if (row_value.isObject()) { - value = row_value.getWithString(this.globalObject, col.name) orelse JSValue.zero; + value = row_value.getOwn(this.globalObject, col.name) orelse JSValue.zero; } if (value.isEmpty()) { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 8409ffa856dd6..0c1edd2273fff 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3712,23 +3712,6 @@ JSC__JSValue JSC__JSValue__getIfPropertyExistsImpl(JSC__JSValue JSValue0, return JSC::JSValue::encode(Bun::getIfPropertyExistsPrototypePollutionMitigation(vm, globalObject, object, property)); } -extern "C" JSC__JSValue JSC__JSValue__getIfPropertyExistsImplString(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObject, BunString* propertyName) -{ - ASSERT_NO_PENDING_EXCEPTION(globalObject); - JSValue value = JSC::JSValue::decode(JSValue0); - JSC::JSObject* object = value.getObject(); - if (UNLIKELY(!object)) - return JSValue::encode({}); - - JSC::VM& vm = globalObject->vm(); - - WTF::String propertyNameString = propertyName->tag == BunStringTag::Empty ? WTF::String(""_s) : propertyName->toWTFString(BunString::ZeroCopy); - auto identifier = JSC::Identifier::fromString(vm, propertyNameString); - auto property = JSC::PropertyName(identifier); - - return JSC::JSValue::encode(Bun::getIfPropertyExistsPrototypePollutionMitigation(vm, globalObject, object, property)); -} - extern "C" JSC__JSValue JSC__JSValue__getOwn(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObject, BunString* propertyName) { ASSERT_NO_PENDING_EXCEPTION(globalObject); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 0623c96d34631..75b755bce1495 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -5275,14 +5275,6 @@ pub const JSValue = enum(JSValueReprInt) { return if (value.isEmpty()) null else value; } - extern fn JSC__JSValue__getIfPropertyExistsImplString(value: JSValue, globalObject: *JSGlobalObject, propertyName: [*c]const bun.String) JSValue; - - pub fn getWithString(this: JSValue, global: *JSGlobalObject, property_name: anytype) ?JSValue { - var property_name_str = bun.String.init(property_name); - const value = JSC__JSValue__getIfPropertyExistsImplString(this, global, &property_name_str); - return if (@intFromEnum(value) != 0) value else return null; - } - extern fn JSC__JSValue__getOwn(value: JSValue, globalObject: *JSGlobalObject, propertyName: [*c]const bun.String) JSValue; /// Get *own* property value (i.e. does not resolve property in the prototype chain) diff --git a/test/js/bun/console/__snapshots__/console-table.test.ts.snap b/test/js/bun/console/__snapshots__/console-table.test.ts.snap index 83bf72ab2b271..28d4755f7efa0 100644 --- a/test/js/bun/console/__snapshots__/console-table.test.ts.snap +++ b/test/js/bun/console/__snapshots__/console-table.test.ts.snap @@ -194,3 +194,12 @@ exports[`console.table expected output for: properties - interesting character 1 └───┴────────┘ " `; + +exports[`console.table expected output for: number keys 1`] = ` +"┌──────┬─────┬─────┐ +│ │ 10 │ 100 │ +├──────┼─────┼─────┤ +│ test │ 123 │ 154 │ +└──────┴─────┴─────┘ +" +`; diff --git a/test/js/bun/console/console-table.test.ts b/test/js/bun/console/console-table.test.ts index 22d780ac82b04..24b5848c1345f 100644 --- a/test/js/bun/console/console-table.test.ts +++ b/test/js/bun/console/console-table.test.ts @@ -134,6 +134,14 @@ describe("console.table", () => { ], }, ], + [ + "number keys", + { + args: () => [ + {test: {"10": 123, "100": 154}}, + ], + }, + ], ])("expected output for: %s", (label, { args }) => { const { stdout } = spawnSync({ cmd: [bunExe(), `${import.meta.dir}/console-table-run.ts`, args.toString()], From 50e9be0dc72c638b6d5957eab135e7dbd286b41c Mon Sep 17 00:00:00 2001 From: 190n Date: Thu, 10 Oct 2024 23:50:39 -0700 Subject: [PATCH 13/15] Fix napi_value<=>integer conversions and napi_create_empty_array (#14479) --- src/bun.js/bindings/napi.cpp | 85 ++++++++++++++++- src/napi/napi.zig | 57 ++---------- test/napi/napi-app/main.cpp | 174 +++++++++++++++++++++++++++++++++++ test/napi/napi-app/module.js | 115 +++++++++++++++++++++++ test/napi/napi.test.ts | 15 +++ 5 files changed, 392 insertions(+), 54 deletions(-) diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 8f60db62928d1..2de81004f431a 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -2172,11 +2172,90 @@ extern "C" napi_status napi_get_value_double(napi_env env, napi_value value, auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + // should never throw as we know it is a number *result = jsValue.toNumber(globalObject); + scope.assertNoException(); - if (UNLIKELY(scope.exception())) { - scope.clearException(); - return napi_generic_failure; + return napi_ok; +} + +extern "C" napi_status napi_get_value_int32(napi_env env, napi_value value, int32_t* result) +{ + NAPI_PREMABLE + + auto* globalObject = toJS(env); + JSC::JSValue jsValue = toJS(value); + + if (UNLIKELY(result == nullptr || !globalObject)) { + return napi_invalid_arg; + } + + if (UNLIKELY(!jsValue || !jsValue.isNumber())) { + return napi_number_expected; + } + + auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + + // should never throw as we know it is a number + *result = jsValue.toInt32(globalObject); + scope.assertNoException(); + + return napi_ok; +} + +extern "C" napi_status napi_get_value_uint32(napi_env env, napi_value value, uint32_t* result) +{ + NAPI_PREMABLE + + auto* globalObject = toJS(env); + JSC::JSValue jsValue = toJS(value); + + if (UNLIKELY(result == nullptr || !globalObject)) { + return napi_invalid_arg; + } + + if (UNLIKELY(!jsValue || !jsValue.isNumber())) { + return napi_number_expected; + } + + auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + + // should never throw as we know it is a number + *result = jsValue.toUInt32(globalObject); + scope.assertNoException(); + + return napi_ok; +} + +extern "C" napi_status napi_get_value_int64(napi_env env, napi_value value, int64_t* result) +{ + NAPI_PREMABLE + + auto* globalObject = toJS(env); + JSC::JSValue jsValue = toJS(value); + + if (UNLIKELY(result == nullptr || !globalObject)) { + return napi_invalid_arg; + } + + if (UNLIKELY(!jsValue || !jsValue.isNumber())) { + return napi_number_expected; + } + + double js_number = jsValue.asNumber(); + if (isfinite(js_number)) { + // upper is 2^63 exactly, not 2^63-1, as the latter can't be represented exactly + constexpr double lower = std::numeric_limits::min(), upper = 1ull << 63; + if (js_number >= upper) { + *result = std::numeric_limits::max(); + } else if (js_number <= lower) { + *result = std::numeric_limits::min(); + } else { + // safe + *result = static_cast(js_number); + } + } else { + *result = 0; } return napi_ok; diff --git a/src/napi/napi.zig b/src/napi/napi.zig index baa675eb31998..fbe6a2d6bcc1b 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -296,29 +296,17 @@ pub export fn napi_create_array(env: napi_env, result_: ?*napi_value) napi_statu result.set(env, JSValue.createEmptyArray(env, 0)); return .ok; } -const prefilled_undefined_args_array: [128]JSC.JSValue = brk: { - var args: [128]JSC.JSValue = undefined; - for (args, 0..) |_, i| { - args[i] = JSValue.jsUndefined(); - } - break :brk args; -}; pub export fn napi_create_array_with_length(env: napi_env, length: usize, result_: ?*napi_value) napi_status { log("napi_create_array_with_length", .{}); const result = result_ orelse { return invalidArg(); }; - const len = @as(u32, @intCast(length)); + // JSC createEmptyArray takes u32 + // Node and V8 convert out-of-bounds array sizes to 0 + const len = std.math.cast(u32, length) orelse 0; const array = JSC.JSValue.createEmptyArray(env, len); - array.ensureStillAlive(); - - var i: u32 = 0; - while (i < len) : (i += 1) { - array.putIndex(env, i, JSValue.jsUndefined()); - } - array.ensureStillAlive(); result.set(env, array); return .ok; @@ -448,42 +436,9 @@ pub extern fn napi_create_type_error(env: napi_env, code: napi_value, msg: napi_ pub extern fn napi_create_range_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_typeof(env: napi_env, value: napi_value, result: *napi_valuetype) napi_status; pub extern fn napi_get_value_double(env: napi_env, value: napi_value, result: *f64) napi_status; -pub export fn napi_get_value_int32(_: napi_env, value_: napi_value, result_: ?*i32) napi_status { - log("napi_get_value_int32", .{}); - const result = result_ orelse { - return invalidArg(); - }; - const value = value_.get(); - if (!value.isNumber()) { - return .number_expected; - } - result.* = value.to(i32); - return .ok; -} -pub export fn napi_get_value_uint32(_: napi_env, value_: napi_value, result_: ?*u32) napi_status { - log("napi_get_value_uint32", .{}); - const result = result_ orelse { - return invalidArg(); - }; - const value = value_.get(); - if (!value.isNumber()) { - return .number_expected; - } - result.* = value.to(u32); - return .ok; -} -pub export fn napi_get_value_int64(_: napi_env, value_: napi_value, result_: ?*i64) napi_status { - log("napi_get_value_int64", .{}); - const result = result_ orelse { - return invalidArg(); - }; - const value = value_.get(); - if (!value.isNumber()) { - return .number_expected; - } - result.* = value.to(i64); - return .ok; -} +pub extern fn napi_get_value_int32(_: napi_env, value_: napi_value, result: ?*i32) napi_status; +pub extern fn napi_get_value_uint32(_: napi_env, value_: napi_value, result_: ?*u32) napi_status; +pub extern fn napi_get_value_int64(_: napi_env, value_: napi_value, result_: ?*i64) napi_status; pub export fn napi_get_value_bool(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_get_value_bool", .{}); const result = result_ orelse { diff --git a/test/napi/napi-app/main.cpp b/test/napi/napi-app/main.cpp index 1e91e2ba9c90f..361e3369c73a3 100644 --- a/test/napi/napi-app/main.cpp +++ b/test/napi/napi-app/main.cpp @@ -4,13 +4,16 @@ #include #include +#include #include #include #include #include +#include #include #include #include +#include napi_value fail(napi_env env, const char *msg) { napi_value result; @@ -786,6 +789,171 @@ napi_value perform_get(const Napi::CallbackInfo &info) { } } +// double_to_i32(any): number|undefined +napi_value double_to_i32(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value input = info[0]; + + int32_t integer; + napi_value result; + napi_status status = napi_get_value_int32(env, input, &integer); + if (status == napi_ok) { + assert(napi_create_int32(env, integer, &result) == napi_ok); + } else { + assert(status == napi_number_expected); + assert(napi_get_undefined(env, &result) == napi_ok); + } + return result; +} + +// double_to_u32(any): number|undefined +napi_value double_to_u32(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value input = info[0]; + + uint32_t integer; + napi_value result; + napi_status status = napi_get_value_uint32(env, input, &integer); + if (status == napi_ok) { + assert(napi_create_uint32(env, integer, &result) == napi_ok); + } else { + assert(status == napi_number_expected); + assert(napi_get_undefined(env, &result) == napi_ok); + } + return result; +} + +// double_to_i64(any): number|undefined +napi_value double_to_i64(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value input = info[0]; + + int64_t integer; + napi_value result; + napi_status status = napi_get_value_int64(env, input, &integer); + if (status == napi_ok) { + assert(napi_create_int64(env, integer, &result) == napi_ok); + } else { + assert(status == napi_number_expected); + assert(napi_get_undefined(env, &result) == napi_ok); + } + return result; +} + +// test from the C++ side +napi_value test_number_integer_conversions(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + using f64_limits = std::numeric_limits; + using i32_limits = std::numeric_limits; + using u32_limits = std::numeric_limits; + using i64_limits = std::numeric_limits; + + std::array, 14> i32_cases{{ + // special values + {f64_limits::infinity(), 0}, + {-f64_limits::infinity(), 0}, + {f64_limits::quiet_NaN(), 0}, + // normal + {0.0, 0}, + {1.0, 1}, + {-1.0, -1}, + // truncation + {1.25, 1}, + {-1.25, -1}, + // limits + {i32_limits::min(), i32_limits::min()}, + {i32_limits::max(), i32_limits::max()}, + // wrap around + {static_cast(i32_limits::min()) - 1.0, i32_limits::max()}, + {static_cast(i32_limits::max()) + 1.0, i32_limits::min()}, + {static_cast(i32_limits::min()) - 2.0, i32_limits::max() - 1}, + {static_cast(i32_limits::max()) + 2.0, i32_limits::min() + 1}, + }}; + + for (const auto &[in, expected_out] : i32_cases) { + napi_value js_in; + assert(napi_create_double(env, in, &js_in) == napi_ok); + int32_t out_from_napi; + assert(napi_get_value_int32(env, js_in, &out_from_napi) == napi_ok); + assert(out_from_napi == expected_out); + } + + std::array, 12> u32_cases{{ + // special values + {f64_limits::infinity(), 0}, + {-f64_limits::infinity(), 0}, + {f64_limits::quiet_NaN(), 0}, + // normal + {0.0, 0}, + {1.0, 1}, + // truncation + {1.25, 1}, + {-1.25, u32_limits::max()}, + // limits + {u32_limits::max(), u32_limits::max()}, + // wrap around + {-1.0, u32_limits::max()}, + {static_cast(u32_limits::max()) + 1.0, 0}, + {-2.0, u32_limits::max() - 1}, + {static_cast(u32_limits::max()) + 2.0, 1}, + + }}; + + for (const auto &[in, expected_out] : u32_cases) { + napi_value js_in; + assert(napi_create_double(env, in, &js_in) == napi_ok); + uint32_t out_from_napi; + assert(napi_get_value_uint32(env, js_in, &out_from_napi) == napi_ok); + assert(out_from_napi == expected_out); + } + + std::array, 12> i64_cases{ + {// special values + {f64_limits::infinity(), 0}, + {-f64_limits::infinity(), 0}, + {f64_limits::quiet_NaN(), 0}, + // normal + {0.0, 0}, + {1.0, 1}, + {-1.0, -1}, + // truncation + {1.25, 1}, + {-1.25, -1}, + // limits + // i64 max can't be precisely represented as double so it would round to + // 1 + // + i64 max, which would clamp and we don't want that yet. so we test + // the + // largest double smaller than i64 max instead (which is i64 max - 1024) + {i64_limits::min(), i64_limits::min()}, + {std::nextafter(static_cast(i64_limits::max()), 0.0), + static_cast( + std::nextafter(static_cast(i64_limits::max()), 0.0))}, + // clamp + {i64_limits::min() - 4096.0, i64_limits::min()}, + {i64_limits::max() + 4096.0, i64_limits::max()}}}; + + for (const auto &[in, expected_out] : i64_cases) { + napi_value js_in; + assert(napi_create_double(env, in, &js_in) == napi_ok); + int64_t out_from_napi; + assert(napi_get_value_int64(env, js_in, &out_from_napi) == napi_ok); + assert(out_from_napi == expected_out); + } + + return ok(env); +} + +napi_value make_empty_array(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value js_size = info[0]; + uint32_t size; + assert(napi_get_value_uint32(env, js_size, &size) == napi_ok); + napi_value array; + assert(napi_create_array_with_length(env, size, &array) == napi_ok); + return array; +} + Napi::Value RunCallback(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); // this function is invoked without the GC callback @@ -840,6 +1008,12 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) { Napi::Function::New(env, call_and_get_exception)); exports.Set("eval_wrapper", Napi::Function::New(env, eval_wrapper)); exports.Set("perform_get", Napi::Function::New(env, perform_get)); + exports.Set("double_to_i32", Napi::Function::New(env, double_to_i32)); + exports.Set("double_to_u32", Napi::Function::New(env, double_to_u32)); + exports.Set("double_to_i64", Napi::Function::New(env, double_to_i64)); + exports.Set("test_number_integer_conversions", + Napi::Function::New(env, test_number_integer_conversions)); + exports.Set("make_empty_array", Napi::Function::New(env, make_empty_array)); exports.Set("throw_error", Napi::Function::New(env, throw_error)); exports.Set("create_and_throw_error", Napi::Function::New(env, create_and_throw_error)); diff --git a/test/napi/napi-app/module.js b/test/napi/napi-app/module.js index 60ce6c4aac683..7d987f87d41ea 100644 --- a/test/napi/napi-app/module.js +++ b/test/napi/napi-app/module.js @@ -91,6 +91,121 @@ nativeTests.test_get_property = () => { } }; +nativeTests.test_number_integer_conversions_from_js = () => { + const i32 = { min: -(2 ** 31), max: 2 ** 31 - 1 }; + const u32Max = 2 ** 32 - 1; + // this is not the actual max value for i64, but rather the highest double that is below the true max value + const i64 = { min: -(2 ** 63), max: 2 ** 63 - 1024 }; + + const i32Cases = [ + // special values + [Infinity, 0], + [-Infinity, 0], + [NaN, 0], + // normal + [0.0, 0], + [1.0, 1], + [-1.0, -1], + // truncation + [1.25, 1], + [-1.25, -1], + // limits + [i32.min, i32.min], + [i32.max, i32.max], + // wrap around + [i32.min - 1.0, i32.max], + [i32.max + 1.0, i32.min], + [i32.min - 2.0, i32.max - 1], + [i32.max + 2.0, i32.min + 1], + // type errors + ["5", undefined], + [new Number(5), undefined], + ]; + + for (const [input, expectedOutput] of i32Cases) { + const actualOutput = nativeTests.double_to_i32(input); + console.log(`${input} as i32 => ${actualOutput}`); + if (actualOutput !== expectedOutput) { + console.error("wrong"); + } + } + + const u32Cases = [ + // special values + [Infinity, 0], + [-Infinity, 0], + [NaN, 0], + // normal + [0.0, 0], + [1.0, 1], + // truncation + [1.25, 1], + [-1.25, u32Max], + // limits + [u32Max, u32Max], + // wrap around + [-1.0, u32Max], + [u32Max + 1.0, 0], + [-2.0, u32Max - 1], + [u32Max + 2.0, 1], + // type errors + ["5", undefined], + [new Number(5), undefined], + ]; + + for (const [input, expectedOutput] of u32Cases) { + const actualOutput = nativeTests.double_to_u32(input); + console.log(`${input} as u32 => ${actualOutput}`); + if (actualOutput !== expectedOutput) { + console.error("wrong"); + } + } + + const i64Cases = [ + // special values + [Infinity, 0], + [-Infinity, 0], + [NaN, 0], + // normal + [0.0, 0], + [1.0, 1], + [-1.0, -1], + // truncation + [1.25, 1], + [-1.25, -1], + // limits + [i64.min, i64.min], + [i64.max, i64.max], + // clamp + [i64.min - 4096.0, i64.min], + // this one clamps to the exact max value of i64 (2**63 - 1), which is then rounded + // to exactly 2**63 since that's the closest double that can be represented + [i64.max + 4096.0, 2 ** 63], + // type errors + ["5", undefined], + [new Number(5), undefined], + ]; + + for (const [input, expectedOutput] of i64Cases) { + const actualOutput = nativeTests.double_to_i64(input); + console.log( + `${typeof input == "number" ? input.toFixed(2) : input} as i64 => ${typeof actualOutput == "number" ? actualOutput.toFixed(2) : actualOutput}`, + ); + if (actualOutput !== expectedOutput) { + console.error("wrong"); + } + } +}; + +nativeTests.test_create_array_with_length = () => { + for (const size of [0, 5]) { + const array = nativeTests.make_empty_array(size); + console.log("length =", array.length); + // should be 0 as array contains empty slots + console.log("number of keys =", Object.keys(array).length); + } +}; + nativeTests.test_throw_functions_exhaustive = () => { for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) { for (const code of [undefined, "", "error code"]) { diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index bbe2836ea632a..75980e8cb7f01 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -288,6 +288,21 @@ describe("napi", () => { }); }); + describe("napi_value <=> integer conversion", () => { + it("works", () => { + checkSameOutput("test_number_integer_conversions_from_js", []); + checkSameOutput("test_number_integer_conversions", []); + }); + }); + + describe("arrays", () => { + describe("napi_create_array_with_length", () => { + it("creates an array with empty slots", () => { + checkSameOutput("test_create_array_with_length", []); + }); + }); + }); + describe("napi_throw functions", () => { it("has the right code and message", () => { checkSameOutput("test_throw_functions_exhaustive", []); From 9fe6e25372741b9d447ada5b7743efd4fcc32491 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Fri, 11 Oct 2024 03:43:37 -0700 Subject: [PATCH 14/15] pm: fix assertion failure when printing lockfile summary after adding git transitive dependency (#14461) Co-authored-by: Jarred Sumner --- src/install/install.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/install/install.zig b/src/install/install.zig index 95e6cb674801a..81e5eba898287 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -12307,6 +12307,16 @@ pub const PackageManager = struct { this.names = packages.items(.name); this.bins = packages.items(.bin); this.resolutions = packages.items(.resolution); + + // fixes an assertion failure where a transitive dependency is a git dependency newly added to the lockfile after the list of dependencies has been resized + // this assertion failure would also only happen after the lockfile has been written to disk and the summary is being printed. + if (this.successfully_installed.bit_length < this.lockfile.packages.len) { + const new = Bitset.initEmpty(bun.default_allocator, this.lockfile.packages.len) catch bun.outOfMemory(); + var old = this.successfully_installed; + defer old.deinit(bun.default_allocator); + old.copyInto(new); + this.successfully_installed = new; + } } /// Install versions of a package which are waiting on a network request From 5fd0a61ae23cc4f4316670e6b8b37cfea01ca1a3 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:16:26 -0700 Subject: [PATCH 15/15] CA support for `bun install` (#14416) --- docs/runtime/bunfig.md | 13 + packages/bun-usockets/src/context.c | 4 +- packages/bun-usockets/src/crypto/openssl.c | 19 +- packages/bun-usockets/src/internal/internal.h | 3 +- packages/bun-usockets/src/libusockets.h | 10 +- packages/bun-uws/src/HttpContext.h | 3 +- src/api/schema.zig | 7 + src/bun.js/api/bun/socket.zig | 15 +- src/bun.js/api/server.zig | 58 +++-- .../bindings/ScriptExecutionContext.cpp | 3 +- src/bun.js/webcore/response.zig | 2 +- src/bun.zig | 42 +++- src/bun_js.zig | 2 +- src/bunfig.zig | 77 ++++-- src/cli/create_command.zig | 2 +- src/cli/init_command.zig | 17 +- src/cli/test_command.zig | 2 +- src/cli/upgrade_command.zig | 4 +- src/compile_target.zig | 2 +- src/deps/uws.zig | 9 +- src/http.zig | 105 ++++++-- src/ini.zig | 26 ++ src/install/install.zig | 124 ++++++++-- src/js_ast.zig | 9 + src/napi/napi.zig | 2 +- src/resolver/resolver.zig | 2 +- src/sql/postgres.zig | 3 +- src/sys.zig | 10 + .../registry/bun-install-registry.test.ts | 226 ++++++++++++++++++ 29 files changed, 677 insertions(+), 124 deletions(-) diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 4af518744556a..1bfcd540e5bb8 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -370,6 +370,19 @@ myorg = { username = "myusername", password = "$npm_password", url = "https://re myorg = { token = "$npm_token", url = "https://registry.myorg.com/" } ``` +### `install.ca` and `install.cafile` + +To configure a CA certificate, use `install.ca` or `install.cafile` to specify a path to a CA certificate file. + +```toml +[install] +# The CA certificate as a string +ca = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" + +# A path to a CA certificate file. The file can contain multiple certificates. +cafile = "path/to/cafile" +``` + ### `install.cache` To configure the cache behavior: diff --git a/packages/bun-usockets/src/context.c b/packages/bun-usockets/src/context.c index a59c80e83a0c5..664f7dabdd477 100644 --- a/packages/bun-usockets/src/context.c +++ b/packages/bun-usockets/src/context.c @@ -278,11 +278,11 @@ struct us_socket_context_t *us_create_socket_context(int ssl, struct us_loop_t * return context; } -struct us_socket_context_t *us_create_bun_socket_context(int ssl, struct us_loop_t *loop, int context_ext_size, struct us_bun_socket_context_options_t options) { +struct us_socket_context_t *us_create_bun_socket_context(int ssl, struct us_loop_t *loop, int context_ext_size, struct us_bun_socket_context_options_t options, enum create_bun_socket_error_t *err) { #ifndef LIBUS_NO_SSL if (ssl) { /* This function will call us, again, with SSL = false and a bigger ext_size */ - return (struct us_socket_context_t *) us_internal_bun_create_ssl_socket_context(loop, context_ext_size, options); + return (struct us_socket_context_t *) us_internal_bun_create_ssl_socket_context(loop, context_ext_size, options, err); } #endif diff --git a/packages/bun-usockets/src/crypto/openssl.c b/packages/bun-usockets/src/crypto/openssl.c index 232d5f8ff9c16..2c0420109595b 100644 --- a/packages/bun-usockets/src/crypto/openssl.c +++ b/packages/bun-usockets/src/crypto/openssl.c @@ -1104,7 +1104,8 @@ int us_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { } SSL_CTX *create_ssl_context_from_bun_options( - struct us_bun_socket_context_options_t options) { + struct us_bun_socket_context_options_t options, + enum create_bun_socket_error_t *err) { /* Create the context */ SSL_CTX *ssl_context = SSL_CTX_new(TLS_method()); @@ -1174,6 +1175,7 @@ SSL_CTX *create_ssl_context_from_bun_options( STACK_OF(X509_NAME) * ca_list; ca_list = SSL_load_client_CA_file(options.ca_file_name); if (ca_list == NULL) { + *err = CREATE_BUN_SOCKET_ERROR_LOAD_CA_FILE; free_ssl_context(ssl_context); return NULL; } @@ -1181,6 +1183,7 @@ SSL_CTX *create_ssl_context_from_bun_options( SSL_CTX_set_client_CA_list(ssl_context, ca_list); if (SSL_CTX_load_verify_locations(ssl_context, options.ca_file_name, NULL) != 1) { + *err = CREATE_BUN_SOCKET_ERROR_INVALID_CA_FILE; free_ssl_context(ssl_context); return NULL; } @@ -1203,6 +1206,7 @@ SSL_CTX *create_ssl_context_from_bun_options( } if (!add_ca_cert_to_ctx_store(ssl_context, options.ca[i], cert_store)) { + *err = CREATE_BUN_SOCKET_ERROR_INVALID_CA; free_ssl_context(ssl_context); return NULL; } @@ -1338,7 +1342,8 @@ void us_bun_internal_ssl_socket_context_add_server_name( struct us_bun_socket_context_options_t options, void *user) { /* Try and construct an SSL_CTX from options */ - SSL_CTX *ssl_context = create_ssl_context_from_bun_options(options); + enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; + SSL_CTX *ssl_context = create_ssl_context_from_bun_options(options, &err); /* Attach the user data to this context */ if (1 != SSL_CTX_set_ex_data(ssl_context, 0, user)) { @@ -1468,14 +1473,15 @@ struct us_internal_ssl_socket_context_t *us_internal_create_ssl_socket_context( struct us_internal_ssl_socket_context_t * us_internal_bun_create_ssl_socket_context( struct us_loop_t *loop, int context_ext_size, - struct us_bun_socket_context_options_t options) { + struct us_bun_socket_context_options_t options, + enum create_bun_socket_error_t *err) { /* If we haven't initialized the loop data yet, do so . * This is needed because loop data holds shared OpenSSL data and * the function is also responsible for initializing OpenSSL */ us_internal_init_loop_ssl_data(loop); /* First of all we try and create the SSL context from options */ - SSL_CTX *ssl_context = create_ssl_context_from_bun_options(options); + SSL_CTX *ssl_context = create_ssl_context_from_bun_options(options, err); if (!ssl_context) { /* We simply fail early if we cannot even create the OpenSSL context */ return NULL; @@ -1487,7 +1493,7 @@ us_internal_bun_create_ssl_socket_context( (struct us_internal_ssl_socket_context_t *)us_create_bun_socket_context( 0, loop, sizeof(struct us_internal_ssl_socket_context_t) + context_ext_size, - options); + options, err); /* I guess this is the only optional callback */ context->on_server_name = NULL; @@ -1983,9 +1989,10 @@ struct us_internal_ssl_socket_t *us_internal_ssl_socket_wrap_with_tls( struct us_socket_context_t *old_context = us_socket_context(0, s); us_socket_context_ref(0,old_context); + enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; struct us_socket_context_t *context = us_create_bun_socket_context( 1, old_context->loop, sizeof(struct us_wrapped_socket_context_t), - options); + options, &err); // Handle SSL context creation failure if (UNLIKELY(!context)) { diff --git a/packages/bun-usockets/src/internal/internal.h b/packages/bun-usockets/src/internal/internal.h index 8c6c717504729..abc24a4e8300c 100644 --- a/packages/bun-usockets/src/internal/internal.h +++ b/packages/bun-usockets/src/internal/internal.h @@ -330,7 +330,8 @@ struct us_internal_ssl_socket_context_t *us_internal_create_ssl_socket_context( struct us_internal_ssl_socket_context_t * us_internal_bun_create_ssl_socket_context( struct us_loop_t *loop, int context_ext_size, - struct us_bun_socket_context_options_t options); + struct us_bun_socket_context_options_t options, + enum create_bun_socket_error_t *err); void us_internal_ssl_socket_context_free( us_internal_ssl_socket_context_r context); diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index b939af53efb20..e4a568cea1ca9 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -246,8 +246,16 @@ void *us_socket_context_get_native_handle(int ssl, us_socket_context_r context); /* A socket context holds shared callbacks and user data extension for associated sockets */ struct us_socket_context_t *us_create_socket_context(int ssl, us_loop_r loop, int ext_size, struct us_socket_context_options_t options) nonnull_fn_decl; + +enum create_bun_socket_error_t { + CREATE_BUN_SOCKET_ERROR_NONE = 0, + CREATE_BUN_SOCKET_ERROR_LOAD_CA_FILE, + CREATE_BUN_SOCKET_ERROR_INVALID_CA_FILE, + CREATE_BUN_SOCKET_ERROR_INVALID_CA, +}; + struct us_socket_context_t *us_create_bun_socket_context(int ssl, struct us_loop_t *loop, - int ext_size, struct us_bun_socket_context_options_t options); + int ext_size, struct us_bun_socket_context_options_t options, enum create_bun_socket_error_t *err); /* Delete resources allocated at creation time (will call unref now and only free when ref count == 0). */ void us_socket_context_free(int ssl, us_socket_context_r context) nonnull_fn_decl; diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 338683f816890..0081779bdaf08 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -433,7 +433,8 @@ struct HttpContext { static HttpContext *create(Loop *loop, us_bun_socket_context_options_t options = {}) { HttpContext *httpContext; - httpContext = (HttpContext *) us_create_bun_socket_context(SSL, (us_loop_t *) loop, sizeof(HttpContextData), options); + enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; + httpContext = (HttpContext *) us_create_bun_socket_context(SSL, (us_loop_t *) loop, sizeof(HttpContextData), options, &err); if (!httpContext) { return nullptr; diff --git a/src/api/schema.zig b/src/api/schema.zig index a7b958c8a5ee5..1c3679be8d434 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -2974,6 +2974,13 @@ pub const Api = struct { /// concurrent_scripts concurrent_scripts: ?u32 = null, + cafile: ?[]const u8 = null, + + ca: ?union(enum) { + str: []const u8, + list: []const []const u8, + } = null, + pub fn decode(reader: anytype) anyerror!BunInstall { var this = std.mem.zeroes(BunInstall); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index d2f1d43c03d80..7d38576bc1c46 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -642,15 +642,20 @@ pub const Listener = struct { } } } - const ctx_opts: uws.us_bun_socket_context_options_t = JSC.API.ServerConfig.SSLConfig.asUSockets(ssl); + const ctx_opts: uws.us_bun_socket_context_options_t = if (ssl != null) + JSC.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + else + .{}; vm.eventLoop().ensureWaker(); + var create_err: uws.create_bun_socket_error_t = .none; const socket_context = uws.us_create_bun_socket_context( @intFromBool(ssl_enabled), uws.Loop.get(), @sizeOf(usize), ctx_opts, + &create_err, ) orelse { var err = globalObject.createErrorInstance("Failed to listen on {s}:{d}", .{ hostname_or_unix.slice(), port orelse 0 }); defer { @@ -1172,9 +1177,13 @@ pub const Listener = struct { } } - const ctx_opts: uws.us_bun_socket_context_options_t = JSC.API.ServerConfig.SSLConfig.asUSockets(socket_config.ssl); + const ctx_opts: uws.us_bun_socket_context_options_t = if (ssl != null) + JSC.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + else + .{}; - const socket_context = uws.us_create_bun_socket_context(@intFromBool(ssl_enabled), uws.Loop.get(), @sizeOf(usize), ctx_opts) orelse { + var create_err: uws.create_bun_socket_error_t = .none; + const socket_context = uws.us_create_bun_socket_context(@intFromBool(ssl_enabled), uws.Loop.get(), @sizeOf(usize), ctx_opts, &create_err) orelse { const err = JSC.SystemError{ .message = bun.String.static("Failed to connect"), .syscall = bun.String.static("connect"), diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 755a6a9d4c5d1..5b176dba47e3c 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -583,41 +583,39 @@ pub const ServerConfig = struct { const log = Output.scoped(.SSLConfig, false); - pub fn asUSockets(this_: ?SSLConfig) uws.us_bun_socket_context_options_t { + pub fn asUSockets(this: SSLConfig) uws.us_bun_socket_context_options_t { var ctx_opts: uws.us_bun_socket_context_options_t = .{}; - if (this_) |ssl_config| { - if (ssl_config.key_file_name != null) - ctx_opts.key_file_name = ssl_config.key_file_name; - if (ssl_config.cert_file_name != null) - ctx_opts.cert_file_name = ssl_config.cert_file_name; - if (ssl_config.ca_file_name != null) - ctx_opts.ca_file_name = ssl_config.ca_file_name; - if (ssl_config.dh_params_file_name != null) - ctx_opts.dh_params_file_name = ssl_config.dh_params_file_name; - if (ssl_config.passphrase != null) - ctx_opts.passphrase = ssl_config.passphrase; - ctx_opts.ssl_prefer_low_memory_usage = @intFromBool(ssl_config.low_memory_mode); + if (this.key_file_name != null) + ctx_opts.key_file_name = this.key_file_name; + if (this.cert_file_name != null) + ctx_opts.cert_file_name = this.cert_file_name; + if (this.ca_file_name != null) + ctx_opts.ca_file_name = this.ca_file_name; + if (this.dh_params_file_name != null) + ctx_opts.dh_params_file_name = this.dh_params_file_name; + if (this.passphrase != null) + ctx_opts.passphrase = this.passphrase; + ctx_opts.ssl_prefer_low_memory_usage = @intFromBool(this.low_memory_mode); - if (ssl_config.key) |key| { - ctx_opts.key = key.ptr; - ctx_opts.key_count = ssl_config.key_count; - } - if (ssl_config.cert) |cert| { - ctx_opts.cert = cert.ptr; - ctx_opts.cert_count = ssl_config.cert_count; - } - if (ssl_config.ca) |ca| { - ctx_opts.ca = ca.ptr; - ctx_opts.ca_count = ssl_config.ca_count; - } + if (this.key) |key| { + ctx_opts.key = key.ptr; + ctx_opts.key_count = this.key_count; + } + if (this.cert) |cert| { + ctx_opts.cert = cert.ptr; + ctx_opts.cert_count = this.cert_count; + } + if (this.ca) |ca| { + ctx_opts.ca = ca.ptr; + ctx_opts.ca_count = this.ca_count; + } - if (ssl_config.ssl_ciphers != null) { - ctx_opts.ssl_ciphers = ssl_config.ssl_ciphers; - } - ctx_opts.request_cert = ssl_config.request_cert; - ctx_opts.reject_unauthorized = ssl_config.reject_unauthorized; + if (this.ssl_ciphers != null) { + ctx_opts.ssl_ciphers = this.ssl_ciphers; } + ctx_opts.request_cert = this.request_cert; + ctx_opts.reject_unauthorized = this.reject_unauthorized; return ctx_opts; } diff --git a/src/bun.js/bindings/ScriptExecutionContext.cpp b/src/bun.js/bindings/ScriptExecutionContext.cpp index 06e5b7ddba82c..34534d6369b04 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.cpp +++ b/src/bun.js/bindings/ScriptExecutionContext.cpp @@ -60,7 +60,8 @@ us_socket_context_t* ScriptExecutionContext::webSocketContextSSL() opts.request_cert = true; // but do not reject unauthorized opts.reject_unauthorized = false; - this->m_ssl_client_websockets_ctx = us_create_bun_socket_context(1, loop, sizeof(size_t), opts); + enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; + this->m_ssl_client_websockets_ctx = us_create_bun_socket_context(1, loop, sizeof(size_t), opts, &err); void** ptr = reinterpret_cast(us_socket_context_ext(1, m_ssl_client_websockets_ctx)); *ptr = this; registerHTTPContextForWebSocket(this, m_ssl_client_websockets_ctx, loop); diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 76d7d07aaa5e7..184c1f9cbbc06 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -1797,7 +1797,7 @@ pub const Fetch = struct { fetch_options: FetchOptions, promise: JSC.JSPromise.Strong, ) !*FetchTasklet { - http.HTTPThread.init(); + http.HTTPThread.init(&.{}); var node = try get( allocator, global, diff --git a/src/bun.zig b/src/bun.zig index efbdce6653379..2453cdcb4d057 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -718,7 +718,7 @@ pub const Analytics = @import("./analytics/analytics_thread.zig"); pub usingnamespace @import("./tagged_pointer.zig"); -pub fn once(comptime function: anytype, comptime ReturnType: type) ReturnType { +pub fn onceUnsafe(comptime function: anytype, comptime ReturnType: type) ReturnType { const Result = struct { var value: ReturnType = undefined; var ran = false; @@ -3938,3 +3938,43 @@ pub fn indexOfPointerInSlice(comptime T: type, slice: []const T, item: *const T) const index = @divExact(offset, @sizeOf(T)); return index; } + +/// Copied from zig std. Modified to accept arguments. +pub fn once(comptime f: anytype) Once(f) { + return Once(f){}; +} + +/// Copied from zig std. Modified to accept arguments. +/// +/// An object that executes the function `f` just once. +/// It is undefined behavior if `f` re-enters the same Once instance. +pub fn Once(comptime f: anytype) type { + return struct { + done: bool = false, + mutex: std.Thread.Mutex = std.Thread.Mutex{}, + + /// Call the function `f`. + /// If `call` is invoked multiple times `f` will be executed only the + /// first time. + /// The invocations are thread-safe. + pub fn call(self: *@This(), args: std.meta.ArgsTuple(@TypeOf(f))) void { + if (@atomicLoad(bool, &self.done, .acquire)) + return; + + return self.callSlow(args); + } + + fn callSlow(self: *@This(), args: std.meta.ArgsTuple(@TypeOf(f))) void { + @setCold(true); + + self.mutex.lock(); + defer self.mutex.unlock(); + + // The first thread to acquire the mutex gets to run the initializer + if (!self.done) { + @call(.auto, f, args); + @atomicStore(bool, &self.done, true, .release); + } + } + }; +} diff --git a/src/bun_js.zig b/src/bun_js.zig index e5eff889cef6f..bb8b1e8c48f7f 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -127,7 +127,7 @@ pub const Run = struct { fn doPreconnect(preconnect: []const string) void { if (preconnect.len == 0) return; - bun.HTTPThread.init(); + bun.HTTPThread.init(&.{}); for (preconnect) |url_str| { const url = bun.URL.parse(url_str); diff --git a/src/bunfig.zig b/src/bunfig.zig index f141edcd2fec0..0ebfb9cb5dfa2 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -336,15 +336,15 @@ pub const Bunfig = struct { } if (comptime cmd.isNPMRelated() or cmd == .RunCommand or cmd == .AutoCommand) { - if (json.get("install")) |_bun| { + if (json.getObject("install")) |install_obj| { var install: *Api.BunInstall = this.ctx.install orelse brk: { - const install_ = try this.allocator.create(Api.BunInstall); - install_.* = std.mem.zeroes(Api.BunInstall); - this.ctx.install = install_; - break :brk install_; + const install = try this.allocator.create(Api.BunInstall); + install.* = std.mem.zeroes(Api.BunInstall); + this.ctx.install = install; + break :brk install; }; - if (_bun.get("auto")) |auto_install_expr| { + if (install_obj.get("auto")) |auto_install_expr| { if (auto_install_expr.data == .e_string) { this.ctx.debug.global_cache = options.GlobalCache.Map.get(auto_install_expr.asString(this.allocator) orelse "") orelse { try this.addError(auto_install_expr.loc, "Invalid auto install setting, must be one of true, false, or \"force\" \"fallback\" \"disable\""); @@ -361,13 +361,46 @@ pub const Bunfig = struct { } } - if (_bun.get("exact")) |exact| { + if (install_obj.get("cafile")) |cafile| { + install.cafile = try cafile.asStringCloned(allocator) orelse { + try this.addError(cafile.loc, "Invalid cafile. Expected a string."); + return; + }; + } + + if (install_obj.get("ca")) |ca| { + switch (ca.data) { + .e_array => |arr| { + var list = try allocator.alloc([]const u8, arr.items.len); + for (arr.items.slice(), 0..) |item, i| { + list[i] = try item.asStringCloned(allocator) orelse { + try this.addError(item.loc, "Invalid CA. Expected a string."); + return; + }; + } + install.ca = .{ + .list = list, + }; + }, + .e_string => |str| { + install.ca = .{ + .str = try str.stringCloned(allocator), + }; + }, + else => { + try this.addError(ca.loc, "Invalid CA. Expected a string or an array of strings."); + return; + }, + } + } + + if (install_obj.get("exact")) |exact| { if (exact.asBool()) |value| { install.exact = value; } } - if (_bun.get("prefer")) |prefer_expr| { + if (install_obj.get("prefer")) |prefer_expr| { try this.expectString(prefer_expr); if (Prefer.get(prefer_expr.asString(bun.default_allocator) orelse "")) |setting| { @@ -377,11 +410,11 @@ pub const Bunfig = struct { } } - if (_bun.get("registry")) |registry| { + if (install_obj.get("registry")) |registry| { install.default_registry = try this.parseRegistry(registry); } - if (_bun.get("scopes")) |scopes| { + if (install_obj.get("scopes")) |scopes| { var registry_map = install.scoped orelse Api.NpmRegistryMap{}; try this.expect(scopes, .e_object); @@ -399,32 +432,32 @@ pub const Bunfig = struct { install.scoped = registry_map; } - if (_bun.get("dryRun")) |dry_run| { + if (install_obj.get("dryRun")) |dry_run| { if (dry_run.asBool()) |value| { install.dry_run = value; } } - if (_bun.get("production")) |production| { + if (install_obj.get("production")) |production| { if (production.asBool()) |value| { install.production = value; } } - if (_bun.get("frozenLockfile")) |frozen_lockfile| { + if (install_obj.get("frozenLockfile")) |frozen_lockfile| { if (frozen_lockfile.asBool()) |value| { install.frozen_lockfile = value; } } - if (_bun.get("concurrentScripts")) |jobs| { + if (install_obj.get("concurrentScripts")) |jobs| { if (jobs.data == .e_number) { install.concurrent_scripts = jobs.data.e_number.toU32(); if (install.concurrent_scripts.? == 0) install.concurrent_scripts = null; } } - if (_bun.get("lockfile")) |lockfile_expr| { + if (install_obj.get("lockfile")) |lockfile_expr| { if (lockfile_expr.get("print")) |lockfile| { try this.expectString(lockfile); if (lockfile.asString(this.allocator)) |value| { @@ -457,41 +490,41 @@ pub const Bunfig = struct { } } - if (_bun.get("optional")) |optional| { + if (install_obj.get("optional")) |optional| { if (optional.asBool()) |value| { install.save_optional = value; } } - if (_bun.get("peer")) |optional| { + if (install_obj.get("peer")) |optional| { if (optional.asBool()) |value| { install.save_peer = value; } } - if (_bun.get("dev")) |optional| { + if (install_obj.get("dev")) |optional| { if (optional.asBool()) |value| { install.save_dev = value; } } - if (_bun.get("globalDir")) |dir| { + if (install_obj.get("globalDir")) |dir| { if (dir.asString(allocator)) |value| { install.global_dir = value; } } - if (_bun.get("globalBinDir")) |dir| { + if (install_obj.get("globalBinDir")) |dir| { if (dir.asString(allocator)) |value| { install.global_bin_dir = value; } } - if (_bun.get("logLevel")) |expr| { + if (install_obj.get("logLevel")) |expr| { try this.loadLogLevel(expr); } - if (_bun.get("cache")) |cache| { + if (install_obj.get("cache")) |cache| { load: { if (cache.asBool()) |value| { if (!value) { diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 16ac76623ecc3..2d6577a4be0d2 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -241,7 +241,7 @@ pub const CreateCommand = struct { @setCold(true); Global.configureAllocator(.{ .long_running = false }); - HTTP.HTTPThread.init(); + HTTP.HTTPThread.init(&.{}); var create_options = try CreateOptions.parse(ctx); const positionals = create_options.positionals; diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 16d4d407a701e..86f6efd224c11 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -21,10 +21,9 @@ const initializeStore = @import("./create_command.zig").initializeStore; const lex = bun.js_lexer; const logger = bun.logger; const JSPrinter = bun.js_printer; +const exists = bun.sys.exists; +const existsZ = bun.sys.existsZ; -fn exists(path: anytype) bool { - return bun.sys.exists(path); -} pub const InitCommand = struct { pub fn prompt( alloc: std.mem.Allocator, @@ -210,7 +209,7 @@ pub const InitCommand = struct { }; for (paths_to_try) |path| { - if (exists(path)) { + if (existsZ(path)) { fields.entry_point = bun.asByteSlice(path); break :infer; } @@ -279,16 +278,16 @@ pub const InitCommand = struct { var steps = Steps{}; - steps.write_gitignore = !exists(".gitignore"); + steps.write_gitignore = !existsZ(".gitignore"); - steps.write_readme = !exists("README.md") and !exists("README") and !exists("README.txt") and !exists("README.mdx"); + steps.write_readme = !existsZ("README.md") and !existsZ("README") and !existsZ("README.txt") and !existsZ("README.mdx"); steps.write_tsconfig = brk: { - if (exists("tsconfig.json")) { + if (existsZ("tsconfig.json")) { break :brk false; } - if (exists("jsconfig.json")) { + if (existsZ("jsconfig.json")) { break :brk false; } @@ -444,7 +443,7 @@ pub const InitCommand = struct { Output.flush(); - if (exists("package.json")) { + if (existsZ("package.json")) { var process = std.process.Child.init( &.{ try bun.selfExePath(), diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index b3b2604d77f78..b0f1000b5bd45 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -741,7 +741,7 @@ pub const TestCommand = struct { break :brk loader; }; bun.JSC.initialize(false); - HTTPThread.init(); + HTTPThread.init(&.{}); var snapshot_file_buf = std.ArrayList(u8).init(ctx.allocator); var snapshot_values = Snapshots.ValuesHashMap.init(ctx.allocator); diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index c75452a0fdfd4..b89d1777addb0 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -133,7 +133,7 @@ pub const UpgradeCheckerThread = struct { std.time.sleep(std.time.ns_per_ms * delay); Output.Source.configureThread(); - HTTP.HTTPThread.init(); + HTTP.HTTPThread.init(&.{}); defer { js_ast.Expr.Data.Store.deinit(); @@ -440,7 +440,7 @@ pub const UpgradeCommand = struct { } fn _exec(ctx: Command.Context) !void { - HTTP.HTTPThread.init(); + HTTP.HTTPThread.init(&.{}); var filesystem = try fs.FileSystem.init(null); var env_loader: DotEnv.Loader = brk: { diff --git a/src/compile_target.zig b/src/compile_target.zig index a6ec5f076c7ae..bd060d24bb281 100644 --- a/src/compile_target.zig +++ b/src/compile_target.zig @@ -137,7 +137,7 @@ const HTTP = bun.http; const MutableString = bun.MutableString; const Global = bun.Global; pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, allocator: std.mem.Allocator, dest_z: [:0]const u8) !void { - HTTP.HTTPThread.init(); + HTTP.HTTPThread.init(&.{}); var refresher = bun.Progress{}; { diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 102858501ac26..3e3f92adf773d 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2539,6 +2539,13 @@ pub const us_bun_socket_context_options_t = extern struct { }; pub extern fn create_ssl_context_from_bun_options(options: us_bun_socket_context_options_t) ?*BoringSSL.SSL_CTX; +pub const create_bun_socket_error_t = enum(i32) { + none = 0, + load_ca_file, + invalid_ca_file, + invalid_ca, +}; + pub const us_bun_verify_error_t = extern struct { error_no: i32 = 0, code: [*c]const u8 = null, @@ -2568,7 +2575,7 @@ pub extern fn us_socket_context_remove_server_name(ssl: i32, context: ?*SocketCo extern fn us_socket_context_on_server_name(ssl: i32, context: ?*SocketContext, cb: ?*const fn (?*SocketContext, [*c]const u8) callconv(.C) void) void; extern fn us_socket_context_get_native_handle(ssl: i32, context: ?*SocketContext) ?*anyopaque; pub extern fn us_create_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_socket_context_options_t) ?*SocketContext; -pub extern fn us_create_bun_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_bun_socket_context_options_t) ?*SocketContext; +pub extern fn us_create_bun_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_bun_socket_context_options_t, err: *create_bun_socket_error_t) ?*SocketContext; pub extern fn us_bun_socket_context_add_server_name(ssl: i32, context: ?*SocketContext, hostname_pattern: [*c]const u8, options: us_bun_socket_context_options_t, ?*anyopaque) void; pub extern fn us_socket_context_free(ssl: i32, context: ?*SocketContext) void; pub extern fn us_socket_context_ref(ssl: i32, context: ?*SocketContext) void; diff --git a/src/http.zig b/src/http.zig index c2c0da7ba577b..de3a58fbec4d4 100644 --- a/src/http.zig +++ b/src/http.zig @@ -516,6 +516,13 @@ pub const HTTPCertError = struct { reason: [:0]const u8 = "", }; +pub const InitError = error{ + FailedToOpenSocket, + LoadCAFile, + InvalidCAFile, + InvalidCA, +}; + fn NewHTTPContext(comptime ssl: bool) type { return struct { const pool_size = 64; @@ -585,16 +592,30 @@ fn NewHTTPContext(comptime ssl: bool) type { bun.default_allocator.destroy(this); } - pub fn initWithClientConfig(this: *@This(), client: *HTTPClient) !void { + pub fn initWithClientConfig(this: *@This(), client: *HTTPClient) InitError!void { if (!comptime ssl) { - unreachable; + @compileError("ssl only"); } var opts = client.tls_props.?.asUSockets(); opts.request_cert = 1; opts.reject_unauthorized = 0; - const socket = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts); + try this.initWithOpts(&opts); + } + + fn initWithOpts(this: *@This(), opts: *const uws.us_bun_socket_context_options_t) InitError!void { + if (!comptime ssl) { + @compileError("ssl only"); + } + + var err: uws.create_bun_socket_error_t = .none; + const socket = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts.*, &err); if (socket == null) { - return error.FailedToOpenSocket; + return switch (err) { + .load_ca_file => error.LoadCAFile, + .invalid_ca_file => error.InvalidCAFile, + .invalid_ca => error.InvalidCA, + else => error.FailedToOpenSocket, + }; } this.us_socket_context = socket.?; this.sslCtx().setup(); @@ -607,7 +628,21 @@ fn NewHTTPContext(comptime ssl: bool) type { ); } - pub fn init(this: *@This()) !void { + pub fn initWithThreadOpts(this: *@This(), init_opts: *const HTTPThread.InitOpts) InitError!void { + if (!comptime ssl) { + @compileError("ssl only"); + } + var opts: uws.us_bun_socket_context_options_t = .{ + .ca = if (init_opts.ca.len > 0) @ptrCast(init_opts.ca) else null, + .ca_count = @intCast(init_opts.ca.len), + .ca_file_name = if (init_opts.abs_ca_file_name.len > 0) init_opts.abs_ca_file_name else null, + .request_cert = 1, + }; + + try this.initWithOpts(&opts); + } + + pub fn init(this: *@This()) void { if (comptime ssl) { const opts: uws.us_bun_socket_context_options_t = .{ // we request the cert so we load root certs and can verify it @@ -615,7 +650,8 @@ fn NewHTTPContext(comptime ssl: bool) type { // we manually abort the connection if the hostname doesn't match .reject_unauthorized = 0, }; - this.us_socket_context = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts).?; + var err: uws.create_bun_socket_error_t = .none; + this.us_socket_context = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts, &err).?; this.sslCtx().setup(); } else { @@ -1005,7 +1041,37 @@ pub const HTTPThread = struct { return this.lazy_libdeflater.?; } - fn initOnce() void { + fn onInitErrorNoop(err: InitError, opts: InitOpts) noreturn { + switch (err) { + error.LoadCAFile => { + if (!bun.sys.existsZ(opts.abs_ca_file_name)) { + Output.err("HTTPThread", "failed to find CA file: '{s}'", .{opts.abs_ca_file_name}); + } else { + Output.err("HTTPThread", "failed to load CA file: '{s}'", .{opts.abs_ca_file_name}); + } + }, + error.InvalidCAFile => { + Output.err("HTTPThread", "the CA file is invalid: '{s}'", .{opts.abs_ca_file_name}); + }, + error.InvalidCA => { + Output.err("HTTPThread", "the provided CA is invalid", .{}); + }, + error.FailedToOpenSocket => { + Output.errGeneric("failed to start HTTP client thread", .{}); + }, + } + Global.crash(); + } + + pub const InitOpts = struct { + ca: []stringZ = &.{}, + abs_ca_file_name: stringZ = &.{}, + for_install: bool = false, + + onInitError: *const fn (err: InitError, opts: InitOpts) noreturn = &onInitErrorNoop, + }; + + fn initOnce(opts: *const InitOpts) void { http_thread = .{ .loop = undefined, .http_context = .{ @@ -1022,17 +1088,17 @@ pub const HTTPThread = struct { .stack_size = bun.default_thread_stack_size, }, onStart, - .{}, + .{opts.*}, ) catch |err| Output.panic("Failed to start HTTP Client thread: {s}", .{@errorName(err)}); thread.detach(); } - var init_once = std.once(initOnce); + var init_once = bun.once(initOnce); - pub fn init() void { - init_once.call(); + pub fn init(opts: *const InitOpts) void { + init_once.call(.{opts}); } - pub fn onStart() void { + pub fn onStart(opts: InitOpts) void { Output.Source.configureNamedThread("HTTP Client"); default_arena = Arena.init() catch unreachable; default_allocator = default_arena.allocator(); @@ -1046,8 +1112,8 @@ pub const HTTPThread = struct { } http_thread.loop = loop; - http_thread.http_context.init() catch @panic("Failed to init http context"); - http_thread.https_context.init() catch @panic("Failed to init https context"); + http_thread.http_context.init(); + http_thread.https_context.initWithThreadOpts(&opts) catch |err| opts.onInitError(err, opts); http_thread.has_awoken.store(true, .monotonic); http_thread.processEvents(); } @@ -1084,7 +1150,14 @@ pub const HTTPThread = struct { requested_config.deinit(); bun.default_allocator.destroy(requested_config); bun.default_allocator.destroy(custom_context); - return err; + + // TODO: these error names reach js. figure out how they should be handled + return switch (err) { + error.FailedToOpenSocket => |e| e, + error.InvalidCA => error.FailedToOpenSocket, + error.InvalidCAFile => error.FailedToOpenSocket, + error.LoadCAFile => error.FailedToOpenSocket, + }; }; try custom_ssl_context_map.put(requested_config, custom_context); // We might deinit the socket context, so we disable keepalive to make sure we don't @@ -2479,7 +2552,7 @@ pub const AsyncHTTP = struct { } pub fn sendSync(this: *AsyncHTTP) anyerror!picohttp.Response { - HTTPThread.init(); + HTTPThread.init(&.{}); var ctx = try bun.default_allocator.create(SingleHTTPChannel); ctx.* = SingleHTTPChannel.init(); diff --git a/src/ini.zig b/src/ini.zig index 0a2e9cb564999..cc9deecd0bbf4 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -962,6 +962,32 @@ pub fn loadNpmrc( } } + if (out.asProperty("ca")) |query| { + if (query.expr.asUtf8StringLiteral()) |str| { + install.ca = .{ + .str = str, + }; + } else if (query.expr.isArray()) { + const arr = query.expr.data.e_array; + var list = try allocator.alloc([]const u8, arr.items.len); + var i: usize = 0; + for (arr.items.slice()) |item| { + list[i] = try item.asStringCloned(allocator) orelse continue; + i += 1; + } + + install.ca = .{ + .list = list, + }; + } + } + + if (out.asProperty("cafile")) |query| { + if (try query.expr.asStringCloned(allocator)) |cafile| { + install.cafile = cafile; + } + } + var registry_map = install.scoped orelse bun.Schema.Api.NpmRegistryMap{}; // Process scopes diff --git a/src/install/install.zig b/src/install/install.zig index 81e5eba898287..bf81425c53dba 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -6943,6 +6943,9 @@ pub const PackageManager = struct { publish_config: PublishConfig = .{}, + ca: []const string = &.{}, + ca_file_name: string = &.{}, + pub const PublishConfig = struct { access: ?Access = null, tag: string = "", @@ -7087,8 +7090,8 @@ pub const PackageManager = struct { .password = "", .token = "", }; - if (bun_install_) |bun_install| { - if (bun_install.default_registry) |registry| { + if (bun_install_) |config| { + if (config.default_registry) |registry| { base = registry; } } @@ -7097,8 +7100,8 @@ pub const PackageManager = struct { defer { this.did_override_default_scope = this.scope.url_hash != Npm.Registry.default_url_hash; } - if (bun_install_) |bun_install| { - if (bun_install.scoped) |scoped| { + if (bun_install_) |config| { + if (config.scoped) |scoped| { for (scoped.scopes.keys(), scoped.scopes.values()) |name, *registry_| { var registry = registry_.*; if (registry.url.len == 0) registry.url = base.url; @@ -7106,42 +7109,57 @@ pub const PackageManager = struct { } } - if (bun_install.disable_cache orelse false) { + if (config.ca) |ca| { + switch (ca) { + .list => |ca_list| { + this.ca = ca_list; + }, + .str => |ca_str| { + this.ca = &.{ca_str}; + }, + } + } + + if (config.cafile) |cafile| { + this.ca_file_name = cafile; + } + + if (config.disable_cache orelse false) { this.enable.cache = false; } - if (bun_install.disable_manifest_cache orelse false) { + if (config.disable_manifest_cache orelse false) { this.enable.manifest_cache = false; } - if (bun_install.force orelse false) { + if (config.force orelse false) { this.enable.manifest_cache_control = false; this.enable.force_install = true; } - if (bun_install.save_yarn_lockfile orelse false) { + if (config.save_yarn_lockfile orelse false) { this.do.save_yarn_lock = true; } - if (bun_install.save_lockfile) |save_lockfile| { + if (config.save_lockfile) |save_lockfile| { this.do.save_lockfile = save_lockfile; this.enable.force_save_lockfile = true; } - if (bun_install.save_dev) |save| { + if (config.save_dev) |save| { this.local_package_features.dev_dependencies = save; } - if (bun_install.save_peer) |save| { + if (config.save_peer) |save| { this.do.install_peer_dependencies = save; this.remote_package_features.peer_dependencies = save; } - if (bun_install.exact) |exact| { + if (config.exact) |exact| { this.enable.exact_versions = exact; } - if (bun_install.production) |production| { + if (config.production) |production| { if (production) { this.local_package_features.dev_dependencies = false; this.enable.fail_early = true; @@ -7150,22 +7168,22 @@ pub const PackageManager = struct { } } - if (bun_install.frozen_lockfile) |frozen_lockfile| { + if (config.frozen_lockfile) |frozen_lockfile| { if (frozen_lockfile) { this.enable.frozen_lockfile = true; } } - if (bun_install.concurrent_scripts) |jobs| { + if (config.concurrent_scripts) |jobs| { this.max_concurrent_lifecycle_scripts = jobs; } - if (bun_install.save_optional) |save| { + if (config.save_optional) |save| { this.remote_package_features.optional_dependencies = save; this.local_package_features.optional_dependencies = save; } - this.explicit_global_directory = bun_install.global_dir orelse this.explicit_global_directory; + this.explicit_global_directory = config.global_dir orelse this.explicit_global_directory; } const default_disable_progress_bar: bool = brk: { @@ -7392,6 +7410,13 @@ pub const PackageManager = struct { if (cli.publish_config.auth_type) |auth_type| { this.publish_config.auth_type = auth_type; } + + if (cli.ca.len > 0) { + this.ca = cli.ca; + } + if (cli.ca_file_name.len > 0) { + this.ca_file_name = cli.ca_file_name; + } } else { this.log_level = if (default_disable_progress_bar) LogLevel.default_no_progress else LogLevel.default; PackageManager.verbose_install = false; @@ -8329,14 +8354,33 @@ pub const PackageManager = struct { } }; + fn httpThreadOnInitError(err: HTTP.InitError, opts: HTTP.HTTPThread.InitOpts) noreturn { + switch (err) { + error.LoadCAFile => { + if (!bun.sys.existsZ(opts.abs_ca_file_name)) { + Output.err("HTTPThread", "could not find CA file: '{s}'", .{opts.abs_ca_file_name}); + } else { + Output.err("HTTPThread", "invalid CA file: '{s}'", .{opts.abs_ca_file_name}); + } + }, + error.InvalidCAFile => { + Output.err("HTTPThread", "invalid CA file: '{s}'", .{opts.abs_ca_file_name}); + }, + error.InvalidCA => { + Output.err("HTTPThread", "the CA is invalid", .{}); + }, + error.FailedToOpenSocket => { + Output.errGeneric("failed to start HTTP client thread", .{}); + }, + } + Global.crash(); + } + pub fn init( ctx: Command.Context, cli: CommandLineArguments, subcommand: Subcommand, ) !struct { *PackageManager, string } { - // assume that spawning a thread will take a lil so we do that asap - HTTP.HTTPThread.init(); - if (cli.global) { var explicit_global_dir: string = ""; if (ctx.install) |opts| { @@ -8677,6 +8721,36 @@ pub const PackageManager = struct { subcommand, ); + var ca: []stringZ = &.{}; + if (manager.options.ca.len > 0) { + ca = try manager.allocator.alloc(stringZ, manager.options.ca.len); + for (ca, manager.options.ca) |*z, s| { + z.* = try manager.allocator.dupeZ(u8, s); + } + } + + var abs_ca_file_name: stringZ = &.{}; + if (manager.options.ca_file_name.len > 0) { + // resolve with original cwd + if (std.fs.path.isAbsolute(manager.options.ca_file_name)) { + abs_ca_file_name = try manager.allocator.dupeZ(u8, manager.options.ca_file_name); + } else { + var path_buf: bun.PathBuffer = undefined; + abs_ca_file_name = try manager.allocator.dupeZ(u8, bun.path.joinAbsStringBuf( + original_cwd_clone, + &path_buf, + &.{manager.options.ca_file_name}, + .auto, + )); + } + } + + HTTP.HTTPThread.init(&.{ + .ca = ca, + .abs_ca_file_name = abs_ca_file_name, + .onInitError = &httpThreadOnInitError, + }); + manager.timestamp_for_manifest_cache_control = brk: { if (comptime bun.Environment.allow_assert) { if (env.get("BUN_CONFIG_MANIFEST_CACHE_CONTROL_TIMESTAMP")) |cache_control| { @@ -9207,6 +9281,8 @@ pub const PackageManager = struct { clap.parseParam("-p, --production Don't install devDependencies") catch unreachable, clap.parseParam("--no-save Don't update package.json or save a lockfile") catch unreachable, clap.parseParam("--save Save to package.json (true by default)") catch unreachable, + clap.parseParam("--ca ... Provide a Certificate Authority signing certificate") catch unreachable, + clap.parseParam("--cafile The same as `--ca`, but is a file path to the certificate") catch unreachable, clap.parseParam("--dry-run Don't install anything") catch unreachable, clap.parseParam("--frozen-lockfile Disallow changes to lockfile") catch unreachable, clap.parseParam("-f, --force Always request the latest versions from the registry & reinstall all dependencies") catch unreachable, @@ -9349,6 +9425,9 @@ pub const PackageManager = struct { publish_config: Options.PublishConfig = .{}, + ca: []const string = &.{}, + ca_file_name: string = "", + const PatchOpts = union(enum) { nothing: struct {}, patch: struct {}, @@ -9688,6 +9767,11 @@ pub const PackageManager = struct { cli.ignore_scripts = args.flag("--ignore-scripts"); cli.trusted = args.flag("--trust"); cli.no_summary = args.flag("--no-summary"); + cli.ca = args.options("--ca"); + + if (args.option("--cafile")) |ca_file_name| { + cli.ca_file_name = ca_file_name; + } // commands that support --filter if (comptime subcommand.supportsWorkspaceFiltering()) { diff --git a/src/js_ast.zig b/src/js_ast.zig index 363a000e6537d..d815627c45c80 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -3436,6 +3436,15 @@ pub const Expr = struct { return if (asProperty(expr, name)) |query| query.expr else null; } + pub fn getObject(expr: *const Expr, name: string) ?Expr { + if (expr.asProperty(name)) |query| { + if (query.expr.isObject()) { + return query.expr; + } + } + return null; + } + pub fn getString(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?struct { string, logger.Loc } { if (asProperty(expr, name)) |q| { if (q.expr.asString(allocator)) |str| { diff --git a/src/napi/napi.zig b/src/napi/napi.zig index fbe6a2d6bcc1b..f5134a21a69e9 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -777,7 +777,7 @@ pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv_: napi_value // We don't want to fail to load the library because of that // so we instead return an error and warn the user fn notImplementedYet(comptime name: []const u8) void { - bun.once( + bun.onceUnsafe( struct { pub fn warn() void { if (JSC.VirtualMachine.get().log.level.atLeast(.warn)) { diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 725a6ea480188..be558fb331810 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -563,7 +563,7 @@ pub const Resolver = struct { pub fn getPackageManager(this: *Resolver) *PackageManager { return this.package_manager orelse brk: { - bun.HTTPThread.init(); + bun.HTTPThread.init(&.{}); const pm = PackageManager.initWithRuntime( this.log, this.opts.install, diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 93168b63e51cb..40b556ab707a7 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -3095,7 +3095,8 @@ pub const PostgresSQLConnection = struct { defer hostname.deinit(); if (tls_object.isEmptyOrUndefinedOrNull()) { const ctx = vm.rareData().postgresql_context.tcp orelse brk: { - const ctx_ = uws.us_create_bun_socket_context(0, vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), uws.us_bun_socket_context_options_t{}).?; + var err: uws.create_bun_socket_error_t = .none; + const ctx_ = uws.us_create_bun_socket_context(0, vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), uws.us_bun_socket_context_options_t{}, &err).?; uws.NewSocketHandler(false).configure(ctx_, true, *PostgresSQLConnection, SocketHandler(false)); vm.rareData().postgresql_context.tcp = ctx_; break :brk ctx_; diff --git a/src/sys.zig b/src/sys.zig index 731b8aa649ac7..c31f67d4a4e09 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -2480,6 +2480,16 @@ pub fn exists(path: []const u8) bool { @compileError("TODO: existsOSPath"); } +pub fn existsZ(path: [:0]const u8) bool { + if (comptime Environment.isPosix) { + return system.access(path, 0) == 0; + } + + if (comptime Environment.isWindows) { + return getFileAttributes(path) != null; + } +} + pub fn faccessat(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { const has_sentinel = std.meta.sentinel(@TypeOf(subpath)) != null; const dir_fd = bun.toFD(dir_); diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index bd1e915a12b98..7c2d32126ef7e 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -22,6 +22,7 @@ import { toMatchNodeModulesAt, writeShebangScript, stderrForInstall, + tls, } from "harness"; import { join, resolve, sep } from "path"; import { readdirSorted } from "../dummy.registry"; @@ -514,6 +515,231 @@ ${Object.keys(opts) ); }); +describe("certificate authority", () => { + const mockRegistryFetch = function (opts?: any): (req: Request) => Promise { + return async function (req: Request) { + if (req.url.includes("no-deps")) { + return new Response(Bun.file(join(import.meta.dir, "packages", "no-deps", "no-deps-1.0.0.tgz"))); + } + return new Response("OK", { status: 200 }); + }; + }; + test("valid --cafile", async () => { + using server = Bun.serve({ + port: 0, + fetch: mockRegistryFetch(), + ...tls, + }); + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.1.1", + dependencies: { + "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "https://localhost:${server.port}/"`, + ), + write(join(packageDir, "cafile"), tls.cert), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", "cafile"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).toContain("+ no-deps@"); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("ConnectionClosed"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(await exited).toBe(0); + }); + test("valid --ca", async () => { + using server = Bun.serve({ + port: 0, + fetch: mockRegistryFetch(), + ...tls, + }); + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.1.1", + dependencies: { + "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "https://localhost:${server.port}/"`, + ), + ]); + + // first without ca, should fail + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + let out = await Bun.readableStreamToText(stdout); + let err = await Bun.readableStreamToText(stderr); + expect(err).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(await exited).toBe(1); + + // now with a valid ca + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ca", tls.cert], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("+ no-deps@"); + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + }); + test(`non-existent --cafile`, async () => { + await write( + join(packageDir, "package.json"), + JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", "does-not-exist"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); + expect(await exited).toBe(1); + }); + + test("cafile from bunfig does not exist", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "http://localhost:${port}/" + cafile = "does-not-exist"`, + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); + expect(await exited).toBe(1); + }); + test("invalid cafile", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ), + write( + join(packageDir, "invalid-cafile"), + `-----BEGIN CERTIFICATE----- +jlwkjekfjwlejlgldjfljlkwjef +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +ljelkjwelkgjw;lekj;lkejflkj +-----END CERTIFICATE-----`, + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", join(packageDir, "invalid-cafile")], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: invalid CA file: '${join(packageDir, "invalid-cafile")}'`); + expect(await exited).toBe(1); + }); + test("invalid --ca", async () => { + await write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ca", "not-valid"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("HTTPThread: the CA is invalid"); + expect(await exited).toBe(1); + }); +}); + export async function publish( env: any, cwd: string,