From fa04aeae994002317423ef383f373a1eb4a75510 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Andr=C3=A9=20Bargull?= Date: Tue, 19 Mar 2024 13:56:45 +0000 Subject: [PATCH] Bug 1885337 - Part 1: Implement to/from hex methods. r=dminor Add preference for the proposal and implement to/from hex-string methods. The initial implementation doesn't yet try to optimise allocations. For example as a follow-up, we could directly allocate in the correct jemalloc arena instead of first creating an intermediate `js::Vector`. Differential Revision: https://phabricator.services.mozilla.com/D204636 --- js/public/friend/ErrorNumbers.msg | 2 + js/src/shell/js.cpp | 5 + js/src/vm/CommonPropertyNames.h | 8 + js/src/vm/JSObject.cpp | 20 +- js/src/vm/TypedArrayObject.cpp | 331 ++++++++++++++++++++++++++++++- modules/libpref/init/StaticPrefList.yaml | 7 + 6 files changed, 369 insertions(+), 4 deletions(-) diff --git a/js/public/friend/ErrorNumbers.msg b/js/public/friend/ErrorNumbers.msg index 12b54ee4d511..c39abcbfd07a 100644 --- a/js/public/friend/ErrorNumbers.msg +++ b/js/public/friend/ErrorNumbers.msg @@ -669,6 +669,8 @@ MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_OFFSET_MISALIGNED, 2, JSEXN_RANGEERR, "buffe MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_OFFSET_LENGTH_BOUNDS, 1, JSEXN_RANGEERR, "size of buffer is too small for {0}Array with byteOffset") MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_ARRAY_LENGTH_BOUNDS, 1, JSEXN_RANGEERR, "attempting to construct out-of-bounds {0}Array on ArrayBuffer") MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_TOO_LARGE, 1, JSEXN_RANGEERR, "{0}Array too large") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_HEX_STRING_LENGTH, 0, JSEXN_SYNTAXERR, "hex-string must have an even number of characters") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_HEX_DIGIT, 1, JSEXN_SYNTAXERR, "'{0}' is not a valid hex-digit") MSG_DEF(JSMSG_TYPED_ARRAY_CALL_OR_CONSTRUCT, 1, JSEXN_TYPEERR, "cannot directly {0} builtin %TypedArray%") MSG_DEF(JSMSG_NON_TYPED_ARRAY_RETURNED, 0, JSEXN_TYPEERR, "constructor didn't return TypedArray object") diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 736e106b863c..f90bd5d6e229 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -12038,6 +12038,8 @@ bool InitOptionParser(OptionParser& op) { !op.addBoolOption( '\0', "enable-arraybuffer-resizable", "Enable resizable ArrayBuffers and growable SharedArrayBuffers") || + !op.addBoolOption('\0', "enable-uint8array-base64", + "Enable Uint8Array base64/hex methods") || !op.addBoolOption('\0', "enable-top-level-await", "Enable top-level await") || !op.addBoolOption('\0', "enable-class-static-blocks", @@ -12426,6 +12428,9 @@ bool SetGlobalOptionsPreJSInit(const OptionParser& op) { if (op.getBoolOption("enable-symbols-as-weakmap-keys")) { JS::Prefs::setAtStartup_experimental_symbols_as_weakmap_keys(true); } + if (op.getBoolOption("enable-uint8array-base64")) { + JS::Prefs::setAtStartup_experimental_uint8array_base64(true); + } #endif if (op.getBoolOption("disable-weak-refs")) { diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index d4376ec6a447..21b903762b87 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -214,6 +214,8 @@ MACRO_(frame, "frame") \ MACRO_(from, "from") \ MACRO_(fromAsync, "fromAsync") \ + MACRO_(fromBase64, "fromBase64") \ + MACRO_(fromHex, "fromHex") \ MACRO_(fulfilled, "fulfilled") \ MACRO_(GatherAsyncParentCompletions, "GatherAsyncParentCompletions") \ MACRO_(gcCycleNumber, "gcCycleNumber") \ @@ -467,6 +469,7 @@ MACRO_(pull, "pull") \ MACRO_(quarter, "quarter") \ MACRO_(raw, "raw") \ + MACRO_(read, "read") \ MACRO_(reason, "reason") \ MACRO_(RegExp_String_Iterator_, "RegExp String Iterator") \ MACRO_(RegExp_prototype_Exec, "RegExp_prototype_Exec") \ @@ -503,6 +506,8 @@ MACRO_(SetConstructorInit, "SetConstructorInit") \ MACRO_(SetIsInlinableLargeFunction, "SetIsInlinableLargeFunction") \ MACRO_(Set_Iterator_, "Set Iterator") \ + MACRO_(setFromBase64, "setFromBase64") \ + MACRO_(setFromHex, "setFromHex") \ MACRO_(setPrototypeOf, "setPrototypeOf") \ MACRO_(shape, "shape") \ MACRO_(shared, "shared") \ @@ -540,7 +545,9 @@ MACRO_(timeStyle, "timeStyle") \ MACRO_(timeZone, "timeZone") \ MACRO_(timeZoneName, "timeZoneName") \ + MACRO_(toBase64, "toBase64") \ MACRO_(toGMTString, "toGMTString") \ + MACRO_(toHex, "toHex") \ MACRO_(toISOString, "toISOString") \ MACRO_(toJSON, "toJSON") \ MACRO_(toLocaleString, "toLocaleString") \ @@ -612,6 +619,7 @@ MACRO_(weeks, "weeks") \ MACRO_(while_, "while") \ MACRO_(with, "with") \ + MACRO_(written, "written") \ MACRO_(toReversed, "toReversed") \ MACRO_(toSorted, "toSorted") \ MACRO_(toSpliced, "toSpliced") \ diff --git a/js/src/vm/JSObject.cpp b/js/src/vm/JSObject.cpp index 549bb99d5d2d..167f082a4285 100644 --- a/js/src/vm/JSObject.cpp +++ b/js/src/vm/JSObject.cpp @@ -2215,9 +2215,7 @@ JS_PUBLIC_API bool js::ShouldIgnorePropertyDefinition(JSContext* cx, id == NameToId(cx->names().symmetricDifference))) { return true; } -#endif -#ifdef NIGHTLY_BUILD if (key == JSProto_ArrayBuffer && !JS::Prefs::arraybuffer_transfer() && (id == NameToId(cx->names().transfer) || id == NameToId(cx->names().transferToFixedLength) || @@ -2240,6 +2238,24 @@ JS_PUBLIC_API bool js::ShouldIgnorePropertyDefinition(JSContext* cx, id == NameToId(cx->names().grow))) { return true; } + + if (key == JSProto_Uint8Array && + !JS::Prefs::experimental_uint8array_base64() && + (id == NameToId(cx->names().setFromBase64) || + id == NameToId(cx->names().setFromHex) || + id == NameToId(cx->names().toBase64) || + id == NameToId(cx->names().toHex))) { + return true; + } + + // It's gently surprising that this is JSProto_Function, but the trick + // to realize is that this is a -constructor function-, not a function + // on the prototype; and the proto of the constructor is JSProto_Function. + if (key == JSProto_Function && !JS::Prefs::experimental_uint8array_base64() && + (id == NameToId(cx->names().fromBase64) || + id == NameToId(cx->names().fromHex))) { + return true; + } #endif return false; diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index e5b9d028e89c..6d9f14dd2ab0 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -9,6 +9,7 @@ #include "mozilla/FloatingPoint.h" #include "mozilla/IntegerTypeTraits.h" +#include "mozilla/Likely.h" #include "mozilla/PodOperations.h" #include "mozilla/TextUtils.h" @@ -39,6 +40,7 @@ #include "js/UniquePtr.h" #include "js/Wrapper.h" #include "util/DifferentialTesting.h" +#include "util/StringBuffer.h" #include "util/Text.h" #include "util/WindowsWrapper.h" #include "vm/ArrayBufferObject.h" @@ -114,6 +116,11 @@ static bool IsTypedArrayObject(HandleValue v) { return v.isObject() && v.toObject().is(); } +static bool IsUint8ArrayObject(HandleValue v) { + return IsTypedArrayObject(v) && + v.toObject().as().type() == Scalar::Uint8; +} + /* static */ bool TypedArrayObject::ensureHasBuffer(JSContext* cx, Handle typedArray) { @@ -2045,6 +2052,300 @@ bool TypedArrayObject::copyWithin(JSContext* cx, unsigned argc, Value* vp) { TypedArrayObject::copyWithin_impl>(cx, args); } +// Byte vector with large enough inline storage to allow constructing small +// typed arrays without extra heap allocations. +using ByteVector = + js::Vector; + +static UniqueChars QuoteString(JSContext* cx, char16_t ch) { + Sprinter sprinter(cx); + if (!sprinter.init()) { + return nullptr; + } + + StringEscape esc{}; + js::EscapePrinter ep(sprinter, esc); + ep.putChar(ch); + + return sprinter.release(); +} + +/** + * FromHex ( string [ , maxLength ] ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-fromhex + */ +static bool FromHex(JSContext* cx, Handle string, size_t maxLength, + ByteVector& bytes, size_t* readLength) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + size_t length = string->length(); + + // Step 3. + if (length % 2 != 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_HEX_STRING_LENGTH); + return false; + } + + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return false; + } + + // Step 4. (Not applicable in our implementation.) + MOZ_ASSERT(bytes.empty()); + + // Step 5. + size_t index = 0; + + // Step 6. + while (index < length && bytes.length() < maxLength) { + // Step 6.a. + char16_t c0 = linear->latin1OrTwoByteChar(index); + char16_t c1 = linear->latin1OrTwoByteChar(index + 1); + + // Step 6.b. + if (MOZ_UNLIKELY(!mozilla::IsAsciiHexDigit(c0) || + !mozilla::IsAsciiHexDigit(c1))) { + char16_t ch = !mozilla::IsAsciiHexDigit(c0) ? c0 : c1; + if (auto str = QuoteString(cx, ch)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_HEX_DIGIT, str.get()); + } + return false; + } + + // Step 6.c. + index += 2; + + // Step 6.d. + uint8_t byte = (mozilla::AsciiAlphanumericToNumber(c0) << 4) + + mozilla::AsciiAlphanumericToNumber(c1); + + // Step 6.e. + if (!bytes.append(byte)) { + return false; + } + } + + // Step 7. + *readLength = index; + return true; +} + +/** + * Uint8Array.fromHex ( string ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.fromhex + */ +static bool uint8array_fromHex(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!args.get(0).isString()) { + return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, + args.get(0), nullptr, "not a string"); + } + Rooted string(cx, args[0].toString()); + + // Step 2. + constexpr size_t maxLength = std::numeric_limits::max(); + ByteVector bytes(cx); + size_t unusedReadLength; + if (!FromHex(cx, string, maxLength, bytes, &unusedReadLength)) { + return false; + } + + // Step 3. + size_t resultLength = bytes.length(); + + // Step 4. + auto* tarray = + TypedArrayObjectTemplate::fromLength(cx, resultLength); + if (!tarray) { + return false; + } + + // Step 5. + auto target = SharedMem::unshared(tarray->dataPointerUnshared()); + auto source = SharedMem::unshared(bytes.begin()); + UnsharedOps::podCopy(target, source, resultLength); + + // Step 6. + args.rval().setObject(*tarray); + return true; +} + +/** + * Uint8Array.prototype.setFromHex ( string ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.setfromhex + */ +static bool uint8array_setFromHex(JSContext* cx, const CallArgs& args) { + Rooted tarray( + cx, &args.thisv().toObject().as()); + + // Step 3. + if (!args.get(0).isString()) { + return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, + args.get(0), nullptr, "not a string"); + } + Rooted string(cx, args[0].toString()); + + // Steps 4-6. + auto length = tarray->length(); + if (!length) { + ReportOutOfBounds(cx, tarray); + return false; + } + + // Step 7. + size_t maxLength = *length; + + // Steps 8-9. + ByteVector bytes(cx); + size_t readLength; + if (!FromHex(cx, string, maxLength, bytes, &readLength)) { + return false; + } + + // Step 10. + size_t written = bytes.length(); + + // Step 11. + // + // The underlying buffer has neither been detached nor shrunk. (It may have + // been grown when it's a growable shared buffer and a concurrent thread + // resized the buffer.) + MOZ_ASSERT(!tarray->hasDetachedBuffer()); + MOZ_ASSERT(tarray->length().valueOr(0) >= *length); + + // Step 12. + MOZ_ASSERT(written <= *length); + + // Step 13. (Inlined SetUint8ArrayBytes) + auto target = tarray->dataPointerEither().cast(); + auto source = SharedMem::unshared(bytes.begin()); + if (tarray->isSharedMemory()) { + SharedOps::podCopy(target, source, written); + } else { + UnsharedOps::podCopy(target, source, written); + } + + // Step 14. + Rooted result(cx, NewPlainObject(cx)); + if (!result) { + return false; + } + + // Step 15. + Rooted readValue(cx, NumberValue(readLength)); + if (!DefineDataProperty(cx, result, cx->names().read, readValue)) { + return false; + } + + // Step 16. + Rooted writtenValue(cx, NumberValue(written)); + if (!DefineDataProperty(cx, result, cx->names().written, writtenValue)) { + return false; + } + + // Step 17. + args.rval().setObject(*result); + return true; +} + +/** + * Uint8Array.prototype.setFromHex ( string ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.setfromhex + */ +static bool uint8array_setFromHex(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + return CallNonGenericMethod(cx, + args); +} + +/** + * Uint8Array.prototype.toHex ( ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tohex + */ +static bool uint8array_toHex(JSContext* cx, const CallArgs& args) { + Rooted tarray( + cx, &args.thisv().toObject().as()); + + // Step 3. (Partial) + auto length = tarray->length(); + if (!length) { + ReportOutOfBounds(cx, tarray); + return false; + } + + // |length| is limited by |ByteLengthLimit|, which ensures that multiplying it + // by two won't overflow. + static_assert(TypedArrayObject::ByteLengthLimit <= + std::numeric_limits::max() / 2); + MOZ_ASSERT(*length <= TypedArrayObject::ByteLengthLimit); + + // Compute the output string length. Each byte is encoded as two characters, + // so the output length is exactly twice as large as |length|. + size_t outLength = *length * 2; + if (outLength > JSString::MAX_LENGTH) { + ReportAllocationOverflow(cx); + return false; + } + + // Step 4. + JSStringBuilder sb(cx); + if (!sb.reserve(outLength)) { + return false; + } + + // NB: Lower case hex digits. + static constexpr char HexDigits[] = "0123456789abcdef"; + static_assert(std::char_traits::length(HexDigits) == 16); + + // Steps 3 and 5. + // + // Our implementation directly converts the bytes to their string + // representation instead of first collecting them into an intermediate list. + auto data = tarray->dataPointerEither().cast(); + for (size_t index = 0; index < *length; index++) { + auto byte = jit::AtomicOperations::loadSafeWhenRacy(data + index); + + sb.infallibleAppend(HexDigits[byte >> 4]); + sb.infallibleAppend(HexDigits[byte & 0xf]); + } + + MOZ_ASSERT(sb.length() == outLength, "all characters were written"); + + // Step 6. + auto* str = sb.finishString(); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Uint8Array.prototype.toHex ( ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tohex + */ +static bool uint8array_toHex(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + return CallNonGenericMethod(cx, args); +} + /* static */ const JSFunctionSpec TypedArrayObject::protoFunctions[] = { JS_SELF_HOSTED_FN("subarray", "TypedArraySubarray", 2, 0), JS_FN("set", TypedArrayObject::set, 1, 0), @@ -2363,15 +2664,41 @@ static const JSPropertySpec #undef IMPL_TYPED_ARRAY_PROPERTIES }; +static const JSFunctionSpec uint8array_static_methods[] = { + JS_FN("fromHex", uint8array_fromHex, 1, 0), + JS_FS_END, +}; + +static const JSFunctionSpec uint8array_methods[] = { + JS_FN("setFromHex", uint8array_setFromHex, 1, 0), + JS_FN("toHex", uint8array_toHex, 0, 0), + JS_FS_END, +}; + +static constexpr const JSFunctionSpec* TypedArrayStaticMethods( + Scalar::Type type) { + if (type == Scalar::Uint8) { + return uint8array_static_methods; + } + return nullptr; +} + +static constexpr const JSFunctionSpec* TypedArrayMethods(Scalar::Type type) { + if (type == Scalar::Uint8) { + return uint8array_methods; + } + return nullptr; +} + static const ClassSpec TypedArrayObjectClassSpecs[Scalar::MaxTypedArrayViewType] = { #define IMPL_TYPED_ARRAY_CLASS_SPEC(ExternalType, NativeType, Name) \ { \ TypedArrayObjectTemplate::createConstructor, \ TypedArrayObjectTemplate::createPrototype, \ - nullptr, \ + TypedArrayStaticMethods(Scalar::Type::Name), \ static_prototype_properties[Scalar::Type::Name], \ - nullptr, \ + TypedArrayMethods(Scalar::Type::Name), \ static_prototype_properties[Scalar::Type::Name], \ nullptr, \ JSProto_TypedArray, \ diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index a73b73b7e827..0d8301eda66d 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -7627,6 +7627,13 @@ value: false mirror: always set_spidermonkey_pref: startup + + # Experimental support for Uint8Array base64/hex in JavaScript. +- name: javascript.options.experimental.uint8array_base64 + type: bool + value: false + mirror: always + set_spidermonkey_pref: startup #endif // NIGHTLY_BUILD # Experimental support for ArrayBuffer.prototype.transfer{,ToFixedLength}() in JavaScript. -- 2.11.4.GIT