diff --git a/build.zig b/build.zig index 9a1e3b25a7..0fc377326f 100644 --- a/build.zig +++ b/build.zig @@ -328,6 +328,12 @@ pub fn build(b: *Build) !void { }); } + // zig build translate-c-headers + { + const step = b.step("translate-c", "Copy generated translated-c-headers.zig to zig-out"); + step.dependOn(&b.addInstallFile(getTranslateC(b, b.host, .Debug).getOutput(), "translated-c-headers.zig").step); + } + // zig build enum-extractor { // const step = b.step("enum-extractor", "Extract enum definitions (invoked by a code generator)"); @@ -380,6 +386,25 @@ pub fn addMultiCheck( } } +fn getTranslateC(b: *Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *Step.TranslateC { + const translate_c = b.addTranslateC(.{ + .root_source_file = b.path("src/c-headers-for-zig.h"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + inline for ([_](struct { []const u8, bool }){ + .{ "WINDOWS", translate_c.target.result.os.tag == .windows }, + .{ "POSIX", translate_c.target.result.os.tag != .windows }, + .{ "LINUX", translate_c.target.result.os.tag == .linux }, + .{ "DARWIN", translate_c.target.result.os.tag.isDarwin() }, + }) |entry| { + const str, const value = entry; + translate_c.defineCMacroRaw(b.fmt("{s}={d}", .{ str, @intFromBool(value) })); + } + return translate_c; +} + pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile { const obj = b.addObject(.{ .name = if (opts.optimize == .Debug) "bun-debug" else "bun", @@ -428,13 +453,8 @@ pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile { addInternalPackages(b, obj, opts); obj.root_module.addImport("build_options", opts.buildOptionsModule(b)); - const translate_plugin_api = b.addTranslateC(.{ - .root_source_file = b.path("./packages/bun-native-bundler-plugin-api/bundler_plugin.h"), - .target = opts.target, - .optimize = opts.optimize, - .link_libc = true, - }); - obj.root_module.addImport("bun-native-bundler-plugin-api", translate_plugin_api.createModule()); + const translate_c = getTranslateC(b, opts.target, opts.optimize); + obj.root_module.addImport("translated-c-headers", translate_c.createModule()); return obj; } diff --git a/docs/project/bindgen.md b/docs/project/bindgen.md index 3144d7f57f..83ee48d63a 100644 --- a/docs/project/bindgen.md +++ b/docs/project/bindgen.md @@ -61,7 +61,10 @@ This function declaration is equivalent to: declare function add(a: number, b: number = 1): number; ``` -The code generator will provide `bun.gen.math.jsAdd`, which is the native function implementation. To pass to JavaScript, use `bun.gen.math.createAddCallback(global)` +The code generator will provide `bun.gen.math.jsAdd`, which is the native +function implementation. To pass to JavaScript, use +`bun.gen.math.createAddCallback(global)`. JS files in `src/js/` may use +`$bindgenFn("math.bind.ts", "add")` to get a handle to the implementation. ## Strings @@ -104,7 +107,7 @@ export const action = fn({ In Zig, each variant gets a number, based on the order the schema defines. -``` +```zig fn action1(a: i32) i32 { return a; } @@ -180,9 +183,9 @@ export const add = fn({ // enforce in i32 range a: t.i32.enforceRange(), // clamp to u16 range - c: t.u16, + b: t.u16, // enforce in arbitrary range, with a default if not provided - b: t.i32.enforceRange(0, 1000).default(5), + c: t.i32.enforceRange(0, 1000).default(5), // clamp to arbitrary range, or null d: t.u16.clamp(0, 10).optional, }, @@ -190,6 +193,29 @@ export const add = fn({ }); ``` +Various Node.js validator functions such as `validateInteger`, `validateNumber`, and more are available. Use these when implementing Node.js APIs, so the error messages match 1:1 what Node would do. + +Unlike `enforceRange`, which is taken from WebIDL, `validate*` functions are much more strict on the input they accept. For example, Node's numerical validator check `typeof value === 'number'`, while WebIDL uses `ToNumber` for lossy conversion. + +```ts +import { t, fn } from "bindgen"; + +export const add = fn({ + args: { + global: t.globalObject, + // throw if not given a number + a: t.f64.validateNumber(), + // valid in i32 range + a: t.i32.validateInt32(), + // f64 within safe integer range + b: t.f64.validateInteger(), + // f64 in given range + c: t.f64.validateNumber(-10000, 10000), + }, + ret: t.i32, +}); +``` + ## Callbacks TODO diff --git a/src/bun.js/bindings/BindgenCustomEnforceRange.h b/src/bun.js/bindings/BindgenCustomEnforceRange.h new file mode 100644 index 0000000000..a207b4e67a --- /dev/null +++ b/src/bun.js/bindings/BindgenCustomEnforceRange.h @@ -0,0 +1,112 @@ +#pragma once +#include "root.h" +#include "IDLTypes.h" +#include "JSDOMConvertBase.h" +#include "ErrorCode.h" + +namespace Bun { + +enum class BindgenCustomEnforceRangeKind { + Node, + Web, +}; + +// This type implements conversion for: +// - t.*.validateInteger() +// - t.*.enforceRange(a, b) when A, B is not the integer's ABI size. +// - t.i32.validateInt32() +// - t.u32.validateUInt32() +template< + typename NumericType, + NumericType Min, + NumericType Max, + BindgenCustomEnforceRangeKind Kind> +struct BindgenCustomEnforceRange : WebCore::IDLType { +}; + +} + +static String rangeErrorString(double value, double min, double max) +{ + return makeString("Value "_s, value, " is outside the range ["_s, min, ", "_s, max, ']'); +} + +namespace WebCore { + +template< + typename NumericType, + NumericType Min, + NumericType Max, + Bun::BindgenCustomEnforceRangeKind Kind> +struct Converter> + : DefaultConverter> { + template + static inline NumericType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower = ExceptionThrower()) + { + auto scope = DECLARE_THROW_SCOPE(lexicalGlobalObject.vm()); + ASSERT(!scope.exception()); + double unrestricted; + if constexpr (Kind == Bun::BindgenCustomEnforceRangeKind::Node) { + // In Node.js, `validateNumber`, `validateInt32`, `validateUint32`, + // and `validateInteger` all start with the following + // + // if (typeof value !== 'number') + // throw new ERR_INVALID_ARG_TYPE(name, 'number', value); + // + if (!value.isNumber()) { + Bun::ERR::INVALID_ARG_TYPE(scope, &lexicalGlobalObject, exceptionThrower(), "number"_s, value); + return 0; + } + unrestricted = value.asNumber(); + ASSERT(!scope.exception()); + + // Node also validates that integer types are integers + if constexpr (std::is_integral_v) { + if (unrestricted != std::round(unrestricted)) { + // ERR_OUT_OF_RANGE "an integer" + Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), "an integer"_s, value); + return 0; + } + } else { + // When a range is specified (what this template is implementing), + // Node also throws on NaN being out of range + if (std::isnan(unrestricted)) { + // ERR_OUT_OF_RANGE `>= ${min} && <= ${max}` + Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), Min, Max, value); + return 0; + } + } + } else { + // WebIDL uses toNumber before applying range restrictions. This + // allows something like `true` to pass for `t.f64.enforceRange(-10, 10)`, + // but this behavior does not appear Node's validators. + unrestricted = value.toNumber(&lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, 0); + + if constexpr (std::is_integral_v) { + if (std::isnan(unrestricted) || std::isinf(unrestricted)) { + throwTypeError(&lexicalGlobalObject, scope, rangeErrorString(unrestricted, Min, Max)); + return 0; + } + + // IDL uses trunc to convert the double to an integer. + unrestricted = trunc(unrestricted); + } + } + + bool inRange = unrestricted >= Min && unrestricted <= Max; + if (!inRange) { + if constexpr (Kind == Bun::BindgenCustomEnforceRangeKind::Node) { + Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), Min, Max, value); + } else { + // WebKit range exception + throwTypeError(&lexicalGlobalObject, scope, rangeErrorString(unrestricted, Min, Max)); + } + return 0; + } + + return static_cast(unrestricted); + } +}; + +} diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 4f2d6eb32f..20f6c0639c 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -302,9 +302,9 @@ WTF::String ERR_OUT_OF_RANGE(JSC::ThrowScope& scope, JSC::JSGlobalObject* global namespace ERR { -JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral arg_name, WTF::ASCIILiteral expected_type, JSC::JSValue val_actual_value) +JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value) { - auto arg_kind = String(arg_name).startsWith("options."_s) ? "property"_s : "argument"_s; + auto arg_kind = arg_name.startsWith("options."_s) ? "property"_s : "argument"_s; auto ty_first_char = expected_type[0]; auto ty_kind = ty_first_char >= 'A' && ty_first_char <= 'Z' ? "an instance of"_s : "of type"_s; @@ -315,7 +315,7 @@ JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalO throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, message)); return {}; } -JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue val_arg_name, WTF::ASCIILiteral expected_type, JSC::JSValue val_actual_value) +JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue val_arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value) { auto arg_name = val_arg_name.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); @@ -332,7 +332,7 @@ JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalO return {}; } -JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, size_t lower, size_t upper, JSC::JSValue actual) +JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, double lower, double upper, JSC::JSValue actual) { auto lowerStr = jsNumber(lower).toWTFString(globalObject); auto upperStr = jsNumber(upper).toWTFString(globalObject); @@ -343,7 +343,7 @@ JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObjec throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_OUT_OF_RANGE, message)); return {}; } -JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, size_t lower, size_t upper, JSC::JSValue actual) +JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, double lower, double upper, JSC::JSValue actual) { auto arg_name = arg_name_val.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); @@ -356,7 +356,7 @@ JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObjec throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_OUT_OF_RANGE, message)); return {}; } -JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, size_t bound_num, Bound bound, JSC::JSValue actual) +JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, double bound_num, Bound bound, JSC::JSValue actual) { auto arg_name = arg_name_val.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index e4a64cff12..a3f4f8a67e 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -75,11 +75,11 @@ enum Bound { namespace ERR { -JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral arg_name, WTF::ASCIILiteral expected_type, JSC::JSValue val_actual_value); -JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name, WTF::ASCIILiteral expected_type, JSC::JSValue val_actual_value); -JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, size_t lower, size_t upper, JSC::JSValue actual); -JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name, size_t lower, size_t upper, JSC::JSValue actual); -JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, size_t bound_num, Bound bound, JSC::JSValue actual); +JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value); +JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value); +JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, double lower, double upper, JSC::JSValue actual); +JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name, double lower, double upper, JSC::JSValue actual); +JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, double bound_num, Bound bound, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, const WTF::String& msg, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name_val, const WTF::String& msg, JSC::JSValue actual); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 1bf4c70f14..b9ed12271f 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -59,10 +59,11 @@ export default [ // Console ["ERR_CONSOLE_WRITABLE_STREAM", TypeError, "TypeError"], - //NET + // NET ["ERR_SOCKET_CLOSED_BEFORE_CONNECTION", Error], ["ERR_SOCKET_CLOSED", Error], - //HTTP2 + + // HTTP2 ["ERR_INVALID_HTTP_TOKEN", TypeError], ["ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED", TypeError], ["ERR_HTTP2_SEND_FILE", Error], @@ -87,4 +88,9 @@ export default [ ["ERR_HTTP2_SOCKET_UNBOUND", Error], ["ERR_HTTP2_ERROR", Error], ["ERR_HTTP2_OUT_OF_STREAMS", Error], + + // AsyncHooks + ["ERR_ASYNC_TYPE", TypeError], + ["ERR_INVALID_ASYNC_ID", RangeError], + ["ERR_ASYNC_CALLBACK", TypeError], ] as ErrorCodeMapping; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 1d6d421503..b370262e00 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1896,7 +1896,6 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, result->putDirect(vm, vm.propertyNames->name, code, JSC::PropertyAttribute::DontEnum | 0); } else { - result->putDirect( vm, vm.propertyNames->name, JSC::JSValue(jsString(vm, String("SystemError"_s))), @@ -1930,6 +1929,60 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, return JSC::JSValue::encode(JSC::JSValue(result)); } +JSC__JSValue SystemError__toErrorInstanceWithInfoObject(const SystemError* arg0, + JSC__JSGlobalObject* globalObject) +{ + ASSERT_NO_PENDING_EXCEPTION(globalObject); + SystemError err = *arg0; + + JSC::VM& vm = globalObject->vm(); + + auto scope = DECLARE_THROW_SCOPE(vm); + + auto codeString = err.code.toWTFString(); + auto syscallString = err.syscall.toWTFString(); + auto messageString = err.message.toWTFString(); + + JSC::JSValue message = JSC::jsString(vm, makeString("A system error occurred: "_s, syscallString, " returned "_s, codeString, " ("_s, messageString, ")"_s)); + + JSC::JSValue options = JSC::jsUndefined(); + JSC::JSObject* result = JSC::ErrorInstance::create(globalObject, JSC::ErrorInstance::createStructure(vm, globalObject, globalObject->errorPrototype()), message, options); + JSC::JSObject* info = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 0); + + auto clientData = WebCore::clientData(vm); + + result->putDirect( + vm, vm.propertyNames->name, + JSC::JSValue(jsString(vm, String("SystemError"_s))), + JSC::PropertyAttribute::DontEnum | 0); + result->putDirect( + vm, clientData->builtinNames().codePublicName(), + JSC::JSValue(jsString(vm, String("ERR_SYSTEM_ERROR"_s))), + JSC::PropertyAttribute::DontEnum | 0); + + info->putDirect(vm, clientData->builtinNames().codePublicName(), jsString(vm, codeString), JSC::PropertyAttribute::DontDelete | 0); + + result->putDirect(vm, JSC::Identifier::fromString(vm, "info"_s), info, JSC::PropertyAttribute::DontDelete | 0); + + auto syscallJsString = jsString(vm, syscallString); + result->putDirect(vm, clientData->builtinNames().syscallPublicName(), syscallJsString, + JSC::PropertyAttribute::DontDelete | 0); + info->putDirect(vm, clientData->builtinNames().syscallPublicName(), syscallJsString, + JSC::PropertyAttribute::DontDelete | 0); + + info->putDirect(vm, clientData->builtinNames().codePublicName(), jsString(vm, codeString), + JSC::PropertyAttribute::DontDelete | 0); + info->putDirect(vm, vm.propertyNames->message, jsString(vm, messageString), + JSC::PropertyAttribute::DontDelete | 0); + + info->putDirect(vm, clientData->builtinNames().errnoPublicName(), jsNumber(err.errno_), + JSC::PropertyAttribute::DontDelete | 0); + result->putDirect(vm, clientData->builtinNames().errnoPublicName(), JSC::JSValue(err.errno_), + JSC::PropertyAttribute::DontDelete | 0); + + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::JSValue(result))); +} + JSC__JSValue JSC__JSObject__create(JSC__JSGlobalObject* globalObject, size_t initialCapacity, void* arg2, void (*ArgFn3)(void* arg0, JSC__JSObject* arg1, JSC__JSGlobalObject* arg2)) diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index ea29db478c..e79bf9799f 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -74,7 +74,8 @@ pub const JSObject = extern struct { JSValue.createEmptyObjectWithNullPrototype(global) else JSValue.createEmptyObject(global, comptime info.fields.len); - bun.debugAssert(val.isObject()); + if (bun.Environment.isDebug) + bun.assert(val.isObject()); break :obj val.uncheckedPtrCast(JSObject); }; @@ -1729,10 +1730,6 @@ pub const SystemError = extern struct { return @enumFromInt(this.errno * -1); } - pub fn toAnyhowError(this: SystemError) bun.anyhow.Error { - return bun.anyhow.Error.newSys(this); - } - pub fn deref(this: *const SystemError) void { this.path.deref(); this.code.deref(); @@ -1758,6 +1755,37 @@ pub const SystemError = extern struct { return shim.cppFn("toErrorInstance", .{ this, global }); } + /// This constructs the ERR_SYSTEM_ERROR error object, which has an `info` + /// property containing the details of the system error: + /// + /// SystemError [ERR_SYSTEM_ERROR]: A system error occurred: {syscall} returned {errno} ({message}) + /// { + /// name: "ERR_SYSTEM_ERROR", + /// info: { + /// errno: -{errno}, + /// code: {code}, // string + /// message: {message}, // string + /// syscall: {syscall}, // string + /// }, + /// errno: -{errno}, + /// syscall: {syscall}, + /// } + /// + /// Before using this function, consider if the Node.js API it is + /// implementing follows this convention. It is exclusively used + /// to match the error code that `node:os` throws. + pub fn toErrorInstanceWithInfoObject(this: *const SystemError, global: *JSGlobalObject) JSValue { + defer { + this.path.deref(); + this.code.deref(); + this.message.deref(); + this.syscall.deref(); + } + + return SystemError__toErrorInstanceWithInfoObject(this, global); + } + extern fn SystemError__toErrorInstanceWithInfoObject(*const SystemError, *JSC.JSGlobalObject) JSValue; + pub fn format(self: SystemError, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { if (!self.path.isEmpty()) { // TODO: remove this hardcoding @@ -1999,6 +2027,7 @@ pub const JSPromiseRejectionOperation = enum(u32) { Handle = 1, }; +// TODO(@paperdave): delete and inline these functions pub fn NewGlobalObject(comptime Type: type) type { return struct { const importNotImpl = "Import not implemented"; @@ -5881,14 +5910,11 @@ pub const JSValue = enum(i64) { } pub fn asInt32(this: JSValue) i32 { - // TODO: add this assertion. currently, there is a mistake in - // argumentCount that mistakenly uses a JSValue instead of a c_int. This - // mistake performs the correct conversion instructions for it's use - // case but is bad code practice to misuse JSValue casts. - // - // if (bun.Environment.allow_assert) { - // bun.assert(this.isInt32()); - // } + // TODO: promote assertion to allow_assert. That has not been done because + // the assertion was commented out until 2024-12-12 + if (bun.Environment.isDebug) { + bun.assert(this.isInt32()); + } return FFI.JSVALUE_TO_INT32(.{ .asJSValue = this }); } @@ -6573,67 +6599,112 @@ pub const CatchScope = extern struct { }; }; -// TODO: callframe cleanup -// - remove all references into sizegen.zig since it is no longer run and may become out of date -// - remove all functions to retrieve arguments, replace with -// - arguments(*CallFrame) []const JSValue (when you want a full slice) -// - argumentsAsArray(*CallFrame, comptime len) [len]JSValue (common case due to destructuring) -// - argument(*CallFrame, i: usize) JSValue (return undefined if not present) -// - argumentCount(*CallFrame) usize -// -// argumentsPtr() -> arguments().ptr -// arguments(n).ptr[k] -> argumentsAsArray(n)[k] -// arguments(n).slice() -> arguments() -// arguments(n).mut() -> `var args = argumentsAsArray(n); &args` -// argumentsCount() -> argumentCount() (to match JSC) -// argument(n) -> arguments().ptr[n] +/// Call Frame for JavaScript -> Native function calls. In Bun, it is +/// preferred to use the bindings generator instead of directly decoding +/// arguments. See `docs/project/bindgen.md` pub const CallFrame = opaque { - /// The value is generated in `make sizegen` - /// The value is 6. - /// On ARM64_32, the value is something else but it really doesn't matter for our case - /// However, I don't want this to subtly break amidst future upgrades to JavaScriptCore - const alignment = Sizes.Bun_CallFrame__align; - - pub const name = "JSC::CallFrame"; - - inline fn asUnsafeJSValueArray(self: *const CallFrame) [*]const JSC.JSValue { - return @as([*]align(alignment) const JSC.JSValue, @ptrCast(@alignCast(self))); + /// A slice of all passed arguments to this function call. + pub fn arguments(self: *const CallFrame) []const JSValue { + return self.asUnsafeJSValueArray()[offset_first_argument..][0..self.argumentsCount()]; } - pub fn format(frame: *CallFrame, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - const args = frame.argumentsPtr()[0..frame.argumentsCount()]; - - for (args[0..@min(args.len, 4)], 0..) |arg, i| { - if (i != 0) { - try writer.writeAll(", "); - } - switch (arg) { - .zero => try writer.writeAll(""), - .undefined => try writer.writeAll("undefined"), - .null => try writer.writeAll("null"), - .true => try writer.writeAll("true"), - .false => try writer.writeAll("false"), - else => { - if (arg.isNumber()) { - try writer.writeAll("number"); - } else { - try writer.writeAll(@tagName(arg.jsType())); - } - }, - } - } - - if (args.len > 4) { - try writer.print(", ... {d} more", .{args.len - 4}); - } + /// Usage: `const arg1, const arg2 = call_frame.argumentsAsArray(2);` + pub fn argumentsAsArray(call_frame: *const CallFrame, comptime count: usize) [count]JSValue { + const slice = call_frame.arguments(); + var value: [count]JSValue = .{.undefined} ** count; + const n = @min(call_frame.argumentsCount(), count); + @memcpy(value[0..n], slice[0..n]); + return value; } - pub fn argumentsPtr(self: *const CallFrame) [*]const JSC.JSValue { - return self.asUnsafeJSValueArray()[Sizes.Bun_CallFrame__firstArgument..]; + /// This function protects out-of-bounds access by returning `JSValue.undefined` + pub fn argument(self: *const CallFrame, i: usize) JSC.JSValue { + return if (self.argumentsCount() > i) self.arguments()[i] else .undefined; } + pub fn argumentsCount(self: *const CallFrame) u32 { + return self.argumentCountIncludingThis() - 1; + } + + /// When this CallFrame belongs to a constructor, this value is not the `this` + /// value, but instead the value of `new.target`. + pub fn this(self: *const CallFrame) JSC.JSValue { + return self.asUnsafeJSValueArray()[offset_this_argument]; + } + + /// `JSValue` for the current function being called. pub fn callee(self: *const CallFrame) JSC.JSValue { - return self.asUnsafeJSValueArray()[Sizes.Bun_CallFrame__callee]; + return self.asUnsafeJSValueArray()[offset_callee]; + } + + /// From JavaScriptCore/interpreter/CallFrame.h + /// + /// | ...... | | + /// +----------------------------+ | + /// | argN | v lower address + /// +----------------------------+ + /// | arg1 | + /// +----------------------------+ + /// | arg0 | + /// +----------------------------+ + /// | this | + /// +----------------------------+ + /// | argumentCountIncludingThis | + /// +----------------------------+ + /// | callee | + /// +----------------------------+ + /// | codeBlock | + /// +----------------------------+ + /// | return-address | + /// +----------------------------+ + /// | callerFrame | + /// +----------------------------+ <- callee's cfr is pointing this address + /// | local0 | + /// +----------------------------+ + /// | local1 | + /// +----------------------------+ + /// | localN | + /// +----------------------------+ + /// | ...... | + /// + /// The proper return type of this should be []Register, but + inline fn asUnsafeJSValueArray(self: *const CallFrame) [*]const JSC.JSValue { + return @ptrCast(@alignCast(self)); + } + + // These constants are from JSC::CallFrameSlot in JavaScriptCore/interpreter/CallFrame.h + const offset_code_block = 2; + const offset_callee = offset_code_block + 1; + const offset_argument_count_including_this = offset_callee + 1; + const offset_this_argument = offset_argument_count_including_this + 1; + const offset_first_argument = offset_this_argument + 1; + + /// This function is manually ported from JSC's equivalent function in C++ + /// See JavaScriptCore/interpreter/CallFrame.h + fn argumentCountIncludingThis(self: *const CallFrame) u32 { + // Register defined in JavaScriptCore/interpreter/Register.h + const Register = extern union { + value: JSValue, // EncodedJSValue + call_frame: *CallFrame, + code_block: *anyopaque, // CodeBlock* + /// EncodedValueDescriptor defined in JavaScriptCore/runtime/JSCJSValue.h + encoded_value: extern union { + ptr: JSValue, // JSCell* + as_bits: extern struct { + payload: i32, + tag: i32, + }, + }, + number: f64, // double + integer: i64, // integer + }; + const registers: [*]const Register = @alignCast(@ptrCast(self)); + // argumentCountIncludingThis takes the register at the defined offset, then + // calls 'ALWAYS_INLINE int32_t Register::unboxedInt32() const', + // which in turn calls 'ALWAYS_INLINE int32_t Register::payload() const' + // which accesses `.encodedValue.asBits.payload` + // JSC stores and works with value as signed, but it is always 1 or more. + return @intCast(registers[offset_argument_count_including_this].encoded_value.as_bits.payload); } fn Arguments(comptime max: usize) type { @@ -6667,54 +6738,35 @@ pub const CallFrame = opaque { }; } - pub fn arguments(self: *const CallFrame) []const JSValue { - // this presumably isn't allowed given that it doesn't exist - return self.argumentsPtr()[0..self.argumentsCount()]; - } + /// Do not use this function. Migration path: + /// arguments(n).ptr[k] -> argumentsAsArray(n)[k] + /// arguments(n).slice() -> arguments() + /// arguments(n).mut() -> `var args = argumentsAsArray(n); &args` pub fn arguments_old(self: *const CallFrame, comptime max: usize) Arguments(max) { - const len = self.argumentsCount(); - const ptr = self.argumentsPtr(); - return switch (@as(u4, @min(len, max))) { + const slice = self.arguments(); + comptime bun.assert(max <= 10); + return switch (@as(u4, @min(slice.len, max))) { 0 => .{ .ptr = undefined, .len = 0 }, - inline 1...10 => |count| Arguments(max).init(comptime @min(count, max), ptr), + inline 1...10 => |count| Arguments(max).init(comptime @min(count, max), slice.ptr), else => unreachable, }; } + /// Do not use this function. Migration path: + /// argumentsAsArray(n) pub fn argumentsUndef(self: *const CallFrame, comptime max: usize) Arguments(max) { - const len = self.argumentsCount(); - const ptr = self.argumentsPtr(); - return switch (@as(u4, @min(len, max))) { + const slice = self.arguments(); + comptime bun.assert(max <= 9); + return switch (@as(u4, @min(slice.len, max))) { 0 => .{ .ptr = .{.undefined} ** max, .len = 0 }, - inline 1...9 => |count| Arguments(max).initUndef(@min(count, max), ptr), + inline 1...9 => |count| Arguments(max).initUndef(@min(count, max), slice.ptr), else => unreachable, }; } - pub inline fn argument(self: *const CallFrame, i: usize) JSC.JSValue { - return self.argumentsPtr()[i]; - } - - pub fn this(self: *const CallFrame) JSC.JSValue { - return self.asUnsafeJSValueArray()[Sizes.Bun_CallFrame__thisArgument]; - } - - pub fn argumentsCount(self: *const CallFrame) usize { - return @as(usize, @intCast(self.asUnsafeJSValueArray()[Sizes.Bun_CallFrame__argumentCountIncludingThis].asInt32() - 1)); - } - extern fn Bun__CallFrame__isFromBunMain(*const CallFrame, *const VM) bool; pub const isFromBunMain = Bun__CallFrame__isFromBunMain; - /// Usage: `const arg1, const arg2 = call_frame.argumentsAsArray(2);` - pub fn argumentsAsArray(call_frame: *const CallFrame, comptime count: usize) [count]JSValue { - var value: [count]JSValue = .{.undefined} ** count; - for (0..@min(call_frame.argumentsCount(), count)) |i| { - value[i] = call_frame.argument(i); - } - return value; - } - extern fn Bun__CallFrame__getCallerSrcLoc(*const CallFrame, *JSGlobalObject, *bun.String, *c_uint, *c_uint) void; pub const CallerSrcLoc = struct { str: bun.String, diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 38f74c731f..8268b1cab2 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -41,19 +41,25 @@ extern "C" void bun_warn_avx_missing(const char* url) } #endif -extern "C" int32_t get_process_priority(uint32_t pid) +// Error condition is encoded as max int32_t. +// The only error in this function is ESRCH (no process found) +extern "C" int32_t get_process_priority(int32_t pid) { #if OS(WINDOWS) int priority = 0; if (uv_os_getpriority(pid, &priority)) - return 0; + return std::numeric_limits::max(); return priority; #else - return getpriority(PRIO_PROCESS, pid); + errno = 0; + int priority = getpriority(PRIO_PROCESS, pid); + if (priority == -1 && errno != 0) + return std::numeric_limits::max(); + return priority; #endif // OS(WINDOWS) } -extern "C" int32_t set_process_priority(uint32_t pid, int32_t priority) +extern "C" int32_t set_process_priority(int32_t pid, int32_t priority) { #if OS(WINDOWS) return uv_os_setpriority(pid, priority); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 283739d987..e12bfc4873 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -3981,7 +3981,7 @@ pub const VirtualMachine = struct { if (show.system_code) { if (show.syscall) { - try writer.writeAll(" "); + try writer.writeAll(" "); } else if (show.errno) { try writer.writeAll(" "); } diff --git a/src/bun.js/node/node_os.bind.ts b/src/bun.js/node/node_os.bind.ts new file mode 100644 index 0000000000..db3082c4b7 --- /dev/null +++ b/src/bun.js/node/node_os.bind.ts @@ -0,0 +1,94 @@ +// Internal bindings for node:os +// The entrypoint for node:os is `src/js/node/os.ts` +import { fn, t } from "bindgen"; + +export const cpus = fn({ + args: { + global: t.globalObject, + }, + ret: t.any, +}); +export const freemem = fn({ + args: {}, + ret: t.u64, +}); +export const getPriority = fn({ + args: { + global: t.globalObject, + pid: t.i32.validateInt32().default(0).nonNull, + }, + ret: t.i32, +}); +export const homedir = fn({ + args: { + global: t.globalObject, + }, + ret: t.DOMString, +}); +export const hostname = fn({ + args: { + global: t.globalObject, + }, + ret: t.any, +}); +export const loadavg = fn({ + args: { + global: t.globalObject, + }, + ret: t.any, +}); +export const networkInterfaces = fn({ + args: { + global: t.globalObject, + }, + ret: t.any, +}); +export const release = fn({ + args: {}, + ret: t.DOMString, +}); +export const totalmem = fn({ + args: {}, + ret: t.u64, +}); +export const uptime = fn({ + args: { + global: t.globalObject, + }, + ret: t.f64, +}); +export const UserInfoOptions = t.dictionary({ + encoding: t.DOMString.default(""), +}); +export const userInfo = fn({ + args: { + global: t.globalObject, + options: UserInfoOptions.default({}), + }, + ret: t.any, +}); +export const version = fn({ + args: {}, + ret: t.DOMString, +}); +const PRI_MIN = -20; +const PRI_MAX = 19; +export const setPriority = fn({ + variants: [ + { + args: { + global: t.globalObject, + pid: t.i32.validateInt32(), + priority: t.i32.validateInt32(PRI_MIN, PRI_MAX), + }, + ret: t.undefined, + }, + { + args: { + global: t.globalObject, + priority: t.i32.validateInt32(PRI_MIN, PRI_MAX), + }, + ret: t.undefined, + }, + ], +}); diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index ef2dfab3f1..fc60648fee 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -7,856 +7,825 @@ const strings = bun.strings; const JSC = bun.JSC; const Environment = bun.Environment; const Global = bun.Global; -const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; - const libuv = bun.windows.libuv; -pub const OS = struct { - pub fn create(globalObject: *JSC.JSGlobalObject) JSC.JSValue { - const module = JSC.JSValue.createEmptyObject(globalObject, 16); +const gen = bun.gen.node_os; - module.put(globalObject, JSC.ZigString.static("cpus"), JSC.NewFunction(globalObject, JSC.ZigString.static("cpus"), 0, cpus, false)); - module.put(globalObject, JSC.ZigString.static("freemem"), JSC.NewFunction(globalObject, JSC.ZigString.static("freemem"), 0, freemem, false)); - module.put(globalObject, JSC.ZigString.static("getPriority"), JSC.NewFunction(globalObject, JSC.ZigString.static("getPriority"), 1, getPriority, false)); - module.put(globalObject, JSC.ZigString.static("homedir"), JSC.NewFunction(globalObject, JSC.ZigString.static("homedir"), 0, homedir, false)); - module.put(globalObject, JSC.ZigString.static("hostname"), JSC.NewFunction(globalObject, JSC.ZigString.static("hostname"), 0, hostname, false)); - module.put(globalObject, JSC.ZigString.static("loadavg"), JSC.NewFunction(globalObject, JSC.ZigString.static("loadavg"), 0, loadavg, false)); - module.put(globalObject, JSC.ZigString.static("machine"), JSC.NewFunction(globalObject, JSC.ZigString.static("machine"), 0, machine, false)); - module.put(globalObject, JSC.ZigString.static("networkInterfaces"), JSC.NewFunction(globalObject, JSC.ZigString.static("networkInterfaces"), 0, networkInterfaces, false)); - module.put(globalObject, JSC.ZigString.static("release"), JSC.NewFunction(globalObject, JSC.ZigString.static("release"), 0, release, false)); - module.put(globalObject, JSC.ZigString.static("setPriority"), JSC.NewFunction(globalObject, JSC.ZigString.static("setPriority"), 2, setPriority, false)); - module.put(globalObject, JSC.ZigString.static("totalmem"), JSC.NewFunction(globalObject, JSC.ZigString.static("totalmem"), 0, totalmem, false)); - module.put(globalObject, JSC.ZigString.static("type"), JSC.NewFunction(globalObject, JSC.ZigString.static("type"), 0, OS.type, false)); - module.put(globalObject, JSC.ZigString.static("uptime"), JSC.NewFunction(globalObject, JSC.ZigString.static("uptime"), 0, uptime, false)); - module.put(globalObject, JSC.ZigString.static("userInfo"), JSC.NewFunction(globalObject, JSC.ZigString.static("userInfo"), 0, userInfo, false)); - module.put(globalObject, JSC.ZigString.static("version"), JSC.NewFunction(globalObject, JSC.ZigString.static("version"), 0, version, false)); - module.put(globalObject, JSC.ZigString.static("machine"), JSC.NewFunction(globalObject, JSC.ZigString.static("machine"), 0, machine, false)); +pub fn createNodeOsBinding(global: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSObject.create(.{ + .cpus = gen.createCpusCallback(global), + .freemem = gen.createFreememCallback(global), + .getPriority = gen.createGetPriorityCallback(global), + .homedir = gen.createHomedirCallback(global), + .hostname = gen.createHostnameCallback(global), + .loadavg = gen.createLoadavgCallback(global), + .networkInterfaces = gen.createNetworkInterfacesCallback(global), + .release = gen.createReleaseCallback(global), + .totalmem = gen.createTotalmemCallback(global), + .uptime = gen.createUptimeCallback(global), + .userInfo = gen.createUserInfoCallback(global), + .version = gen.createVersionCallback(global), + .setPriority = gen.createSetPriorityCallback(global), + }, global).toJS(); +} - return module; +const CPUTimes = struct { + user: u64 = 0, + nice: u64 = 0, + sys: u64 = 0, + idle: u64 = 0, + irq: u64 = 0, + + pub fn toValue(self: CPUTimes, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + const fields = comptime std.meta.fieldNames(CPUTimes); + const ret = JSC.JSValue.createEmptyObject(globalThis, fields.len); + inline for (fields) |fieldName| { + ret.put(globalThis, JSC.ZigString.static(fieldName), JSC.JSValue.jsNumberFromUint64(@field(self, fieldName))); + } + return ret; + } +}; + +pub fn cpus(global: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + const cpusImpl = switch (Environment.os) { + .linux => cpusImplLinux, + .mac => cpusImplDarwin, + .windows => cpusImplWindows, + else => @compileError("Unsupported OS"), + }; + + return cpusImpl(global) catch { + const err = JSC.SystemError{ + .message = bun.String.static("Failed to get CPU information"), + .code = bun.String.static(@tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR)), + }; + return global.throwValue(err.toErrorInstance(global)); + }; +} + +fn cpusImplLinux(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { + // Create the return array + const values = JSC.JSValue.createEmptyArray(globalThis, 0); + var num_cpus: u32 = 0; + + var stack_fallback = std.heap.stackFallback(1024 * 8, bun.default_allocator); + var file_buf = std.ArrayList(u8).init(stack_fallback.get()); + defer file_buf.deinit(); + + // Read /proc/stat to get number of CPUs and times + { + const file = try std.fs.openFileAbsolute("/proc/stat", .{}); + defer file.close(); + + const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); + defer file_buf.clearRetainingCapacity(); + const contents = file_buf.items[0..read]; + + var line_iter = std.mem.tokenizeScalar(u8, contents, '\n'); + + // Skip the first line (aggregate of all CPUs) + _ = line_iter.next(); + + // Read each CPU line + while (line_iter.next()) |line| { + // CPU lines are formatted as `cpu0 user nice sys idle iowait irq softirq` + var toks = std.mem.tokenize(u8, line, " \t"); + const cpu_name = toks.next(); + if (cpu_name == null or !std.mem.startsWith(u8, cpu_name.?, "cpu")) break; // done with CPUs + + //NOTE: libuv assumes this is fixed on Linux, not sure that's actually the case + const scale = 10; + + var times = CPUTimes{}; + times.user = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + times.nice = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + times.sys = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + times.idle = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + _ = try (toks.next() orelse error.eol); // skip iowait + times.irq = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + + // Actually create the JS object representing the CPU + const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); + cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); + values.putIndex(globalThis, num_cpus, cpu); + + num_cpus += 1; + } } - const CPUTimes = struct { - user: u64 = 0, - nice: u64 = 0, - sys: u64 = 0, - idle: u64 = 0, - irq: u64 = 0, + // Read /proc/cpuinfo to get model information (optional) + if (std.fs.openFileAbsolute("/proc/cpuinfo", .{})) |file| { + defer file.close(); - pub fn toValue(self: CPUTimes, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const fields = comptime std.meta.fieldNames(CPUTimes); - const ret = JSC.JSValue.createEmptyObject(globalThis, fields.len); - inline for (fields) |fieldName| { - ret.put(globalThis, JSC.ZigString.static(fieldName), JSC.JSValue.jsNumberFromUint64(@field(self, fieldName))); + const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); + defer file_buf.clearRetainingCapacity(); + const contents = file_buf.items[0..read]; + + var line_iter = std.mem.tokenizeScalar(u8, contents, '\n'); + + const key_processor = "processor\t: "; + const key_model_name = "model name\t: "; + + var cpu_index: u32 = 0; + var has_model_name = true; + while (line_iter.next()) |line| { + if (strings.hasPrefixComptime(line, key_processor)) { + if (!has_model_name) { + const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); + } + // If this line starts a new processor, parse the index from the line + const digits = std.mem.trim(u8, line[key_processor.len..], " \t\n"); + cpu_index = try std.fmt.parseInt(u32, digits, 10); + if (cpu_index >= num_cpus) return error.too_may_cpus; + has_model_name = false; + } else if (strings.hasPrefixComptime(line, key_model_name)) { + // If this is the model name, extract it and store on the current cpu + const model_name = line[key_model_name.len..]; + const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(model_name).withEncoding().toJS(globalThis)); + has_model_name = true; } - return ret; + } + if (!has_model_name) { + const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); + } + } else |_| { + // Initialize model name to "unknown" + var it = values.arrayIterator(globalThis); + while (it.next()) |cpu| { + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); + } + } + + // Read /sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq to get current frequency (optional) + for (0..num_cpus) |cpu_index| { + const cpu = JSC.JSObject.getIndex(values, globalThis, @truncate(cpu_index)); + + var path_buf: [128]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq", .{cpu_index}); + if (std.fs.openFileAbsolute(path, .{})) |file| { + defer file.close(); + + const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); + defer file_buf.clearRetainingCapacity(); + const contents = file_buf.items[0..read]; + + const digits = std.mem.trim(u8, contents, " \n"); + const speed = (std.fmt.parseInt(u64, digits, 10) catch 0) / 1000; + + cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed)); + } else |_| { + // Initialize CPU speed to 0 + cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(0)); + } + } + + return values; +} + +extern fn bun_sysconf__SC_CLK_TCK() isize; +fn cpusImplDarwin(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { + const local_bindings = @import("../../darwin_c.zig"); + const c = std.c; + + // Fetch the CPU info structure + var num_cpus: c.natural_t = 0; + var info: [*]local_bindings.processor_cpu_load_info = undefined; + var info_size: std.c.mach_msg_type_number_t = 0; + if (local_bindings.host_processor_info(std.c.mach_host_self(), local_bindings.PROCESSOR_CPU_LOAD_INFO, &num_cpus, @as(*local_bindings.processor_info_array_t, @ptrCast(&info)), &info_size) != .SUCCESS) { + return error.no_processor_info; + } + defer _ = std.c.vm_deallocate(std.c.mach_task_self(), @intFromPtr(info), info_size); + + // Ensure we got the amount of data we expected to guard against buffer overruns + if (info_size != C.PROCESSOR_CPU_LOAD_INFO_COUNT * num_cpus) { + return error.broken_process_info; + } + + // Get CPU model name + var model_name_buf: [512]u8 = undefined; + var len: usize = model_name_buf.len; + // Try brand_string first and if it fails try hw.model + if (!(std.c.sysctlbyname("machdep.cpu.brand_string", &model_name_buf, &len, null, 0) == 0 or + std.c.sysctlbyname("hw.model", &model_name_buf, &len, null, 0) == 0)) + { + return error.no_processor_info; + } + // NOTE: sysctlbyname doesn't update len if it was large enough, so we + // still have to find the null terminator. All cpus can share the same + // model name. + const model_name = JSC.ZigString.init(std.mem.sliceTo(&model_name_buf, 0)).withEncoding().toJS(globalThis); + + // Get CPU speed + var speed: u64 = 0; + len = @sizeOf(@TypeOf(speed)); + _ = std.c.sysctlbyname("hw.cpufrequency", &speed, &len, null, 0); + if (speed == 0) { + // Suggested by Node implementation: + // If sysctl hw.cputype == CPU_TYPE_ARM64, the correct value is unavailable + // from Apple, but we can hard-code it here to a plausible value. + speed = 2_400_000_000; + } + + // Get the multiplier; this is the number of ms/tick + const ticks: i64 = bun_sysconf__SC_CLK_TCK(); + const multiplier = 1000 / @as(u64, @intCast(ticks)); + + // Set up each CPU value in the return + const values = JSC.JSValue.createEmptyArray(globalThis, @as(u32, @intCast(num_cpus))); + var cpu_index: u32 = 0; + while (cpu_index < num_cpus) : (cpu_index += 1) { + const times = CPUTimes{ + .user = info[cpu_index].cpu_ticks[0] * multiplier, + .nice = info[cpu_index].cpu_ticks[3] * multiplier, + .sys = info[cpu_index].cpu_ticks[1] * multiplier, + .idle = info[cpu_index].cpu_ticks[2] * multiplier, + .irq = 0, // not available + }; + + const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); + cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed / 1_000_000)); + cpu.put(globalThis, JSC.ZigString.static("model"), model_name); + cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); + + values.putIndex(globalThis, cpu_index, cpu); + } + return values; +} + +pub fn cpusImplWindows(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { + var cpu_infos: [*]libuv.uv_cpu_info_t = undefined; + var count: c_int = undefined; + const err = libuv.uv_cpu_info(&cpu_infos, &count); + if (err != 0) { + return error.NoProcessorInfo; + } + defer libuv.uv_free_cpu_info(cpu_infos, count); + + const values = JSC.JSValue.createEmptyArray(globalThis, @intCast(count)); + + for (cpu_infos[0..@intCast(count)], 0..@intCast(count)) |cpu_info, i| { + const times = CPUTimes{ + .user = cpu_info.cpu_times.user, + .nice = cpu_info.cpu_times.nice, + .sys = cpu_info.cpu_times.sys, + .idle = cpu_info.cpu_times.idle, + .irq = cpu_info.cpu_times.irq, + }; + + const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(bun.span(cpu_info.model)).withEncoding().toJS(globalThis)); + cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(cpu_info.speed)); + cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); + + values.putIndex(globalThis, @intCast(i), cpu); + } + + return values; +} + +pub fn freemem() u64 { + return C.getFreeMemory(); +} + +pub fn getPriority(global: *JSC.JSGlobalObject, pid: i32) bun.JSError!i32 { + return C.getProcessPriority(pid) orelse { + const err = JSC.SystemError{ + .message = bun.String.static("no such process"), + .code = bun.String.static("ESRCH"), + .errno = comptime switch (bun.Environment.os) { + else => -@as(c_int, @intFromEnum(std.posix.E.SRCH)), + .windows => libuv.UV_ESRCH, + }, + .syscall = bun.String.static("uv_os_getpriority"), + }; + return global.throwValue(err.toErrorInstanceWithInfoObject(global)); + }; +} + +pub fn homedir(global: *JSC.JSGlobalObject) !bun.String { + // In Node.js, this is a wrapper around uv_os_homedir. + if (Environment.isWindows) { + var out: bun.PathBuffer = undefined; + var size: usize = out.len; + if (libuv.uv_os_homedir(&out, &size).toError(.uv_os_homedir)) |err| { + return global.throwValue(err.toJSC(global)); + } + return bun.String.createUTF8(out[0..size]); + } else { + + // The posix implementation of uv_os_homedir first checks the HOME + // environment variable, then falls back to reading the passwd entry. + if (bun.getenvZ("HOME")) |home| { + if (home.len > 0) + return bun.String.init(home); + } + + // From libuv: + // > Calling sysconf(_SC_GETPW_R_SIZE_MAX) would get the suggested size, but it + // > is frequently 1024 or 4096, so we can just use that directly. The pwent + // > will not usually be large. + // Instead of always using an allocation, first try a stack allocation + // of 4096, then fallback to heap. + var stack_string_bytes: [4096]u8 = undefined; + var string_bytes: []u8 = &stack_string_bytes; + defer if (string_bytes.ptr != &stack_string_bytes) + bun.default_allocator.free(string_bytes); + + var pw: bun.C.passwd = undefined; + var result: ?*bun.C.passwd = null; + + const ret = while (true) { + const ret = bun.C.getpwuid_r( + bun.C.geteuid(), + &pw, + string_bytes.ptr, + string_bytes.len, + &result, + ); + + if (ret == @intFromEnum(bun.C.E.INTR)) + continue; + + // If the system call wants more memory, double it. + if (ret == @intFromEnum(bun.C.E.RANGE)) { + const len = string_bytes.len; + bun.default_allocator.free(string_bytes); + string_bytes = ""; + string_bytes = try bun.default_allocator.alloc(u8, len * 2); + continue; + } + + break ret; + }; + + if (ret != 0) { + return global.throwValue(bun.sys.Error.fromCode( + @enumFromInt(ret), + .uv_os_homedir, + ).toJSC(global)); + } + + if (result == null) { + // in uv__getpwuid_r, null result throws UV_ENOENT. + return global.throwValue(bun.sys.Error.fromCode( + .NOENT, + .uv_os_homedir, + ).toJSC(global)); + } + + return if (pw.pw_dir) |dir| + bun.String.createUTF8(bun.span(dir)) + else + bun.String.empty; + } +} + +pub fn hostname(global: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + if (Environment.isWindows) { + var name_buffer: [129:0]u16 = undefined; + if (bun.windows.GetHostNameW(&name_buffer, name_buffer.len) == 0) { + const str = bun.String.createUTF16(bun.sliceTo(&name_buffer, 0)); + defer str.deref(); + return str.toJS(global); + } + + var result: std.os.windows.ws2_32.WSADATA = undefined; + if (std.os.windows.ws2_32.WSAStartup(0x202, &result) == 0) { + if (bun.windows.GetHostNameW(&name_buffer, name_buffer.len) == 0) { + var y = bun.String.createUTF16(bun.sliceTo(&name_buffer, 0)); + defer y.deref(); + return y.toJS(global); + } + } + + return JSC.ZigString.init("unknown").withEncoding().toJS(global); + } else { + var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; + return JSC.ZigString.init(std.posix.gethostname(&name_buffer) catch "unknown").withEncoding().toJS(global); + } +} + +pub fn loadavg(global: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + const result = C.getSystemLoadavg(); + return JSC.JSArray.create(global, &.{ + JSC.JSValue.jsNumber(result[0]), + JSC.JSValue.jsNumber(result[1]), + JSC.JSValue.jsNumber(result[2]), + }); +} + +pub const networkInterfaces = switch (Environment.os) { + .linux, .mac => networkInterfacesPosix, + .windows => networkInterfacesWindows, + else => @compileError("Unsupported OS"), +}; + +fn networkInterfacesPosix(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + // getifaddrs sets a pointer to a linked list + var interface_start: ?*C.ifaddrs = null; + const rc = C.getifaddrs(&interface_start); + if (rc != 0) { + const err = JSC.SystemError{ + .message = bun.String.static("A system error occurred: getifaddrs returned an error"), + .code = bun.String.static("ERR_SYSTEM_ERROR"), + .errno = @intFromEnum(std.posix.errno(rc)), + .syscall = bun.String.static("getifaddrs"), + }; + + return globalThis.throwValue(err.toErrorInstance(globalThis)); + } + defer C.freeifaddrs(interface_start); + + const helpers = struct { + // We'll skip interfaces that aren't actually available + pub fn skip(iface: *C.ifaddrs) bool { + // Skip interfaces that aren't actually available + if (iface.ifa_flags & C.IFF_RUNNING == 0) return true; + if (iface.ifa_flags & C.IFF_UP == 0) return true; + if (iface.ifa_addr == null) return true; + + return false; + } + + // We won't actually return link-layer interfaces but we need them for + // extracting the MAC address + pub fn isLinkLayer(iface: *C.ifaddrs) bool { + if (iface.ifa_addr == null) return false; + return if (comptime Environment.isLinux) + return iface.ifa_addr.*.sa_family == std.posix.AF.PACKET + else if (comptime Environment.isMac) + return iface.ifa_addr.?.*.family == std.posix.AF.LINK + else + unreachable; + } + + pub fn isLoopback(iface: *C.ifaddrs) bool { + return iface.ifa_flags & C.IFF_LOOPBACK == C.IFF_LOOPBACK; } }; - pub fn cpus(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return switch (Environment.os) { - .linux => cpusImplLinux(globalThis), - .mac => cpusImplDarwin(globalThis), - .windows => cpusImplWindows(globalThis), - else => @compileError("unsupported OS"), - } catch { - const err = JSC.SystemError{ - .message = bun.String.static("Failed to get cpu information"), - .code = bun.String.static(@tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR)), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - }; + // The list currently contains entries for link-layer interfaces + // and the IPv4, IPv6 interfaces. We only want to return the latter two + // but need the link-layer entries to determine MAC address. + // So, on our first pass through the linked list we'll count the number of + // INET interfaces only. + var num_inet_interfaces: usize = 0; + var it = interface_start; + while (it) |iface| : (it = iface.ifa_next) { + if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; + num_inet_interfaces += 1; } - fn cpusImplLinux(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { - // Create the return array - const values = JSC.JSValue.createEmptyArray(globalThis, 0); - var num_cpus: u32 = 0; + var ret = JSC.JSValue.createEmptyObject(globalThis, 8); - var stack_fallback = std.heap.stackFallback(1024 * 8, bun.default_allocator); - var file_buf = std.ArrayList(u8).init(stack_fallback.get()); - defer file_buf.deinit(); + // Second pass through, populate each interface object + it = interface_start; + while (it) |iface| : (it = iface.ifa_next) { + if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; - // Read /proc/stat to get number of CPUs and times - if (std.fs.openFileAbsolute("/proc/stat", .{})) |file| { - defer file.close(); + const interface_name = std.mem.sliceTo(iface.ifa_name, 0); + const addr = std.net.Address.initPosix(@alignCast(@as(*std.posix.sockaddr, @ptrCast(iface.ifa_addr)))); + const netmask = std.net.Address.initPosix(@alignCast(@as(*std.posix.sockaddr, @ptrCast(iface.ifa_netmask)))); - const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); - defer file_buf.clearRetainingCapacity(); - const contents = file_buf.items[0..read]; + var interface = JSC.JSValue.createEmptyObject(globalThis, 7); - var line_iter = std.mem.tokenizeScalar(u8, contents, '\n'); - - // Skip the first line (aggregate of all CPUs) - _ = line_iter.next(); - - // Read each CPU line - while (line_iter.next()) |line| { - // CPU lines are formatted as `cpu0 user nice sys idle iowait irq softirq` - var toks = std.mem.tokenize(u8, line, " \t"); - const cpu_name = toks.next(); - if (cpu_name == null or !std.mem.startsWith(u8, cpu_name.?, "cpu")) break; // done with CPUs - - //NOTE: libuv assumes this is fixed on Linux, not sure that's actually the case - const scale = 10; - - var times = CPUTimes{}; - times.user = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - times.nice = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - times.sys = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - times.idle = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - _ = try (toks.next() orelse error.eol); // skip iowait - times.irq = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - - // Actually create the JS object representing the CPU - const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); - cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); - values.putIndex(globalThis, num_cpus, cpu); - - num_cpus += 1; - } - } else |_| { - return error.no_proc_stat; - } - - // Read /proc/cpuinfo to get model information (optional) - if (std.fs.openFileAbsolute("/proc/cpuinfo", .{})) |file| { - defer file.close(); - - const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); - defer file_buf.clearRetainingCapacity(); - const contents = file_buf.items[0..read]; - - var line_iter = std.mem.tokenizeScalar(u8, contents, '\n'); - - const key_processor = "processor\t: "; - const key_model_name = "model name\t: "; - - var cpu_index: u32 = 0; - var has_model_name = true; - while (line_iter.next()) |line| { - if (strings.hasPrefixComptime(line, key_processor)) { - if (!has_model_name) { - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); - } - // If this line starts a new processor, parse the index from the line - const digits = std.mem.trim(u8, line[key_processor.len..], " \t\n"); - cpu_index = try std.fmt.parseInt(u32, digits, 10); - if (cpu_index >= num_cpus) return error.too_may_cpus; - has_model_name = false; - } else if (strings.hasPrefixComptime(line, key_model_name)) { - // If this is the model name, extract it and store on the current cpu - const model_name = line[key_model_name.len..]; - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(model_name).withEncoding().toJS(globalThis)); - has_model_name = true; - } - } - if (!has_model_name) { - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); - } - } else |_| { - // Initialize model name to "unknown" - var it = values.arrayIterator(globalThis); - while (it.next()) |cpu| { - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); - } - } - - // Read /sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq to get current frequency (optional) - for (0..num_cpus) |cpu_index| { - const cpu = JSC.JSObject.getIndex(values, globalThis, @truncate(cpu_index)); - - var path_buf: [128]u8 = undefined; - const path = try std.fmt.bufPrint(&path_buf, "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq", .{cpu_index}); - if (std.fs.openFileAbsolute(path, .{})) |file| { - defer file.close(); - - const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); - defer file_buf.clearRetainingCapacity(); - const contents = file_buf.items[0..read]; - - const digits = std.mem.trim(u8, contents, " \n"); - const speed = (std.fmt.parseInt(u64, digits, 10) catch 0) / 1000; - - cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed)); - } else |_| { - // Initialize CPU speed to 0 - cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(0)); - } - } - - return values; - } - - extern fn bun_sysconf__SC_CLK_TCK() isize; - fn cpusImplDarwin(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { - const local_bindings = @import("../../darwin_c.zig"); - const c = std.c; - - // Fetch the CPU info structure - var num_cpus: c.natural_t = 0; - var info: [*]local_bindings.processor_cpu_load_info = undefined; - var info_size: std.c.mach_msg_type_number_t = 0; - if (local_bindings.host_processor_info(std.c.mach_host_self(), local_bindings.PROCESSOR_CPU_LOAD_INFO, &num_cpus, @as(*local_bindings.processor_info_array_t, @ptrCast(&info)), &info_size) != .SUCCESS) { - return error.no_processor_info; - } - defer _ = std.c.vm_deallocate(std.c.mach_task_self(), @intFromPtr(info), info_size); - - // Ensure we got the amount of data we expected to guard against buffer overruns - if (info_size != C.PROCESSOR_CPU_LOAD_INFO_COUNT * num_cpus) { - return error.broken_process_info; - } - - // Get CPU model name - var model_name_buf: [512]u8 = undefined; - var len: usize = model_name_buf.len; - // Try brand_string first and if it fails try hw.model - if (!(std.c.sysctlbyname("machdep.cpu.brand_string", &model_name_buf, &len, null, 0) == 0 or - std.c.sysctlbyname("hw.model", &model_name_buf, &len, null, 0) == 0)) + // address The assigned IPv4 or IPv6 address + // cidr The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. { - return error.no_processor_info; - } - //NOTE: sysctlbyname doesn't update len if it was large enough, so we - // still have to find the null terminator. All cpus can share the same - // model name. - const model_name = JSC.ZigString.init(std.mem.sliceTo(&model_name_buf, 0)).withEncoding().toJS(globalThis); - - // Get CPU speed - var speed: u64 = 0; - len = @sizeOf(@TypeOf(speed)); - _ = std.c.sysctlbyname("hw.cpufrequency", &speed, &len, null, 0); - if (speed == 0) { - // Suggested by Node implementation: - // If sysctl hw.cputype == CPU_TYPE_ARM64, the correct value is unavailable - // from Apple, but we can hard-code it here to a plausible value. - speed = 2_400_000_000; - } - - // Get the multiplier; this is the number of ms/tick - const ticks: i64 = bun_sysconf__SC_CLK_TCK(); - const multiplier = 1000 / @as(u64, @intCast(ticks)); - - // Set up each CPU value in the return - const values = JSC.JSValue.createEmptyArray(globalThis, @as(u32, @intCast(num_cpus))); - var cpu_index: u32 = 0; - while (cpu_index < num_cpus) : (cpu_index += 1) { - const times = CPUTimes{ - .user = info[cpu_index].cpu_ticks[0] * multiplier, - .nice = info[cpu_index].cpu_ticks[3] * multiplier, - .sys = info[cpu_index].cpu_ticks[1] * multiplier, - .idle = info[cpu_index].cpu_ticks[2] * multiplier, - .irq = 0, // not available + // Compute the CIDR suffix; returns null if the netmask cannot + // be converted to a CIDR suffix + const maybe_suffix: ?u8 = switch (addr.any.family) { + std.posix.AF.INET => netmaskToCIDRSuffix(netmask.in.sa.addr), + std.posix.AF.INET6 => netmaskToCIDRSuffix(@as(u128, @bitCast(netmask.in6.sa.addr))), + else => null, }; - const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); - cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed / 1_000_000)); - cpu.put(globalThis, JSC.ZigString.static("model"), model_name); - cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); - - values.putIndex(globalThis, cpu_index, cpu); - } - return values; - } - - pub fn cpusImplWindows(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { - var cpu_infos: [*]libuv.uv_cpu_info_t = undefined; - var count: c_int = undefined; - const err = libuv.uv_cpu_info(&cpu_infos, &count); - if (err != 0) { - return error.no_processor_info; - } - defer libuv.uv_free_cpu_info(cpu_infos, count); - - const values = JSC.JSValue.createEmptyArray(globalThis, 0); - - for (cpu_infos[0..@intCast(count)], 0..@intCast(count)) |cpu_info, i| { - const times = CPUTimes{ - .user = cpu_info.cpu_times.user, - .nice = cpu_info.cpu_times.nice, - .sys = cpu_info.cpu_times.sys, - .idle = cpu_info.cpu_times.idle, - .irq = cpu_info.cpu_times.irq, - }; - - const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(bun.span(cpu_info.model)).withEncoding().toJS(globalThis)); - cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(cpu_info.speed)); - cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); - - values.putIndex(globalThis, @intCast(i), cpu); - } - - return values; - } - - pub fn endianness(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return JSC.ZigString.init("LE").withEncoding().toJS(globalThis); - } - - pub fn freemem(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return JSC.JSValue.jsNumberFromUint64(C.getFreeMemory()); - } - - pub fn getPriority(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - var args_ = callframe.arguments_old(1); - const arguments: []const JSC.JSValue = args_.ptr[0..args_.len]; - - if (arguments.len > 0 and !arguments[0].isNumber()) { - return globalThis.ERR_INVALID_ARG_TYPE("getPriority() expects a number", .{}).throw(); - } - - const pid = if (arguments.len > 0) arguments[0].asInt32() else 0; - - const priority = C.getProcessPriority(pid); - if (priority == -1) { - //const info = JSC.JSValue.createEmptyObject(globalThis, 4); - //info.put(globalThis, JSC.ZigString.static("errno"), JSC.JSValue.jsNumberFromInt32(-3)); - //info.put(globalThis, JSC.ZigString.static("code"), JSC.ZigString.init("ESRCH").withEncoding().toValueGC(globalThis)); - //info.put(globalThis, JSC.ZigString.static("message"), JSC.ZigString.init("no such process").withEncoding().toValueGC(globalThis)); - //info.put(globalThis, JSC.ZigString.static("syscall"), JSC.ZigString.init("uv_os_getpriority").withEncoding().toValueGC(globalThis)); - - const err = JSC.SystemError{ - .message = bun.String.static("A system error occurred: uv_os_getpriority returned ESRCH (no such process)"), - .code = bun.String.static("ERR_SYSTEM_ERROR"), - //.info = info, - .errno = -3, - .syscall = bun.String.static("uv_os_getpriority"), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - } - - return JSC.JSValue.jsNumberFromInt32(priority); - } - - pub fn homedir(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - const dir: []const u8 = brk: { - if (comptime Environment.isWindows) { - if (bun.getenvZ("USERPROFILE")) |env| - break :brk bun.asByteSlice(env); - } else { - if (bun.getenvZ("HOME")) |env| - break :brk bun.asByteSlice(env); + // Format the address and then, if valid, the CIDR suffix; both + // the address and cidr values can be slices into this same buffer + // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" + var buf: [64]u8 = undefined; + const addr_str = bun.fmt.formatIp(addr, &buf) catch unreachable; + var cidr = JSC.JSValue.null; + if (maybe_suffix) |suffix| { + //NOTE addr_str might not start at buf[0] due to slicing in formatIp + const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&buf[0]); + // Start writing the suffix immediately after the address + const suffix_str = std.fmt.bufPrint(buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; + // The full cidr value is the address + the suffix + const cidr_str = buf[start .. start + addr_str.len + suffix_str.len]; + cidr = JSC.ZigString.init(cidr_str).withEncoding().toJS(globalThis); } - break :brk "unknown"; - }; - - return JSC.ZigString.init(dir).withEncoding().toJS(globalThis); - } - - pub fn hostname(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - if (comptime Environment.isWindows) { - var name_buffer: [129:0]u16 = undefined; - if (bun.windows.GetHostNameW(&name_buffer, name_buffer.len) == 0) { - const str = bun.String.createUTF16(bun.sliceTo(&name_buffer, 0)); - defer str.deref(); - return str.toJS(globalThis); - } - - var result: std.os.windows.ws2_32.WSADATA = undefined; - if (std.os.windows.ws2_32.WSAStartup(0x202, &result) == 0) { - if (bun.windows.GetHostNameW(&name_buffer, name_buffer.len) == 0) { - const str = bun.String.createUTF16(bun.sliceTo(&name_buffer, 0)); - defer str.deref(); - return str.toJS(globalThis); - } - } - - return JSC.ZigString.init("unknown").withEncoding().toJS(globalThis); + interface.put(globalThis, JSC.ZigString.static("address"), JSC.ZigString.init(addr_str).withEncoding().toJS(globalThis)); + interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); } - var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; - - return JSC.ZigString.init(std.posix.gethostname(&name_buffer) catch "unknown").withEncoding().toJS(globalThis); - } - - pub fn loadavg(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - const result = C.getSystemLoadavg(); - return JSC.JSArray.create(globalThis, &.{ - JSC.JSValue.jsNumber(result[0]), - JSC.JSValue.jsNumber(result[1]), - JSC.JSValue.jsNumber(result[2]), - }); - } - - pub fn networkInterfaces(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - return switch (Environment.os) { - .windows => networkInterfacesWindows(globalThis), - else => networkInterfacesPosix(globalThis), - }; - } - - fn networkInterfacesPosix(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { - // getifaddrs sets a pointer to a linked list - var interface_start: ?*C.ifaddrs = null; - const rc = C.getifaddrs(&interface_start); - if (rc != 0) { - const err = JSC.SystemError{ - .message = bun.String.static("A system error occurred: getifaddrs returned an error"), - .code = bun.String.static("ERR_SYSTEM_ERROR"), - .errno = @intFromEnum(std.posix.errno(rc)), - .syscall = bun.String.static("getifaddrs"), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - } - defer C.freeifaddrs(interface_start); - - const helpers = struct { - // We'll skip interfaces that aren't actually available - pub fn skip(iface: *C.ifaddrs) bool { - // Skip interfaces that aren't actually available - if (iface.ifa_flags & C.IFF_RUNNING == 0) return true; - if (iface.ifa_flags & C.IFF_UP == 0) return true; - if (iface.ifa_addr == null) return true; - - return false; - } - - // We won't actually return link-layer interfaces but we need them for - // extracting the MAC address - pub fn isLinkLayer(iface: *C.ifaddrs) bool { - if (iface.ifa_addr == null) return false; - return if (comptime Environment.isLinux) - return iface.ifa_addr.*.sa_family == std.posix.AF.PACKET - else if (comptime Environment.isMac) - return iface.ifa_addr.?.*.family == std.posix.AF.LINK - else - unreachable; - } - - pub fn isLoopback(iface: *C.ifaddrs) bool { - return iface.ifa_flags & C.IFF_LOOPBACK == C.IFF_LOOPBACK; - } - }; - - // The list currently contains entries for link-layer interfaces - // and the IPv4, IPv6 interfaces. We only want to return the latter two - // but need the link-layer entries to determine MAC address. - // So, on our first pass through the linked list we'll count the number of - // INET interfaces only. - var num_inet_interfaces: usize = 0; - var it = interface_start; - while (it) |iface| : (it = iface.ifa_next) { - if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; - num_inet_interfaces += 1; + // netmask The IPv4 or IPv6 network mask + { + var buf: [64]u8 = undefined; + const str = bun.fmt.formatIp(netmask, &buf) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("netmask"), JSC.ZigString.init(str).withEncoding().toJS(globalThis)); } - var ret = JSC.JSValue.createEmptyObject(globalThis, 8); + // family Either IPv4 or IPv6 + interface.put(globalThis, JSC.ZigString.static("family"), (switch (addr.any.family) { + std.posix.AF.INET => JSC.ZigString.static("IPv4"), + std.posix.AF.INET6 => JSC.ZigString.static("IPv6"), + else => JSC.ZigString.static("unknown"), + }).toJS(globalThis)); - // Second pass through, populate each interface object - it = interface_start; - while (it) |iface| : (it = iface.ifa_next) { - if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; + // mac The MAC address of the network interface + { + // We need to search for the link-layer interface whose name matches this one + var ll_it = interface_start; + const maybe_ll_addr = while (ll_it) |ll_iface| : (ll_it = ll_iface.ifa_next) { + if (helpers.skip(ll_iface) or !helpers.isLinkLayer(ll_iface)) continue; - const interface_name = std.mem.sliceTo(iface.ifa_name, 0); - const addr = std.net.Address.initPosix(@alignCast(@as(*std.posix.sockaddr, @ptrCast(iface.ifa_addr)))); - const netmask = std.net.Address.initPosix(@alignCast(@as(*std.posix.sockaddr, @ptrCast(iface.ifa_netmask)))); + const ll_name = bun.sliceTo(ll_iface.ifa_name, 0); + if (!strings.hasPrefix(ll_name, interface_name)) continue; + if (ll_name.len > interface_name.len and ll_name[interface_name.len] != ':') continue; - var interface = JSC.JSValue.createEmptyObject(globalThis, 7); - - // address The assigned IPv4 or IPv6 address - // cidr The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. - { - // Compute the CIDR suffix; returns null if the netmask cannot - // be converted to a CIDR suffix - const maybe_suffix: ?u8 = switch (addr.any.family) { - std.posix.AF.INET => netmaskToCIDRSuffix(netmask.in.sa.addr), - std.posix.AF.INET6 => netmaskToCIDRSuffix(@as(u128, @bitCast(netmask.in6.sa.addr))), - else => null, - }; - - // Format the address and then, if valid, the CIDR suffix; both - // the address and cidr values can be slices into this same buffer - // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" - var buf: [64]u8 = undefined; - const addr_str = bun.fmt.formatIp(addr, &buf) catch unreachable; - var cidr = JSC.JSValue.null; - if (maybe_suffix) |suffix| { - //NOTE addr_str might not start at buf[0] due to slicing in formatIp - const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&buf[0]); - // Start writing the suffix immediately after the address - const suffix_str = std.fmt.bufPrint(buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; - // The full cidr value is the address + the suffix - const cidr_str = buf[start .. start + addr_str.len + suffix_str.len]; - cidr = JSC.ZigString.init(cidr_str).withEncoding().toJS(globalThis); - } - - interface.put(globalThis, JSC.ZigString.static("address"), JSC.ZigString.init(addr_str).withEncoding().toJS(globalThis)); - interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); - } - - // netmask The IPv4 or IPv6 network mask - { - var buf: [64]u8 = undefined; - const str = bun.fmt.formatIp(netmask, &buf) catch unreachable; - interface.put(globalThis, JSC.ZigString.static("netmask"), JSC.ZigString.init(str).withEncoding().toJS(globalThis)); - } - - // family Either IPv4 or IPv6 - interface.put(globalThis, JSC.ZigString.static("family"), (switch (addr.any.family) { - std.posix.AF.INET => JSC.ZigString.static("IPv4"), - std.posix.AF.INET6 => JSC.ZigString.static("IPv6"), - else => JSC.ZigString.static("unknown"), - }).toJS(globalThis)); - - // mac The MAC address of the network interface - { - // We need to search for the link-layer interface whose name matches this one - var ll_it = interface_start; - const maybe_ll_addr = while (ll_it) |ll_iface| : (ll_it = ll_iface.ifa_next) { - if (helpers.skip(ll_iface) or !helpers.isLinkLayer(ll_iface)) continue; - - const ll_name = bun.sliceTo(ll_iface.ifa_name, 0); - if (!strings.hasPrefix(ll_name, interface_name)) continue; - if (ll_name.len > interface_name.len and ll_name[interface_name.len] != ':') continue; - - // This is the correct link-layer interface entry for the current interface, - // cast to a link-layer socket address - if (comptime Environment.isLinux) { - break @as(?*std.posix.sockaddr.ll, @ptrCast(@alignCast(ll_iface.ifa_addr))); - } else if (comptime Environment.isMac) { - break @as(?*C.sockaddr_dl, @ptrCast(@alignCast(ll_iface.ifa_addr))); - } else { - @compileError("unreachable"); - } - } else null; - - if (maybe_ll_addr) |ll_addr| { - // Encode its link-layer address. We need 2*6 bytes for the - // hex characters and 5 for the colon separators - var mac_buf: [17]u8 = undefined; - const addr_data = if (comptime Environment.isLinux) ll_addr.addr else if (comptime Environment.isMac) ll_addr.sdl_data[ll_addr.sdl_nlen..] else @compileError("unreachable"); - if (addr_data.len < 6) { - const mac = "00:00:00:00:00:00"; - interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); - } else { - const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ - addr_data[0], addr_data[1], addr_data[2], - addr_data[3], addr_data[4], addr_data[5], - }) catch unreachable; - interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); - } + // This is the correct link-layer interface entry for the current interface, + // cast to a link-layer socket address + if (comptime Environment.isLinux) { + break @as(?*std.posix.sockaddr.ll, @ptrCast(@alignCast(ll_iface.ifa_addr))); + } else if (comptime Environment.isMac) { + break @as(?*C.sockaddr_dl, @ptrCast(@alignCast(ll_iface.ifa_addr))); } else { + @compileError("unreachable"); + } + } else null; + + if (maybe_ll_addr) |ll_addr| { + // Encode its link-layer address. We need 2*6 bytes for the + // hex characters and 5 for the colon separators + var mac_buf: [17]u8 = undefined; + const addr_data = if (comptime Environment.isLinux) ll_addr.addr else if (comptime Environment.isMac) ll_addr.sdl_data[ll_addr.sdl_nlen..] else @compileError("unreachable"); + if (addr_data.len < 6) { const mac = "00:00:00:00:00:00"; interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); + } else { + const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ + addr_data[0], addr_data[1], addr_data[2], + addr_data[3], addr_data[4], addr_data[5], + }) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); } - } - - // internal true if the network interface is a loopback or similar interface that is not remotely accessible; otherwise false - interface.put(globalThis, JSC.ZigString.static("internal"), JSC.JSValue.jsBoolean(helpers.isLoopback(iface))); - - // scopeid The numeric IPv6 scope ID (only specified when family is IPv6) - if (addr.any.family == std.posix.AF.INET6) { - interface.put(globalThis, JSC.ZigString.static("scope_id"), JSC.JSValue.jsNumber(addr.in6.sa.scope_id)); - } - - // Does this entry already exist? - if (ret.get_unsafe(globalThis, interface_name)) |array| { - // Add this interface entry to the existing array - const next_index = @as(u32, @intCast(array.getLength(globalThis))); - array.putIndex(globalThis, next_index, interface); } else { - // Add it as an array with this interface as an element - const member_name = JSC.ZigString.init(interface_name); - var array = JSC.JSValue.createEmptyArray(globalThis, 1); - array.putIndex(globalThis, 0, interface); - ret.put(globalThis, &member_name, array); - } - } - - return ret; - } - - fn networkInterfacesWindows(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { - var ifaces: [*]libuv.uv_interface_address_t = undefined; - var count: c_int = undefined; - const err = libuv.uv_interface_addresses(&ifaces, &count); - if (err != 0) { - const sys_err = JSC.SystemError{ - .message = bun.String.static("uv_interface_addresses failed"), - .code = bun.String.static("ERR_SYSTEM_ERROR"), - //.info = info, - .errno = err, - .syscall = bun.String.static("uv_interface_addresses"), - }; - return globalThis.throwValue(sys_err.toErrorInstance(globalThis)); - } - defer libuv.uv_free_interface_addresses(ifaces, count); - - var ret = JSC.JSValue.createEmptyObject(globalThis, 8); - - // 65 comes from: https://stackoverflow.com/questions/39443413/why-is-inet6-addrstrlen-defined-as-46-in-c - var ip_buf: [65]u8 = undefined; - var mac_buf: [17]u8 = undefined; - - for (ifaces[0..@intCast(count)]) |iface| { - var interface = JSC.JSValue.createEmptyObject(globalThis, 7); - - // address The assigned IPv4 or IPv6 address - // cidr The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. - var cidr = JSC.JSValue.null; - { - // Compute the CIDR suffix; returns null if the netmask cannot - // be converted to a CIDR suffix - const maybe_suffix: ?u8 = switch (iface.address.address4.family) { - std.posix.AF.INET => netmaskToCIDRSuffix(iface.netmask.netmask4.addr), - std.posix.AF.INET6 => netmaskToCIDRSuffix(@as(u128, @bitCast(iface.netmask.netmask6.addr))), - else => null, - }; - - // Format the address and then, if valid, the CIDR suffix; both - // the address and cidr values can be slices into this same buffer - // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" - const addr_str = bun.fmt.formatIp( - // std.net.Address will do ptrCast depending on the family so this is ok - std.net.Address.initPosix(@ptrCast(&iface.address.address4)), - &ip_buf, - ) catch unreachable; - if (maybe_suffix) |suffix| { - //NOTE addr_str might not start at buf[0] due to slicing in formatIp - const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&ip_buf[0]); - // Start writing the suffix immediately after the address - const suffix_str = std.fmt.bufPrint(ip_buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; - // The full cidr value is the address + the suffix - const cidr_str = ip_buf[start .. start + addr_str.len + suffix_str.len]; - cidr = JSC.ZigString.init(cidr_str).withEncoding().toJS(globalThis); - } - - interface.put(globalThis, JSC.ZigString.static("address"), JSC.ZigString.init(addr_str).withEncoding().toJS(globalThis)); - } - - // netmask - { - const str = bun.fmt.formatIp( - // std.net.Address will do ptrCast depending on the family so this is ok - std.net.Address.initPosix(@ptrCast(&iface.netmask.netmask4)), - &ip_buf, - ) catch unreachable; - interface.put(globalThis, JSC.ZigString.static("netmask"), JSC.ZigString.init(str).withEncoding().toJS(globalThis)); - } - // family - interface.put(globalThis, JSC.ZigString.static("family"), (switch (iface.address.address4.family) { - std.posix.AF.INET => JSC.ZigString.static("IPv4"), - std.posix.AF.INET6 => JSC.ZigString.static("IPv6"), - else => JSC.ZigString.static("unknown"), - }).toJS(globalThis)); - - // mac - { - const phys = iface.phys_addr; - const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ - phys[0], phys[1], phys[2], phys[3], phys[4], phys[5], - }) catch unreachable; + const mac = "00:00:00:00:00:00"; interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); } - - // internal - { - interface.put(globalThis, JSC.ZigString.static("internal"), JSC.JSValue.jsBoolean(iface.is_internal != 0)); - } - - // cidr. this is here to keep ordering consistent with the node implementation - interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); - - // scopeid - if (iface.address.address4.family == std.posix.AF.INET6) { - interface.put(globalThis, JSC.ZigString.static("scopeid"), JSC.JSValue.jsNumber(iface.address.address6.scope_id)); - } - - // Does this entry already exist? - const interface_name = bun.span(iface.name); - if (ret.get_unsafe(globalThis, interface_name)) |array| { - // Add this interface entry to the existing array - const next_index = @as(u32, @intCast(array.getLength(globalThis))); - array.putIndex(globalThis, next_index, interface); - } else { - // Add it as an array with this interface as an element - const member_name = JSC.ZigString.init(interface_name); - var array = JSC.JSValue.createEmptyArray(globalThis, 1); - array.putIndex(globalThis, 0, interface); - ret.put(globalThis, &member_name, array); - } } - return ret; - } + // internal true if the network interface is a loopback or similar interface that is not remotely accessible; otherwise false + interface.put(globalThis, JSC.ZigString.static("internal"), JSC.JSValue.jsBoolean(helpers.isLoopback(iface))); - pub fn platform(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return JSC.ZigString.init(Global.os_name).withEncoding().toJS(globalThis); - } - - pub fn release(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; - return JSC.ZigString.init(C.getRelease(&name_buffer)).withEncoding().toJS(globalThis); - } - - pub fn setPriority(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - var args_ = callframe.arguments_old(2); - var arguments: []const JSC.JSValue = args_.ptr[0..args_.len]; - - if (arguments.len == 0) { - const err = JSC.toTypeError( - .ERR_INVALID_ARG_TYPE, - "The \"priority\" argument must be of type number. Received undefined", - .{}, - globalThis, - ); - return globalThis.throwValue(err); + // scopeid The numeric IPv6 scope ID (only specified when family is IPv6) + if (addr.any.family == std.posix.AF.INET6) { + interface.put(globalThis, JSC.ZigString.static("scope_id"), JSC.JSValue.jsNumber(addr.in6.sa.scope_id)); } - const pid = if (arguments.len == 2) arguments[0].coerce(i32, globalThis) else 0; - const priority = if (arguments.len == 2) arguments[1].coerce(i32, globalThis) else arguments[0].coerce(i32, globalThis); - - if (priority < -20 or priority > 19) { - const err = JSC.toTypeError( - .ERR_OUT_OF_RANGE, - "The value of \"priority\" is out of range. It must be >= -20 && <= 19", - .{}, - globalThis, - ); - return globalThis.throwValue(err); - } - - const errcode = C.setProcessPriority(pid, priority); - switch (errcode) { - .SRCH => { - const err = JSC.SystemError{ - .message = bun.String.static("A system error occurred: uv_os_setpriority returned ESRCH (no such process)"), - .code = bun.String.static(@tagName(.ERR_SYSTEM_ERROR)), - //.info = info, - .errno = -3, - .syscall = bun.String.static("uv_os_setpriority"), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - }, - .ACCES => { - const err = JSC.SystemError{ - .message = bun.String.static("A system error occurred: uv_os_setpriority returned EACCESS (permission denied)"), - .code = bun.String.static(@tagName(.ERR_SYSTEM_ERROR)), - //.info = info, - .errno = -13, - .syscall = bun.String.static("uv_os_setpriority"), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - }, - else => {}, - } - - return .undefined; - } - - pub fn totalmem(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return JSC.JSValue.jsNumberFromUint64(C.getTotalMemory()); - } - - pub fn @"type"(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - if (comptime Environment.isWindows) - return bun.String.static("Windows_NT").toJS(globalThis) - else if (comptime Environment.isMac) - return bun.String.static("Darwin").toJS(globalThis) - else if (comptime Environment.isLinux) - return bun.String.static("Linux").toJS(globalThis); - - return JSC.ZigString.init(Global.os_name).withEncoding().toJS(globalThis); - } - - pub fn uptime(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - if (Environment.isWindows) { - var uptime_value: f64 = undefined; - const err = libuv.uv_uptime(&uptime_value); - if (err != 0) { - const sys_err = JSC.SystemError{ - .message = bun.String.static("failed to get system uptime"), - .code = bun.String.static("ERR_SYSTEM_ERROR"), - .errno = err, - .syscall = bun.String.static("uv_uptime"), - }; - return globalThis.throwValue(sys_err.toErrorInstance(globalThis)); - } - return JSC.JSValue.jsNumber(uptime_value); - } - - return JSC.JSValue.jsNumberFromUint64(C.getSystemUptime()); - } - - pub fn userInfo(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const result = JSC.JSValue.createEmptyObject(globalThis, 5); - - result.put(globalThis, JSC.ZigString.static("homedir"), try homedir(globalThis, callframe)); - - if (comptime Environment.isWindows) { - result.put(globalThis, JSC.ZigString.static("username"), JSC.ZigString.init(bun.getenvZ("USERNAME") orelse "unknown").withEncoding().toJS(globalThis)); - result.put(globalThis, JSC.ZigString.static("uid"), JSC.JSValue.jsNumber(-1)); - result.put(globalThis, JSC.ZigString.static("gid"), JSC.JSValue.jsNumber(-1)); - result.put(globalThis, JSC.ZigString.static("shell"), JSC.JSValue.jsNull()); + // Does this entry already exist? + if (ret.get_unsafe(globalThis, interface_name)) |array| { + // Add this interface entry to the existing array + const next_index = @as(u32, @intCast(array.getLength(globalThis))); + array.putIndex(globalThis, next_index, interface); } else { - const username = bun.getenvZ("USER") orelse "unknown"; + // Add it as an array with this interface as an element + const member_name = JSC.ZigString.init(interface_name); + var array = JSC.JSValue.createEmptyArray(globalThis, 1); + array.putIndex(globalThis, 0, interface); + ret.put(globalThis, &member_name, array); + } + } - result.put(globalThis, JSC.ZigString.static("username"), JSC.ZigString.init(username).withEncoding().toJS(globalThis)); - result.put(globalThis, JSC.ZigString.static("shell"), JSC.ZigString.init(bun.getenvZ("SHELL") orelse "unknown").withEncoding().toJS(globalThis)); + return ret; +} - result.put(globalThis, JSC.ZigString.static("uid"), JSC.JSValue.jsNumber(C.getuid())); - result.put(globalThis, JSC.ZigString.static("gid"), JSC.JSValue.jsNumber(C.getgid())); +fn networkInterfacesWindows(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + var ifaces: [*]libuv.uv_interface_address_t = undefined; + var count: c_int = undefined; + const err = libuv.uv_interface_addresses(&ifaces, &count); + if (err != 0) { + const sys_err = JSC.SystemError{ + .message = bun.String.static("uv_interface_addresses failed"), + .code = bun.String.static("ERR_SYSTEM_ERROR"), + //.info = info, + .errno = err, + .syscall = bun.String.static("uv_interface_addresses"), + }; + return globalThis.throwValue(sys_err.toErrorInstance(globalThis)); + } + defer libuv.uv_free_interface_addresses(ifaces, count); + + var ret = JSC.JSValue.createEmptyObject(globalThis, 8); + + // 65 comes from: https://stackoverflow.com/questions/39443413/why-is-inet6-addrstrlen-defined-as-46-in-c + var ip_buf: [65]u8 = undefined; + var mac_buf: [17]u8 = undefined; + + for (ifaces[0..@intCast(count)]) |iface| { + var interface = JSC.JSValue.createEmptyObject(globalThis, 7); + + // address The assigned IPv4 or IPv6 address + // cidr The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. + var cidr = JSC.JSValue.null; + { + // Compute the CIDR suffix; returns null if the netmask cannot + // be converted to a CIDR suffix + const maybe_suffix: ?u8 = switch (iface.address.address4.family) { + std.posix.AF.INET => netmaskToCIDRSuffix(iface.netmask.netmask4.addr), + std.posix.AF.INET6 => netmaskToCIDRSuffix(@as(u128, @bitCast(iface.netmask.netmask6.addr))), + else => null, + }; + + // Format the address and then, if valid, the CIDR suffix; both + // the address and cidr values can be slices into this same buffer + // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" + const addr_str = bun.fmt.formatIp( + // std.net.Address will do ptrCast depending on the family so this is ok + std.net.Address.initPosix(@ptrCast(&iface.address.address4)), + &ip_buf, + ) catch unreachable; + if (maybe_suffix) |suffix| { + //NOTE addr_str might not start at buf[0] due to slicing in formatIp + const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&ip_buf[0]); + // Start writing the suffix immediately after the address + const suffix_str = std.fmt.bufPrint(ip_buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; + // The full cidr value is the address + the suffix + const cidr_str = ip_buf[start .. start + addr_str.len + suffix_str.len]; + cidr = JSC.ZigString.init(cidr_str).withEncoding().toJS(globalThis); + } + + interface.put(globalThis, JSC.ZigString.static("address"), JSC.ZigString.init(addr_str).withEncoding().toJS(globalThis)); } - return result; + // netmask + { + const str = bun.fmt.formatIp( + // std.net.Address will do ptrCast depending on the family so this is ok + std.net.Address.initPosix(@ptrCast(&iface.netmask.netmask4)), + &ip_buf, + ) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("netmask"), JSC.ZigString.init(str).withEncoding().toJS(globalThis)); + } + // family + interface.put(globalThis, JSC.ZigString.static("family"), (switch (iface.address.address4.family) { + std.posix.AF.INET => JSC.ZigString.static("IPv4"), + std.posix.AF.INET6 => JSC.ZigString.static("IPv6"), + else => JSC.ZigString.static("unknown"), + }).toJS(globalThis)); + + // mac + { + const phys = iface.phys_addr; + const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ + phys[0], phys[1], phys[2], phys[3], phys[4], phys[5], + }) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); + } + + // internal + { + interface.put(globalThis, JSC.ZigString.static("internal"), JSC.JSValue.jsBoolean(iface.is_internal != 0)); + } + + // cidr. this is here to keep ordering consistent with the node implementation + interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); + + // scopeid + if (iface.address.address4.family == std.posix.AF.INET6) { + interface.put(globalThis, JSC.ZigString.static("scopeid"), JSC.JSValue.jsNumber(iface.address.address6.scope_id)); + } + + // Does this entry already exist? + const interface_name = bun.span(iface.name); + if (ret.get_unsafe(globalThis, interface_name)) |array| { + // Add this interface entry to the existing array + const next_index = @as(u32, @intCast(array.getLength(globalThis))); + array.putIndex(globalThis, next_index, interface); + } else { + // Add it as an array with this interface as an element + const member_name = JSC.ZigString.init(interface_name); + var array = JSC.JSValue.createEmptyArray(globalThis, 1); + array.putIndex(globalThis, 0, interface); + ret.put(globalThis, &member_name, array); + } } - pub fn version(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; - return JSC.ZigString.init(C.getVersion(&name_buffer)).withEncoding().toJS(globalThis); + return ret; +} + +pub fn release() bun.String { + var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; + return bun.String.createUTF8(C.getRelease(&name_buffer)); +} + +pub fn setPriority1(global: *JSC.JSGlobalObject, pid: i32, priority: i32) !void { + const errcode = C.setProcessPriority(pid, priority); + switch (errcode) { + .SRCH => { + const err = JSC.SystemError{ + .message = bun.String.static("no such process"), + .code = bun.String.static("ESRCH"), + .errno = comptime switch (bun.Environment.os) { + else => -@as(c_int, @intFromEnum(std.posix.E.SRCH)), + .windows => libuv.UV_ESRCH, + }, + .syscall = bun.String.static("uv_os_getpriority"), + }; + return global.throwValue(err.toErrorInstanceWithInfoObject(global)); + }, + .ACCES => { + const err = JSC.SystemError{ + .message = bun.String.static("permission denied"), + .code = bun.String.static("EACCES"), + .errno = comptime switch (bun.Environment.os) { + else => -@as(c_int, @intFromEnum(std.posix.E.ACCES)), + .windows => libuv.UV_EACCES, + }, + .syscall = bun.String.static("uv_os_getpriority"), + }; + return global.throwValue(err.toErrorInstanceWithInfoObject(global)); + }, + .PERM => { + const err = JSC.SystemError{ + .message = bun.String.static("operation not permitted"), + .code = bun.String.static("EPERM"), + .errno = comptime switch (bun.Environment.os) { + else => -@as(c_int, @intFromEnum(std.posix.E.SRCH)), + .windows => libuv.UV_ESRCH, + }, + .syscall = bun.String.static("uv_os_getpriority"), + }; + return global.throwValue(err.toErrorInstanceWithInfoObject(global)); + }, + else => { + // no other error codes can be emitted + }, + } +} + +pub fn setPriority2(global: *JSC.JSGlobalObject, priority: i32) !void { + return setPriority1(global, 0, priority); +} + +pub fn totalmem() u64 { + return C.getTotalMemory(); +} + +pub fn uptime(global: *JSC.JSGlobalObject) bun.JSError!f64 { + if (Environment.isWindows) { + var uptime_value: f64 = undefined; + const err = libuv.uv_uptime(&uptime_value); + if (err != 0) { + const sys_err = JSC.SystemError{ + .message = bun.String.static("failed to get system uptime"), + .code = bun.String.static("ERR_SYSTEM_ERROR"), + .errno = err, + .syscall = bun.String.static("uv_uptime"), + }; + return global.throwValue(sys_err.toErrorInstance(global)); + } + return uptime_value; } - inline fn getMachineName() [:0]const u8 { - return switch (@import("builtin").target.cpu.arch) { - .arm => "arm", - .aarch64 => "arm64", - .mips => "mips", - .mips64 => "mips64", - .powerpc64 => "ppc64", - .powerpc64le => "ppc64le", - .s390x => "s390x", - .x86 => "i386", - .x86_64 => "x86_64", - else => "unknown", - }; + return @floatFromInt(C.getSystemUptime()); +} + +pub fn userInfo(globalThis: *JSC.JSGlobalObject, options: gen.UserInfoOptions) bun.JSError!JSC.JSValue { + _ = options; // TODO: + + const result = JSC.JSValue.createEmptyObject(globalThis, 5); + + const home = try homedir(globalThis); + defer home.deref(); + + result.put(globalThis, JSC.ZigString.static("homedir"), home.toJS(globalThis)); + + if (comptime Environment.isWindows) { + result.put(globalThis, JSC.ZigString.static("username"), JSC.ZigString.init(bun.getenvZ("USERNAME") orelse "unknown").withEncoding().toJS(globalThis)); + result.put(globalThis, JSC.ZigString.static("uid"), JSC.JSValue.jsNumber(-1)); + result.put(globalThis, JSC.ZigString.static("gid"), JSC.JSValue.jsNumber(-1)); + result.put(globalThis, JSC.ZigString.static("shell"), JSC.JSValue.jsNull()); + } else { + const username = bun.getenvZ("USER") orelse "unknown"; + + result.put(globalThis, JSC.ZigString.static("username"), JSC.ZigString.init(username).withEncoding().toJS(globalThis)); + result.put(globalThis, JSC.ZigString.static("shell"), JSC.ZigString.init(bun.getenvZ("SHELL") orelse "unknown").withEncoding().toJS(globalThis)); + result.put(globalThis, JSC.ZigString.static("uid"), JSC.JSValue.jsNumber(C.getuid())); + result.put(globalThis, JSC.ZigString.static("gid"), JSC.JSValue.jsNumber(C.getgid())); } - pub fn machine(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - return JSC.ZigString.static(comptime getMachineName()).toJS(globalThis); - } -}; + return result; +} + +pub fn version() bun.JSError!bun.String { + var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; + return bun.String.createUTF8(C.getVersion(&name_buffer)); +} /// Given a netmask returns a CIDR suffix. Returns null if the mask is not valid. /// `@TypeOf(mask)` must be one of u32 (IPv4) or u128 (IPv6) diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index c664c48954..5a2730770e 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -1726,7 +1726,7 @@ pub fn StatType(comptime Big: bool) type { } // dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeMs, mtimeMs, ctimeMs, birthtimeMs - var args = callFrame.argumentsPtr()[0..@min(callFrame.argumentsCount(), 14)]; + var args = callFrame.arguments(); const atime_ms: f64 = if (args.len > 10 and args[10].isNumber()) args[10].asNumber() else 0; const mtime_ms: f64 = if (args.len > 11 and args[11].isNumber()) args[11].asNumber() else 0; diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index e09000c7b0..d16b4e4be9 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -4161,7 +4161,7 @@ pub const Expect = struct { JSC.markBinding(@src()); const thisValue = callframe.this(); - const arguments = callframe.argumentsPtr()[0..callframe.argumentsCount()]; + const arguments = callframe.arguments(); defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledWith", "expected"); @@ -4220,7 +4220,7 @@ pub const Expect = struct { JSC.markBinding(@src()); const thisValue = callframe.this(); - const arguments = callframe.argumentsPtr()[0..callframe.argumentsCount()]; + const arguments = callframe.arguments(); defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenLastCalledWith", "expected"); @@ -4278,7 +4278,7 @@ pub const Expect = struct { JSC.markBinding(@src()); const thisValue = callframe.this(); - const arguments = callframe.argumentsPtr()[0..callframe.argumentsCount()]; + const arguments = callframe.arguments(); defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenNthCalledWith", "expected"); @@ -4721,12 +4721,11 @@ pub const Expect = struct { incrementExpectCallCounter(); // prepare the args array - const args_ptr = callFrame.argumentsPtr(); - const args_count = callFrame.argumentsCount(); + const args = callFrame.arguments(); var allocator = std.heap.stackFallback(8 * @sizeOf(JSValue), globalThis.allocator()); - var matcher_args = try std.ArrayList(JSValue).initCapacity(allocator.get(), args_count + 1); + var matcher_args = try std.ArrayList(JSValue).initCapacity(allocator.get(), args.len + 1); matcher_args.appendAssumeCapacity(value); - for (0..args_count) |i| matcher_args.appendAssumeCapacity(args_ptr[i]); + for (args) |arg| matcher_args.appendAssumeCapacity(arg); _ = try executeCustomMatcher(globalThis, matcher_name, matcher_fn, matcher_args.items, expect.flags, false); @@ -5202,14 +5201,13 @@ pub const ExpectCustomAsymmetricMatcher = struct { ExpectCustomAsymmetricMatcher.matcherFnSetCached(instance_jsvalue, globalThis, matcher_fn); // capture the args as a JS array saved in the instance, so the matcher can be executed later on with them - const args_ptr = callFrame.argumentsPtr(); - const args_count: usize = callFrame.argumentsCount(); - var args = JSValue.createEmptyArray(globalThis, args_count); - for (0..args_count) |i| { - args.putIndex(globalThis, @truncate(i), args_ptr[i]); + const args = callFrame.arguments(); + const array = JSValue.createEmptyArray(globalThis, args.len); + for (args, 0..) |arg, i| { + array.putIndex(globalThis, @truncate(i), arg); } - args.ensureStillAlive(); - ExpectCustomAsymmetricMatcher.capturedArgsSetCached(instance_jsvalue, globalThis, args); + ExpectCustomAsymmetricMatcher.capturedArgsSetCached(instance_jsvalue, globalThis, array); + array.ensureStillAlive(); // return the same instance, now fully initialized including the captured args (previously it was incomplete) return instance_jsvalue; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index c03935eecb..b708cc6810 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -3909,7 +3909,7 @@ pub const ParseTask = struct { result: ?*OnBeforeParseResult = null, - const headers = @import("bun-native-bundler-plugin-api"); + const headers = bun.C.translated; comptime { bun.assert(@sizeOf(OnBeforeParseArguments) == @sizeOf(headers.OnBeforeParseArguments)); diff --git a/src/c-headers-for-zig.h b/src/c-headers-for-zig.h new file mode 100644 index 0000000000..42c03aca64 --- /dev/null +++ b/src/c-headers-for-zig.h @@ -0,0 +1,21 @@ +// This file is run through translate-c and exposed to Zig code +// under the namespace bun.C.translated. Prefer adding includes +// to this file instead of manually porting struct definitions +// into Zig code. By using automatic translation, differences +// in platforms can be avoided. +// +// When Zig is translating this file, it will define these macros: +// - WINDOWS +// - DARWIN +// - LINUX +// - POSIX + +// OnBeforeParseResult, etc... +#include "../packages/bun-native-bundler-plugin-api/bundler_plugin.h" + +#if POSIX +// passwd, getpwuid_r +#include "pwd.h" +// geteuid +#include +#endif diff --git a/src/c.zig b/src/c.zig index e0b8238fca..4b70e7c3fc 100644 --- a/src/c.zig +++ b/src/c.zig @@ -2,6 +2,8 @@ const std = @import("std"); const bun = @import("root").bun; const Environment = @import("./env.zig"); +pub const translated = @import("translated-c-headers"); + const PlatformSpecific = switch (Environment.os) { .mac => @import("./darwin_c.zig"), .linux => @import("./linux_c.zig"), @@ -44,7 +46,7 @@ pub extern "c" fn strchr(str: [*]const u8, char: u8) ?[*]const u8; pub fn lstat_absolute(path: [:0]const u8) !Stat { if (builtin.os.tag == .windows) { - @compileError("Not implemented yet, conside using bun.sys.lstat()"); + @compileError("Not implemented yet, consider using bun.sys.lstat()"); } var st = zeroes(libc_stat); @@ -341,8 +343,8 @@ pub fn getSelfExeSharedLibPaths(allocator: std.mem.Allocator) error{OutOfMemory} /// Same as MADV_DONTNEED but used with posix_madvise() sys-tem system /// tem call. /// -/// MADV_FREE Indicates that the application will not need the infor-mation information -/// mation contained in this address range, so the pages may +/// MADV_FREE Indicates that the application will not need the information +/// contained in this address range, so the pages may /// be reused right away. The address range will remain /// valid. This is used with madvise() system call. /// @@ -350,16 +352,8 @@ pub fn getSelfExeSharedLibPaths(allocator: std.mem.Allocator) error{OutOfMemory} /// with POSIX_ prefix for the advice system call argument. pub extern "c" fn posix_madvise(ptr: *anyopaque, len: usize, advice: i32) c_int; -pub fn getProcessPriority(pid_: i32) i32 { - const pid = @as(c_uint, @intCast(pid_)); - return get_process_priority(pid); -} - -pub fn setProcessPriority(pid_: i32, priority_: i32) std.c.E { - if (pid_ < 0) return .SRCH; - - const pid = @as(c_uint, @intCast(pid_)); - const priority = @as(c_int, @intCast(priority_)); +pub fn setProcessPriority(pid: i32, priority: i32) std.c.E { + if (pid < 0) return .SRCH; const code: i32 = set_process_priority(pid, priority); @@ -468,9 +462,18 @@ pub fn dlsym(comptime Type: type, comptime name: [:0]const u8) ?Type { return dlsymWithHandle(Type, name, handle_getter); } +/// Error condition is encoded as null +/// The only error in this function is ESRCH (no process found) +pub fn getProcessPriority(pid: i32) ?i32 { + return switch (get_process_priority(pid)) { + std.math.maxInt(i32) => null, + else => |prio| prio, + }; +} + // set in c-bindings.cpp -pub extern fn get_process_priority(pid: c_uint) i32; -pub extern fn set_process_priority(pid: c_uint, priority: c_int) i32; +extern fn get_process_priority(pid: i32) i32; +pub extern fn set_process_priority(pid: i32, priority: i32) i32; pub extern fn strncasecmp(s1: [*]const u8, s2: [*]const u8, n: usize) i32; pub extern fn memmove(dest: [*]u8, src: [*]const u8, n: usize) void; @@ -493,3 +496,7 @@ pub extern "C" fn bun_restore_stdio() void; pub extern "C" fn open_as_nonblocking_tty(i32, i32) i32; pub extern fn strlen(ptr: [*c]const u8) usize; + +pub const passwd = translated.passwd; +pub const geteuid = translated.geteuid; +pub const getpwuid_r = translated.getpwuid_r; diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 3a47ad6663..7c4ee555fa 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -47,6 +47,8 @@ export const extJsFunction = (namespaceVar: string, fnLabel: string) => /** Each variant gets a dispatcher function. */ export const extDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: number) => `bindgen_${cap(namespaceVar)}_dispatch${cap(fnLabel)}${variantNumber}`; +export const extInternalDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: string | number) => + `bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}_v${variantNumber}`; interface TypeDataDefs { /** The name */ @@ -70,12 +72,18 @@ interface TypeDataDefs { } type TypeData = K extends keyof TypeDataDefs ? TypeDataDefs[K] : any; +export const enum NodeValidator { + validateInteger = "validateInteger", +} + interface Flags { + nodeValidator?: NodeValidator; optional?: boolean; required?: boolean; - nullable?: boolean; + nonNull?: boolean; default?: any; range?: ["clamp" | "enforce", bigint, bigint] | ["clamp" | "enforce", "abi", "abi"]; + finite?: boolean; } export interface DictionaryField { @@ -85,6 +93,8 @@ export interface DictionaryField { export declare const isType: unique symbol; +const numericTypes = new Set(["f64", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "usize"]); + /** * Implementation of the Type interface. All types are immutable and hashable. * Hashes de-duplicate structure and union definitions. Flags do not account for @@ -263,7 +273,7 @@ export class TypeImpl { } cppClassName() { - assert(this.lowersToNamedType()); + assert(this.lowersToNamedType(), `Does not lower to named type: ${inspect(this)}`); const name = this.name(); const namespace = typeHashToNamespace.get(this.hash()); return namespace ? `${namespace}::${cap(name)}` : name; @@ -327,48 +337,6 @@ export class TypeImpl { } } - // Interface definition API - get optional() { - if (this.flags.required) { - throw new Error("Cannot derive optional on a required type"); - } - if (this.flags.default) { - throw new Error("Cannot derive optional on a something with a default value (default implies optional)"); - } - return new TypeImpl(this.kind, this.data, { - ...this.flags, - optional: true, - }); - } - - get nullable() { - return new TypeImpl(this.kind, this.data, { - ...this.flags, - nullable: true, - }); - } - - get required() { - if (this.flags.required) { - throw new Error("This type already has required set"); - } - if (this.flags.required) { - throw new Error("Cannot derive required on an optional type"); - } - return new TypeImpl(this.kind, this.data, { - ...this.flags, - required: true, - }); - } - - clamp(min?: number | bigint, max?: number | bigint) { - return this.#rangeModifier(min, max, "clamp"); - } - - enforceRange(min?: number | bigint, max?: number | bigint) { - return this.#rangeModifier(min, max, "enforce"); - } - #rangeModifier(min: undefined | number | bigint, max: undefined | number | bigint, kind: "clamp" | "enforce") { if (this.flags.range) { throw new Error("This type already has a range modifier set"); @@ -467,6 +435,9 @@ export class TypeImpl { } } break; + case "undefined": + assert(value === undefined, `Expected undefined, got ${inspect(value)}`); + break; default: throw new Error(`TODO: set default value on type ${this.kind}`); } @@ -515,6 +486,8 @@ export class TypeImpl { throw new Error(`TODO: non-empty string default`); } break; + case "undefined": + throw new Error("Zero-sized type"); default: throw new Error(`TODO: set default value on type ${this.kind}`); } @@ -527,25 +500,31 @@ export class TypeImpl { throw new Error("TODO: generate non-extern struct for representing this data type"); } - default(def: any) { - if ("default" in this.flags) { - throw new Error("This type already has a default value"); - } - if (this.flags.required) { - throw new Error("Cannot derive default on a required type"); - } - this.assertDefaultIsValid(def); - return new TypeImpl(this.kind, this.data, { - ...this.flags, - default: def, - }); + isIgnoredUndefinedType() { + return this.kind === "undefined"; + } + + isStringType() { + return ( + this.kind === "DOMString" || this.kind === "ByteString" || this.kind === "USVString" || this.kind === "UTF8String" + ); + } + + isNumberType() { + return numericTypes.has(this.kind); + } + + isObjectType() { + return this.kind === "externalClass" || this.kind === "dictionary"; } [Symbol.toStringTag] = "Type"; [Bun.inspect.custom](depth, options, inspect) { return ( `${options.stylize("Type", "special")} ${ - this.nameDeduplicated ? options.stylize(JSON.stringify(this.nameDeduplicated), "string") + " " : "" + this.lowersToNamedType() && this.nameDeduplicated + ? options.stylize(JSON.stringify(this.nameDeduplicated), "string") + " " + : "" }${options.stylize( `[${this.kind}${["required", "optional", "nullable"] .filter(k => this.flags[k]) @@ -562,9 +541,105 @@ export class TypeImpl { : "") ); } + + // Public interface definition API + get optional() { + if (this.flags.required) { + throw new Error("Cannot derive optional on a required type"); + } + if (this.flags.default) { + throw new Error("Cannot derive optional on a something with a default value (default implies optional)"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + optional: true, + }); + } + + get finite() { + if (this.kind !== "f64") { + throw new Error("finite can only be used on f64"); + } + if (this.flags.finite) { + throw new Error("This type already has finite set"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + finite: true, + }); + } + + get required() { + if (this.flags.required) { + throw new Error("This type already has required set"); + } + if (this.flags.required) { + throw new Error("Cannot derive required on an optional type"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + required: true, + }); + } + + default(def: any) { + if ("default" in this.flags) { + throw new Error("This type already has a default value"); + } + if (this.flags.required) { + throw new Error("Cannot derive default on a required type"); + } + this.assertDefaultIsValid(def); + return new TypeImpl(this.kind, this.data, { + ...this.flags, + default: def, + }); + } + + clamp(min?: number | bigint, max?: number | bigint) { + return this.#rangeModifier(min, max, "clamp"); + } + + enforceRange(min?: number | bigint, max?: number | bigint) { + return this.#rangeModifier(min, max, "enforce"); + } + + get nonNull() { + if (this.flags.nonNull) { + throw new Error("Cannot derive nonNull on a nonNull type"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + nonNull: true, + }); + } + + validateInt32(min?: number, max?: number) { + if (this.kind !== "i32") { + throw new Error("validateInt32 can only be used on i32 or u32"); + } + const rangeInfo = cAbiIntegerLimits("i32"); + return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]); + } + + validateUint32(min?: number, max?: number) { + if (this.kind !== "u32") { + throw new Error("validateUint32 can only be used on i32 or u32"); + } + const rangeInfo = cAbiIntegerLimits("u32"); + return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]); + } + + validateInteger(min?: number | bigint, max?: number | bigint) { + min ??= Number.MIN_SAFE_INTEGER; + max ??= Number.MAX_SAFE_INTEGER; + const enforceRange = this.#rangeModifier(min, max, "enforce") as TypeImpl; + enforceRange.flags.nodeValidator = NodeValidator.validateInteger; + return enforceRange; + } } -function cAbiIntegerLimits(type: CAbiType) { +export function cAbiIntegerLimits(type: CAbiType) { switch (type) { case "u8": return [0, 255]; @@ -584,6 +659,8 @@ function cAbiIntegerLimits(type: CAbiType) { return [-2147483648, 2147483647]; case "i64": return [-9223372036854775808n, 9223372036854775807n]; + case "f64": + return [-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; default: throw new Error(`Unexpected type ${type}`); } @@ -603,9 +680,6 @@ export function oneOfImpl(types: TypeImpl[]): TypeImpl { if (type.kind === "oneOf") { out.push(...type.data); } else { - if (type.flags.nullable) { - throw new Error("Union type cannot include nullable"); - } if (type.flags.default) { throw new Error( "Union type cannot include a default value. Instead, set a default value on the union type itself", @@ -711,6 +785,8 @@ export type ArgStrategyChildItem = export type ReturnStrategy = // JSValue is special cased because it encodes exception as 0x0 | { type: "jsvalue" } + // Return value doesnt exist. function returns a boolean indicating success/error. + | { type: "void" } // For primitives and simple structures where direct assignment into a // pointer is possible. function returns a boolean indicating success/error. | { type: "basic-out-param"; abiType: CAbiType }; @@ -741,7 +817,8 @@ export function registerFunction(opts: FuncOptions) { for (const variant of opts.variants) { const { minRequiredArgs } = validateVariant(variant); variants.push({ - ...variant, + args: Object.entries(variant.args).map(([name, type]) => ({ name, type })) as Arg[], + ret: variant.ret as TypeImpl, suffix: `${i}`, minRequiredArgs, } as unknown as Variant); diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 2ef06e92ed..483e6d2531 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -10,43 +10,107 @@ import { isFunc, } from "./bindgen-lib-internal"; -export type Type = { - [isType]: true | [T, K, Flags]; -} & (Flags extends null - ? { - /** - * Optional means the value may be omitted from a parameter definition. - * Parameters are required by default. - */ - optional: Type; - /** - * When this is used as a dictionary value, this makes that parameter - * required. Dictionary entries are optional by default. - */ - required: Type, K, false>; +/** A type definition for argument parsing. See `bindgen.md` for usage details. */ +export type Type< + /** T = JavaScript type that the Type represents */ + T, + /** K = "kind" a string pertaining to the `t.` that created this type. affects method listing */ + K extends TypeKind = TypeKind, + /** F = "flags" defining if the value is optional. null = not set, false = required, true = optional. */ + F extends TypeFlag = null, +> = F extends null + ? Props + : F extends true + ? { + [isType]: true | [T, K, F]; + nonNull: Type; + } + : { [isType]: true | [T, K, F] }; - /** Implies `optional`, this sets a default value if omitted */ - default(def: T): Type; - } & (K extends IntegerTypeKind - ? { - /** - * Applies [Clamp] semantics - * https://webidl.spec.whatwg.org/#Clamp - * If a custom numeric range is provided, it will be used instead of the built-in clamp rules. - */ - clamp(min?: T, max?: T): Type; - /** - * Applies [EnforceRange] semantics - * https://webidl.spec.whatwg.org/#EnforceRange - * If a custom numeric range is provided, it will be used instead of the built-in enforce rules. - */ - enforceRange(min?: T, max?: T): Type; - } - : {}) - : {}); +type TypeFlag = boolean | "opt-nonnull" | null; + +interface BaseTypeProps { + [isType]: true | [T, K]; + /** + * Optional means the value may be omitted from a parameter definition. + * Parameters are required by default. + */ + optional: Type; + /** + * When this is used as a dictionary value, this makes that parameter + * required. Dictionary entries are optional by default. + */ + required: Type, K, false>; + + /** Implies `optional`, this sets a default value if omitted */ + default(def: T): Type; +} + +interface NumericTypeProps extends BaseTypeProps { + /** + * Applies [Clamp] semantics + * https://webidl.spec.whatwg.org/#Clamp + * If a custom numeric range is provided, it will be used instead of the built-in clamp rules. + */ + clamp(min?: T, max?: T): Type; + /** + * Applies [EnforceRange] semantics + * https://webidl.spec.whatwg.org/#EnforceRange + * If a custom numeric range is provided, it will be used instead of the built-in enforce rules. + */ + enforceRange(min?: T, max?: T): Type; + + /** + * Equivalent to calling Node.js' `validateInteger(val, prop, min, max)` + */ + validateInteger(min?: T, max?: T): Type; +} + +interface I32TypeProps extends NumericTypeProps { + /** + * Equivalent to calling Node.js' `validateInt32(val, prop, min, max)` + */ + validateInt32(min?: number, max?: number): Type; +} + +interface U32TypeProps extends NumericTypeProps { + /** + * Equivalent to calling Node.js' `validateUint32(val, prop, min, max)` + */ + validateUint32(min?: number, max?: number): Type; +} + +interface F64TypeProps extends NumericTypeProps { + /** + * Throws an error if the input is non-finite (NaN, ±Infinity) + */ + finite: Type; + /** + * Equivalent to calling Node.js' `validateNumber(val, prop, min, max)` + */ + validateNumber(min?: number, max?: number): Type; +} + +// If an entry does not exist, then `BaseTypeProps` is assumed. +// T = JavaScript type that the Type represents +interface TypePropsMap { + // Integer types are always numbers, so T is not passed + ["u8"]: NumericTypeProps; + ["i8"]: NumericTypeProps; + ["u16"]: NumericTypeProps; + ["i16"]: NumericTypeProps; + ["u32"]: U32TypeProps; + ["i32"]: I32TypeProps; + ["u64"]: NumericTypeProps; + ["i64"]: NumericTypeProps; + // F64 is always a number, so T is not passed. + ["f64"]: F64TypeProps; +} + +type PropertyMapKeys = keyof TypePropsMap; +type Props = K extends PropertyMapKeys ? TypePropsMap[K] : BaseTypeProps; export type AcceptedDictionaryTypeKind = Exclude; -export type IntegerTypeKind = "usize" | "i32" | "i64" | "u32" | "u64" | "i8" | "u8" | "i16" | "u16"; function builtinType() { return (kind: K) => new TypeImpl(kind, undefined as any, {}) as Type as Type; @@ -78,6 +142,10 @@ export namespace t { /** Throws if the value is not a boolean. */ export const strictBoolean = builtinType()("strictBoolean"); + /** + * Equivalent to IDL's `unrestricted double`, allowing NaN and Infinity. + * To restrict to finite values, use `f64.finite`. + */ export const f64 = builtinType()("f64"); export const u8 = builtinType()("u8"); diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index ef6d870908..f9f9d5e402 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -7,7 +7,6 @@ import * as path from "node:path"; import { CodeWriter, TypeImpl, - cAbiTypeInfo, cAbiTypeName, cap, extDispatchVariant, @@ -31,10 +30,12 @@ import { alignForward, isFunc, Func, + NodeValidator, + cAbiIntegerLimits, + extInternalDispatchVariant, } from "./bindgen-lib-internal"; import assert from "node:assert"; import { argParse, readdirRecursiveWithExclusionsAndExtensionsSync, writeIfNotChanged } from "./helpers"; -import { type IntegerTypeKind } from "bindgen"; // arg parsing let { "codegen-root": codegenRoot, debug } = argParse(["codegen-root", "debug"]); @@ -54,7 +55,7 @@ function resolveVariantStrategies(vari: Variant, name: string) { argIndex += 1; // If `extern struct` can represent this type, that is the simplest way to cross the C-ABI boundary. - const isNullable = (arg.type.flags.optional && !("default" in arg.type.flags)) || arg.type.flags.nullable; + const isNullable = arg.type.flags.optional && !("default" in arg.type.flags); const abiType = !isNullable && arg.type.canDirectlyMapToCAbi(); if (abiType) { arg.loweringStrategy = { @@ -85,6 +86,10 @@ function resolveVariantStrategies(vari: Variant, name: string) { } return_strategy: { + if (vari.ret.kind === "undefined") { + vari.returnStrategy = { type: "void" }; + break return_strategy; + } if (vari.ret.kind === "any") { vari.returnStrategy = { type: "jsvalue" }; break return_strategy; @@ -109,7 +114,7 @@ function resolveNullableArgumentStrategy( prefix: string, communicationStruct: Struct, ): ArgStrategyChildItem[] { - assert((type.flags.optional && !("default" in type.flags)) || type.flags.nullable); + assert(type.flags.optional && !("default" in type.flags)); communicationStruct.add(`${prefix}Set`, "bool"); return resolveComplexArgumentStrategy(type, `${prefix}Value`, communicationStruct); } @@ -155,6 +160,10 @@ function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionNa for (const arg of variant.args) { const type = arg.type; if (type.isVirtualArgument()) continue; + if (type.isIgnoredUndefinedType()) { + i += 1; + continue; + } const exceptionContext: ExceptionContext = { type: "argument", @@ -187,21 +196,22 @@ function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionNa const jsValueRef = `arg${i}.value()`; /** If JavaScript may pass null or undefined */ - const isOptionalToUser = type.flags.nullable || type.flags.optional || "default" in type.flags; + const isOptionalToUser = type.flags.optional || "default" in type.flags; /** If the final representation may include null */ - const isNullable = type.flags.nullable || (type.flags.optional && !("default" in type.flags)); + const isNullable = type.flags.optional && !("default" in type.flags); if (isOptionalToUser) { if (needDeclare) { addHeaderForType(type); cpp.line(`${type.cppName()} ${storageLocation};`); } + const isUndefinedOrNull = type.flags.nonNull ? "isUndefined" : "isUndefinedOrNull"; if (isNullable) { assert(strategy.type === "uses-communication-buffer"); - cpp.line(`if ((${storageLocation}Set = !${jsValueRef}.isUndefinedOrNull())) {`); + cpp.line(`if ((${storageLocation}Set = !${jsValueRef}.${isUndefinedOrNull}())) {`); storageLocation = `${storageLocation}Value`; } else { - cpp.line(`if (!${jsValueRef}.isUndefinedOrNull()) {`); + cpp.line(`if (!${jsValueRef}.${isUndefinedOrNull}()) {`); } cpp.indent(); emitConvertValue(storageLocation, arg.type, jsValueRef, exceptionContext, "assign"); @@ -233,6 +243,9 @@ function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionNa cpp.line(`${cAbiTypeName(returnStrategy.abiType)} out;`); cpp.line(`if (!${dispatchFunctionName}(`); break; + case "void": + cpp.line(`if (!${dispatchFunctionName}(`); + break; default: throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); } @@ -257,6 +270,8 @@ function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionNa for (const arg of variant.args) { i += 1; + if (arg.type.isIgnoredUndefinedType()) continue; + if (arg.type.isVirtualArgument()) { switch (arg.type.kind) { case "zigVirtualMachine": @@ -300,6 +315,13 @@ function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionNa } cpp.line(");"); break; + case "void": + cpp.dedent(); + cpp.line(")) {"); + cpp.line(` return {};`); + cpp.line("}"); + cpp.line("return JSC::JSValue::encode(JSC::jsUndefined());"); + break; case "basic-out-param": addCommaAfterArgument(); cpp.add("&out"); @@ -335,7 +357,6 @@ function getSimpleIdlType(type: TypeImpl): string | undefined { const map: { [K in TypeKind]?: string } = { boolean: "WebCore::IDLBoolean", undefined: "WebCore::IDLUndefined", - f64: "WebCore::IDLDouble", usize: "WebCore::IDLUnsignedLongLong", u8: "WebCore::IDLOctet", u16: "WebCore::IDLUnsignedShort", @@ -349,6 +370,11 @@ function getSimpleIdlType(type: TypeImpl): string | undefined { let entry = map[type.kind]; if (!entry) { switch (type.kind) { + case "f64": + entry = type.flags.finite // + ? "WebCore::IDLDouble" + : "WebCore::IDLUnrestrictedDouble"; + break; case "stringEnum": type.lowersToNamedType; // const cType = cAbiTypeForEnum(type.data.length); @@ -361,14 +387,27 @@ function getSimpleIdlType(type: TypeImpl): string | undefined { } if (type.flags.range) { - // TODO: when enforceRange is used, a custom adaptor should be used instead - // of chaining both `WebCore::IDLEnforceRangeAdaptor` and custom logic. - const rangeAdaptor = { - "clamp": "WebCore::IDLClampAdaptor", - "enforce": "WebCore::IDLEnforceRangeAdaptor", - }[type.flags.range[0]]; - assert(rangeAdaptor); - entry = `${rangeAdaptor}<${entry}>`; + const { range, nodeValidator } = type.flags; + if ((range[0] === "enforce" && range[1] !== "abi") || nodeValidator) { + if (nodeValidator) assert(nodeValidator === NodeValidator.validateInteger); // TODO? + + const [abiMin, abiMax] = cAbiIntegerLimits(type.kind as CAbiType); + let [_, min, max] = range as [string, bigint | number | "abi", bigint | number | "abi"]; + if (min === "abi") min = abiMin; + if (max === "abi") max = abiMax; + + headers.add("BindgenCustomEnforceRange.h"); + entry = `Bun::BindgenCustomEnforceRange<${cAbiTypeName(type.kind as CAbiType)}, ${min}, ${max}, Bun::BindgenCustomEnforceRangeKind::${ + nodeValidator ? "Node" : "Web" + }>`; + } else { + const rangeAdaptor = { + "clamp": "WebCore::IDLClampAdaptor", + "enforce": "WebCore::IDLEnforceRangeAdaptor", + }[range[0]]; + assert(rangeAdaptor); + entry = `${rangeAdaptor}<${entry}>`; + } } return entry; @@ -393,29 +432,30 @@ function emitConvertValue( if (simpleType) { const cAbiType = type.canDirectlyMapToCAbi(); assert(cAbiType); - let exceptionHandlerBody; + let exceptionHandler: ExceptionHandler | undefined; + switch (exceptionContext.type) { + case "none": + break; + case "argument": + exceptionHandler = getArgumentExceptionHandler( + type, + exceptionContext.argumentIndex, + exceptionContext.name, + exceptionContext.functionName, + ); + } switch (type.kind) { - case "zigEnum": - case "stringEnum": { - if (exceptionContext.type === "argument") { - const { argumentIndex, name, functionName: quotedFunctionName } = exceptionContext; - exceptionHandlerBody = `WebCore::throwArgumentMustBeEnumError(lexicalGlobalObject, scope, ${argumentIndex}, ${str(name)}_s, ${str(type.name())}_s, ${str(quotedFunctionName)}_s, WebCore::expectedEnumerationValues<${type.cppClassName()}>());`; - } - break; - } } if (decl === "declare") { cpp.add(`${type.cppName()} `); } - let exceptionHandler = exceptionHandlerBody - ? `, [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { ${exceptionHandlerBody} }` - : ""; - cpp.line(`${storageLocation} = WebCore::convert<${simpleType}>(*global, ${jsValueRef}${exceptionHandler});`); + let exceptionHandlerText = exceptionHandler ? `, ${exceptionHandler.params} { ${exceptionHandler.body} }` : ""; + cpp.line(`${storageLocation} = WebCore::convert<${simpleType}>(*global, ${jsValueRef}${exceptionHandlerText});`); - if (type.flags.range && type.flags.range[1] !== "abi") { + if (type.flags.range && type.flags.range[0] === "clamp" && type.flags.range[1] !== "abi") { emitRangeModifierCheck(cAbiType, storageLocation, type.flags.range); } @@ -469,6 +509,47 @@ function emitConvertValue( } } +interface ExceptionHandler { + /** @example "[](JSC::JSGlobalObject& global, ThrowScope& scope)" */ + params: string; + /** @example "WebCore::throwTypeError(global, scope)" */ + body: string; +} + +function getArgumentExceptionHandler(type: TypeImpl, argumentIndex: number, name: string, functionName: string) { + const { nodeValidator } = type.flags; + if (nodeValidator) { + switch (nodeValidator) { + case NodeValidator.validateInteger: + headers.add("ErrorCode.h"); + return { + params: `[]()`, + body: `return ${str(name)}_s;`, + }; + default: + throw new Error(`TODO: implement exception thrower for node validator ${nodeValidator}`); + } + } + switch (type.kind) { + case "zigEnum": + case "stringEnum": { + return { + params: `[](JSC::JSGlobalObject& global, JSC::ThrowScope& scope)`, + body: `WebCore::throwArgumentMustBeEnumError(${[ + `global`, + `scope`, + `${argumentIndex}`, + `${str(name)}_s`, + `${str(type.name())}_s`, + `${str(functionName)}_s`, + `WebCore::expectedEnumerationValues<${type.cppClassName()}>()`, + ].join(", ")});`, + }; + break; + } + } +} + /** * The built in WebCore range adaptors do not support arbitrary ranges, but that * is something we want to have. They aren't common, so they are just tacked @@ -483,23 +564,15 @@ function emitRangeModifierCheck( if (kind === "clamp") { cpp.line(`if (${storageLocation} < ${min}) ${storageLocation} = ${min};`); cpp.line(`else if (${storageLocation} > ${max}) ${storageLocation} = ${max};`); - } else if (kind === "enforce") { - cpp.line(`if (${storageLocation} < ${min} || ${storageLocation} > ${max}) {`); - cpp.indent(); - cpp.line( - `throwTypeError(global, throwScope, rangeErrorString<${cAbiTypeName(cAbiType)}>(${storageLocation}, ${min}, ${max}));`, - ); - cpp.line(`return {};`); - cpp.dedent(); - cpp.line(`}`); } else { - throw new Error(`TODO: range modifier ${kind}`); + // Implemented in BindgenCustomEnforceRange + throw new Error(`This should not be called for 'enforceRange' types.`); } } function addHeaderForType(type: TypeImpl) { if (type.lowersToNamedType() && type.ownerFile) { - headers.add(`Generated${cap(type.ownerFile)}.h`); + headers.add(`Generated${pascal(type.ownerFile)}.h`); } } @@ -747,6 +820,7 @@ function zigTypeNameInner(type: TypeImpl): string { function returnStrategyCppType(strategy: ReturnStrategy): string { switch (strategy.type) { case "basic-out-param": + case "void": return "bool"; // true=success, false=exception case "jsvalue": return "JSC::EncodedJSValue"; @@ -760,6 +834,7 @@ function returnStrategyCppType(strategy: ReturnStrategy): string { function returnStrategyZigType(strategy: ReturnStrategy): string { switch (strategy.type) { case "basic-out-param": + case "void": return "bool"; // true=success, false=exception case "jsvalue": return "JSC.JSValue"; @@ -809,6 +884,190 @@ function emitComplexZigDecoder(w: CodeWriter, prefix: string, type: TypeImpl, ch } } +type DistinguishablePrimitive = "undefined" | "string" | "number" | "boolean" | "object"; +type DistinguishStrategy = DistinguishablePrimitive; + +function typeCanDistinguish(t: TypeImpl[]) { + const seen: Record = { + undefined: false, + string: false, + number: false, + boolean: false, + object: false, + }; + let strategies: DistinguishStrategy[] = []; + + for (const type of t) { + let primitive: DistinguishablePrimitive | null = null; + if (type.kind === "undefined") { + primitive = "undefined"; + } else if (type.isStringType()) { + primitive = "string"; + } else if (type.isNumberType()) { + primitive = "number"; + } else if (type.kind === "boolean") { + primitive = "boolean"; + } else if (type.isObjectType()) { + primitive = "object"; + } + if (primitive) { + if (seen[primitive]) { + return null; + } + seen[primitive] = true; + strategies.push(primitive); + continue; + } + return null; // TODO: + } + + return strategies; +} + +/** This is an arbitrary classifier to allow consistent sorting for distinguishing arguments */ +function typeDistinguishmentWeight(type: TypeImpl): number { + if (type.kind === "undefined") { + return 100; + } + + if (type.isObjectType()) { + return 10; + } + + if (type.isStringType()) { + return 5; + } + + if (type.isNumberType()) { + return 3; + } + + if (type.kind === "boolean") { + return -1; + } + + return 0; +} + +function getDistinguishCode(strategy: DistinguishStrategy, type: TypeImpl, value: string) { + switch (strategy) { + case "string": + return { condition: `${value}.isString()`, canThrow: false }; + case "number": + return { condition: `${value}.isNumber()`, canThrow: false }; + case "boolean": + return { condition: `${value}.isBoolean()`, canThrow: false }; + case "object": + return { condition: `${value}.isObject()`, canThrow: false }; + case "undefined": + return { condition: `${value}.isUndefined()`, canThrow: false }; + default: + throw new Error(`TODO: getDistinguishCode for ${strategy}`); + } +} + +/** The variation selector implementation decides which variation dispatch to call. */ +function emitCppVariationSelector(fn: Func, namespaceVar: string) { + let minRequiredArgs = Infinity; + let maxArgs = 0; + + const variationsByArgumentCount = new Map(); + + const pushToList = (argCount: number, vari: Variant) => { + assert(typeof argCount === "number"); + let list = variationsByArgumentCount.get(argCount); + if (!list) { + list = []; + variationsByArgumentCount.set(argCount, list); + } + list.push(vari); + }; + + for (const vari of fn.variants) { + const vmra = vari.minRequiredArgs; + minRequiredArgs = Math.min(minRequiredArgs, vmra); + maxArgs = Math.max(maxArgs, vari.args.length); + const allArgCount = vari.args.filter(arg => !arg.type.isVirtualArgument()).length; + pushToList(vmra, vari); + if (allArgCount != vmra) { + pushToList(allArgCount, vari); + } + } + + cpp.line(`auto& vm = JSC::getVM(global);`); + cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); + if (minRequiredArgs > 0) { + cpp.line(`size_t argumentCount = std::min(callFrame->argumentCount(), ${maxArgs});`); + cpp.line(`if (argumentCount < ${minRequiredArgs}) {`); + cpp.line(` return JSC::throwVMError(global, throwScope, createNotEnoughArgumentsError(global));`); + cpp.line(`}`); + } + + const sorted = [...variationsByArgumentCount.entries()] + .map(([key, value]) => ({ argCount: key, variants: value })) + .sort((a, b) => b.argCount - a.argCount); + let argCountI = 0; + for (const { argCount, variants } of sorted) { + argCountI++; + const checkArgCount = argCountI < sorted.length && argCount !== minRequiredArgs; + if (checkArgCount) { + cpp.line(`if (argumentCount >= ${argCount}) {`); + cpp.indent(); + } + + if (variants.length === 1) { + cpp.line(`return ${extInternalDispatchVariant(namespaceVar, fn.name, variants[0].suffix)}(global, callFrame);`); + } else { + let argIndex = 0; + let strategies: DistinguishStrategy[] | null = null; + while (argIndex < argCount) { + strategies = typeCanDistinguish( + variants.map(v => v.args.filter(v => !v.type.isVirtualArgument())[argIndex].type), + ); + if (strategies) { + break; + } + argIndex++; + } + if (!strategies) { + const err = new Error( + `\x1b[0mVariations with ${argCount} required arguments must have at least one argument that can distinguish between them.\n` + + `Variations:\n${variants.map(v => ` ${inspect(v.args.filter(a => !a.type.isVirtualArgument()).map(x => x.type))}`).join("\n")}`, + ); + err.stack = `Error: ${err.message}\n${fn.snapshot}`; + throw err; + } + + const getArgument = minRequiredArgs > 0 ? "uncheckedArgument" : "argument"; + cpp.line(`JSC::JSValue distinguishingValue = callFrame->${getArgument}(${argIndex});`); + const sortedVariants = variants + .map((v, i) => ({ + variant: v, + type: v.args.filter(a => !a.type.isVirtualArgument())[argIndex].type, + strategy: strategies[i], + })) + .sort((a, b) => typeDistinguishmentWeight(a.type) - typeDistinguishmentWeight(b.type)); + for (const { variant: v, strategy: s } of sortedVariants) { + const arg = v.args[argIndex]; + const { condition, canThrow } = getDistinguishCode(s, arg.type, "distinguishingValue"); + cpp.line(`if (${condition}) {`); + cpp.indent(); + cpp.line(`return ${extInternalDispatchVariant(namespaceVar, fn.name, v.suffix)}(global, callFrame);`); + cpp.dedent(); + cpp.line(`}`); + if (canThrow) { + cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); + } + } + } + + if (checkArgCount) { + cpp.dedent(); + cpp.line(`}`); + } + } +} + // BEGIN MAIN CODE GENERATION // Search for all .bind.ts files @@ -866,12 +1125,6 @@ zigInternal.indent(); cpp.line("namespace Generated {"); cpp.line(); -cpp.line("template"); -cpp.line("static String rangeErrorString(T value, T min, T max)"); -cpp.line("{"); -cpp.line(` return makeString("Value "_s, value, " is outside the range ["_s, min, ", "_s, max, ']');`); -cpp.line("}"); -cpp.line(); cppInternal.line('// These "Arguments" definitions are for communication between C++ and Zig.'); cppInternal.line('// Field layout depends on implementation details in "bindgen.ts", and'); @@ -953,15 +1206,15 @@ for (const [filename, { functions, typedefs }] of files) { `${pascal(namespaceVar)}${pascal(fn.name)}Arguments${fn.variants.length > 1 ? variNum : ""}`, ); const dispatchName = extDispatchVariant(namespaceVar, fn.name, variNum); + const internalDispatchName = extInternalDispatchVariant(namespaceVar, fn.name, variNum); const args: string[] = []; - let argNum = 0; if (vari.globalObjectArg === "hidden") { args.push("JSC::JSGlobalObject*"); } for (const arg of vari.args) { - argNum += 1; + if (arg.type.isIgnoredUndefinedType()) continue; const strategy = arg.loweringStrategy!; switch (strategy.type) { case "c-abi-pointer": @@ -989,6 +1242,18 @@ for (const [filename, { functions, typedefs }] of files) { cpp.line(`extern "C" ${returnStrategyCppType(vari.returnStrategy!)} ${dispatchName}(${args.join(", ")});`); + if (fn.variants.length > 1) { + // Emit separate variant dispatch functions + cpp.line( + `extern "C" SYSV_ABI JSC::EncodedJSValue ${internalDispatchName}(JSC::JSGlobalObject* global, JSC::CallFrame* callFrame)`, + ); + cpp.line(`{`); + cpp.indent(); + cpp.resetTemporaries(); + emitCppCallToVariant(fn.name, vari, dispatchName); + cpp.dedent(); + cpp.line(`}`); + } variNum += 1; } @@ -1008,7 +1273,7 @@ for (const [filename, { functions, typedefs }] of files) { if (fn.variants.length === 1) { emitCppCallToVariant(fn.name, fn.variants[0], extDispatchVariant(namespaceVar, fn.name, 1)); } else { - throw new Error(`TODO: multiple variant dispatch`); + emitCppVariationSelector(fn, namespaceVar); } cpp.dedent(); @@ -1036,6 +1301,7 @@ for (const [filename, { functions, typedefs }] of files) { } let argNum = 0; for (const arg of vari.args) { + if (arg.type.isIgnoredUndefinedType()) continue; let argName = `arg_${snake(arg.name)}`; if (vari.globalObjectArg === argNum) { if (arg.type.kind !== "globalObject") { @@ -1093,6 +1359,9 @@ for (const [filename, { functions, typedefs }] of files) { case "basic-out-param": zigInternal.add(`out.* = @as(bun.JSError!${returnStrategy.abiType}, `); break; + case "void": + zigInternal.add(`@as(bun.JSError!void, `); + break; } zigInternal.line(`${zid("import_" + namespaceVar)}.${fn.zigPrefix}${fn.name + vari.suffix}(`); @@ -1100,6 +1369,8 @@ for (const [filename, { functions, typedefs }] of files) { for (const arg of vari.args) { const argName = arg.zigMappedName!; + if (arg.type.isIgnoredUndefinedType()) continue; + if (arg.type.isVirtualArgument()) { switch (arg.type.kind) { case "zigVirtualMachine": @@ -1129,7 +1400,7 @@ for (const [filename, { functions, typedefs }] of files) { case "uses-communication-buffer": const prefix = `buf.${snake(arg.name)}`; const type = arg.type; - const isNullable = (type.flags.optional && !("default" in type.flags)) || type.flags.nullable; + const isNullable = type.flags.optional && !("default" in type.flags); if (isNullable) emitNullableZigDecoder(zigInternal, prefix, type, strategy.children); else emitComplexZigDecoder(zigInternal, prefix, type, strategy.children); zigInternal.line(`,`); @@ -1144,6 +1415,7 @@ for (const [filename, { functions, typedefs }] of files) { zigInternal.line(`));`); break; case "basic-out-param": + case "void": zigInternal.line(`)) catch |err| switch (err) {`); zigInternal.line(` error.JSError => return false,`); zigInternal.line(` error.OutOfMemory => ${globalObjectArg}.throwOutOfMemory() catch return false,`); diff --git a/src/darwin_c.zig b/src/darwin_c.zig index 7fb07e64d9..ed367f5f05 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -652,9 +652,6 @@ pub extern fn host_processor_info(host: std.c.host_t, flavor: processor_flavor_t pub extern fn getuid(...) std.posix.uid_t; pub extern fn getgid(...) std.posix.gid_t; -pub extern fn get_process_priority(pid: c_uint) i32; -pub extern fn set_process_priority(pid: c_uint, priority: c_int) i32; - pub fn get_version(buf: []u8) []const u8 { @memset(buf, 0); diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index 0940fdf3de..071b1862b1 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -2327,7 +2327,7 @@ pub const uv_rusage_t = extern struct { ru_nivcsw: u64, }; pub extern fn uv_getrusage(rusage: [*c]uv_rusage_t) c_int; -pub extern fn uv_os_homedir(buffer: [*]u8, size: [*c]usize) c_int; +pub extern fn uv_os_homedir(buffer: [*]u8, size: *usize) ReturnCode; pub extern fn uv_os_tmpdir(buffer: [*]u8, size: [*c]usize) c_int; pub extern fn uv_os_get_passwd(pwd: [*c]uv_passwd_t) c_int; pub extern fn uv_os_free_passwd(pwd: [*c]uv_passwd_t) void; diff --git a/src/js/node/async_hooks.ts b/src/js/node/async_hooks.ts index 5f109ae3ea..840afac3b9 100644 --- a/src/js/node/async_hooks.ts +++ b/src/js/node/async_hooks.ts @@ -23,7 +23,7 @@ // calls to $assert which will verify this invariant (only during bun-debug) // const [setAsyncHooksEnabled, cleanupLater] = $cpp("NodeAsyncHooks.cpp", "createAsyncHooksBinding"); -const { validateFunction, validateString } = require("internal/validators"); +const { validateFunction, validateString, validateObject } = require("internal/validators"); // Only run during debug function assertValidAsyncContextArray(array: unknown): array is ReadonlyArray | undefined { @@ -260,8 +260,22 @@ class AsyncResource { type; #snapshot; - constructor(type, options?) { + constructor(type, opts?) { validateString(type, "type"); + + let triggerAsyncId = opts; + if (opts != null) { + if (typeof opts !== "number") { + triggerAsyncId = opts.triggerAsyncId === undefined ? 1 : opts.triggerAsyncId; + } + if (!Number.isSafeInteger(triggerAsyncId) || triggerAsyncId < -1) { + throw $ERR_INVALID_ASYNC_ID(`Invalid triggerAsyncId value: ${triggerAsyncId}`); + } + } + if (hasEnabledCreateHook && type.length === 0) { + throw $ERR_ASYNC_TYPE(`Invalid name for async "type": ${type}`); + } + setAsyncHooksEnabled(true); this.type = type; this.#snapshot = get(); @@ -300,6 +314,7 @@ class AsyncResource { } bind(fn, thisArg) { + validateFunction(fn, "fn"); return this.runInAsyncScope.bind(this, fn, thisArg ?? this); } @@ -354,10 +369,28 @@ const createHookNotImpl = createWarning( true, ); -function createHook(callbacks) { +let hasEnabledCreateHook = false; +function createHook(hook) { + validateObject(hook, "hook"); + const { init, before, after, destroy, promiseResolve } = hook; + if (init !== undefined && typeof init !== "function") throw $ERR_ASYNC_CALLBACK("hook.init must be a function"); + if (before !== undefined && typeof before !== "function") throw $ERR_ASYNC_CALLBACK("hook.before must be a function"); + if (after !== undefined && typeof after !== "function") throw $ERR_ASYNC_CALLBACK("hook.after must be a function"); + if (destroy !== undefined && typeof destroy !== "function") + throw $ERR_ASYNC_CALLBACK("hook.destroy must be a function"); + if (promiseResolve !== undefined && typeof promiseResolve !== "function") + throw $ERR_ASYNC_CALLBACK("hook.promiseResolve must be a function"); + return { - enable: () => createHookNotImpl(callbacks), - disable: createHookNotImpl, + enable() { + createHookNotImpl(hook); + hasEnabledCreateHook = true; + return this; + }, + disable() { + createHookNotImpl(); + return this; + }, }; } diff --git a/src/js/node/os.ts b/src/js/node/os.ts index f962ed31e2..53d6fd5b8f 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -1,5 +1,4 @@ // Hardcoded module "node:os" - var tmpdir = function () { var env = Bun.env; @@ -19,6 +18,8 @@ var tmpdir = function () { return path; }; + tmpdir[Symbol.toPrimitive] = tmpdir; + return tmpdir(); }; @@ -85,7 +86,7 @@ function lazyCpus({ cpus }) { } // all logic based on `process.platform` and `process.arch` is inlined at bundle time -function bound(obj) { +function bound(binding) { return { availableParallelism: function () { return navigator.hardwareConcurrency; @@ -93,25 +94,27 @@ function bound(obj) { arch: function () { return process.arch; }, - cpus: lazyCpus(obj), + cpus: lazyCpus(binding), endianness: function () { - return process.arch === "arm64" || process.arch === "x64" ? "LE" : $bundleError("TODO: endianness"); + return process.arch === "arm64" || process.arch === "x64" // + ? "LE" + : $bundleError("TODO: endianness"); }, - freemem: obj.freemem.bind(obj), - getPriority: obj.getPriority.bind(obj), - homedir: obj.homedir.bind(obj), - hostname: obj.hostname.bind(obj), - loadavg: obj.loadavg.bind(obj), - networkInterfaces: obj.networkInterfaces.bind(obj), + freemem: binding.freemem, + getPriority: binding.getPriority, + homedir: binding.homedir, + hostname: binding.hostname, + loadavg: binding.loadavg, + networkInterfaces: binding.networkInterfaces, platform: function () { return process.platform; }, - release: obj.release.bind(obj), - setPriority: obj.setPriority.bind(obj), + release: binding.release, + setPriority: binding.setPriority, get tmpdir() { return tmpdir; }, - totalmem: obj.totalmem.bind(obj), + totalmem: binding.totalmem, type: function () { return process.platform === "win32" ? "Windows_NT" @@ -121,17 +124,25 @@ function bound(obj) { ? "Linux" : $bundleError("TODO: type"); }, - uptime: obj.uptime.bind(obj), - userInfo: obj.userInfo.bind(obj), - version: obj.version.bind(obj), - machine: obj.machine.bind(obj), + uptime: binding.uptime, + userInfo: binding.userInfo, + version: binding.version, + machine: function () { + return process.arch === "arm64" // + ? "arm64" + : process.arch === "x64" + ? "x86_64" + : $bundleError("TODO: machine"); + }, devNull: process.platform === "win32" ? "\\\\.\\nul" : "/dev/null", - EOL: process.platform === "win32" ? "\r\n" : "\n", + get EOL() { + return process.platform === "win32" ? "\r\n" : "\n"; + }, constants: $processBindingConstants.os, }; } -const out = bound($zig("node_os.zig", "OS.create")); +const out = bound($zig("node_os.zig", "createNodeOsBinding")); symbolToStringify(out, "arch"); symbolToStringify(out, "availableParallelism"); @@ -147,8 +158,10 @@ symbolToStringify(out, "type"); symbolToStringify(out, "uptime"); symbolToStringify(out, "version"); symbolToStringify(out, "machine"); + function symbolToStringify(obj, key) { - obj[key][Symbol.toPrimitive] = function (hint) { + $assert(obj[key] !== undefined, `Missing ${key}`); + obj[key][Symbol.toPrimitive] = function (hint: string) { return obj[key](); }; } diff --git a/src/js/private.d.ts b/src/js/private.d.ts index aeb86e97d2..b058cc40c5 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -210,6 +210,9 @@ declare function $newZigFunction any>( argCount: number, ): T; /** + * Retrieves a handle to a function defined in Zig or C++, defined in a + * `.bind.ts` file. For more information on how to define bindgen functions, see + * [bindgen's documentation](https://bun.sh/docs/project/bindgen). * @param filename - The basename of the `.bind.ts` file. * @param symbol - The name of the function to call. */ diff --git a/src/sys.zig b/src/sys.zig index aa9df098d5..179ed41fec 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -255,6 +255,7 @@ pub const Tag = enum(u8) { uv_pipe, uv_tty_set_mode, uv_open_osfhandle, + uv_os_homedir, // Below this line are Windows API calls only. @@ -3462,8 +3463,8 @@ pub const File = struct { }; } - /// Use this function on small files < 1024 bytes. - /// This will skip the fstat() call. + /// Use this function on small files <= 1024 bytes. + /// This will skip the fstat() call, preallocating 64 bytes instead of the file's size. pub fn readToEndSmall(this: File, allocator: std.mem.Allocator) ReadToEndResult { var list = std.ArrayList(u8).init(allocator); return switch (readToEndWithArrayList(this, &list, true)) { diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index 469089c2a6..a887b113cd 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -222,3 +222,22 @@ describe("toString works like node", () => { }); } }); + +it("getPriority system error object", () => { + try { + os.getPriority(-1); + expect.unreachable(); + } catch (err) { + expect(err.name).toBe("SystemError"); + expect(err.message).toBe("A system error occurred: uv_os_getpriority returned ESRCH (no such process)"); + expect(err.code).toBe("ERR_SYSTEM_ERROR"); + expect(err.info).toEqual({ + errno: isWindows ? -4040 : -3, + code: "ESRCH", + message: "no such process", + syscall: "uv_os_getpriority", + }); + expect(err.errno).toBe(isWindows ? -4040 : -3); + expect(err.syscall).toBe("uv_os_getpriority"); + } +}); diff --git a/test/js/node/test/parallel/test-async-hooks-asyncresource-constructor.js b/test/js/node/test/parallel/test-async-hooks-asyncresource-constructor.js new file mode 100644 index 0000000000..8b504aa7a7 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-asyncresource-constructor.js @@ -0,0 +1,41 @@ +'use strict'; + +// This tests that AsyncResource throws an error if bad parameters are passed + +require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const { AsyncResource } = async_hooks; + +// Setup init hook such parameters are validated +async_hooks.createHook({ + init() {} +}).enable(); + +assert.throws(() => { + return new AsyncResource(); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', +}); + +assert.throws(() => { + new AsyncResource(''); +}, { + code: 'ERR_ASYNC_TYPE', + name: 'TypeError', +}); + +assert.throws(() => { + new AsyncResource('type', -4); +}, { + code: 'ERR_INVALID_ASYNC_ID', + name: 'RangeError', +}); + +assert.throws(() => { + new AsyncResource('type', Math.PI); +}, { + code: 'ERR_INVALID_ASYNC_ID', + name: 'RangeError', +}); diff --git a/test/js/node/test/parallel/test-async-hooks-constructor.js b/test/js/node/test/parallel/test-async-hooks-constructor.js new file mode 100644 index 0000000000..62ec854108 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-constructor.js @@ -0,0 +1,21 @@ +'use strict'; + +// This tests that AsyncHooks throws an error if bad parameters are passed. + +require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const nonFunctionArray = [null, -1, 1, {}, []]; + +['init', 'before', 'after', 'destroy', 'promiseResolve'].forEach( + (functionName) => { + nonFunctionArray.forEach((nonFunction) => { + assert.throws(() => { + async_hooks.createHook({ [functionName]: nonFunction }); + }, { + code: 'ERR_ASYNC_CALLBACK', + name: 'TypeError', + message: `hook.${functionName} must be a function`, + }); + }); + }); diff --git a/test/js/node/test/parallel/test-async-wrap-constructor.js b/test/js/node/test/parallel/test-async-wrap-constructor.js new file mode 100644 index 0000000000..853898aa0a --- /dev/null +++ b/test/js/node/test/parallel/test-async-wrap-constructor.js @@ -0,0 +1,21 @@ +'use strict'; + +// This tests that using falsy values in createHook throws an error. + +require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); + +const falsyValues = [0, 1, false, true, null, 'hello']; +for (const badArg of falsyValues) { + const hookNames = ['init', 'before', 'after', 'destroy', 'promiseResolve']; + for (const hookName of hookNames) { + assert.throws(() => { + async_hooks.createHook({ [hookName]: badArg }); + }, { + code: 'ERR_ASYNC_CALLBACK', + name: 'TypeError', + message: `hook.${hookName} must be a function` + }); + } +} diff --git a/test/js/node/test/parallel/test-os-eol.js b/test/js/node/test/parallel/test-os-eol.js new file mode 100644 index 0000000000..412751a151 --- /dev/null +++ b/test/js/node/test/parallel/test-os-eol.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const os = require('os'); + +const eol = common.isWindows ? '\r\n' : '\n'; + +assert.strictEqual(os.EOL, eol); + +// Test that the `Error` is a `TypeError` but do not check the message as it +// varies between different JavaScript engines. +assert.throws(function() { os.EOL = 123; }, TypeError); + +const foo = 'foo'; +Object.defineProperties(os, { + EOL: { + configurable: true, + enumerable: true, + writable: false, + value: foo + } +}); +assert.strictEqual(os.EOL, foo); diff --git a/test/js/node/test/parallel/test-os-process-priority.js b/test/js/node/test/parallel/test-os-process-priority.js new file mode 100644 index 0000000000..2edabf53df --- /dev/null +++ b/test/js/node/test/parallel/test-os-process-priority.js @@ -0,0 +1,145 @@ +'use strict'; +const common = require('../common'); +// IBMi process priority is different. +if (common.isIBMi) + common.skip('IBMi has a different process priority'); + +const assert = require('assert'); +const os = require('os'); +const { + PRIORITY_LOW, + PRIORITY_BELOW_NORMAL, + PRIORITY_NORMAL, + PRIORITY_ABOVE_NORMAL, + PRIORITY_HIGH, + PRIORITY_HIGHEST +} = os.constants.priority; + +// Validate priority constants. +assert.strictEqual(typeof PRIORITY_LOW, 'number'); +assert.strictEqual(typeof PRIORITY_BELOW_NORMAL, 'number'); +assert.strictEqual(typeof PRIORITY_NORMAL, 'number'); +assert.strictEqual(typeof PRIORITY_ABOVE_NORMAL, 'number'); +assert.strictEqual(typeof PRIORITY_HIGH, 'number'); +assert.strictEqual(typeof PRIORITY_HIGHEST, 'number'); + +// Test pid type validation. +[null, true, false, 'foo', {}, [], /x/].forEach((pid) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "pid" argument must be of type number\./ + }; + + assert.throws(() => { + os.setPriority(pid, PRIORITY_NORMAL); + }, errObj); + + assert.throws(() => { + os.getPriority(pid); + }, errObj); +}); + +// Test pid range validation. +[NaN, Infinity, -Infinity, 3.14, 2 ** 32].forEach((pid) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + message: /The value of "pid" is out of range\./ + }; + + assert.throws(() => { + os.setPriority(pid, PRIORITY_NORMAL); + }, errObj); + + assert.throws(() => { + os.getPriority(pid); + }, errObj); +}); + +// Test priority type validation. +[null, true, false, 'foo', {}, [], /x/].forEach((priority) => { + assert.throws(() => { + os.setPriority(0, priority); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "priority" argument must be of type number\./ + }); +}); + +// Test priority range validation. +[ + NaN, + Infinity, + -Infinity, + 3.14, + 2 ** 32, + PRIORITY_HIGHEST - 1, + PRIORITY_LOW + 1, +].forEach((priority) => { + assert.throws(() => { + os.setPriority(0, priority); + }, { + code: 'ERR_OUT_OF_RANGE', + message: /The value of "priority" is out of range\./ + }); +}); + +// Verify that valid values work. +for (let i = PRIORITY_HIGHEST; i <= PRIORITY_LOW; i++) { + // A pid of 0 corresponds to the current process. + try { + os.setPriority(0, i); + } catch (err) { + // The current user might not have sufficient permissions to set this + // specific priority level. Skip this priority, but keep trying lower + // priorities. + if (err.info.code === 'EACCES') + continue; + + assert(err); + } + + checkPriority(0, i); + + // An undefined pid corresponds to the current process. + os.setPriority(i); + checkPriority(undefined, i); + + // Specifying the actual pid works. + os.setPriority(process.pid, i); + checkPriority(process.pid, i); +} + +{ + assert.throws(() => { os.getPriority(-1); }, { + code: 'ERR_SYSTEM_ERROR', + message: /A system error occurred: uv_os_getpriority returned /, + name: 'SystemError' + }); +} + + +function checkPriority(pid, expected) { + const priority = os.getPriority(pid); + + // Verify that the priority values match on Unix, and are range mapped on + // Windows. + if (!common.isWindows) { + assert.strictEqual(priority, expected); + return; + } + + // On Windows setting PRIORITY_HIGHEST will only work for elevated user, + // for others it will be silently reduced to PRIORITY_HIGH + if (expected < PRIORITY_HIGH) + assert.ok(priority === PRIORITY_HIGHEST || priority === PRIORITY_HIGH); + else if (expected < PRIORITY_ABOVE_NORMAL) + assert.strictEqual(priority, PRIORITY_HIGH); + else if (expected < PRIORITY_NORMAL) + assert.strictEqual(priority, PRIORITY_ABOVE_NORMAL); + else if (expected < PRIORITY_BELOW_NORMAL) + assert.strictEqual(priority, PRIORITY_NORMAL); + else if (expected < PRIORITY_LOW) + assert.strictEqual(priority, PRIORITY_BELOW_NORMAL); + else + assert.strictEqual(priority, PRIORITY_LOW); +} diff --git a/test/js/node/test/parallel/test-os-userinfo-handles-getter-errors.js b/test/js/node/test/parallel/test-os-userinfo-handles-getter-errors.js new file mode 100644 index 0000000000..ca7b560012 --- /dev/null +++ b/test/js/node/test/parallel/test-os-userinfo-handles-getter-errors.js @@ -0,0 +1,19 @@ +'use strict'; +// Tests that os.userInfo correctly handles errors thrown by option property +// getters. See https://github.com/nodejs/node/issues/12370. + +const common = require('../common'); +const assert = require('assert'); +const execFile = require('child_process').execFile; + +const script = `os.userInfo({ + get encoding() { + throw new Error('xyz'); + } +})`; + +const node = process.execPath; +execFile(node, [ '-e', script ], common.mustCall((err, stdout, stderr) => { + // Edited for Bun to lowercase `error` + assert(stderr.includes('xyz'), 'userInfo crashes'); +})); diff --git a/test/js/node/test/parallel/test-os.js b/test/js/node/test/parallel/test-os.js new file mode 100644 index 0000000000..3d9fe5c1a6 --- /dev/null +++ b/test/js/node/test/parallel/test-os.js @@ -0,0 +1,281 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const os = require('os'); +const path = require('path'); +const { inspect } = require('util'); + +const is = { + number: (value, key) => { + assert(!Number.isNaN(value), `${key} should not be NaN`); + assert.strictEqual(typeof value, 'number'); + }, + string: (value) => { assert.strictEqual(typeof value, 'string'); }, + array: (value) => { assert.ok(Array.isArray(value)); }, + object: (value) => { + assert.strictEqual(typeof value, 'object'); + assert.notStrictEqual(value, null); + } +}; + +process.env.TMPDIR = '/tmpdir'; +process.env.TMP = '/tmp'; +process.env.TEMP = '/temp'; +if (common.isWindows) { + assert.strictEqual(os.tmpdir(), '/temp'); + process.env.TEMP = ''; + assert.strictEqual(os.tmpdir(), '/tmp'); + process.env.TMP = ''; + const expected = `${process.env.SystemRoot || process.env.windir}\\temp`; + assert.strictEqual(os.tmpdir(), expected); + process.env.TEMP = '\\temp\\'; + assert.strictEqual(os.tmpdir(), '\\temp'); + process.env.TEMP = '\\tmpdir/'; + assert.strictEqual(os.tmpdir(), '\\tmpdir/'); + process.env.TEMP = '\\'; + assert.strictEqual(os.tmpdir(), '\\'); + process.env.TEMP = 'C:\\'; + assert.strictEqual(os.tmpdir(), 'C:\\'); +} else { + assert.strictEqual(os.tmpdir(), '/tmpdir'); + process.env.TMPDIR = ''; + assert.strictEqual(os.tmpdir(), '/tmp'); + process.env.TMP = ''; + assert.strictEqual(os.tmpdir(), '/temp'); + process.env.TEMP = ''; + assert.strictEqual(os.tmpdir(), '/tmp'); + process.env.TMPDIR = '/tmpdir/'; + assert.strictEqual(os.tmpdir(), '/tmpdir'); + process.env.TMPDIR = '/tmpdir\\'; + assert.strictEqual(os.tmpdir(), '/tmpdir\\'); + process.env.TMPDIR = '/'; + assert.strictEqual(os.tmpdir(), '/'); +} + +const endianness = os.endianness(); +is.string(endianness); +assert.match(endianness, /[BL]E/); + +const hostname = os.hostname(); +is.string(hostname); +assert.ok(hostname.length > 0); + +// IBMi process priority is different. +if (!common.isIBMi) { + const { PRIORITY_BELOW_NORMAL, PRIORITY_LOW } = os.constants.priority; + // Priority means niceness: higher numeric value <=> lower priority + const LOWER_PRIORITY = os.getPriority() < PRIORITY_BELOW_NORMAL ? PRIORITY_BELOW_NORMAL : PRIORITY_LOW; + os.setPriority(LOWER_PRIORITY); + const priority = os.getPriority(); + is.number(priority); + assert.strictEqual(priority, LOWER_PRIORITY); +} + +// On IBMi, os.uptime() returns 'undefined' +if (!common.isIBMi) { + const uptime = os.uptime(); + is.number(uptime); + assert.ok(uptime > 0); +} + +const cpus = os.cpus(); +is.array(cpus); +assert.ok(cpus.length > 0); +for (const cpu of cpus) { + assert.strictEqual(typeof cpu.model, 'string'); + assert.strictEqual(typeof cpu.speed, 'number'); + assert.strictEqual(typeof cpu.times.user, 'number'); + assert.strictEqual(typeof cpu.times.nice, 'number'); + assert.strictEqual(typeof cpu.times.sys, 'number'); + assert.strictEqual(typeof cpu.times.idle, 'number'); + assert.strictEqual(typeof cpu.times.irq, 'number'); +} + +const type = os.type(); +is.string(type); +assert.ok(type.length > 0); + +const release = os.release(); +is.string(release); +assert.ok(release.length > 0); +// TODO: Check format on more than just AIX +if (common.isAIX) + assert.match(release, /^\d+\.\d+$/); + +const platform = os.platform(); +is.string(platform); +assert.ok(platform.length > 0); + +const arch = os.arch(); +is.string(arch); +assert.ok(arch.length > 0); + +if (!common.isSunOS) { + // not implemented yet + assert.ok(os.loadavg().length > 0); + assert.ok(os.freemem() > 0); + assert.ok(os.totalmem() > 0); +} + +const interfaces = os.networkInterfaces(); +switch (platform) { + case 'linux': { + const filter = (e) => + e.address === '127.0.0.1' && + e.netmask === '255.0.0.0'; + + const actual = interfaces.lo.filter(filter); + const expected = [{ + address: '127.0.0.1', + netmask: '255.0.0.0', + family: 'IPv4', + mac: '00:00:00:00:00:00', + internal: true, + cidr: '127.0.0.1/8' + }]; + assert.deepStrictEqual(actual, expected); + break; + } + case 'win32': { + const filter = (e) => + e.address === '127.0.0.1'; + + const actual = interfaces['Loopback Pseudo-Interface 1'].filter(filter); + const expected = [{ + address: '127.0.0.1', + netmask: '255.0.0.0', + family: 'IPv4', + mac: '00:00:00:00:00:00', + internal: true, + cidr: '127.0.0.1/8' + }]; + assert.deepStrictEqual(actual, expected); + break; + } +} +const netmaskToCIDRSuffixMap = new Map(Object.entries({ + '255.0.0.0': 8, + '255.255.255.0': 24, + 'ffff:ffff:ffff:ffff::': 64, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff': 128 +})); + +Object.values(interfaces) + .flat(Infinity) + .map((v) => ({ v, mask: netmaskToCIDRSuffixMap.get(v.netmask) })) + .forEach(({ v, mask }) => { + assert.ok('cidr' in v, `"cidr" prop not found in ${inspect(v)}`); + if (mask) { + assert.strictEqual(v.cidr, `${v.address}/${mask}`); + } + }); + +const EOL = os.EOL; +if (common.isWindows) { + assert.strictEqual(EOL, '\r\n'); +} else { + assert.strictEqual(EOL, '\n'); +} + +const home = os.homedir(); +is.string(home); +assert.ok(home.includes(path.sep)); + +const version = os.version(); +assert.strictEqual(typeof version, 'string'); +assert(version); + +if (common.isWindows && process.env.USERPROFILE) { + assert.strictEqual(home, process.env.USERPROFILE); + delete process.env.USERPROFILE; + assert.ok(os.homedir().includes(path.sep)); + process.env.USERPROFILE = home; +} else if (!common.isWindows && process.env.HOME) { + assert.strictEqual(home, process.env.HOME); + delete process.env.HOME; + assert.ok(os.homedir().includes(path.sep)); + process.env.HOME = home; +} + +const pwd = os.userInfo(); +is.object(pwd); +const pwdBuf = os.userInfo({ encoding: 'buffer' }); + +if (common.isWindows) { + assert.strictEqual(pwd.uid, -1); + assert.strictEqual(pwd.gid, -1); + assert.strictEqual(pwd.shell, null); + assert.strictEqual(pwdBuf.uid, -1); + assert.strictEqual(pwdBuf.gid, -1); + assert.strictEqual(pwdBuf.shell, null); +} else { + is.number(pwd.uid); + is.number(pwd.gid); + assert.strictEqual(typeof pwd.shell, 'string'); + // It's possible for /etc/passwd to leave the user's shell blank. + if (pwd.shell.length > 0) { + assert(pwd.shell.includes(path.sep)); + } + assert.strictEqual(pwd.uid, pwdBuf.uid); + assert.strictEqual(pwd.gid, pwdBuf.gid); + assert.strictEqual(pwd.shell, pwdBuf.shell.toString('utf8')); +} + +is.string(pwd.username); +assert.ok(pwd.homedir.includes(path.sep)); +assert.strictEqual(pwd.username, pwdBuf.username.toString('utf8')); +assert.strictEqual(pwd.homedir, pwdBuf.homedir.toString('utf8')); + +assert.strictEqual(`${os.hostname}`, os.hostname()); +assert.strictEqual(`${os.homedir}`, os.homedir()); +assert.strictEqual(`${os.release}`, os.release()); +assert.strictEqual(`${os.type}`, os.type()); +assert.strictEqual(`${os.endianness}`, os.endianness()); +assert.strictEqual(`${os.tmpdir}`, os.tmpdir()); +assert.strictEqual(`${os.arch}`, os.arch()); +assert.strictEqual(`${os.platform}`, os.platform()); +assert.strictEqual(`${os.version}`, os.version()); +assert.strictEqual(`${os.machine}`, os.machine()); +assert.strictEqual(+os.totalmem, os.totalmem()); + +// Assert that the following values are coercible to numbers. +// On IBMi, os.uptime() returns 'undefined' +if (!common.isIBMi) { + is.number(+os.uptime, 'uptime'); + is.number(os.uptime(), 'uptime'); +} + +is.number(+os.availableParallelism, 'availableParallelism'); +is.number(os.availableParallelism(), 'availableParallelism'); +is.number(+os.freemem, 'freemem'); +is.number(os.freemem(), 'freemem'); + +const devNull = os.devNull; +if (common.isWindows) { + assert.strictEqual(devNull, '\\\\.\\nul'); +} else { + assert.strictEqual(devNull, '/dev/null'); +} + +assert.ok(os.availableParallelism() > 0);