зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
ae4cdfc2b9
Коммит
fa04aeae99
|
@ -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.
|
||||
|
|
Загрузка…
Ссылка в новой задаче