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
This commit is contained in:
André Bargull 2024-03-19 13:56:45 +00:00
Родитель ae4cdfc2b9
Коммит fa04aeae99
6 изменённых файлов: 369 добавлений и 4 удалений

Просмотреть файл

@ -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")

Просмотреть файл

@ -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")) {

Просмотреть файл

@ -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") \

Просмотреть файл

@ -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;

Просмотреть файл

@ -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<TypedArrayObject>();
}
static bool IsUint8ArrayObject(HandleValue v) {
return IsTypedArrayObject(v) &&
v.toObject().as<TypedArrayObject>().type() == Scalar::Uint8;
}
/* static */
bool TypedArrayObject::ensureHasBuffer(JSContext* cx,
Handle<TypedArrayObject*> 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<uint8_t, FixedLengthTypedArrayObject::INLINE_BUFFER_LIMIT>;
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<JSString*> 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<JSString*> string(cx, args[0].toString());
// Step 2.
constexpr size_t maxLength = std::numeric_limits<size_t>::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<uint8_t>::fromLength(cx, resultLength);
if (!tarray) {
return false;
}
// Step 5.
auto target = SharedMem<uint8_t*>::unshared(tarray->dataPointerUnshared());
auto source = SharedMem<uint8_t*>::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<TypedArrayObject*> tarray(
cx, &args.thisv().toObject().as<TypedArrayObject>());
// Step 3.
if (!args.get(0).isString()) {
return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK,
args.get(0), nullptr, "not a string");
}
Rooted<JSString*> 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<uint8_t*>();
auto source = SharedMem<uint8_t*>::unshared(bytes.begin());
if (tarray->isSharedMemory()) {
SharedOps::podCopy(target, source, written);
} else {
UnsharedOps::podCopy(target, source, written);
}
// Step 14.
Rooted<PlainObject*> result(cx, NewPlainObject(cx));
if (!result) {
return false;
}
// Step 15.
Rooted<Value> readValue(cx, NumberValue(readLength));
if (!DefineDataProperty(cx, result, cx->names().read, readValue)) {
return false;
}
// Step 16.
Rooted<Value> 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<IsUint8ArrayObject, uint8array_setFromHex>(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<TypedArrayObject*> tarray(
cx, &args.thisv().toObject().as<TypedArrayObject>());
// 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<size_t>::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<char>::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<uint8_t*>();
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<IsUint8ArrayObject, uint8array_toHex>(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<NativeType>::createConstructor, \
TypedArrayObjectTemplate<NativeType>::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, \

Просмотреть файл

@ -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.