Skip to content
Closed
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
288 changes: 241 additions & 47 deletions src/js/node/dns.ts

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions src/js/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,66 @@ function styleText(format, text) {
return `\u001b[${formatCodes[0]}m${text}\u001b[${formatCodes[1]}m`;
}

// Port of node's `internal/util/diff` (v26.3.0), the implementation behind
// `util.diff()`.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/util/diff.js
//
// The native comparator reports `{ kind, value }` with kind Insert=0, Delete=1,
// Equal=2; node's public shape is `[operation, value]` with INSERT=1,
// DELETE=-1, NOP=0.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/assert/myers_diff.js#L18-L22
const kOperationForDiffKind = [1, -1, 0];
let myersDiff;

function validateDiffInput(value, name) {
if (!$isJSArray(value)) {
validateString(value, name);
return;
}
for (let i = 0; i < value.length; ++i) {
if (typeof value[i] !== "string") {
throw $ERR_INVALID_ARG_TYPE(`${name}[${i}]`, "string", value[i]);
}
}
}

// node's myersDiff indexes both operands uniformly, so a string paired with an
// array is compared code-unit-to-element. The native comparator takes two
// strings or two arrays, so widen the odd one out.
function toDiffLines(value) {
if ($isJSArray(value)) return value;
const length = value.length;
const lines = $newArrayWithSize(length);
for (let i = 0; i < length; i++) {
lines[i] = value[i];
}
return lines;
}

function diff(actual, expected) {
if (actual === expected) {
return [];
}

validateDiffInput(actual, "actual");
validateDiffInput(expected, "expected");

myersDiff ??= require("internal/assert/myers_diff").myersDiff;
const raw =
$isJSArray(actual) === $isJSArray(expected)
? myersDiff(actual, expected)
: myersDiff(toDiffLines(actual), toDiffLines(expected));

// myersDiff walks the edit path backwards; node reverses it before returning.
const length = raw.length;
const result = $newArrayWithSize(length);
for (let i = 0; i < length; i++) {
const { kind, value } = raw[length - 1 - i];
result[i] = [kOperationForDiffKind[kind], typeof value === "number" ? String.fromCharCode(value) : value];
}
return result;
}

function getSystemErrorName(err: any) {
if (typeof err !== "number") throw $ERR_INVALID_ARG_TYPE("err", "number", err);
if (err >= 0 || !NumberIsSafeInteger(err)) throw $ERR_OUT_OF_RANGE("err", "a negative integer", err);
Expand Down Expand Up @@ -481,6 +541,7 @@ cjs_exports = {
TextEncoder,
MIMEType,
MIMEParams,
diff,

// Deprecated in Node.js 22, removed in 23
isArray: $isArray,
Expand Down
28 changes: 20 additions & 8 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,18 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
}
}

if constexpr (checkPrototypes) {
// Distinct WeakMaps, WeakSets and Promises are never equal - their
// contents cannot be inspected. node tests this on the LEFT operand
// only, which makes the comparison asymmetric: a Proxy wrapping a
// promise equals that promise, but only with the proxy on the left.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/util/comparisons.js#L449-L451
uint8_t leftType = c1->type();
if (leftType == JSC::JSWeakMapType || leftType == JSC::JSWeakSetType || leftType == JSC::JSPromiseType) {
return false;
}
}

std::optional<bool> isSpecialEqual = specialObjectsDequal<isStrict, enableAsymmetricMatchers, checkPrototypes, skipPrototypeIdentity>(globalObject, gcBuffer, stack, scope, c1, c2);
RETURN_IF_EXCEPTION(scope, false);
if (isSpecialEqual.has_value()) return WTF::move(*isSpecialEqual);
Expand Down Expand Up @@ -936,8 +948,14 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
}

if constexpr (isStrict && !skipPrototypeIdentity) {
if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) {
return false;
// A Proxy is transparent to this comparison: its traps forward
// [[GetPrototypeOf]] and own-property enumeration to the target, but
// JSC reports the proxy's own class name, which would reject every
// proxy/target pair. node compares prototypes, never class names.
if (!o1->isProxy() && !o2->isProxy()) {
if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) {
return false;
}
}
}

Expand Down Expand Up @@ -1772,12 +1790,6 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
// Symbol/BigInt wrapper objects have no dedicated JSType; node compares
// their internal values. Only for the node entry point.
if constexpr (checkPrototypes) {
// node never considers distinct WeakMaps, WeakSets, or Promises equal
// (their contents cannot be inspected).
if (c1Type == JSC::JSWeakMapType || c1Type == JSC::JSWeakSetType || c1Type == JSC::JSPromiseType
|| c2Type == JSC::JSWeakMapType || c2Type == JSC::JSWeakSetType || c2Type == JSC::JSPromiseType) {
return false;
}
auto* symbolObject1 = dynamicDowncast<JSC::SymbolObject>(c1);
auto* symbolObject2 = dynamicDowncast<JSC::SymbolObject>(c2);
if (symbolObject1 || symbolObject2) {
Expand Down
37 changes: 37 additions & 0 deletions src/runtime/node/node_assert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,43 @@ pub(crate) fn myers_diff(
}
}

/// `util.diff()` accepts arrays of strings as well as strings. Each element is
/// one line, compared byte-wise after UTF-8 conversion.
pub(crate) fn myers_diff_arrays(
global: &JSGlobalObject,
actual: JSValue,
expected: JSValue,
check_comma_disparity: bool,
) -> JsResult<JSValue> {
let actual_lines = collect_utf8_elements(global, actual)?;
let expected_lines = collect_utf8_elements(global, expected)?;
if actual_lines.is_empty() && expected_lines.is_empty() {
return JSValue::create_empty_array(global, 0);
}

let a: Vec<&[u8]> = actual_lines.iter().map(Vec::as_slice).collect();
let e: Vec<&[u8]> = expected_lines.iter().map(Vec::as_slice).collect();

let diff: MyersDiff::DiffList<&[u8]> = if check_comma_disparity {
MyersDiff::Differ::<&[u8], true>::diff(&a, &e).map_err(|err| map_diff_error(global, err))?
} else {
MyersDiff::Differ::<&[u8], false>::diff(&a, &e)
.map_err(|err| map_diff_error(global, err))?
};
diff_list_to_js(global, &diff)
}

fn collect_utf8_elements(global: &JSGlobalObject, array: JSValue) -> JsResult<Vec<Vec<u8>>> {
let length = array.get_length(global)?;
let mut lines = Vec::with_capacity(length as usize);
for index in 0..length {
let element = array.get_index(global, index as u32)?;
let element = bun_core::OwnedString::new(element.to_bun_string(global)?);
lines.push(element.to_utf8_bytes());
}
Ok(lines)
}

fn diff_chars<T>(global: &JSGlobalObject, actual: &[T], expected: &[T]) -> JsResult<JSValue>
where
T: Line + FromAny,
Expand Down
22 changes: 21 additions & 1 deletion src/runtime/node/node_assert_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::node_assert;
/// Equal = 2,
/// }
/// type Diff = { operation: DiffType, text: string };
/// declare function myersDiff(actual: string, expected: string): Diff[];
/// declare function myersDiff(actual: string | string[], expected: string | string[]): Diff[];
/// ```
#[bun_jsc::host_fn]
pub(crate) fn myers_diff(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue> {
Expand All @@ -28,6 +28,26 @@ pub(crate) fn myers_diff(global: &JSGlobalObject, frame: &CallFrame) -> JsResult
_ => (frame.argument(2).is_truthy(), frame.argument(3).is_truthy()),
};

// `util.diff()` also diffs arrays of strings, one element per line.
if actual_arg.is_array() || expected_arg.is_array() {
if !actual_arg.is_array() {
return Err(global.throw_invalid_argument_type_value("actual", "array", actual_arg));
}
if !expected_arg.is_array() {
return Err(global.throw_invalid_argument_type_value(
"expected",
"array",
expected_arg,
));
}
return node_assert::myers_diff_arrays(
global,
actual_arg,
expected_arg,
check_comma_disparity,
);
}

if !actual_arg.is_string() {
return Err(global.throw_invalid_argument_type_value("actual", "string", actual_arg));
}
Expand Down
Loading
Loading