From 04a67b54b757a29727d49426fc0542fcb0f25283 Mon Sep 17 00:00:00 2001 From: Syd Pleno Date: Sun, 19 May 2024 22:54:16 +1000 Subject: [PATCH] Update JSSet/JSArray implementations --- dfindexeddb/indexeddb/chromium/v8.py | 31 +++++++++--- dfindexeddb/indexeddb/cli.py | 2 + dfindexeddb/indexeddb/safari/webkit.py | 50 +++++++++++++++----- tests/dfindexeddb/indexeddb/chromium/v8.py | 10 +++- tests/dfindexeddb/indexeddb/safari/record.py | 24 ++++++++-- tests/dfindexeddb/indexeddb/safari/webkit.py | 17 +++++-- 6 files changed, 104 insertions(+), 30 deletions(-) diff --git a/dfindexeddb/indexeddb/chromium/v8.py b/dfindexeddb/indexeddb/chromium/v8.py index e178410..c803cec 100644 --- a/dfindexeddb/indexeddb/chromium/v8.py +++ b/dfindexeddb/indexeddb/chromium/v8.py @@ -34,15 +34,22 @@ class BufferArrayView: flags: int -class JSArray(list): +class JSArray: """A parsed Javascript array. - This is a wrapper around a standard Python list to allow assigning arbitrary - properties as is possible in the Javascript equivalent. + A Javascript array behaves like a Python list but allows assigning arbitrary + properties. The array is stored in the attribute __array__. """ + def __init__(self): + self.__array__ = [] + + def Append(self, element: Any): + """Appends a new element to the array.""" + self.__array__.append(element) def __repr__(self): - array_entries = ", ".join([str(entry) for entry in list(self)]) + array_entries = ", ".join( + [str(entry) for entry in list(self.__array__)]) properties = ", ".join( f'{key}: {value}' for key, value in self.properties.items()) return f'[{array_entries}, {properties}]' @@ -52,6 +59,11 @@ def properties(self) -> Dict[str, Any]: """Returns the object properties.""" return self.__dict__ + def __eq__(self, other): + return ( + self.__array__ == other.__array__ + and self.properties == other.properties) + def __contains__(self, item): return item in self.__dict__ @@ -194,6 +206,7 @@ def _ReadObject(self): if tag and tag == definitions.V8SerializationTag.ARRAY_BUFFER_VIEW: self._ConsumeTag(tag) result = self._ReadJSArrayBufferView(result) + self.objects[self._GetNextId()] = result return result def _ReadObjectInternal(self) -> Tuple[definitions.V8SerializationTag, Any]: @@ -385,7 +398,10 @@ def _ReadJSObjectProperties( while self._PeekTag() != end_tag: key = self._ReadObject() value = self._ReadObject() - js_object[key] = value + if isinstance(js_object, dict): + js_object[key] = value + else: + js_object.properties[key] = value num_properties += 1 self._ConsumeTag(end_tag) return num_properties @@ -407,7 +423,7 @@ def ReadSparseJSArray(self) -> JSArray: js_array = JSArray() _, length = self.decoder.DecodeUint32Varint() for _ in range(length): - js_array.append(Undefined()) + js_array.Append(Undefined()) num_properties = self._ReadJSObjectProperties( js_array.__dict__, definitions.V8SerializationTag.END_SPARSE_JS_ARRAY) @@ -440,7 +456,7 @@ def ReadDenseJSArray(self) -> JSArray: if self.version < 11 and isinstance(array_object, Undefined): continue - js_array.append(array_object) + js_array.Append(array_object) num_properties = self._ReadJSObjectProperties( js_array.__dict__, definitions.V8SerializationTag.END_DENSE_JS_ARRAY) @@ -571,6 +587,7 @@ def _ReadJSArrayBuffer( if is_resizable: _, max_byte_length = self.decoder.DecodeUint32Varint() if byte_length > max_byte_length: + self.objects[next_id] = array_buffer return array_buffer if byte_length: _, array_buffer = self.decoder.ReadBytes(byte_length) diff --git a/dfindexeddb/indexeddb/cli.py b/dfindexeddb/indexeddb/cli.py index 5246dce..d07bffd 100644 --- a/dfindexeddb/indexeddb/cli.py +++ b/dfindexeddb/indexeddb/cli.py @@ -51,6 +51,8 @@ def default(self, o): return o.isoformat() if isinstance(o, v8.Undefined): return "" + if isinstance(o, v8.JSArray): + return o.__dict__ if isinstance(o, v8.Null): return "" if isinstance(o, set): diff --git a/dfindexeddb/indexeddb/safari/webkit.py b/dfindexeddb/indexeddb/safari/webkit.py index ed8023e..e9d6110 100644 --- a/dfindexeddb/indexeddb/safari/webkit.py +++ b/dfindexeddb/indexeddb/safari/webkit.py @@ -82,15 +82,22 @@ class FileList: files: List[FileData] -class JSArray(list): - """A parsed JavaScript array. +class JSArray: + """A parsed Javascript array. - This is a wrapper around a standard Python list to allow assigning arbitrary - properties as is possible in the JavaScript equivalent. + A Javascript array behaves like a Python list but allows assigning arbitrary + properties. The array is stored in the attribute __array__. """ + def __init__(self): + self.__array__ = [] + + def Append(self, element: Any): + """Appends a new element to the array.""" + self.__array__.append(element) def __repr__(self): - array_entries = ", ".join([str(entry) for entry in list(self)]) + array_entries = ", ".join( + [str(entry) for entry in list(self.__array__)]) properties = ", ".join( f'{key}: {value}' for key, value in self.properties.items()) return f'[{array_entries}, {properties}]' @@ -100,6 +107,11 @@ def properties(self) -> Dict[str, Any]: """Returns the object properties.""" return self.__dict__ + def __eq__(self, other): + return ( + self.__array__ == other.__array__ + and self.properties == other.properties) + def __contains__(self, item): return item in self.__dict__ @@ -107,24 +119,36 @@ def __getitem__(self, name): return self.__dict__[name] -class JSSet(set): - """A parsed JavaScript set. +class JSSet: + """A parsed Javascript set. - This is a wrapper around a standard Python set to allow assigning arbitrary - properties as is possible in the JavaScript equivalent. + A Javascript set behaves like a Python set but allows assigning arbitrary + properties. The array is stored in the attribute __set__. """ + def __init__(self): + self.__set__ = set() + + def Add(self, element: Any): + """Adds a new element to the set.""" + self.__set__.add(element) def __repr__(self): - set_entries = ", ".join([str(entry) for entry in list(self)]) + array_entries = ", ".join( + [str(entry) for entry in list(self.__set__)]) properties = ", ".join( f'{key}: {value}' for key, value in self.properties.items()) - return f'[{set_entries}, {properties}]' + return f'[{array_entries}, {properties}]' @property def properties(self) -> Dict[str, Any]: """Returns the object properties.""" return self.__dict__ + def __eq__(self, other): + return ( + self.__set__ == other.__set__ + and self.properties == other.properties) + def __contains__(self, item): return item in self.__dict__ @@ -294,7 +318,7 @@ def DecodeArray(self) -> JSArray: for _ in range(length): _, _ = self.decoder.DecodeUint32() _, value = self.DecodeValue() - array.append(value) + array.Append(value) offset, terminator_tag = self.decoder.DecodeUint32() if terminator_tag != definitions.TerminatorTag: @@ -480,7 +504,7 @@ def DecodeSetData(self) -> JSSet: while tag != definitions.SerializationTag.NON_SET_PROPERTIES: _, key = self.DecodeValue() - js_set.add(key) + js_set.Add(key) tag = self.PeekSerializationTag() # consume the NonSetPropertiesTag diff --git a/tests/dfindexeddb/indexeddb/chromium/v8.py b/tests/dfindexeddb/indexeddb/chromium/v8.py index dda6ebb..a73fa10 100644 --- a/tests/dfindexeddb/indexeddb/chromium/v8.py +++ b/tests/dfindexeddb/indexeddb/chromium/v8.py @@ -163,7 +163,11 @@ def test_jsarray(self): # > x['propa'] = 123 # > console.log(v8.serialize(x)).toString('hex')) buffer = bytes.fromhex('ff0d4103490249044906220570726f706149f601240103') - expected_value = v8.JSArray([1, 2, 3]) + expected_value = v8.JSArray() + expected_value.Append(1) + expected_value.Append(2) + expected_value.Append(3) + expected_value.properties['propa'] = 123 parsed_value = v8.ValueDeserializer.FromBytes(buffer, None) self.assertEqual(parsed_value, expected_value) self.assertTrue(hasattr(parsed_value, 'propa')) @@ -174,7 +178,9 @@ def test_jsarray(self): # > x.length = 10 # > console.log(v8.serialize(x)).toString('hex')) buffer = bytes.fromhex('ff0d610a40000a') - expected_value = v8.JSArray([v8.Undefined() for i in range(10)]) + expected_value = v8.JSArray() + for _ in range(10): + expected_value.Append(v8.Undefined()) parsed_value = v8.ValueDeserializer.FromBytes(buffer, None) self.assertEqual(parsed_value, expected_value) diff --git a/tests/dfindexeddb/indexeddb/safari/record.py b/tests/dfindexeddb/indexeddb/safari/record.py index 2a780d9..aca9883 100644 --- a/tests/dfindexeddb/indexeddb/safari/record.py +++ b/tests/dfindexeddb/indexeddb/safari/record.py @@ -241,9 +241,13 @@ def test_empty_string_object_record(self): def test_set_record(self): """Tests for an IndexedDB record with a set value.""" + expected_set = webkit.JSSet() + for i in range(1, 4): + expected_set.Add(i) + expected_record = record.IndexedDBRecord( key=27, - value={'id': 27, 'value': {1, 2, 3}}, + value={'id': 27, 'value': expected_set}, object_store_id=1, object_store_name='test store a', database_name='IndexedDB test', @@ -289,6 +293,13 @@ def test_empty_object_record(self): def test_mixed_object_record(self): """Tests for an IndexedDB record with mixed values within an object.""" + expected_test_array = webkit.JSArray() + for value in [123, 456, 'abc', 'def']: + expected_test_array.Append(value) + expected_set = webkit.JSSet() + for i in range(1, 4): + expected_set.Add(i) + expected_record = record.IndexedDBRecord( key=1, value={ @@ -305,13 +316,13 @@ def test_mixed_object_record(self): 'test_boolean_false_object': False, 'test_bigint': 12300000000000001048576, 'test_date': datetime.datetime(2023, 2, 12, 23, 20, 30, 456000), - 'test_set': set([1, 2, 3]), + 'test_set': expected_set, 'test_map': { 'a': 1, 'b': 2, 'c': 3}, 'test_regexp': webkit.RegExp('\\w+', ''), - 'test_array': [123, 456, 'abc', 'def'], + 'test_array': expected_test_array, 'test_object': { 'name': { 'first': 'Jane', @@ -460,10 +471,13 @@ def test_buffer_key_record(self): def test_array_key_record(self): """Tests for an IndexedDB record with an array in the key.""" + expected_test_array = webkit.JSArray() + for value in [1, 2, 3]: + expected_test_array.Append(value) expected_record = record.IndexedDBRecord( - key=[1, 2, 3], + key=[1.0, 2.0, 3.0], value={ - 'id': [1, 2, 3], + 'id': expected_test_array, 'value': {}}, object_store_id=1, object_store_name='test store a', diff --git a/tests/dfindexeddb/indexeddb/safari/webkit.py b/tests/dfindexeddb/indexeddb/safari/webkit.py index a15a24e..4ae51d6 100644 --- a/tests/dfindexeddb/indexeddb/safari/webkit.py +++ b/tests/dfindexeddb/indexeddb/safari/webkit.py @@ -211,7 +211,10 @@ def test_parse_set(self): '0F00000002020000806964051B000000' '0500008076616C75651D070502000000' '050300000020FFFFFFFFFFFFFFFF') - expected_value = {'id': 27, 'value': {1, 2, 3}} + expected_set = webkit.JSSet() + for i in range(1, 4): + expected_set.Add(i) + expected_value = {'id': 27, 'value': expected_set} parsed_value = webkit.SerializedScriptValueDecoder.FromBytes( value_bytes) self.assertEqual(parsed_value, expected_value) @@ -268,6 +271,14 @@ def test_mixed_object(self): '6A65637402040000806E616D650205000080666972737410040000804A616E65' '040000806C6173741003000080446F65FFFFFFFF030000806167650515000000' 'FFFFFFFFFFFFFFFF') + + expected_test_array = webkit.JSArray() + for value in [123, 456, 'abc', 'def']: + expected_test_array.Append(value) + expected_set = webkit.JSSet() + for i in range(1, 4): + expected_set.Add(i) + expected_value = { 'id': 1, 'test_undef': webkit.Undefined(), @@ -282,13 +293,13 @@ def test_mixed_object(self): 'test_boolean_false_object': False, 'test_bigint': 12300000000000001048576, 'test_date': datetime.datetime(2023, 2, 12, 23, 20, 30, 456000), - 'test_set': set([1, 2, 3]), + 'test_set': expected_set, 'test_map': { 'a': 1, 'b': 2, 'c': 3}, 'test_regexp': webkit.RegExp('\\w+', ''), - 'test_array': [123, 456, 'abc', 'def'], + 'test_array': expected_test_array, 'test_object': { 'name': { 'first': 'Jane',