summaryrefslogtreecommitdiffstats
path: root/dom/indexedDB/Key.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/indexedDB/Key.cpp
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/indexedDB/Key.cpp')
-rw-r--r--dom/indexedDB/Key.cpp962
1 files changed, 962 insertions, 0 deletions
diff --git a/dom/indexedDB/Key.cpp b/dom/indexedDB/Key.cpp
new file mode 100644
index 0000000000..8b82e70e37
--- /dev/null
+++ b/dom/indexedDB/Key.cpp
@@ -0,0 +1,962 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Key.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <stdint.h> // for UINT32_MAX, uintptr_t
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/ArrayBuffer.h" // JS::{IsArrayBufferObject,NewArrayBuffer{,WithContents},GetArrayBufferLengthAndData}
+#include "js/Date.h"
+#include "js/experimental/TypedData.h" // JS_IsArrayBufferViewObject, JS_GetObjectAsArrayBufferView
+#include "js/MemoryFunctions.h"
+#include "js/Object.h" // JS::GetBuiltinClass
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_GetProperty, JS_GetPropertyById, JS_HasOwnProperty, JS_HasOwnPropertyById
+#include "js/Value.h"
+#include "jsfriendapi.h"
+#include "mozilla/Casting.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/intl/Collator.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ReverseIterator.h"
+#include "mozilla/dom/indexedDB/IDBResult.h"
+#include "mozilla/dom/indexedDB/Key.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozIStorageStatement.h"
+#include "mozIStorageValueArray.h"
+#include "nsJSUtils.h"
+#include "nsTStringRepr.h"
+#include "ReportInternalError.h"
+#include "xpcpublic.h"
+
+namespace mozilla::dom::indexedDB {
+
+namespace {
+// Implementation of the array branch of step 3 of
+// https://w3c.github.io/IndexedDB/#convert-value-to-key
+template <typename ArrayConversionPolicy>
+IDBResult<Ok, IDBSpecialValue::Invalid> ConvertArrayValueToKey(
+ JSContext* const aCx, JS::Handle<JSObject*> aObject,
+ ArrayConversionPolicy&& aPolicy) {
+ // 1. Let `len` be ? ToLength( ? Get(`input`, "length")).
+ uint32_t len;
+ if (!JS::GetArrayLength(aCx, aObject, &len)) {
+ return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR));
+ }
+
+ // 2. Add `input` to `seen`.
+ aPolicy.AddToSeenSet(aCx, aObject);
+
+ // 3. Let `keys` be a new empty list.
+ aPolicy.BeginSubkeyList();
+
+ // 4. Let `index` be 0.
+ uint32_t index = 0;
+
+ // 5. While `index` is less than `len`:
+ while (index < len) {
+ JS::Rooted<JS::PropertyKey> indexId(aCx);
+ if (!JS_IndexToId(aCx, index, &indexId)) {
+ return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR));
+ }
+
+ // 1. Let `hop` be ? HasOwnProperty(`input`, `index`).
+ bool hop;
+ if (!JS_HasOwnPropertyById(aCx, aObject, indexId, &hop)) {
+ return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR));
+ }
+
+ // 2. If `hop` is false, return invalid.
+ if (!hop) {
+ return Err(IDBError(SpecialValues::Invalid));
+ }
+
+ // 3. Let `entry` be ? Get(`input`, `index`).
+ JS::Rooted<JS::Value> entry(aCx);
+ if (!JS_GetPropertyById(aCx, aObject, indexId, &entry)) {
+ return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR));
+ }
+
+ // 4. Let `key` be the result of running the steps to convert a value to a
+ // key with arguments `entry` and `seen`.
+ // 5. ReturnIfAbrupt(`key`).
+ // 6. If `key` is invalid abort these steps and return invalid.
+ // 7. Append `key` to `keys`.
+ auto result = aPolicy.ConvertSubkey(aCx, entry, index);
+ if (result.isErr()) {
+ return result;
+ }
+
+ // 8. Increase `index` by 1.
+ index += 1;
+ }
+
+ // 6. Return a new array key with value `keys`.
+ aPolicy.EndSubkeyList();
+ return Ok();
+}
+} // namespace
+
+/*
+ Here's how we encode keys:
+
+ Basic strategy is the following
+
+ Numbers: 0x10 n n n n n n n n ("n"s are encoded 64bit float)
+ Dates: 0x20 n n n n n n n n ("n"s are encoded 64bit float)
+ Strings: 0x30 s s s ... 0 ("s"s are encoded unicode bytes)
+ Binaries: 0x40 s s s ... 0 ("s"s are encoded unicode bytes)
+ Arrays: 0x50 i i i ... 0 ("i"s are encoded array items)
+
+
+ When encoding floats, 64bit IEEE 754 are almost sortable, except that
+ positive sort lower than negative, and negative sort descending. So we use
+ the following encoding:
+
+ value < 0 ?
+ (-to64bitInt(value)) :
+ (to64bitInt(value) | 0x8000000000000000)
+
+
+ When encoding strings, we use variable-size encoding per the following table
+
+ Chars 0 - 7E are encoded as 0xxxxxxx with 1 added
+ Chars 7F - (3FFF+7F) are encoded as 10xxxxxx xxxxxxxx with 7F
+ subtracted
+ Chars (3FFF+80) - FFFF are encoded as 11xxxxxx xxxxxxxx xx000000
+
+ This ensures that the first byte is never encoded as 0, which means that the
+ string terminator (per basic-strategy table) sorts before any character.
+ The reason that (3FFF+80) - FFFF is encoded "shifted up" 6 bits is to maximize
+ the chance that the last character is 0. See below for why.
+
+ When encoding binaries, the algorithm is the same to how strings are encoded.
+ Since each octet in binary is in the range of [0-255], it'll take 1 to 2
+ encoded unicode bytes.
+
+ When encoding Arrays, we use an additional trick. Rather than adding a byte
+ containing the value 0x50 to indicate type, we instead add 0x50 to the next
+ byte. This is usually the byte containing the type of the first item in the
+ array. So simple examples are
+
+ ["foo"] 0x80 s s s 0 0 // 0x80 is 0x30 + 0x50
+ [1, 2] 0x60 n n n n n n n n 1 n n n n n n n n 0 // 0x60 is 0x10 + 0x50
+
+ Whe do this iteratively if the first item in the array is also an array
+
+ [["foo"]] 0xA0 s s s 0 0 0
+
+ However, to avoid overflow in the byte, we only do this 3 times. If the first
+ item in an array is an array, and that array also has an array as first item,
+ we simply write out the total value accumulated so far and then follow the
+ "normal" rules.
+
+ [[["foo"]]] 0xF0 0x30 s s s 0 0 0 0
+
+ There is another edge case that can happen though, which is that the array
+ doesn't have a first item to which we can add 0x50 to the type. Instead the
+ next byte would normally be the array terminator (per basic-strategy table)
+ so we simply add the 0x50 there.
+
+ [[]] 0xA0 0 // 0xA0 is 0x50 + 0x50 + 0
+ [] 0x50 // 0x50 is 0x50 + 0
+ [[], "foo"] 0xA0 0x30 s s s 0 0 // 0xA0 is 0x50 + 0x50 + 0
+
+ Note that the max-3-times rule kicks in before we get a chance to add to the
+ array terminator
+
+ [[[]]] 0xF0 0 0 0 // 0xF0 is 0x50 + 0x50 + 0x50
+
+ As a final optimization we do a post-encoding step which drops all 0s at the
+ end of the encoded buffer.
+
+ "foo" // 0x30 s s s
+ 1 // 0x10 bf f0
+ ["a", "b"] // 0x80 s 0 0x30 s
+ [1, 2] // 0x60 bf f0 0 0 0 0 0 0 0x10 c0
+ [[]] // 0x80
+*/
+
+Result<Ok, nsresult> Key::SetFromString(const nsAString& aString) {
+ mBuffer.Truncate();
+ auto result = EncodeString(aString, 0);
+ if (result.isOk()) {
+ TrimBuffer();
+ }
+ return result;
+}
+
+// |aPos| should point to the type indicator.
+// The returned length doesn't include the type indicator
+// or the terminator.
+// static
+uint32_t Key::LengthOfEncodedBinary(const EncodedDataType* aPos,
+ const EncodedDataType* aEnd) {
+ MOZ_ASSERT(*aPos % Key::eMaxType == Key::eBinary, "Don't call me!");
+
+ const auto* iter = aPos + 1;
+ for (; iter < aEnd && *iter != eTerminator; ++iter) {
+ if (*iter & 0x80) {
+ ++iter;
+ // XXX if iter == aEnd now, we got a bad enconding, should we report that
+ // also in non-debug builds?
+ MOZ_ASSERT(iter < aEnd);
+ }
+ }
+
+ return iter - aPos - 1;
+}
+
+Result<Key, nsresult> Key::ToLocaleAwareKey(const nsCString& aLocale) const {
+ Key res;
+
+ if (IsUnset()) {
+ return res;
+ }
+
+ if (IsFloat() || IsDate() || IsBinary()) {
+ res.mBuffer = mBuffer;
+ return res;
+ }
+
+ auto* it = BufferStart();
+ auto* const end = BufferEnd();
+
+ // First we do a pass and see if there are any strings in this key. We only
+ // want to copy/decode when necessary.
+ bool canShareBuffers = true;
+ while (it < end) {
+ const auto type = *it % eMaxType;
+ if (type == eTerminator) {
+ it++;
+ } else if (type == eFloat || type == eDate) {
+ it++;
+ it += std::min(sizeof(uint64_t), size_t(end - it));
+ } else if (type == eBinary) {
+ // skip all binary data
+ const auto binaryLength = LengthOfEncodedBinary(it, end);
+ it++;
+ it += binaryLength;
+ } else {
+ // We have a string!
+ canShareBuffers = false;
+ break;
+ }
+ }
+
+ if (canShareBuffers) {
+ MOZ_ASSERT(it == end);
+ res.mBuffer = mBuffer;
+ return res;
+ }
+
+ if (!res.mBuffer.SetCapacity(mBuffer.Length(), fallible)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ // A string was found, so we need to copy the data we've read so far
+ auto* const start = BufferStart();
+ if (it > start) {
+ char* buffer;
+ MOZ_ALWAYS_TRUE(res.mBuffer.GetMutableData(&buffer, it - start));
+ std::copy(start, it, buffer);
+ }
+
+ // Now continue decoding
+ while (it < end) {
+ char* buffer;
+ const size_t oldLen = res.mBuffer.Length();
+ const auto type = *it % eMaxType;
+
+ // Note: Do not modify |it| before calling |updateBufferAndIter|;
+ // |byteCount| doesn't include the type indicator
+ const auto updateBufferAndIter = [&](size_t byteCount) -> bool {
+ if (!res.mBuffer.GetMutableData(&buffer, oldLen + 1 + byteCount)) {
+ return false;
+ }
+ buffer += oldLen;
+
+ // should also copy the type indicator at the begining
+ std::copy_n(it, byteCount + 1, buffer);
+ it += (byteCount + 1);
+ return true;
+ };
+
+ if (type == eTerminator) {
+ // Copy array TypeID and terminator from raw key
+ if (!updateBufferAndIter(0)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ } else if (type == eFloat || type == eDate) {
+ // Copy number from raw key
+ const size_t byteCount = std::min(sizeof(uint64_t), size_t(end - it - 1));
+
+ if (!updateBufferAndIter(byteCount)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ } else if (type == eBinary) {
+ // skip all binary data
+ const auto binaryLength = LengthOfEncodedBinary(it, end);
+
+ if (!updateBufferAndIter(binaryLength)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ } else {
+ // Decode string and reencode
+ const uint8_t typeOffset = *it - eString;
+ MOZ_ASSERT((typeOffset % eArray == 0) && (typeOffset / eArray <= 2));
+
+ auto str = DecodeString(it, end);
+ auto result = res.EncodeLocaleString(str, typeOffset, aLocale);
+ if (NS_WARN_IF(result.isErr())) {
+ return result.propagateErr();
+ }
+ }
+ }
+ res.TrimBuffer();
+ return res;
+}
+
+class MOZ_STACK_CLASS Key::ArrayValueEncoder final {
+ public:
+ ArrayValueEncoder(Key& aKey, const uint8_t aTypeOffset,
+ const uint16_t aRecursionDepth)
+ : mKey(aKey),
+ mTypeOffset(aTypeOffset),
+ mRecursionDepth(aRecursionDepth) {}
+
+ void AddToSeenSet(JSContext* const aCx, JS::Handle<JSObject*>) {
+ ++mRecursionDepth;
+ }
+
+ void BeginSubkeyList() {
+ mTypeOffset += Key::eMaxType;
+ if (mTypeOffset == eMaxType * kMaxArrayCollapse) {
+ mKey.mBuffer.Append(mTypeOffset);
+ mTypeOffset = 0;
+ }
+ MOZ_ASSERT(mTypeOffset % eMaxType == 0,
+ "Current type offset must indicate beginning of array");
+ MOZ_ASSERT(mTypeOffset < eMaxType * kMaxArrayCollapse);
+ }
+
+ IDBResult<Ok, IDBSpecialValue::Invalid> ConvertSubkey(
+ JSContext* const aCx, JS::Handle<JS::Value> aEntry,
+ const uint32_t aIndex) {
+ auto result =
+ mKey.EncodeJSValInternal(aCx, aEntry, mTypeOffset, mRecursionDepth);
+ mTypeOffset = 0;
+ return result;
+ }
+
+ void EndSubkeyList() const { mKey.mBuffer.Append(eTerminator + mTypeOffset); }
+
+ private:
+ Key& mKey;
+ uint8_t mTypeOffset;
+ uint16_t mRecursionDepth;
+};
+
+// Implements the following algorithm:
+// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-key
+IDBResult<Ok, IDBSpecialValue::Invalid> Key::EncodeJSValInternal(
+ JSContext* const aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset,
+ const uint16_t aRecursionDepth) {
+ static_assert(eMaxType * kMaxArrayCollapse < 256, "Unable to encode jsvals.");
+
+ // 1. If `seen` was not given, let `seen` be a new empty set.
+ // 2. If `input` is in `seen` return invalid.
+ // Note: we replace this check with a simple recursion depth check.
+ if (NS_WARN_IF(aRecursionDepth == kMaxRecursionDepth)) {
+ return Err(IDBError(SpecialValues::Invalid));
+ }
+
+ // 3. Jump to the appropriate step below:
+ // Note: some cases appear out of order to make the implementation more
+ // straightforward. This shouldn't affect observable behavior.
+
+ // If Type(`input`) is Number
+ if (aVal.isNumber()) {
+ const auto number = aVal.toNumber();
+
+ // 1. If `input` is NaN then return invalid.
+ if (std::isnan(number)) {
+ return Err(IDBError(SpecialValues::Invalid));
+ }
+
+ // 2. Otherwise, return a new key with type `number` and value `input`.
+ return EncodeNumber(number, eFloat + aTypeOffset);
+ }
+
+ // If Type(`input`) is String
+ if (aVal.isString()) {
+ // 1. Return a new key with type `string` and value `input`.
+ nsAutoJSString string;
+ if (!string.init(aCx, aVal)) {
+ IDB_REPORT_INTERNAL_ERR();
+ return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR));
+ }
+ return EncodeString(string, aTypeOffset);
+ }
+
+ if (aVal.isObject()) {
+ JS::Rooted<JSObject*> object(aCx, &aVal.toObject());
+
+ js::ESClass builtinClass;
+ if (!JS::GetBuiltinClass(aCx, object, &builtinClass)) {
+ IDB_REPORT_INTERNAL_ERR();
+ return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR));
+ }
+
+ // If `input` is a Date (has a [[DateValue]] internal slot)
+ if (builtinClass == js::ESClass::Date) {
+ // 1. Let `ms` be the value of `input`’s [[DateValue]] internal slot.
+ double ms;
+ if (!js::DateGetMsecSinceEpoch(aCx, object, &ms)) {
+ IDB_REPORT_INTERNAL_ERR();
+ return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR));
+ }
+
+ // 2. If `ms` is NaN then return invalid.
+ if (std::isnan(ms)) {
+ return Err(IDBError(SpecialValues::Invalid));
+ }
+
+ // 3. Otherwise, return a new key with type `date` and value `ms`.
+ return EncodeNumber(ms, eDate + aTypeOffset);
+ }
+
+ // If `input` is a buffer source type
+ if (JS::IsArrayBufferObject(object) || JS_IsArrayBufferViewObject(object)) {
+ const bool isViewObject = JS_IsArrayBufferViewObject(object);
+ return EncodeBinary(object, isViewObject, aTypeOffset);
+ }
+
+ // If IsArray(`input`)
+ if (builtinClass == js::ESClass::Array) {
+ return ConvertArrayValueToKey(
+ aCx, object, ArrayValueEncoder{*this, aTypeOffset, aRecursionDepth});
+ }
+ }
+
+ // Otherwise
+ // Return invalid.
+ return Err(IDBError(SpecialValues::Invalid));
+}
+
+// static
+nsresult Key::DecodeJSValInternal(const EncodedDataType*& aPos,
+ const EncodedDataType* aEnd, JSContext* aCx,
+ uint8_t aTypeOffset,
+ JS::MutableHandle<JS::Value> aVal,
+ uint16_t aRecursionDepth) {
+ if (NS_WARN_IF(aRecursionDepth == kMaxRecursionDepth)) {
+ return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
+ }
+
+ if (*aPos - aTypeOffset >= eArray) {
+ JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, 0));
+ if (!array) {
+ NS_WARNING("Failed to make array!");
+ IDB_REPORT_INTERNAL_ERR();
+ return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+ }
+
+ aTypeOffset += eMaxType;
+
+ if (aTypeOffset == eMaxType * kMaxArrayCollapse) {
+ ++aPos;
+ aTypeOffset = 0;
+ }
+
+ uint32_t index = 0;
+ JS::Rooted<JS::Value> val(aCx);
+ while (aPos < aEnd && *aPos - aTypeOffset != eTerminator) {
+ QM_TRY(MOZ_TO_RESULT(DecodeJSValInternal(aPos, aEnd, aCx, aTypeOffset,
+ &val, aRecursionDepth + 1)));
+
+ aTypeOffset = 0;
+
+ if (!JS_DefineElement(aCx, array, index++, val, JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to set array element!");
+ IDB_REPORT_INTERNAL_ERR();
+ return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+ }
+ }
+
+ NS_ASSERTION(aPos >= aEnd || (*aPos % eMaxType) == eTerminator,
+ "Should have found end-of-array marker");
+ ++aPos;
+
+ aVal.setObject(*array);
+ } else if (*aPos - aTypeOffset == eString) {
+ auto key = DecodeString(aPos, aEnd);
+ if (!xpc::StringToJsval(aCx, key, aVal)) {
+ IDB_REPORT_INTERNAL_ERR();
+ return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+ }
+ } else if (*aPos - aTypeOffset == eDate) {
+ double msec = static_cast<double>(DecodeNumber(aPos, aEnd));
+ JS::ClippedTime time = JS::TimeClip(msec);
+ MOZ_ASSERT(msec == time.toDouble(),
+ "encoding from a Date object not containing an invalid date "
+ "means we should always have clipped values");
+ JSObject* date = JS::NewDateObject(aCx, time);
+ if (!date) {
+ IDB_WARNING("Failed to make date!");
+ return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+ }
+
+ aVal.setObject(*date);
+ } else if (*aPos - aTypeOffset == eFloat) {
+ aVal.setDouble(DecodeNumber(aPos, aEnd));
+ } else if (*aPos - aTypeOffset == eBinary) {
+ JSObject* binary = DecodeBinary(aPos, aEnd, aCx);
+ if (!binary) {
+ IDB_REPORT_INTERNAL_ERR();
+ return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+ }
+
+ aVal.setObject(*binary);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Unknown key type!");
+ }
+
+ return NS_OK;
+}
+
+#define ONE_BYTE_LIMIT 0x7E
+#define TWO_BYTE_LIMIT (0x3FFF + 0x7F)
+
+#define ONE_BYTE_ADJUST 1
+#define TWO_BYTE_ADJUST (-0x7F)
+#define THREE_BYTE_SHIFT 6
+
+IDBResult<Ok, IDBSpecialValue::Invalid> Key::EncodeJSVal(
+ JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset) {
+ return EncodeJSValInternal(aCx, aVal, aTypeOffset, 0);
+}
+
+Result<Ok, nsresult> Key::EncodeString(const nsAString& aString,
+ uint8_t aTypeOffset) {
+ return EncodeString(Span{aString}, aTypeOffset);
+}
+
+template <typename T>
+Result<Ok, nsresult> Key::EncodeString(const Span<const T> aInput,
+ uint8_t aTypeOffset) {
+ return EncodeAsString(aInput, eString + aTypeOffset);
+}
+
+// nsCString maximum length is limited by INT32_MAX.
+// XXX: We probably want to enforce even shorter keys, though.
+#define KEY_MAXIMUM_BUFFER_LENGTH \
+ ::mozilla::detail::nsTStringLengthStorage<char>::kMax
+
+template <typename T>
+Result<Ok, nsresult> Key::EncodeAsString(const Span<const T> aInput,
+ uint8_t aType) {
+ // Please note that the input buffer can either be based on two-byte UTF-16
+ // values or on arbitrary single byte binary values. Only the first case
+ // needs to account for the TWO_BYTE_LIMIT of UTF-8.
+ // First we measure how long the encoded string will be.
+
+ // The 2 is for initial aType and trailing 0. We'll compensate for multi-byte
+ // chars below.
+ size_t size = 2;
+
+ // We construct a range over the raw pointers here because this loop is
+ // time-critical.
+ // XXX It might be good to encapsulate this in some function to make it less
+ // error-prone and more expressive.
+ const auto inputRange = mozilla::detail::IteratorRange(
+ aInput.Elements(), aInput.Elements() + aInput.Length());
+
+ size_t payloadSize = aInput.Length();
+ bool anyMultibyte = false;
+ for (const T val : inputRange) {
+ if (val > ONE_BYTE_LIMIT) {
+ anyMultibyte = true;
+ payloadSize += char16_t(val) > TWO_BYTE_LIMIT ? 2 : 1;
+ if (payloadSize > KEY_MAXIMUM_BUFFER_LENGTH) {
+ return Err(NS_ERROR_DOM_INDEXEDDB_KEY_ERR);
+ }
+ }
+ }
+
+ size += payloadSize;
+
+ // Now we allocate memory for the new size
+ size_t oldLen = mBuffer.Length();
+ size += oldLen;
+
+ if (size > KEY_MAXIMUM_BUFFER_LENGTH) {
+ return Err(NS_ERROR_DOM_INDEXEDDB_KEY_ERR);
+ }
+
+ char* buffer;
+ if (!mBuffer.GetMutableData(&buffer, size)) {
+ IDB_REPORT_INTERNAL_ERR();
+ return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+ }
+ buffer += oldLen;
+
+ // Write type marker
+ *(buffer++) = aType;
+
+ // Encode string
+ if (anyMultibyte) {
+ for (const auto val : inputRange) {
+ if (val <= ONE_BYTE_LIMIT) {
+ *(buffer++) = val + ONE_BYTE_ADJUST;
+ } else if (char16_t(val) <= TWO_BYTE_LIMIT) {
+ char16_t c = char16_t(val) + TWO_BYTE_ADJUST + 0x8000;
+ *(buffer++) = (char)(c >> 8);
+ *(buffer++) = (char)(c & 0xFF);
+ } else {
+ uint32_t c = (uint32_t(val) << THREE_BYTE_SHIFT) | 0x00C00000;
+ *(buffer++) = (char)(c >> 16);
+ *(buffer++) = (char)(c >> 8);
+ *(buffer++) = (char)c;
+ }
+ }
+ } else {
+ // Optimization for the case where there are no multibyte characters.
+ // This is ca. 13 resp. 5.8 times faster than the non-optimized version in
+ // an -O2 build: https://quick-bench.com/q/v1oBpLGifs-3w_pkZG8alVSWVAw, for
+ // the T==uint8_t resp. T==char16_t cases (for the char16_t case, copying
+ // and then adjusting could even be slightly faster, but then we would need
+ // another case distinction here)
+ size_t inputLen = std::distance(inputRange.cbegin(), inputRange.cend());
+ MOZ_ASSERT(inputLen == payloadSize);
+ std::transform(inputRange.cbegin(), inputRange.cend(), buffer,
+ [](auto value) { return value + ONE_BYTE_ADJUST; });
+ buffer += inputLen;
+ }
+
+ // Write end marker
+ *(buffer++) = eTerminator;
+
+ NS_ASSERTION(buffer == mBuffer.EndReading(), "Wrote wrong number of bytes");
+
+ return Ok();
+}
+
+Result<Ok, nsresult> Key::EncodeLocaleString(const nsAString& aString,
+ uint8_t aTypeOffset,
+ const nsCString& aLocale) {
+ const int length = aString.Length();
+ if (length == 0) {
+ return Ok();
+ }
+
+ auto collResult = intl::Collator::TryCreate(aLocale.get());
+ if (collResult.isErr()) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ auto collator = collResult.unwrap();
+ MOZ_ASSERT(collator);
+
+ AutoTArray<uint8_t, 128> keyBuffer;
+ MOZ_TRY(collator->GetSortKey(Span{aString}, keyBuffer)
+ .mapErr([](intl::ICUError icuError) {
+ return icuError == intl::ICUError::OutOfMemory
+ ? NS_ERROR_OUT_OF_MEMORY
+ : NS_ERROR_FAILURE;
+ }));
+
+ size_t sortKeyLength = keyBuffer.Length();
+ return EncodeString(Span{keyBuffer}.AsConst().First(sortKeyLength),
+ aTypeOffset);
+}
+
+// static
+nsresult Key::DecodeJSVal(const EncodedDataType*& aPos,
+ const EncodedDataType* aEnd, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aVal) {
+ return DecodeJSValInternal(aPos, aEnd, aCx, 0, aVal, 0);
+}
+
+// static
+template <typename T>
+uint32_t Key::CalcDecodedStringySize(
+ const EncodedDataType* const aBegin, const EncodedDataType* const aEnd,
+ const EncodedDataType** aOutEncodedSectionEnd) {
+ static_assert(sizeof(T) <= 2,
+ "Only implemented for 1 and 2 byte decoded types");
+ uint32_t decodedSize = 0;
+ auto* iter = aBegin;
+ for (; iter < aEnd && *iter != eTerminator; ++iter) {
+ if (*iter & 0x80) {
+ iter += (sizeof(T) > 1 && (*iter & 0x40)) ? 2 : 1;
+ }
+ ++decodedSize;
+ }
+ *aOutEncodedSectionEnd = std::min(aEnd, iter);
+ return decodedSize;
+}
+
+// static
+template <typename T>
+void Key::DecodeAsStringy(const EncodedDataType* const aEncodedSectionBegin,
+ const EncodedDataType* const aEncodedSectionEnd,
+ const uint32_t aDecodedLength, T* const aOut) {
+ static_assert(sizeof(T) <= 2,
+ "Only implemented for 1 and 2 byte decoded types");
+ T* decodedPos = aOut;
+ for (const EncodedDataType* iter = aEncodedSectionBegin;
+ iter < aEncodedSectionEnd;) {
+ if (!(*iter & 0x80)) {
+ *decodedPos = *(iter++) - ONE_BYTE_ADJUST;
+ } else if (sizeof(T) == 1 || !(*iter & 0x40)) {
+ auto c = static_cast<uint16_t>(*(iter++)) << 8;
+ if (iter < aEncodedSectionEnd) {
+ c |= *(iter++);
+ }
+ *decodedPos = static_cast<T>(c - TWO_BYTE_ADJUST - 0x8000);
+ } else if (sizeof(T) > 1) {
+ auto c = static_cast<uint32_t>(*(iter++)) << (16 - THREE_BYTE_SHIFT);
+ if (iter < aEncodedSectionEnd) {
+ c |= static_cast<uint32_t>(*(iter++)) << (8 - THREE_BYTE_SHIFT);
+ }
+ if (iter < aEncodedSectionEnd) {
+ c |= *(iter++) >> THREE_BYTE_SHIFT;
+ }
+ *decodedPos = static_cast<T>(c);
+ }
+ ++decodedPos;
+ }
+
+ MOZ_ASSERT(static_cast<uint32_t>(decodedPos - aOut) == aDecodedLength,
+ "Should have written the whole decoded area");
+}
+
+// static
+template <Key::EncodedDataType TypeMask, typename T, typename AcquireBuffer,
+ typename AcquireEmpty>
+void Key::DecodeStringy(const EncodedDataType*& aPos,
+ const EncodedDataType* aEnd,
+ const AcquireBuffer& acquireBuffer,
+ const AcquireEmpty& acquireEmpty) {
+ NS_ASSERTION(*aPos % eMaxType == TypeMask, "Don't call me!");
+
+ // First measure how big the decoded stringy data will be.
+ const EncodedDataType* const encodedSectionBegin = aPos + 1;
+ const EncodedDataType* encodedSectionEnd;
+ // decodedLength does not include the terminating 0 (in case of a string)
+ const uint32_t decodedLength =
+ CalcDecodedStringySize<T>(encodedSectionBegin, aEnd, &encodedSectionEnd);
+ aPos = encodedSectionEnd + 1;
+
+ if (!decodedLength) {
+ acquireEmpty();
+ return;
+ }
+
+ T* out;
+ if (!acquireBuffer(&out, decodedLength)) {
+ return;
+ }
+
+ DecodeAsStringy(encodedSectionBegin, encodedSectionEnd, decodedLength, out);
+}
+
+// static
+nsAutoString Key::DecodeString(const EncodedDataType*& aPos,
+ const EncodedDataType* const aEnd) {
+ nsAutoString res;
+ DecodeStringy<eString, char16_t>(
+ aPos, aEnd,
+ [&res](char16_t** out, uint32_t decodedLength) {
+ return 0 != res.GetMutableData(out, decodedLength);
+ },
+ [] {});
+ return res;
+}
+
+Result<Ok, nsresult> Key::EncodeNumber(double aFloat, uint8_t aType) {
+ // Allocate memory for the new size
+ size_t oldLen = mBuffer.Length();
+ size_t newLen = oldLen + 1 + sizeof(double);
+ if (newLen > KEY_MAXIMUM_BUFFER_LENGTH) {
+ return Err(NS_ERROR_DOM_INDEXEDDB_KEY_ERR);
+ }
+
+ char* buffer;
+ if (!mBuffer.GetMutableData(&buffer, newLen)) {
+ return Err(NS_ERROR_DOM_INDEXEDDB_KEY_ERR);
+ }
+ buffer += oldLen;
+
+ *(buffer++) = aType;
+
+ uint64_t bits = BitwiseCast<uint64_t>(aFloat);
+ // Note: The subtraction from 0 below is necessary to fix
+ // MSVC build warning C4146 (negating an unsigned value).
+ const uint64_t signbit = FloatingPoint<double>::kSignBit;
+ uint64_t number = bits & signbit ? (0 - bits) : (bits | signbit);
+
+ mozilla::BigEndian::writeUint64(buffer, number);
+ return Ok();
+}
+
+// static
+double Key::DecodeNumber(const EncodedDataType*& aPos,
+ const EncodedDataType* aEnd) {
+ NS_ASSERTION(*aPos % eMaxType == eFloat || *aPos % eMaxType == eDate,
+ "Don't call me!");
+
+ ++aPos;
+
+ uint64_t number = 0;
+ memcpy(&number, aPos, std::min<size_t>(sizeof(number), aEnd - aPos));
+ number = mozilla::NativeEndian::swapFromBigEndian(number);
+
+ aPos += sizeof(number);
+
+ // Note: The subtraction from 0 below is necessary to fix
+ // MSVC build warning C4146 (negating an unsigned value).
+ const uint64_t signbit = FloatingPoint<double>::kSignBit;
+ uint64_t bits = number & signbit ? (number & ~signbit) : (0 - number);
+
+ return BitwiseCast<double>(bits);
+}
+
+Result<Ok, nsresult> Key::EncodeBinary(JSObject* aObject, bool aIsViewObject,
+ uint8_t aTypeOffset) {
+ uint8_t* bufferData;
+ size_t bufferLength;
+
+ // We must use JS::GetObjectAsArrayBuffer()/JS_GetObjectAsArrayBufferView()
+ // instead of js::GetArrayBufferLengthAndData(). The object might be wrapped,
+ // the former will handle the wrapped case, the later won't.
+ if (aIsViewObject) {
+ bool unused;
+ JS_GetObjectAsArrayBufferView(aObject, &bufferLength, &unused, &bufferData);
+ } else {
+ JS::GetObjectAsArrayBuffer(aObject, &bufferLength, &bufferData);
+ }
+
+ return EncodeAsString(Span{bufferData, bufferLength}.AsConst(),
+ eBinary + aTypeOffset);
+}
+
+// static
+JSObject* Key::DecodeBinary(const EncodedDataType*& aPos,
+ const EncodedDataType* aEnd, JSContext* aCx) {
+ JS::Rooted<JSObject*> rv(aCx);
+ DecodeStringy<eBinary, uint8_t>(
+ aPos, aEnd,
+ [&rv, aCx](uint8_t** out, uint32_t decodedSize) {
+ *out = static_cast<uint8_t*>(JS_malloc(aCx, decodedSize));
+ if (NS_WARN_IF(!*out)) {
+ rv = nullptr;
+ return false;
+ }
+ rv = JS::NewArrayBufferWithContents(aCx, decodedSize, *out);
+ return true;
+ },
+ [&rv, aCx] { rv = JS::NewArrayBuffer(aCx, 0); });
+ return rv;
+}
+
+nsresult Key::BindToStatement(mozIStorageStatement* aStatement,
+ const nsACString& aParamName) const {
+ nsresult rv;
+ if (IsUnset()) {
+ rv = aStatement->BindNullByName(aParamName);
+ } else {
+ rv = aStatement->BindBlobByName(
+ aParamName, reinterpret_cast<const uint8_t*>(mBuffer.get()),
+ mBuffer.Length());
+ }
+
+ return NS_SUCCEEDED(rv) ? NS_OK : NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+}
+
+nsresult Key::SetFromStatement(mozIStorageStatement* aStatement,
+ uint32_t aIndex) {
+ return SetFromSource(aStatement, aIndex);
+}
+
+nsresult Key::SetFromValueArray(mozIStorageValueArray* aValues,
+ uint32_t aIndex) {
+ return SetFromSource(aValues, aIndex);
+}
+
+IDBResult<Ok, IDBSpecialValue::Invalid> Key::SetFromJSVal(
+ JSContext* aCx, JS::Handle<JS::Value> aVal) {
+ mBuffer.Truncate();
+
+ if (aVal.isNull() || aVal.isUndefined()) {
+ Unset();
+ return Ok();
+ }
+
+ auto result = EncodeJSVal(aCx, aVal, 0);
+ if (result.isErr()) {
+ Unset();
+ return result;
+ }
+ TrimBuffer();
+ return Ok();
+}
+
+nsresult Key::ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) const {
+ if (IsUnset()) {
+ aVal.setUndefined();
+ return NS_OK;
+ }
+
+ const EncodedDataType* pos = BufferStart();
+ nsresult rv = DecodeJSVal(pos, BufferEnd(), aCx, aVal);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(pos >= BufferEnd());
+
+ return NS_OK;
+}
+
+nsresult Key::ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aVal) const {
+ JS::Rooted<JS::Value> value(aCx);
+ nsresult rv = ToJSVal(aCx, &value);
+ if (NS_SUCCEEDED(rv)) {
+ aVal = value;
+ }
+ return rv;
+}
+
+IDBResult<Ok, IDBSpecialValue::Invalid> Key::AppendItem(
+ JSContext* aCx, bool aFirstOfArray, JS::Handle<JS::Value> aVal) {
+ auto result = EncodeJSVal(aCx, aVal, aFirstOfArray ? eMaxType : 0);
+ if (result.isErr()) {
+ Unset();
+ }
+ return result;
+}
+
+template <typename T>
+nsresult Key::SetFromSource(T* aSource, uint32_t aIndex) {
+ const uint8_t* data;
+ uint32_t dataLength = 0;
+
+ nsresult rv = aSource->GetSharedBlob(aIndex, &dataLength, &data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+ }
+
+ mBuffer.Assign(reinterpret_cast<const char*>(data), dataLength);
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::indexedDB