Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/js/internal/fs/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type FSStream = import("node:fs").ReadStream &
type FD = number;

const { validateInteger, validateInt32, validateFunction } = require("internal/validators");
const { isURL } = require("internal/url");

const kIsPerformingIO = Symbol("kIsPerformingIO");
const kIoDone = Symbol("kIoDone");
Expand Down Expand Up @@ -88,7 +89,7 @@ function streamFileHandleClose(this: FileHandle, fd: FD, cb: (err?: any) => void
}

function getValidatedPath(p: any) {
if (p instanceof URL) return Bun.fileURLToPath(p as URL);
if (isURL(p)) return Bun.fileURLToPath(p);
if (typeof p !== "string") throw $ERR_INVALID_ARG_TYPE("path", "string or URL", p);
return require("node:path").resolve(p);
}
Expand Down
3 changes: 2 additions & 1 deletion src/js/internal/fs/watch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// fs.watch is lazily loaded so the FSWatcher class is only set up when it is used.
const EventEmitter = require("node:events");
const { basename } = require("node:path");
const { isURL } = require("internal/url");

// The native `node:fs` binding, shared via `internal/fs/binding`.
const fs = require("internal/fs/binding");
Expand Down Expand Up @@ -138,7 +139,7 @@ class FSWatcher extends EventEmitter {
constructor(path, options, listener) {
super();

if (path instanceof URL) {
if (isURL(path)) {
path = Bun.fileURLToPath(path);
} else if (typeof path === "string" && path.startsWith("file:")) {
path = Bun.fileURLToPath(path);
Expand Down
6 changes: 6 additions & 0 deletions src/js/internal/url.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Node's isURL (lib/internal/url.js). Legacy url.parse() objects have auth and path.
function isURL(self) {
return Boolean(self?.href && self.protocol && self.auth === undefined && self.path === undefined);
}

function urlToHttpOptions(url) {
const options = {
...url,
Expand All @@ -23,5 +28,6 @@ function urlToHttpOptions(url) {
}

export default {
isURL,
urlToHttpOptions,
};
19 changes: 9 additions & 10 deletions src/js/internal/validators.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const { hideFromStack } = require("internal/shared");
const { isURL } = require("internal/url");
const { isUint8Array } = require("node:util/types");

const RegExpPrototypeExec = RegExp.prototype.exec;
const ArrayIsArray = Array.isArray;
const Uint8ArrayPrototypeIncludes = Uint8Array.prototype.includes;

const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
/**
Expand Down Expand Up @@ -87,7 +90,7 @@ function validateBoolean(value, name) {

/** Validate a string-or-URL path and return it resolved to an absolute path string. */
function getValidatedPath(p: any) {
if (p instanceof URL) return Bun.fileURLToPath(p as URL);
if (isURL(p)) return Bun.fileURLToPath(p);
if (typeof p !== "string") throw $ERR_INVALID_ARG_TYPE("path", "string or URL", p);
if (p.startsWith("file:")) return Bun.fileURLToPath(p);
return require("node:path").resolve(p);
Expand All @@ -99,21 +102,17 @@ function throwIfNullBytesInFileName(filename: string) {
}
}

/**
* node's fs getValidatedPath (lib/internal/fs/utils.js): converts URL
* *instances* via fileURLToPath, accepts strings and Buffers as-is (no
* path.resolve, no "file:"-prefix string sniffing), and rejects null bytes.
*/
/** node's fs getValidatedPath (lib/internal/fs/utils.js): a URL becomes a path, strings and Buffers pass as-is. */
function getValidatedFsPath(p: any, propName: string = "path") {
if (p instanceof URL) p = Bun.fileURLToPath(p);
if (isURL(p)) p = Bun.fileURLToPath(p);
if (typeof p === "string") {
if (p.indexOf("\u0000") !== -1) {
throw $ERR_INVALID_ARG_VALUE(propName, p, "must be a string, Uint8Array, or URL without null bytes");
}
return p;
}
if (p instanceof Uint8Array) {
if (p.indexOf(0) !== -1) {
if (isUint8Array(p)) {
if (Uint8ArrayPrototypeIncludes.$call(p, 0)) {
throw $ERR_INVALID_ARG_VALUE(propName, p, "must be a string, Uint8Array, or URL without null bytes");
}
return p;
Expand Down Expand Up @@ -171,7 +170,7 @@ export default {
validateBuffer: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBuffer", 0),
/** `(value, name, oneOf)` */
validateOneOf: $newCppFunction("NodeValidator.cpp", "jsFunction_validateOneOf", 0),
isUint8Array: value => value instanceof Uint8Array,
isUint8Array,
/** `(path)` — accepts a string or file URL, returns it resolved to an absolute path string */
getValidatedPath,
getValidatedFsPath,
Expand Down
8 changes: 2 additions & 6 deletions src/js/node/_http_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const {
} = require("node:_http_common");
const { kUniqueHeaders, parseUniqueHeadersOption, OutgoingMessage } = require("node:_http_outgoing");
const Agent = require("node:_http_agent");
const { urlToHttpOptions } = require("internal/url");
const { isURL, urlToHttpOptions } = require("internal/url");
const { kOutHeaders, kNeedDrain, kProxyConfig, checkShouldUseProxy } = require("internal/http");
const { validateInteger, validateBoolean, validateString, validateOneOf } = require("internal/validators");
const { getTimerDuration } = require("internal/timers");
Expand Down Expand Up @@ -95,10 +95,6 @@ function closeRequest(req) {
req.emit("close");
}

function isURLInstance(input) {
return input != null && typeof input === "object" && input instanceof URL;
}

// When proxying a HTTP request, the following needs to be done:
// https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
// 1. Rewrite the request path to absolute-form.
Expand Down Expand Up @@ -169,7 +165,7 @@ function ClientRequest(input, options, cb) {
if (typeof input === "string") {
const urlStr = input;
input = urlToHttpOptions(new URL(urlStr));
} else if (isURLInstance(input)) {
} else if (isURL(input)) {
// url.URL instance
input = urlToHttpOptions(input);
} else {
Expand Down
5 changes: 1 addition & 4 deletions src/js/node/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
validateArray,
validateObject,
validateOneOf,
isUint8Array,
} = require("internal/validators");

var NetModule;
Expand Down Expand Up @@ -1925,10 +1926,6 @@ function getValidatedPath(fileURLOrPath, propName = "path") {
return path;
}

function isUint8Array(value) {
return typeof value === "object" && value !== null && value instanceof Uint8Array;
}

//------------------------------------------------------------------------------
// Section 6. Random utilities
//------------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions src/js/node/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// https://github.com/nodejs/node/blob/v26.3.0/lib/https.js
const http = require("node:http");
const { isIP } = require("internal/net/isIP");
const { urlToHttpOptions } = require("internal/url");
const { isURL, urlToHttpOptions } = require("internal/url");
const { kEmptyObject, once } = require("internal/shared");
const { validateObject } = require("internal/validators");
const { kProxyConfig, checkShouldUseProxy, kWaitForProxyTunnel } = require("internal/http");
Expand All @@ -20,7 +20,7 @@ function request(...args) {
if (typeof args[0] === "string") {
const urlStr = ArrayPrototypeShift.$call(args);
options = urlToHttpOptions(new URL(urlStr));
} else if (args[0] instanceof URL) {
} else if (isURL(args[0])) {
options = urlToHttpOptions(ArrayPrototypeShift.$call(args));
}

Expand Down
10 changes: 1 addition & 9 deletions src/js/node/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

const { URL, URLSearchParams, URLPattern } = globalThis;
const [domainToASCII, domainToUnicode, idnaToASCII] = $cpp("NodeURL.cpp", "Bun::createNodeURLBinding");
const { urlToHttpOptions } = require("internal/url");
const { isURL, urlToHttpOptions } = require("internal/url");
const { validateString, validateObject } = require("internal/validators");
const ObjectSetPrototypeOf = Object.setPrototypeOf;

Expand Down Expand Up @@ -1330,14 +1330,6 @@ function hexByteToNumber(byte: number): number {
return -1;
}

// Node's isURL (lib/internal/url.js): a duck-type check rather than
// `instanceof`, so cross-realm URLs and compatible foreign implementations
// are accepted; `auth`/`path` must be absent to exclude legacy `url.parse`
// objects, which carry both.
function isURL(self: any): boolean {
return Boolean(self?.href && self.protocol && self.auth === undefined && self.path === undefined);
}

function fileURLToPathBuffer(path: unknown, options?: { windows?: boolean }): Buffer {
const windows = options?.windows;
if (typeof path === "string") {
Expand Down
7 changes: 4 additions & 3 deletions src/js/node/worker_threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ function normalizeWorkerName(rawName) {
}

const { isAbsolute: pathIsAbsolute } = require("node:path");
const { isURL } = require("internal/url");

// node's filename validation for non-eval workers: absolute or "./"/"../"-relative
// paths and file: URL objects; bare specifiers and string URLs are rejected.
function validateWorkerFilename(filename) {
if (filename instanceof URL) {
if (filename.protocol === "data:") return `${filename}`;
if (isURL(filename)) {
if (filename.protocol === "data:") return filename.href;
// throws ERR_INVALID_URL_SCHEME (TypeError) for non-file: URLs
return Bun.fileURLToPath(filename);
return Bun.fileURLToPath(filename.href);
}
if (typeof filename !== "string") {
// Not a string or URL: defer to the native Worker constructor, which
Expand Down
20 changes: 20 additions & 0 deletions test/js/node/child_process/child_process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ChildProcess, exec, execFile, execFileSync, execSync, fork, spawn, spaw
import { getEventListeners, once, setMaxListeners } from "node:events";
import os from "node:os";
import { promisify } from "node:util";
import vm from "node:vm";
import path from "path";
const debug = process.env.DEBUG ? console.log : () => {};

Expand Down Expand Up @@ -744,6 +745,25 @@ describe("spawnSync()", () => {
signal: null,
});
});

it("validates a cwd Uint8Array from another realm like a same-realm one", () => {
// A typed array from a node:vm context has that realm's prototype, so
// `instanceof Uint8Array` is false for it. node checks the cell type
// (util.types.isUint8Array), so both inputs take the same route. A bare
// Uint8Array cwd does not spawn in either runtime (only a Buffer
// stringifies to a path), so compare the outcomes instead of asserting one.
const bytes = [...Buffer.from(tmpdirSync())];
const sameRealm = new Uint8Array(bytes);
const otherRealm = vm.runInNewContext(`new Uint8Array(${JSON.stringify(bytes)})`);
expect(otherRealm instanceof Uint8Array).toBe(false);

const run = (cwd: Uint8Array) => {
const { status, error } = spawnSync(bunExe(), ["-e", ""], { cwd: cwd as any, env: bunEnv });
return { status, code: (error as any)?.code };
};
// Before: ERR_INVALID_ARG_TYPE for options.cwd "Received an instance of Uint8Array".
expect(run(otherRealm)).toEqual(run(sameRealm));
});
});

describe("execFileSync()", () => {
Expand Down
Loading