From cde668b54ce39790e350432a7d3871fc7e4f12ff Mon Sep 17 00:00:00 2001 From: 190n Date: Wed, 12 Mar 2025 18:15:00 -0700 Subject: [PATCH] Better edge case handling in napi_value<->String conversion (#18107) --- src/bun.js/bindings/JSBuffer.cpp | 4 +- src/bun.js/bindings/ZigString.zig | 2 +- src/bun.js/bindings/headers-handwritten.h | 4 +- src/bun.js/bindings/napi.cpp | 71 ++++++++++++++--- src/bun.js/webcore/encoding.zig | 60 ++------------ src/napi/js_native_api.h | 28 ++++++- src/napi/napi.zig | 6 +- src/string/WTFStringImpl.zig | 2 +- test/napi/napi-app/binding.gyp | 2 +- test/napi/napi-app/get_string_tests.cpp | 79 +++++++++++++++++++ test/napi/napi-app/get_string_tests.h | 11 +++ test/napi/napi-app/main.cpp | 2 + test/napi/napi-app/module.js | 42 ++++++++++ test/napi/napi.test.ts | 6 ++ .../test/js-native-api/test_string/test.js | 10 ++- test/napi/node-napi.test.ts | 5 +- 16 files changed, 253 insertions(+), 81 deletions(-) create mode 100644 test/napi/napi-app/get_string_tests.cpp create mode 100644 test/napi/napi-app/get_string_tests.h diff --git a/src/bun.js/bindings/JSBuffer.cpp b/src/bun.js/bindings/JSBuffer.cpp index 4d63479b5e..2d4de70c87 100644 --- a/src/bun.js/bindings/JSBuffer.cpp +++ b/src/bun.js/bindings/JSBuffer.cpp @@ -203,10 +203,10 @@ std::optional byteLength(JSC::JSString* str, JSC::JSGlobalObject* lexica if (view->is8Bit()) { const auto span = view->span8(); - return Bun__encoding__byteLengthLatin1(span.data(), span.size(), static_cast(encoding)); + return Bun__encoding__byteLengthLatin1AsUTF8(span.data(), span.size()); } else { const auto span = view->span16(); - return Bun__encoding__byteLengthUTF16(span.data(), span.size(), static_cast(encoding)); + return Bun__encoding__byteLengthUTF16AsUTF8(span.data(), span.size()); } } default: { diff --git a/src/bun.js/bindings/ZigString.zig b/src/bun.js/bindings/ZigString.zig index 79d190aab7..04db088aa3 100644 --- a/src/bun.js/bindings/ZigString.zig +++ b/src/bun.js/bindings/ZigString.zig @@ -252,7 +252,7 @@ pub const ZigString = extern struct { } if (this.is16Bit()) { - return JSC.WebCore.Encoder.byteLengthU16(this.utf16SliceAligned().ptr, this.utf16Slice().len, .utf8); + return strings.elementLengthUTF16IntoUTF8([]const u16, this.utf16SliceAligned()); } return JSC.WebCore.Encoder.byteLengthU8(this.slice().ptr, this.slice().len, .utf8); diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 28ceb71086..2f751c4448 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -384,8 +384,8 @@ extern "C" void ZigString__free_global(const unsigned char* ptr, size_t len); extern "C" size_t Bun__encoding__writeLatin1(const unsigned char* ptr, size_t len, unsigned char* to, size_t other_len, Encoding encoding); extern "C" size_t Bun__encoding__writeUTF16(const UChar* ptr, size_t len, unsigned char* to, size_t other_len, Encoding encoding); -extern "C" size_t Bun__encoding__byteLengthLatin1(const unsigned char* ptr, size_t len, Encoding encoding); -extern "C" size_t Bun__encoding__byteLengthUTF16(const UChar* ptr, size_t len, Encoding encoding); +extern "C" size_t Bun__encoding__byteLengthLatin1AsUTF8(const unsigned char* ptr, size_t len); +extern "C" size_t Bun__encoding__byteLengthUTF16AsUTF8(const UChar* ptr, size_t len); extern "C" int64_t Bun__encoding__constructFromLatin1(void*, const unsigned char* ptr, size_t len, Encoding encoding); extern "C" int64_t Bun__encoding__constructFromUTF16(void*, const UChar* ptr, size_t len, Encoding encoding); diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index ee95c89759..0e8cf0bca5 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -2134,8 +2134,25 @@ extern "C" napi_status napi_get_value_int64(napi_env env, napi_value value, int6 NAPI_RETURN_SUCCESS(env); } -template -napi_status napi_get_value_string_any_encoding(napi_env env, napi_value napiValue, BufferElement* buf, size_t bufsize, size_t* writtenPtr) +// must match src/bun.js/node/types.zig#Encoding, which matches WebCore::BufferEncodingType +enum class NapiStringEncoding : uint8_t { + utf8 = static_cast(WebCore::BufferEncodingType::utf8), + utf16le = static_cast(WebCore::BufferEncodingType::utf16le), + latin1 = static_cast(WebCore::BufferEncodingType::latin1), +}; + +template +struct BufferElement { + using Type = char; +}; + +template<> +struct BufferElement { + using Type = char16_t; +}; + +template +napi_status napi_get_value_string_any_encoding(napi_env env, napi_value napiValue, typename BufferElement::Type* buf, size_t bufsize, size_t* writtenPtr) { NAPI_CHECK_ARG(env, napiValue); JSValue jsValue = toJS(napiValue); @@ -2148,10 +2165,22 @@ napi_status napi_get_value_string_any_encoding(napi_env env, napi_value napiValu if (buf == nullptr) { // they just want to know the length NAPI_CHECK_ARG(env, writtenPtr); - if (view.is8Bit()) { - *writtenPtr = Bun__encoding__byteLengthLatin1(view.span8().data(), length, static_cast(EncodeTo)); - } else { - *writtenPtr = Bun__encoding__byteLengthUTF16(view.span16().data(), length, static_cast(EncodeTo)); + switch (EncodeTo) { + case NapiStringEncoding::utf8: + if (view.is8Bit()) { + *writtenPtr = Bun__encoding__byteLengthLatin1AsUTF8(view.span8().data(), length); + } else { + *writtenPtr = Bun__encoding__byteLengthUTF16AsUTF8(view.span16().data(), length); + } + break; + case NapiStringEncoding::utf16le: + [[fallthrough]]; + case NapiStringEncoding::latin1: + // if the string's encoding is the same as the destination encoding, this is trivially correct + // if we are converting UTF-16 to Latin-1, then we do so by truncating each code unit, so the length is the same + // if we are converting Latin-1 to UTF-16, then we do so by extending each code unit, so the length is also the same + *writtenPtr = length; + break; } return napi_set_last_error(env, napi_ok); } @@ -2168,10 +2197,30 @@ napi_status napi_get_value_string_any_encoding(napi_env env, napi_value napiValu } size_t written; + std::span writable_byte_slice(reinterpret_cast(buf), + EncodeTo == NapiStringEncoding::utf16le + // don't write encoded text to the last element of the destination buffer + // since we need to put a null terminator there + ? 2 * (bufsize - 1) + : bufsize - 1); if (view.is8Bit()) { - written = Bun__encoding__writeLatin1(view.span8().data(), view.length(), reinterpret_cast(buf), bufsize - 1, static_cast(EncodeTo)); + if constexpr (EncodeTo == NapiStringEncoding::utf16le) { + // pass subslice to work around Bun__encoding__writeLatin1 asserting that the output has room + written = Bun__encoding__writeLatin1(view.span8().data(), + std::min(static_cast(view.span8().size()), bufsize), + writable_byte_slice.data(), + writable_byte_slice.size(), + static_cast(EncodeTo)); + } else { + written = Bun__encoding__writeLatin1(view.span8().data(), view.length(), writable_byte_slice.data(), writable_byte_slice.size(), static_cast(EncodeTo)); + } } else { - written = Bun__encoding__writeUTF16(view.span16().data(), view.length(), reinterpret_cast(buf), bufsize - 1, static_cast(EncodeTo)); + written = Bun__encoding__writeUTF16(view.span16().data(), view.length(), writable_byte_slice.data(), writable_byte_slice.size(), static_cast(EncodeTo)); + } + + // convert bytes to code units + if constexpr (EncodeTo == NapiStringEncoding::utf16le) { + written /= 2; } if (writtenPtr != nullptr) { @@ -2193,7 +2242,7 @@ extern "C" napi_status napi_get_value_string_utf8(napi_env env, NAPI_PREAMBLE_NO_THROW_SCOPE(env); NAPI_CHECK_ENV_NOT_IN_GC(env); // this function does set_last_error - return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); + return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); } extern "C" napi_status napi_get_value_string_latin1(napi_env env, napi_value napiValue, char* buf, size_t bufsize, size_t* writtenPtr) @@ -2201,7 +2250,7 @@ extern "C" napi_status napi_get_value_string_latin1(napi_env env, napi_value nap NAPI_PREAMBLE_NO_THROW_SCOPE(env); NAPI_CHECK_ENV_NOT_IN_GC(env); // this function does set_last_error - return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); + return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); } extern "C" napi_status napi_get_value_string_utf16(napi_env env, napi_value napiValue, char16_t* buf, size_t bufsize, size_t* writtenPtr) @@ -2209,7 +2258,7 @@ extern "C" napi_status napi_get_value_string_utf16(napi_env env, napi_value napi NAPI_PREAMBLE_NO_THROW_SCOPE(env); NAPI_CHECK_ENV_NOT_IN_GC(env); // this function does set_last_error - return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); + return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); } extern "C" napi_status napi_get_value_bool(napi_env env, napi_value value, bool* result) diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index 9d2fedf3f9..89e01415cf 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -1006,31 +1006,13 @@ pub const Encoder = struct { else => unreachable, } catch 0; } - export fn Bun__encoding__byteLengthLatin1(input: [*]const u8, len: usize, encoding: u8) usize { - return switch (@as(JSC.Node.Encoding, @enumFromInt(encoding))) { - .utf8 => byteLengthU8(input, len, .utf8), - .latin1 => byteLengthU8(input, len, .ascii), - .ascii => byteLengthU8(input, len, .ascii), - .ucs2 => byteLengthU8(input, len, .utf16le), - .utf16le => byteLengthU8(input, len, .utf16le), - .base64 => byteLengthU8(input, len, .base64), - .base64url => byteLengthU8(input, len, .base64url), - .hex => byteLengthU8(input, len, .hex), - else => unreachable, - }; + // TODO(@190n) handle unpaired surrogates + export fn Bun__encoding__byteLengthLatin1AsUTF8(input: [*]const u8, len: usize) usize { + return byteLengthU8(input, len, .utf8); } - export fn Bun__encoding__byteLengthUTF16(input: [*]const u16, len: usize, encoding: u8) usize { - return switch (@as(JSC.Node.Encoding, @enumFromInt(encoding))) { - .utf8 => byteLengthU16(input, len, .utf8), - .latin1 => byteLengthU16(input, len, .ascii), - .ascii => byteLengthU16(input, len, .ascii), - .ucs2 => byteLengthU16(input, len, .utf16le), - .utf16le => byteLengthU16(input, len, .utf16le), - .base64 => byteLengthU16(input, len, .base64), - .base64url => byteLengthU16(input, len, .base64url), - .hex => byteLengthU16(input, len, .hex), - else => unreachable, - }; + // TODO(@190n) handle unpaired surrogates + export fn Bun__encoding__byteLengthUTF16AsUTF8(input: [*]const u16, len: usize) usize { + return strings.elementLengthUTF16IntoUTF8([]const u16, input[0..len]); } export fn Bun__encoding__constructFromLatin1(globalObject: *JSGlobalObject, input: [*]const u8, len: usize, encoding: u8) JSValue { const slice = switch (@as(JSC.Node.Encoding, @enumFromInt(encoding))) { @@ -1394,32 +1376,6 @@ pub const Encoder = struct { } } - /// Node returns imprecise byte length here - /// Should be fast enough for us to return precise length - pub fn byteLengthU16(input: [*]const u16, len: usize, comptime encoding: JSC.Node.Encoding) usize { - if (len == 0) - return 0; - - switch (comptime encoding) { - // these should be the same size - .ascii, .latin1, .utf8 => { - return strings.elementLengthUTF16IntoUTF8([]const u16, input[0..len]); - }, - .ucs2, .buffer, .utf16le => { - return len * 2; - }, - - .hex => { - return len / 2; - }, - - .base64, .base64url => { - return bun.base64.decodeLenUpperBound(len); - }, - // else => return &[_]u8{}; - } - } - pub fn constructFrom(comptime T: type, input: []const T, allocator: std.mem.Allocator, comptime encoding: JSC.Node.Encoding) []u8 { return switch (comptime T) { u16 => constructFromU16(input.ptr, input.len, allocator, encoding), @@ -1517,8 +1473,8 @@ pub const Encoder = struct { _ = Bun__encoding__writeLatin1; _ = Bun__encoding__writeUTF16; - _ = Bun__encoding__byteLengthLatin1; - _ = Bun__encoding__byteLengthUTF16; + _ = Bun__encoding__byteLengthLatin1AsUTF8; + _ = Bun__encoding__byteLengthUTF16AsUTF8; _ = Bun__encoding__toString; _ = Bun__encoding__toStringUTF8; diff --git a/src/napi/js_native_api.h b/src/napi/js_native_api.h index e84f5444c4..3840310cdd 100644 --- a/src/napi/js_native_api.h +++ b/src/napi/js_native_api.h @@ -238,8 +238,6 @@ NAPI_EXTERN napi_status napi_unwrap(napi_env env, napi_value js_object, void **result); NAPI_EXTERN napi_status napi_remove_wrap(napi_env env, napi_value js_object, void **result); -NAPI_EXTERN napi_status napi_create_object(napi_env env, - napi_value *result); NAPI_EXTERN napi_status napi_create_external(napi_env env, void *data, napi_finalize finalize_cb, void *finalize_hint, @@ -455,6 +453,32 @@ NAPI_EXTERN napi_status napi_object_freeze(napi_env env, napi_value object); NAPI_EXTERN napi_status napi_object_seal(napi_env env, napi_value object); #endif // NAPI_VERSION >= 8 +#if NAPI_VERSION >= 10 +NAPI_EXTERN napi_status node_api_create_external_string_latin1( + napi_env env, + char* str, + size_t length, + napi_finalize finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied); +NAPI_EXTERN napi_status +node_api_create_external_string_utf16(napi_env env, + char16_t* str, + size_t length, + napi_finalize finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied); + +NAPI_EXTERN napi_status node_api_create_property_key_latin1( + napi_env env, const char* str, size_t length, napi_value* result); +NAPI_EXTERN napi_status node_api_create_property_key_utf8( + napi_env env, const char* str, size_t length, napi_value* result); +NAPI_EXTERN napi_status node_api_create_property_key_utf16( + napi_env env, const char16_t* str, size_t length, napi_value* result); +#endif // NAPI_VERSION >= 10 + EXTERN_C_END #endif // SRC_JS_NATIVE_API_H_ diff --git a/src/napi/napi.zig b/src/napi/napi.zig index ee12904b7f..681af567d5 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -383,7 +383,7 @@ pub export fn napi_create_string_latin1(env_: napi_env, str: ?[*]const u8, lengt if (str) |ptr| { if (NAPI_AUTO_LENGTH == length) { break :brk bun.sliceTo(@as([*:0]const u8, @ptrCast(ptr)), 0); - } else if (length > std.math.maxInt(u32)) { + } else if (length > std.math.maxInt(i32)) { return env.invalidArg(); } else { break :brk ptr[0..length]; @@ -424,7 +424,7 @@ pub export fn napi_create_string_utf8(env_: napi_env, str: ?[*]const u8, length: if (str) |ptr| { if (NAPI_AUTO_LENGTH == length) { break :brk bun.sliceTo(@as([*:0]const u8, @ptrCast(str)), 0); - } else if (length > std.math.maxInt(u32)) { + } else if (length > std.math.maxInt(i32)) { return env.invalidArg(); } else { break :brk ptr[0..length]; @@ -460,7 +460,7 @@ pub export fn napi_create_string_utf16(env_: napi_env, str: ?[*]const char16_t, if (str) |ptr| { if (NAPI_AUTO_LENGTH == length) { break :brk bun.sliceTo(@as([*:0]const u16, @ptrCast(str)), 0); - } else if (length > std.math.maxInt(u32)) { + } else if (length > std.math.maxInt(i32)) { return env.invalidArg(); } else { break :brk ptr[0..length]; diff --git a/src/string/WTFStringImpl.zig b/src/string/WTFStringImpl.zig index 650d53d51f..c762f39d73 100644 --- a/src/string/WTFStringImpl.zig +++ b/src/string/WTFStringImpl.zig @@ -208,7 +208,7 @@ pub const WTFStringImplStruct = extern struct { return if (input.len > 0) JSC.WebCore.Encoder.byteLengthU8(input.ptr, input.len, .utf8) else 0; } else { const input = this.utf16Slice(); - return if (input.len > 0) JSC.WebCore.Encoder.byteLengthU16(input.ptr, input.len, .utf8) else 0; + return if (input.len > 0) bun.strings.elementLengthUTF16IntoUTF8([]const u16, input) else 0; } } diff --git a/test/napi/napi-app/binding.gyp b/test/napi/napi-app/binding.gyp index 6e10e64386..a7bd22a6e9 100644 --- a/test/napi/napi-app/binding.gyp +++ b/test/napi/napi-app/binding.gyp @@ -11,7 +11,7 @@ }, }, # leak tests are unused as of #14501 - "sources": ["main.cpp", "async_tests.cpp", "class_test.cpp", "conversion_tests.cpp", "js_test_helpers.cpp", "standalone_tests.cpp", "wrap_tests.cpp"], + "sources": ["main.cpp", "async_tests.cpp", "class_test.cpp", "conversion_tests.cpp", "js_test_helpers.cpp", "standalone_tests.cpp", "wrap_tests.cpp", "get_string_tests.cpp"], "include_dirs": [" +#include +#include +#include +#include + +namespace napitests { + +template +static napi_value +test_get_value_string_any_encoding(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + static constexpr size_t BUFSIZE = 32; + std::array buf; + napi_value string = info[0]; + + size_t full_length; + NODE_API_CALL(env, + get_value_string_fn(env, string, nullptr, 0, &full_length)); + printf("full encoded size = %zu\n", full_length); + + // try to write into every subslice of the buffer + for (size_t len = 0; len < BUFSIZE; len++) { + // initialize so we can tell which parts of the buffer were overwritten and + // which were not + buf.fill(std::is_same_v ? 0xaa : 0xaaaa); + + size_t written = SIZE_MAX; + NODE_API_CALL(env, + get_value_string_fn(env, string, buf.data(), len, &written)); + printf("tried to fill %zu/%zu units of buffer, got %zu (+ terminator)\n", + len, BUFSIZE, written); + printf("["); + for (const auto &elem : buf) { + size_t i = &elem - buf.data(); + if (i == written) { + printf("|"); + } + if (i == len) { + printf("]"); + } + + if constexpr (std::is_same_v) { + printf("%02x", reinterpret_cast(elem)); + } else { + printf("%04x", reinterpret_cast(elem)); + } + } + printf("\n"); + if (written == full_length) { + // at this point we are encoding the whole string, so no need to keep + // trying larger buffers + return env.Undefined(); + } + } + + return env.Undefined(); +} + +void register_get_string_tests(Napi::Env env, Napi::Object exports) { + exports.Set( + "test_get_value_string_latin1", + Napi::Function::New(env, test_get_value_string_any_encoding< + char, napi_get_value_string_latin1>)); + exports.Set("test_get_value_string_utf8", + Napi::Function::New(env, test_get_value_string_any_encoding< + char, napi_get_value_string_utf8>)); + exports.Set( + "test_get_value_string_utf16", + Napi::Function::New(env, test_get_value_string_any_encoding< + char16_t, napi_get_value_string_utf16>)); +} + +} // namespace napitests diff --git a/test/napi/napi-app/get_string_tests.h b/test/napi/napi-app/get_string_tests.h new file mode 100644 index 0000000000..1f9cc98fe4 --- /dev/null +++ b/test/napi/napi-app/get_string_tests.h @@ -0,0 +1,11 @@ +#pragma once + +// Exposes functions to JavaScript to test the `napi_get_value_string_*` methods + +#include "napi_with_version.h" + +namespace napitests { + +void register_get_string_tests(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi-app/main.cpp b/test/napi/napi-app/main.cpp index bd020144f2..37116c4b32 100644 --- a/test/napi/napi-app/main.cpp +++ b/test/napi/napi-app/main.cpp @@ -3,6 +3,7 @@ #include "async_tests.h" #include "class_test.h" #include "conversion_tests.h" +#include "get_string_tests.h" #include "js_test_helpers.h" #include "standalone_tests.h" #include "wrap_tests.h" @@ -35,6 +36,7 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) { register_js_test_helpers(env, exports); register_wrap_tests(env, exports); register_conversion_tests(env, exports); + register_get_string_tests(env, exports); return exports; } diff --git a/test/napi/napi-app/module.js b/test/napi/napi-app/module.js index 9a04c1b109..d332b2ece1 100644 --- a/test/napi/napi-app/module.js +++ b/test/napi/napi-app/module.js @@ -576,4 +576,46 @@ nativeTests.test_create_bigint_words = () => { console.log(nativeTests.create_weird_bigints()); }; +nativeTests.test_get_value_string = () => { + function to16Bit(string) { + if (typeof Bun != "object") return string; + const jsc = require("bun:jsc"); + const codeUnits = new DataView(new ArrayBuffer(2 * string.length)); + for (let i = 0; i < string.length; i++) { + codeUnits.setUint16(2 * i, string.charCodeAt(i), true); + } + const decoder = new TextDecoder("utf-16le"); + const string16Bit = decoder.decode(codeUnits); + // make sure we succeeded in making a UTF-16 string + assert(jsc.jscDescribe(string16Bit).includes("8Bit:(0)")); + return string16Bit; + } + function assert8Bit(string) { + if (typeof Bun != "object") return string; + const jsc = require("bun:jsc"); + // make sure we succeeded in making a Latin-1 string + assert(jsc.jscDescribe(string).includes("8Bit:(1)")); + return string; + } + // test all of our get_value_string_XXX functions on a variety of inputs + for (const [string, description] of [ + ["hello", "simple latin-1"], + [to16Bit("hello"), "16-bit encoded with only BMP characters"], + [assert8Bit("café"), "8-bit with non-ascii characters"], + [to16Bit("café"), "16-bit with non-ascii but latin-1 characters"], + ["你好小圆面包", "16-bit, all BMP, all outside latin-1"], + ["🐱🏳️‍⚧️", "16-bit with many surrogate pairs"], + // TODO(@190n) handle these correctly + // ["\ud801", "unpaired high surrogate"], + // ["\udc02", "unpaired low surrogate"], + ]) { + console.log(`test napi_get_value_string on ${string} (${description})`); + for (const encoding of ["latin1", "utf8", "utf16"]) { + console.log(encoding); + const fn = nativeTests[`test_get_value_string_${encoding}`]; + fn(string); + } + } +}; + module.exports = nativeTests; diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index f7920b5f91..68a060bf55 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -182,6 +182,12 @@ describe("napi", () => { }); }); + describe("napi_get_value_string_*", () => { + it("behaves like node on edge cases", () => { + checkSameOutput("test_get_value_string", []); + }); + }); + it("#1288", async () => { const result = checkSameOutput("self", []); expect(result).toBe("hello world!"); diff --git a/test/napi/node-napi-tests/test/js-native-api/test_string/test.js b/test/napi/node-napi-tests/test/js-native-api/test_string/test.js index 04515a286c..7178d28cd9 100644 --- a/test/napi/node-napi-tests/test/js-native-api/test_string/test.js +++ b/test/napi/node-napi-tests/test/js-native-api/test_string/test.js @@ -39,8 +39,9 @@ const unicodeCases = [ function testLatin1Cases(str) { assert.strictEqual(test_string.TestLatin1(str), str); assert.strictEqual(test_string.TestLatin1AutoLength(str), str); - assert.strictEqual(test_string.TestLatin1External(str), str); - assert.strictEqual(test_string.TestLatin1ExternalAutoLength(str), str); + // BUN: skip these tests until we support making empty external strings + if (str.length > 0) assert.strictEqual(test_string.TestLatin1External(str), str); + if (str.length > 0) assert.strictEqual(test_string.TestLatin1ExternalAutoLength(str), str); assert.strictEqual(test_string.TestPropertyKeyLatin1(str), str); assert.strictEqual(test_string.TestPropertyKeyLatin1AutoLength(str), str); assert.strictEqual(test_string.Latin1Length(str), str.length); @@ -55,8 +56,9 @@ function testUnicodeCases(str, utf8Length, utf8InsufficientIdx) { assert.strictEqual(test_string.TestUtf16(str), str); assert.strictEqual(test_string.TestUtf8AutoLength(str), str); assert.strictEqual(test_string.TestUtf16AutoLength(str), str); - assert.strictEqual(test_string.TestUtf16External(str), str); - assert.strictEqual(test_string.TestUtf16ExternalAutoLength(str), str); + // BUN: skip these tests until we support making empty external strings + if (str.length > 0) assert.strictEqual(test_string.TestUtf16External(str), str); + if (str.length > 0) assert.strictEqual(test_string.TestUtf16ExternalAutoLength(str), str); assert.strictEqual(test_string.TestPropertyKeyUtf8(str), str); assert.strictEqual(test_string.TestPropertyKeyUtf8AutoLength(str), str); assert.strictEqual(test_string.TestPropertyKeyUtf16(str), str); diff --git a/test/napi/node-napi.test.ts b/test/napi/node-napi.test.ts index 327a02b203..1f78c2b8d1 100644 --- a/test/napi/node-napi.test.ts +++ b/test/napi/node-napi.test.ts @@ -12,8 +12,9 @@ const nodeApiTests = Array.from(new Glob("**/*.js").scanSync(nodeApiRoot)); // These js-native-api tests are known to fail and will be fixed in later PRs let failingJsNativeApiTests = [ - // Fails because Bun doesn't support creating empty external strings - "test_string/test.js", + // We skip certain parts of test_string/test.js because we don't support creating empty external + // strings. We don't skip the entire thing because the other tests are useful to check. + // "test_string/test.js", ]; // These are the tests from node-api that failed as of commit 83f536f4d, except for those that