1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5  * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "gtest/gtest.h"
8 
9 #include "mozilla/dom/SimpleGlobalObject.h"
10 #include "mozilla/dom/ScriptSettings.h"
11 #include "mozilla/dom/indexedDB/Key.h"
12 #include "mozilla/IntegerRange.h"
13 #include "mozilla/Unused.h"
14 
15 #include "jsapi.h"
16 #include "js/Array.h"  // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject
17 #include "js/ArrayBuffer.h"
18 
19 // TODO: This PrintTo overload is defined in dom/media/gtest/TestGroupId.cpp.
20 // However, it is not used, probably because of
21 // https://stackoverflow.com/a/36941270
22 void PrintTo(const nsString& value, std::ostream* os);
23 
24 using namespace mozilla;
25 using namespace mozilla::dom::indexedDB;
26 using JS::Rooted;
27 
28 // DOM_IndexedDB_Key_Ctor tests test the construction of a Key, and check the
29 // properties of the constructed key with the const methods afterwards. The
30 // tested ctors include the default ctor, which constructs an unset key, and the
31 // ctors that accepts an encoded buffer, which is then decoded using the
32 // Key::To* method corresponding to its type.
33 //
34 // So far, only some cases are tested:
35 // - scalar binary
36 // -- empty
37 // -- with 1-byte encoded representation
38 // - scalar string
39 // -- empty
40 // -- with 1-byte encoded representation
41 //
42 // TODO More test cases should be added, including
43 // - empty (?)
44 // - scalar binary
45 // -- containing 0 byte(s)
46 // -- with 2-byte encoded representation
47 // - scalar string
48 // -- with 2-byte and 3-byte encoded representation
49 // - scalar number
50 // - scalar date
51 // - arrays, incl. nested arrays, with various combinations of contained types
52 
TEST(DOM_IndexedDB_Key,Ctor_Default)53 TEST(DOM_IndexedDB_Key, Ctor_Default)
54 {
55   auto key = Key{};
56 
57   EXPECT_TRUE(key.IsUnset());
58 }
59 
60 // TODO does such a helper function already exist?
61 template <size_t N>
BufferAsCString(const uint8_t (& aBuffer)[N])62 static auto BufferAsCString(const uint8_t (&aBuffer)[N]) {
63   return nsCString{reinterpret_cast<const char*>(
64                        static_cast<std::decay_t<const uint8_t[]>>(aBuffer)),
65                    N};
66 }
67 
ExpectKeyIsBinary(const Key & aKey)68 static void ExpectKeyIsBinary(const Key& aKey) {
69   EXPECT_FALSE(aKey.IsUnset());
70 
71   EXPECT_FALSE(aKey.IsArray());
72   EXPECT_TRUE(aKey.IsBinary());
73   EXPECT_FALSE(aKey.IsDate());
74   EXPECT_FALSE(aKey.IsFloat());
75   EXPECT_FALSE(aKey.IsString());
76 }
77 
ExpectKeyIsString(const Key & aKey)78 static void ExpectKeyIsString(const Key& aKey) {
79   EXPECT_FALSE(aKey.IsUnset());
80 
81   EXPECT_FALSE(aKey.IsArray());
82   EXPECT_FALSE(aKey.IsBinary());
83   EXPECT_FALSE(aKey.IsDate());
84   EXPECT_FALSE(aKey.IsFloat());
85   EXPECT_TRUE(aKey.IsString());
86 }
87 
ExpectKeyIsArray(const Key & aKey)88 static void ExpectKeyIsArray(const Key& aKey) {
89   EXPECT_FALSE(aKey.IsUnset());
90 
91   EXPECT_TRUE(aKey.IsArray());
92   EXPECT_FALSE(aKey.IsBinary());
93   EXPECT_FALSE(aKey.IsDate());
94   EXPECT_FALSE(aKey.IsFloat());
95   EXPECT_FALSE(aKey.IsString());
96 }
97 
ExpectArrayBufferObject(const JS::Value & aValue)98 static JSObject* ExpectArrayBufferObject(const JS::Value& aValue) {
99   EXPECT_TRUE(aValue.isObject());
100   auto& object = aValue.toObject();
101   EXPECT_TRUE(JS::IsArrayBufferObject(&object));
102   return &object;
103 }
104 
ExpectArrayObject(JSContext * const aContext,JS::Handle<JS::Value> aValue)105 static JSObject* ExpectArrayObject(JSContext* const aContext,
106                                    JS::Handle<JS::Value> aValue) {
107   EXPECT_TRUE(aValue.isObject());
108   bool rv;
109   EXPECT_TRUE(JS::IsArrayObject(aContext, aValue, &rv));
110   EXPECT_TRUE(rv);
111   return &aValue.toObject();
112 }
113 
CheckArrayBuffer(const nsCString & aExpected,const JS::Value & aActual)114 static void CheckArrayBuffer(const nsCString& aExpected,
115                              const JS::Value& aActual) {
116   auto obj = ExpectArrayBufferObject(aActual);
117   size_t length;
118   bool isSharedMemory;
119   uint8_t* data;
120   JS::GetArrayBufferLengthAndData(obj, &length, &isSharedMemory, &data);
121 
122   EXPECT_EQ(aExpected.Length(), length);
123   EXPECT_EQ(0, memcmp(aExpected.get(), data, length));
124 }
125 
CheckString(JSContext * const aContext,const nsString & aExpected,JS::Handle<JS::Value> aActual)126 static void CheckString(JSContext* const aContext, const nsString& aExpected,
127                         JS::Handle<JS::Value> aActual) {
128   EXPECT_TRUE(aActual.isString());
129   int32_t rv;
130   EXPECT_TRUE(JS_CompareStrings(aContext,
131                                 JS_NewUCStringCopyZ(aContext, aExpected.get()),
132                                 aActual.toString(), &rv));
133   EXPECT_EQ(0, rv);
134 }
135 
136 namespace {
137 // This is modeled after dom/base/test/gtest/TestContentUtils.cpp
138 struct AutoTestJSContext {
AutoTestJSContext__anon96d2b3580111::AutoTestJSContext139   AutoTestJSContext()
140       : mGlobalObject(
141             mozilla::dom::RootingCx(),
142             mozilla::dom::SimpleGlobalObject::Create(
143                 mozilla::dom::SimpleGlobalObject::GlobalType::BindingDetail)) {
144     EXPECT_TRUE(mJsAPI.Init(mGlobalObject));
145     mContext = mJsAPI.cx();
146   }
147 
operator JSContext*__anon96d2b3580111::AutoTestJSContext148   operator JSContext*() const { return mContext; }
149 
150  private:
151   Rooted<JSObject*> mGlobalObject;
152   mozilla::dom::AutoJSAPI mJsAPI;
153   JSContext* mContext;
154 };
155 
156 // The following classes serve as base classes for the parametrized tests below.
157 // The name of each class reflects the parameter type.
158 
159 class TestWithParam_CString_ArrayBuffer_Pair
160     : public ::testing::TestWithParam<std::pair<nsCString, nsLiteralCString>> {
161 };
162 
163 class TestWithParam_CString_String_Pair
164     : public ::testing::TestWithParam<std::pair<nsCString, nsLiteralString>> {};
165 
166 class TestWithParam_LiteralString
167     : public ::testing::TestWithParam<nsLiteralString> {};
168 
169 class TestWithParam_StringArray
170     : public ::testing::TestWithParam<std::vector<nsString>> {};
171 
172 class TestWithParam_ArrayBufferArray
173     : public ::testing::TestWithParam<std::vector<nsCString>> {};
174 
175 }  // namespace
176 
TEST_P(TestWithParam_CString_ArrayBuffer_Pair,Ctor_EncodedBinary)177 TEST_P(TestWithParam_CString_ArrayBuffer_Pair, Ctor_EncodedBinary) {
178   const auto key = Key{GetParam().first};
179 
180   ExpectKeyIsBinary(key);
181 
182   AutoTestJSContext context;
183 
184   Rooted<JS::Value> rv(context);
185   EXPECT_EQ(NS_OK, key.ToJSVal(context, &rv));
186 
187   CheckArrayBuffer(GetParam().second, rv);
188 }
189 
190 static const uint8_t zeroLengthBinaryEncodedBuffer[] = {Key::eBinary};
191 static const uint8_t nonZeroLengthBinaryEncodedBuffer[] = {Key::eBinary,
192                                                            'a' + 1, 'b' + 1};
193 INSTANTIATE_TEST_CASE_P(
194     DOM_IndexedDB_Key, TestWithParam_CString_ArrayBuffer_Pair,
195     ::testing::Values(
196         std::make_pair(BufferAsCString(zeroLengthBinaryEncodedBuffer), ""_ns),
197         std::make_pair(BufferAsCString(nonZeroLengthBinaryEncodedBuffer),
198                        "ab"_ns)));
199 
TEST_P(TestWithParam_CString_String_Pair,Ctor_EncodedString)200 TEST_P(TestWithParam_CString_String_Pair, Ctor_EncodedString) {
201   const auto key = Key{GetParam().first};
202 
203   ExpectKeyIsString(key);
204 
205   EXPECT_EQ(GetParam().second, key.ToString());
206 }
207 
208 static const uint8_t zeroLengthStringEncodedBuffer[] = {Key::eString};
209 static const uint8_t nonZeroLengthStringEncodedBuffer[] = {Key::eString,
210                                                            'a' + 1, 'b' + 1};
211 
212 INSTANTIATE_TEST_CASE_P(
213     DOM_IndexedDB_Key, TestWithParam_CString_String_Pair,
214     ::testing::Values(
215         std::make_pair(BufferAsCString(zeroLengthStringEncodedBuffer), u""_ns),
216         std::make_pair(BufferAsCString(nonZeroLengthStringEncodedBuffer),
217                        u"ab"_ns)));
218 
TEST_P(TestWithParam_LiteralString,SetFromString)219 TEST_P(TestWithParam_LiteralString, SetFromString) {
220   auto key = Key{};
221   const auto result = key.SetFromString(GetParam());
222   EXPECT_TRUE(result.isOk());
223 
224   ExpectKeyIsString(key);
225 
226   EXPECT_EQ(GetParam(), key.ToString());
227 }
228 
229 INSTANTIATE_TEST_CASE_P(DOM_IndexedDB_Key, TestWithParam_LiteralString,
230                         ::testing::Values(u""_ns, u"abc"_ns, u"\u007f"_ns,
231                                           u"\u0080"_ns, u"\u1fff"_ns,
232                                           u"\u7fff"_ns, u"\u8000"_ns,
233                                           u"\uffff"_ns));
234 
CreateArrayBufferValue(JSContext * const aContext,const size_t aSize,char * const aData)235 static JS::Value CreateArrayBufferValue(JSContext* const aContext,
236                                         const size_t aSize, char* const aData) {
237   Rooted<JSObject*> arrayBuffer{
238       aContext, JS::NewArrayBufferWithContents(aContext, aSize, aData)};
239   EXPECT_TRUE(arrayBuffer);
240   return JS::ObjectValue(*arrayBuffer);
241 }
242 
243 // This tests calling SetFromJSVal with an ArrayBuffer scalar of length 0.
244 // TODO Probably there should be more test cases for SetFromJSVal with other
245 // ArrayBuffer scalars, which convert this into a parametrized test as well.
TEST(DOM_IndexedDB_Key,SetFromJSVal_ZeroLengthArrayBuffer)246 TEST(DOM_IndexedDB_Key, SetFromJSVal_ZeroLengthArrayBuffer)
247 {
248   AutoTestJSContext context;
249 
250   auto key = Key{};
251   Rooted<JS::Value> arrayBuffer(context,
252                                 CreateArrayBufferValue(context, 0, nullptr));
253   const auto result = key.SetFromJSVal(context, arrayBuffer);
254   EXPECT_TRUE(result.isOk());
255 
256   ExpectKeyIsBinary(key);
257 
258   Rooted<JS::Value> rv2(context);
259   EXPECT_EQ(NS_OK, key.ToJSVal(context, &rv2));
260 
261   CheckArrayBuffer(""_ns, rv2);
262 }
263 
264 template <typename CheckElement>
CheckArray(JSContext * const context,JS::Handle<JS::Value> arrayValue,const size_t expectedLength,const CheckElement & checkElement)265 static void CheckArray(JSContext* const context,
266                        JS::Handle<JS::Value> arrayValue,
267                        const size_t expectedLength,
268                        const CheckElement& checkElement) {
269   Rooted<JSObject*> actualArray(context,
270                                 ExpectArrayObject(context, arrayValue));
271 
272   uint32_t actualLength;
273   EXPECT_TRUE(JS::GetArrayLength(context, actualArray, &actualLength));
274   EXPECT_EQ(expectedLength, actualLength);
275   for (size_t i = 0; i < expectedLength; ++i) {
276     Rooted<JS::Value> element(static_cast<JSContext*>(context));
277     EXPECT_TRUE(JS_GetElement(context, actualArray, i, &element));
278 
279     checkElement(i, element);
280   }
281 }
282 
CreateArrayBufferArray(JSContext * const context,const std::vector<nsCString> & elements)283 static JS::Value CreateArrayBufferArray(
284     JSContext* const context, const std::vector<nsCString>& elements) {
285   Rooted<JSObject*> arrayObject(context,
286                                 JS::NewArrayObject(context, elements.size()));
287   EXPECT_TRUE(arrayObject);
288 
289   Rooted<JS::Value> arrayBuffer(context);
290   for (size_t i = 0; i < elements.size(); ++i) {
291     // TODO strdup only works if the element is actually 0-terminated
292     arrayBuffer = CreateArrayBufferValue(
293         context, elements[i].Length(),
294         elements[i].Length() ? strdup(elements[i].get()) : nullptr);
295     EXPECT_TRUE(JS_SetElement(context, arrayObject, i, arrayBuffer));
296   }
297 
298   return JS::ObjectValue(*arrayObject);
299 }
300 
TEST_P(TestWithParam_ArrayBufferArray,SetFromJSVal)301 TEST_P(TestWithParam_ArrayBufferArray, SetFromJSVal) {
302   const auto& elements = GetParam();
303 
304   AutoTestJSContext context;
305   Rooted<JS::Value> arrayValue(context);
306   arrayValue = CreateArrayBufferArray(context, elements);
307 
308   auto key = Key{};
309   const auto result = key.SetFromJSVal(context, arrayValue);
310   EXPECT_TRUE(result.isOk());
311 
312   ExpectKeyIsArray(key);
313 
314   Rooted<JS::Value> rv2(context);
315   EXPECT_EQ(NS_OK, key.ToJSVal(context, &rv2));
316 
317   CheckArray(context, rv2, elements.size(),
318              [&elements](const size_t i, const JS::HandleValue& element) {
319                CheckArrayBuffer(elements[i], element);
320              });
321 }
322 
323 const uint8_t element2[] = "foo";
324 INSTANTIATE_TEST_CASE_P(
325     DOM_IndexedDB_Key, TestWithParam_ArrayBufferArray,
326     testing::Values(std::vector<nsCString>{}, std::vector<nsCString>{""_ns},
327                     std::vector<nsCString>{""_ns, BufferAsCString(element2)}));
328 
CreateStringValue(JSContext * const context,const nsString & string)329 static JS::Value CreateStringValue(JSContext* const context,
330                                    const nsString& string) {
331   JSString* str = JS_NewUCStringCopyZ(context, string.get());
332   EXPECT_TRUE(str);
333   return JS::StringValue(str);
334 }
335 
CreateStringArray(JSContext * const context,const std::vector<nsString> & elements)336 static JS::Value CreateStringArray(JSContext* const context,
337                                    const std::vector<nsString>& elements) {
338   Rooted<JSObject*> array(context,
339                           JS::NewArrayObject(context, elements.size()));
340   EXPECT_TRUE(array);
341 
342   for (size_t i = 0; i < elements.size(); ++i) {
343     Rooted<JS::Value> string(context, CreateStringValue(context, elements[i]));
344     EXPECT_TRUE(JS_SetElement(context, array, i, string));
345   }
346 
347   return JS::ObjectValue(*array);
348 }
349 
TEST_P(TestWithParam_StringArray,SetFromJSVal)350 TEST_P(TestWithParam_StringArray, SetFromJSVal) {
351   const auto& elements = GetParam();
352 
353   AutoTestJSContext context;
354   Rooted<JS::Value> arrayValue(context, CreateStringArray(context, elements));
355 
356   auto key = Key{};
357   const auto result = key.SetFromJSVal(context, arrayValue);
358   EXPECT_TRUE(result.isOk());
359 
360   ExpectKeyIsArray(key);
361 
362   Rooted<JS::Value> rv2(context);
363   EXPECT_EQ(NS_OK, key.ToJSVal(context, &rv2));
364 
365   CheckArray(
366       context, rv2, elements.size(),
367       [&elements, &context](const size_t i, JS::Handle<JS::Value> element) {
368         CheckString(context, elements[i], element);
369       });
370 }
371 
372 INSTANTIATE_TEST_CASE_P(
373     DOM_IndexedDB_Key, TestWithParam_StringArray,
374     testing::Values(std::vector<nsString>{u""_ns, u"abc\u0080\u1fff"_ns},
375                     std::vector<nsString>{u"abc\u0080\u1fff"_ns,
376                                           u"abc\u0080\u1fff"_ns}));
377 
TEST(DOM_IndexedDB_Key,CompareKeys_NonZeroLengthArrayBuffer)378 TEST(DOM_IndexedDB_Key, CompareKeys_NonZeroLengthArrayBuffer)
379 {
380   AutoTestJSContext context;
381   const char buf[] = "abc\x80";
382 
383   auto first = Key{};
384   Rooted<JS::Value> arrayBuffer1(
385       context, CreateArrayBufferValue(context, sizeof buf, strdup(buf)));
386   const auto result1 = first.SetFromJSVal(context, arrayBuffer1);
387   EXPECT_TRUE(result1.isOk());
388 
389   auto second = Key{};
390   Rooted<JS::Value> arrayBuffer2(
391       context, CreateArrayBufferValue(context, sizeof buf, strdup(buf)));
392   const auto result2 = second.SetFromJSVal(context, arrayBuffer2);
393   EXPECT_TRUE(result2.isOk());
394 
395   EXPECT_EQ(0, Key::CompareKeys(first, second));
396 }
397 
398 constexpr auto kTestLocale = "e"_ns;
399 
TEST(DOM_IndexedDB_Key,ToLocaleAwareKey_Empty)400 TEST(DOM_IndexedDB_Key, ToLocaleAwareKey_Empty)
401 {
402   const auto input = Key{};
403 
404   auto res = input.ToLocaleAwareKey(kTestLocale);
405   EXPECT_TRUE(res.isOk());
406 
407   EXPECT_TRUE(res.inspect().IsUnset());
408 }
409 
TEST(DOM_IndexedDB_Key,ToLocaleAwareKey_Bug_1641598)410 TEST(DOM_IndexedDB_Key, ToLocaleAwareKey_Bug_1641598)
411 {
412   const auto buffer = [] {
413     nsCString res;
414     // This is the encoded representation produced by the test case from bug
415     // 1641598.
416     res.AppendLiteral("\x90\x01\x01\x01\x01\x00\x40");
417     for (const size_t unused : IntegerRange<size_t>(256)) {
418       Unused << unused;
419       res.AppendLiteral("\x01\x01\x80\x03\x43");
420     }
421     return res;
422   }();
423   const auto input = Key{buffer};
424 
425   auto res = input.ToLocaleAwareKey(kTestLocale);
426   EXPECT_TRUE(res.isOk());
427 
428   EXPECT_EQ(input, res.inspect());
429 }
430