diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 32624f38b7..b34bea815b 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -348,7 +348,7 @@ deno_core::extension!(deno_node, ops::http2::op_http2_send_response, ops::os::op_node_os_get_priority
, ops::os::op_node_os_set_priority
, - ops::os::op_node_os_username
, + ops::os::op_node_os_user_info
, ops::os::op_geteuid
, ops::os::op_getegid
, ops::os::op_cpus
,
diff --git a/ext/node/ops/os/mod.rs b/ext/node/ops/os/mod.rs
index b4c9eaa8ca..ea7e6b99fd 100644
--- a/ext/node/ops/os/mod.rs
+++ b/ext/node/ops/os/mod.rs
@@ -1,5 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+use std::mem::MaybeUninit;
+
use crate::NodePermissions;
use deno_core::op2;
use deno_core::OpState;
@@ -15,6 +17,8 @@ pub enum OsError {
Permission(deno_core::error::AnyError),
#[error("Failed to get cpu info")]
FailedToGetCpuInfo,
+ #[error("Failed to get user info")]
+ FailedToGetUserInfo(#[source] std::io::Error),
}
#[op2(fast)]
@@ -54,20 +58,162 @@ where
priority::set_priority(pid, priority).map_err(OsError::Priority)
}
+#[derive(serde::Serialize)]
+pub struct UserInfo {
+ username: String,
+ homedir: String,
+ shell: Option (
+#[serde]
+pub fn op_node_os_user_info (
state: &mut OpState,
-) -> Result ();
- permissions.check_sys("username", "node:os.userInfo()")?;
+ permissions
+ .check_sys("userInfo", "node:os.userInfo()")
+ .map_err(OsError::Permission)?;
}
- Ok(deno_whoami::username())
+ get_user_info(uid)
}
#[op2(fast)]
diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts
index 51bd7a0250..5a3d4437a1 100644
--- a/ext/node/polyfills/internal/errors.ts
+++ b/ext/node/polyfills/internal/errors.ts
@@ -2558,19 +2558,6 @@ export class ERR_FS_RMDIR_ENOTDIR extends NodeSystemError {
}
}
-export class ERR_OS_NO_HOMEDIR extends NodeSystemError {
- constructor() {
- const code = isWindows ? "ENOENT" : "ENOTDIR";
- const ctx: NodeSystemErrorCtx = {
- message: "not a directory",
- syscall: "home",
- code,
- errno: isWindows ? osConstants.errno.ENOENT : osConstants.errno.ENOTDIR,
- };
- super(code, ctx, "Path is not a directory");
- }
-}
-
export class ERR_HTTP_SOCKET_ASSIGNED extends NodeError {
constructor() {
super(
diff --git a/ext/node/polyfills/os.ts b/ext/node/polyfills/os.ts
index e47e8679ec..edc89ed2c3 100644
--- a/ext/node/polyfills/os.ts
+++ b/ext/node/polyfills/os.ts
@@ -28,16 +28,17 @@ import {
op_homedir,
op_node_os_get_priority,
op_node_os_set_priority,
- op_node_os_username,
+ op_node_os_user_info,
} from "ext:core/ops";
import { validateIntegerRange } from "ext:deno_node/_utils.ts";
import process from "node:process";
import { isWindows } from "ext:deno_node/_util/os.ts";
-import { ERR_OS_NO_HOMEDIR } from "ext:deno_node/internal/errors.ts";
import { os } from "ext:deno_node/internal_binding/constants.ts";
import { osUptime } from "ext:runtime/30_os.js";
import { Buffer } from "ext:deno_node/internal/buffer.mjs";
+import { primordials } from "ext:core/mod.js";
+const { StringPrototypeEndsWith, StringPrototypeSlice } = primordials;
export const constants = os;
@@ -136,6 +137,8 @@ export function arch(): string {
(uptime as any)[Symbol.toPrimitive] = (): number => uptime();
// deno-lint-ignore no-explicit-any
(machine as any)[Symbol.toPrimitive] = (): string => machine();
+// deno-lint-ignore no-explicit-any
+(tmpdir as any)[Symbol.toPrimitive] = (): string | null => tmpdir();
export function cpus(): CPUCoreInfo[] {
return op_cpus();
@@ -268,26 +271,27 @@ export function setPriority(pid: number, priority?: number) {
export function tmpdir(): string | null {
/* This follows the node js implementation, but has a few
differences:
- * On windows, if none of the environment variables are defined,
- we return null.
- * On unix we use a plain Deno.env.get, instead of safeGetenv,
+ * We use a plain Deno.env.get, instead of safeGetenv,
which special cases setuid binaries.
- * Node removes a single trailing / or \, we remove all.
*/
if (isWindows) {
- const temp = Deno.env.get("TEMP") || Deno.env.get("TMP");
- if (temp) {
- return temp.replace(/(? 1 && StringPrototypeEndsWith(temp, "\\") &&
+ !StringPrototypeEndsWith(temp, ":\\")
+ ) {
+ temp = StringPrototypeSlice(temp, 0, -1);
}
- const base = Deno.env.get("SYSTEMROOT") || Deno.env.get("WINDIR");
- if (base) {
- return base + "\\temp";
- }
- return null;
+
+ return temp;
} else { // !isWindows
- const temp = Deno.env.get("TMPDIR") || Deno.env.get("TMP") ||
+ let temp = Deno.env.get("TMPDIR") || Deno.env.get("TMP") ||
Deno.env.get("TEMP") || "/tmp";
- return temp.replace(/(? 1 && StringPrototypeEndsWith(temp, "/")) {
+ temp = StringPrototypeSlice(temp, 0, -1);
+ }
+ return temp;
}
}
@@ -320,7 +324,6 @@ export function uptime(): number {
return osUptime();
}
-/** Not yet implemented */
export function userInfo(
options: UserInfoOptions = { encoding: "utf-8" },
): UserInfo {
@@ -331,20 +334,10 @@ export function userInfo(
uid = -1;
gid = -1;
}
-
- // TODO(@crowlKats): figure out how to do this correctly:
- // The value of homedir returned by os.userInfo() is provided by the operating system.
- // This differs from the result of os.homedir(), which queries environment
- // variables for the home directory before falling back to the operating system response.
- let _homedir = homedir();
- if (!_homedir) {
- throw new ERR_OS_NO_HOMEDIR();
- }
- let shell = isWindows ? null : (Deno.env.get("SHELL") || null);
- let username = op_node_os_username();
+ let { username, homedir, shell } = op_node_os_user_info(uid);
if (options?.encoding === "buffer") {
- _homedir = _homedir ? Buffer.from(_homedir) : _homedir;
+ homedir = homedir ? Buffer.from(homedir) : homedir;
shell = shell ? Buffer.from(shell) : shell;
username = Buffer.from(username);
}
@@ -352,7 +345,7 @@ export function userInfo(
return {
uid,
gid,
- homedir: _homedir,
+ homedir,
shell,
username,
};
diff --git a/runtime/errors.rs b/runtime/errors.rs
index 07bf694dc1..a5c436e751 100644
--- a/runtime/errors.rs
+++ b/runtime/errors.rs
@@ -1086,6 +1086,7 @@ mod node {
},
OsError::Permission(e) => get_error_class_name(e).unwrap_or("Error"),
OsError::FailedToGetCpuInfo => "TypeError",
+ OsError::FailedToGetUserInfo(e) => get_io_error_class(e),
}
}
diff --git a/runtime/permissions/lib.rs b/runtime/permissions/lib.rs
index 1e1321bb2f..84503f025b 100644
--- a/runtime/permissions/lib.rs
+++ b/runtime/permissions/lib.rs
@@ -1368,8 +1368,12 @@ impl SysDescriptor {
match kind.as_str() {
"hostname" | "osRelease" | "osUptime" | "loadavg"
| "networkInterfaces" | "systemMemoryInfo" | "uid" | "gid" | "cpus"
- | "homedir" | "getegid" | "username" | "statfs" | "getPriority"
- | "setPriority" => Ok(Self(kind)),
+ | "homedir" | "getegid" | "statfs" | "getPriority" | "setPriority"
+ | "userInfo" => Ok(Self(kind)),
+
+ // the underlying permission check changed to `userInfo` to better match the API,
+ // alias this to avoid breaking existing projects with `--allow-sys=username`
+ "username" => Ok(Self("userInfo".into())),
_ => Err(type_error(format!("unknown system info kind \"{kind}\""))),
}
}
diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc
index a99a427900..16951d9ede 100644
--- a/tests/node_compat/config.jsonc
+++ b/tests/node_compat/config.jsonc
@@ -87,8 +87,6 @@
"test-net-server-try-ports.js",
"test-net-socket-timeout.js",
"test-net-write-arguments.js",
- // TODO(nathanwhit): Disable os.userInfo is slightly incorrect
- // "test-os.js",
"test-path-resolve.js",
"test-querystring.js",
"test-readline-interface.js",
@@ -448,6 +446,7 @@
"test-next-tick-when-exiting.js",
"test-next-tick.js",
"test-nodeeventtarget.js",
+ "test-os.js",
"test-outgoing-message-destroy.js",
"test-outgoing-message-pipe.js",
"test-parse-args.mjs",
diff --git a/tests/node_compat/runner/TODO.md b/tests/node_compat/runner/TODO.md
index 35a67e72d2..231a4f62c9 100644
--- a/tests/node_compat/runner/TODO.md
+++ b/tests/node_compat/runner/TODO.md
@@ -1878,7 +1878,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co
- [parallel/test-os-homedir-no-envvar.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-os-homedir-no-envvar.js)
- [parallel/test-os-process-priority.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-os-process-priority.js)
- [parallel/test-os-userinfo-handles-getter-errors.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-os-userinfo-handles-getter-errors.js)
-- [parallel/test-os.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-os.js)
- [parallel/test-path-posix-relative-on-windows.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-path-posix-relative-on-windows.js)
- [parallel/test-pending-deprecation.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-pending-deprecation.js)
- [parallel/test-perf-gc-crash.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-perf-gc-crash.js)
diff --git a/tests/node_compat/test/parallel/test-os.js b/tests/node_compat/test/parallel/test-os.js
new file mode 100644
index 0000000000..f7c24342ad
--- /dev/null
+++ b/tests/node_compat/test/parallel/test-os.js
@@ -0,0 +1,286 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 18.12.1
+// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
+
+// 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 DUMMY_PRIORITY = 10;
+ os.setPriority(DUMMY_PRIORITY);
+ const priority = os.getPriority();
+ is.number(priority);
+ assert.strictEqual(priority, DUMMY_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);