dfindexeddb 20251109__py3-none-any.whl → 20260205__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dfindexeddb/indexeddb/chromium/definitions.py +92 -0
- dfindexeddb/indexeddb/chromium/record.py +266 -63
- dfindexeddb/indexeddb/chromium/sqlite.py +362 -0
- dfindexeddb/indexeddb/cli.py +169 -12
- dfindexeddb/indexeddb/firefox/record.py +12 -2
- dfindexeddb/indexeddb/safari/record.py +20 -4
- dfindexeddb/leveldb/utils.py +84 -0
- dfindexeddb/version.py +1 -1
- {dfindexeddb-20251109.dist-info → dfindexeddb-20260205.dist-info}/METADATA +35 -86
- {dfindexeddb-20251109.dist-info → dfindexeddb-20260205.dist-info}/RECORD +14 -13
- {dfindexeddb-20251109.dist-info → dfindexeddb-20260205.dist-info}/WHEEL +1 -1
- {dfindexeddb-20251109.dist-info → dfindexeddb-20260205.dist-info}/entry_points.txt +0 -0
- {dfindexeddb-20251109.dist-info → dfindexeddb-20260205.dist-info}/licenses/LICENSE +0 -0
- {dfindexeddb-20251109.dist-info → dfindexeddb-20260205.dist-info}/top_level.txt +0 -0
|
@@ -14,11 +14,14 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Definitions for IndexedDB."""
|
|
16
16
|
from enum import Enum, IntEnum, IntFlag
|
|
17
|
+
import textwrap
|
|
17
18
|
|
|
18
19
|
REQUIRES_PROCESSING_SSV_PSEUDO_VERSION = 0x11
|
|
19
20
|
REPLACE_WITH_BLOB = 0x01
|
|
20
21
|
COMPRESSED_WITH_SNAPPY = 0x02
|
|
21
22
|
|
|
23
|
+
SENTINEL = 0x00
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class DatabaseMetaDataKeyType(IntEnum):
|
|
24
27
|
"""Database Metadata key types."""
|
|
@@ -72,6 +75,16 @@ class IDBKeyType(IntEnum):
|
|
|
72
75
|
BINARY = 6
|
|
73
76
|
|
|
74
77
|
|
|
78
|
+
class OrderedIDBKeyType(IntEnum):
|
|
79
|
+
"""Ordered IndexedDB key types."""
|
|
80
|
+
|
|
81
|
+
NUMBER = 0x10
|
|
82
|
+
DATE = 0x20
|
|
83
|
+
STRING = 0x30
|
|
84
|
+
BINARY = 0x40
|
|
85
|
+
ARRAY = 0x50
|
|
86
|
+
|
|
87
|
+
|
|
75
88
|
class IndexMetaDataKeyType(IntEnum):
|
|
76
89
|
"""IndexedDB metadata key types."""
|
|
77
90
|
|
|
@@ -398,3 +411,82 @@ class SerializedImageOrientation(IntEnum):
|
|
|
398
411
|
RIGHT_BOTTOM = 6
|
|
399
412
|
LEFT_BOTTOM = 7
|
|
400
413
|
LAST = LEFT_BOTTOM
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class DatabaseCompressionType(IntEnum):
|
|
417
|
+
"""Database Compression Types."""
|
|
418
|
+
|
|
419
|
+
UNCOMPRESSED = 0
|
|
420
|
+
ZSTD = 1
|
|
421
|
+
SNAPPY = 2
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
SQL_RECORDS_QUERY_BASE = textwrap.dedent(
|
|
425
|
+
"""
|
|
426
|
+
SELECT
|
|
427
|
+
row_id,
|
|
428
|
+
object_store_id,
|
|
429
|
+
compression_type,
|
|
430
|
+
key,
|
|
431
|
+
value,
|
|
432
|
+
EXISTS (
|
|
433
|
+
SELECT 1
|
|
434
|
+
FROM blob_references
|
|
435
|
+
WHERE record_row_id = records.row_id
|
|
436
|
+
) AS has_blobs
|
|
437
|
+
FROM records"""
|
|
438
|
+
).strip()
|
|
439
|
+
|
|
440
|
+
SQL_RECORDS_QUERY = SQL_RECORDS_QUERY_BASE
|
|
441
|
+
|
|
442
|
+
SQL_RECORDS_BY_ID_QUERY = f"{SQL_RECORDS_QUERY_BASE} WHERE object_store_id = ?"
|
|
443
|
+
|
|
444
|
+
SQL_RECORDS_BY_NAME_QUERY = textwrap.dedent(
|
|
445
|
+
f"""
|
|
446
|
+
{SQL_RECORDS_QUERY_BASE}
|
|
447
|
+
JOIN object_stores ON records.object_store_id = object_stores.id
|
|
448
|
+
WHERE object_stores.name = ?"""
|
|
449
|
+
).strip()
|
|
450
|
+
|
|
451
|
+
SQL_OBJECT_STORES_QUERY = textwrap.dedent(
|
|
452
|
+
"""
|
|
453
|
+
SELECT
|
|
454
|
+
id,
|
|
455
|
+
name,
|
|
456
|
+
key_path,
|
|
457
|
+
auto_increment,
|
|
458
|
+
key_generator_current_number
|
|
459
|
+
FROM object_stores"""
|
|
460
|
+
).strip()
|
|
461
|
+
|
|
462
|
+
SQL_BLOB_DATA_QUERY = textwrap.dedent(
|
|
463
|
+
"""
|
|
464
|
+
SELECT
|
|
465
|
+
b.row_id,
|
|
466
|
+
b.object_type,
|
|
467
|
+
b.mime_type,
|
|
468
|
+
b.size_bytes,
|
|
469
|
+
b.file_name,
|
|
470
|
+
0 AS chunk_index,
|
|
471
|
+
b.bytes
|
|
472
|
+
FROM blobs b
|
|
473
|
+
JOIN blob_references r ON b.row_id = r.blob_row_id
|
|
474
|
+
WHERE r.record_row_id = ?
|
|
475
|
+
|
|
476
|
+
UNION ALL
|
|
477
|
+
|
|
478
|
+
SELECT
|
|
479
|
+
c.blob_row_id AS row_id,
|
|
480
|
+
b.object_type,
|
|
481
|
+
b.mime_type,
|
|
482
|
+
b.size_bytes,
|
|
483
|
+
b.file_name,
|
|
484
|
+
c.chunk_index,
|
|
485
|
+
c.bytes
|
|
486
|
+
FROM overflow_blob_chunks c
|
|
487
|
+
JOIN blobs b ON c.blob_row_id = b.row_id
|
|
488
|
+
JOIN blob_references r ON b.row_id = r.blob_row_id
|
|
489
|
+
WHERE r.record_row_id = ?
|
|
490
|
+
|
|
491
|
+
ORDER BY row_id, chunk_index"""
|
|
492
|
+
).strip()
|
|
@@ -24,6 +24,7 @@ from datetime import datetime
|
|
|
24
24
|
from typing import (
|
|
25
25
|
Any,
|
|
26
26
|
BinaryIO,
|
|
27
|
+
ClassVar,
|
|
27
28
|
Generator,
|
|
28
29
|
Optional,
|
|
29
30
|
Tuple,
|
|
@@ -36,7 +37,7 @@ from dfindexeddb import errors
|
|
|
36
37
|
from dfindexeddb.indexeddb.chromium import blink, definitions
|
|
37
38
|
from dfindexeddb.leveldb import record, utils
|
|
38
39
|
|
|
39
|
-
T = TypeVar("T")
|
|
40
|
+
T = TypeVar("T", bound="BaseIndexedDBKey")
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
@dataclass(frozen=True)
|
|
@@ -74,10 +75,9 @@ class KeyPrefix(utils.FromDecoderMixin):
|
|
|
74
75
|
"""
|
|
75
76
|
offset, raw_prefix = decoder.ReadBytes(1)
|
|
76
77
|
|
|
77
|
-
database_id_length = (raw_prefix[0] & 0xE0 >> 5) + 1
|
|
78
|
-
object_store_id_length = (raw_prefix[0] & 0x1C >> 2) + 1
|
|
78
|
+
database_id_length = ((raw_prefix[0] & 0xE0) >> 5) + 1
|
|
79
|
+
object_store_id_length = ((raw_prefix[0] & 0x1C) >> 2) + 1
|
|
79
80
|
index_id_length = (raw_prefix[0] & 0x03) + 1
|
|
80
|
-
|
|
81
81
|
if database_id_length < 1 or database_id_length > 8:
|
|
82
82
|
raise errors.ParserError("Invalid database ID length")
|
|
83
83
|
|
|
@@ -90,7 +90,6 @@ class KeyPrefix(utils.FromDecoderMixin):
|
|
|
90
90
|
_, database_id = decoder.DecodeInt(database_id_length, signed=False)
|
|
91
91
|
_, object_store_id = decoder.DecodeInt(object_store_id_length, signed=False)
|
|
92
92
|
_, index_id = decoder.DecodeInt(index_id_length, signed=False)
|
|
93
|
-
|
|
94
93
|
return cls(
|
|
95
94
|
offset=base_offset + offset,
|
|
96
95
|
database_id=database_id,
|
|
@@ -217,6 +216,95 @@ class IDBKey(utils.FromDecoderMixin):
|
|
|
217
216
|
return cls(base_offset + offset, key_type, value)
|
|
218
217
|
|
|
219
218
|
|
|
219
|
+
@dataclass(frozen=True)
|
|
220
|
+
class SortableIDBKey(utils.FromDecoderMixin):
|
|
221
|
+
"""A sortable IDBKey.
|
|
222
|
+
|
|
223
|
+
Attributes:
|
|
224
|
+
offset: the offset of the IDBKey.
|
|
225
|
+
type: the type of the IDBKey.
|
|
226
|
+
value: the value of the IDBKey.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
offset: int = field(compare=False)
|
|
230
|
+
type: definitions.IDBKeyType
|
|
231
|
+
value: Union[list[Any], bytes, str, float, datetime, None]
|
|
232
|
+
|
|
233
|
+
_MAXIMUM_DEPTH = 2000
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def FromDecoder(
|
|
237
|
+
cls,
|
|
238
|
+
decoder: utils.LevelDBDecoder,
|
|
239
|
+
base_offset: int = 0,
|
|
240
|
+
) -> SortableIDBKey:
|
|
241
|
+
"""Decodes a sortable IDBKey from the current position of a LevelDBDecoder.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
decoder: the LevelDBDecoder.
|
|
245
|
+
base_offset: the base offset.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The decoded SortableIDBKey.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ParserError: on invalid key type or truncated data.
|
|
252
|
+
RecursionError: if maximum depth encountered.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def RecursiveParse(depth: int) -> Tuple[int, definitions.IDBKeyType, Any]:
|
|
256
|
+
"""Recursively parses sortable IDBKeys.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
depth: the current recursion depth.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
A tuple of the offset, the key type and the key value (where the value
|
|
263
|
+
can be bytes, str, float, datetime or a list of these types).
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ParserError: on invalid IDBKeyType or invalid array length during
|
|
267
|
+
parsing.
|
|
268
|
+
RecursionError: if maximum depth encountered during parsing.
|
|
269
|
+
"""
|
|
270
|
+
if depth == cls._MAXIMUM_DEPTH:
|
|
271
|
+
raise RecursionError("Maximum recursion depth encountered")
|
|
272
|
+
|
|
273
|
+
value: Any = None
|
|
274
|
+
offset, ordered_type = decoder.DecodeUint8()
|
|
275
|
+
if ordered_type == definitions.OrderedIDBKeyType.NUMBER:
|
|
276
|
+
_, value = decoder.DecodeSortableDouble()
|
|
277
|
+
return offset, definitions.IDBKeyType.NUMBER, value
|
|
278
|
+
if ordered_type == definitions.OrderedIDBKeyType.DATE:
|
|
279
|
+
_, raw_date = decoder.DecodeSortableDouble()
|
|
280
|
+
return (
|
|
281
|
+
offset,
|
|
282
|
+
definitions.IDBKeyType.DATE,
|
|
283
|
+
datetime.utcfromtimestamp(raw_date / 1000.0),
|
|
284
|
+
)
|
|
285
|
+
if ordered_type == definitions.OrderedIDBKeyType.STRING:
|
|
286
|
+
_, value = decoder.DecodeSortableString()
|
|
287
|
+
return offset, definitions.IDBKeyType.STRING, value
|
|
288
|
+
if ordered_type == definitions.OrderedIDBKeyType.BINARY:
|
|
289
|
+
_, value = decoder.DecodeSortableBinary()
|
|
290
|
+
return offset, definitions.IDBKeyType.BINARY, value
|
|
291
|
+
if ordered_type == definitions.OrderedIDBKeyType.ARRAY:
|
|
292
|
+
value = []
|
|
293
|
+
while True:
|
|
294
|
+
_, next_byte = decoder.PeekBytes(1)
|
|
295
|
+
if next_byte[0] == definitions.SENTINEL:
|
|
296
|
+
decoder.ReadBytes(1)
|
|
297
|
+
break
|
|
298
|
+
_, _, item = RecursiveParse(depth + 1)
|
|
299
|
+
value.append(item)
|
|
300
|
+
return offset, definitions.IDBKeyType.ARRAY, value
|
|
301
|
+
|
|
302
|
+
raise errors.ParserError(f"Unknown ordered key type {ordered_type}")
|
|
303
|
+
|
|
304
|
+
offset, key_type, value = RecursiveParse(0)
|
|
305
|
+
return cls(base_offset + offset, key_type, value)
|
|
306
|
+
|
|
307
|
+
|
|
220
308
|
@dataclass
|
|
221
309
|
class IDBKeyPath(utils.FromDecoderMixin):
|
|
222
310
|
"""An IDBKeyPath.
|
|
@@ -383,6 +471,9 @@ class BaseIndexedDBKey:
|
|
|
383
471
|
Args:
|
|
384
472
|
decoder: the stream decoder
|
|
385
473
|
|
|
474
|
+
Returns:
|
|
475
|
+
The decoded value.
|
|
476
|
+
|
|
386
477
|
Raises:
|
|
387
478
|
NotImplementedError.
|
|
388
479
|
"""
|
|
@@ -392,7 +483,7 @@ class BaseIndexedDBKey:
|
|
|
392
483
|
"""Parses the value from raw bytes.
|
|
393
484
|
|
|
394
485
|
Args:
|
|
395
|
-
value_data: the raw value
|
|
486
|
+
value_data: the raw value data.
|
|
396
487
|
|
|
397
488
|
Returns:
|
|
398
489
|
The parsed value.
|
|
@@ -408,13 +499,15 @@ class BaseIndexedDBKey:
|
|
|
408
499
|
decoder: utils.LevelDBDecoder,
|
|
409
500
|
key_prefix: KeyPrefix,
|
|
410
501
|
base_offset: int = 0,
|
|
411
|
-
) -> T:
|
|
412
|
-
"""
|
|
502
|
+
) -> T:
|
|
503
|
+
"""Parses the key from the current position of the LevelDBDecoder.
|
|
504
|
+
|
|
505
|
+
To be implemented by subclasses.
|
|
413
506
|
|
|
414
507
|
Args:
|
|
415
508
|
decoder: the stream decoder.
|
|
416
|
-
key_prefix: the
|
|
417
|
-
base_offset: the base offset.
|
|
509
|
+
key_prefix: the key prefix.
|
|
510
|
+
base_offset: the base offset of the key.
|
|
418
511
|
|
|
419
512
|
Returns:
|
|
420
513
|
The decoded key.
|
|
@@ -437,7 +530,7 @@ class BaseIndexedDBKey:
|
|
|
437
530
|
"""
|
|
438
531
|
decoder = utils.LevelDBDecoder(stream)
|
|
439
532
|
key_prefix = KeyPrefix.FromDecoder(decoder, base_offset=base_offset)
|
|
440
|
-
return cls.FromDecoder(
|
|
533
|
+
return cls.FromDecoder(
|
|
441
534
|
decoder=decoder, key_prefix=key_prefix, base_offset=base_offset
|
|
442
535
|
)
|
|
443
536
|
|
|
@@ -453,9 +546,7 @@ class BaseIndexedDBKey:
|
|
|
453
546
|
The decoded key.
|
|
454
547
|
"""
|
|
455
548
|
stream = io.BytesIO(raw_data)
|
|
456
|
-
return cls.FromStream(
|
|
457
|
-
stream=stream, base_offset=base_offset
|
|
458
|
-
)
|
|
549
|
+
return cls.FromStream(stream=stream, base_offset=base_offset)
|
|
459
550
|
|
|
460
551
|
|
|
461
552
|
@dataclass
|
|
@@ -716,7 +807,11 @@ class GlobalMetaDataKey(BaseIndexedDBKey):
|
|
|
716
807
|
"""A GlobalMetaDataKey parser."""
|
|
717
808
|
|
|
718
809
|
# pylint: disable=line-too-long
|
|
719
|
-
METADATA_TYPE_TO_CLASS
|
|
810
|
+
METADATA_TYPE_TO_CLASS: ClassVar[
|
|
811
|
+
dict[ # pylint: disable=invalid-name
|
|
812
|
+
definitions.GlobalMetadataKeyType, type[BaseIndexedDBKey]
|
|
813
|
+
]
|
|
814
|
+
] = {
|
|
720
815
|
definitions.GlobalMetadataKeyType.ACTIVE_BLOB_JOURNAL: ActiveBlobJournalKey,
|
|
721
816
|
definitions.GlobalMetadataKeyType.DATA_VERSION: DataVersionKey,
|
|
722
817
|
definitions.GlobalMetadataKeyType.DATABASE_FREE_LIST: DatabaseFreeListKey,
|
|
@@ -743,18 +838,7 @@ class GlobalMetaDataKey(BaseIndexedDBKey):
|
|
|
743
838
|
decoder: utils.LevelDBDecoder,
|
|
744
839
|
key_prefix: KeyPrefix,
|
|
745
840
|
base_offset: int = 0,
|
|
746
|
-
) ->
|
|
747
|
-
ActiveBlobJournalKey,
|
|
748
|
-
DataVersionKey,
|
|
749
|
-
DatabaseFreeListKey,
|
|
750
|
-
DatabaseNameKey,
|
|
751
|
-
EarliestSweepKey,
|
|
752
|
-
EarliestCompactionTimeKey,
|
|
753
|
-
MaxDatabaseIdKey,
|
|
754
|
-
RecoveryBlobJournalKey,
|
|
755
|
-
SchemaVersionKey,
|
|
756
|
-
ScopesPrefixKey,
|
|
757
|
-
]:
|
|
841
|
+
) -> BaseIndexedDBKey:
|
|
758
842
|
"""Decodes the global metadata key.
|
|
759
843
|
|
|
760
844
|
Raises:
|
|
@@ -766,9 +850,7 @@ class GlobalMetaDataKey(BaseIndexedDBKey):
|
|
|
766
850
|
key_class = cls.METADATA_TYPE_TO_CLASS.get(metadata_type)
|
|
767
851
|
if not key_class:
|
|
768
852
|
raise errors.ParserError("Unknown metadata key type")
|
|
769
|
-
return key_class.FromDecoder(
|
|
770
|
-
decoder, key_prefix, base_offset
|
|
771
|
-
)
|
|
853
|
+
return key_class.FromDecoder(decoder, key_prefix, base_offset)
|
|
772
854
|
|
|
773
855
|
|
|
774
856
|
@dataclass
|
|
@@ -1284,7 +1366,11 @@ class IndexedDbKey(BaseIndexedDBKey):
|
|
|
1284
1366
|
A factory class for parsing IndexedDB keys.
|
|
1285
1367
|
"""
|
|
1286
1368
|
|
|
1287
|
-
METADATA_TYPE_TO_CLASS
|
|
1369
|
+
METADATA_TYPE_TO_CLASS: ClassVar[
|
|
1370
|
+
dict[ # pylint: disable=invalid-name
|
|
1371
|
+
definitions.KeyPrefixType, Optional[type[BaseIndexedDBKey]]
|
|
1372
|
+
]
|
|
1373
|
+
] = {
|
|
1288
1374
|
definitions.KeyPrefixType.BLOB_ENTRY: BlobEntryKey,
|
|
1289
1375
|
definitions.KeyPrefixType.DATABASE_METADATA: DatabaseMetaDataKey,
|
|
1290
1376
|
definitions.KeyPrefixType.EXISTS_ENTRY: ExistsEntryKey,
|
|
@@ -1307,14 +1393,7 @@ class IndexedDbKey(BaseIndexedDBKey):
|
|
|
1307
1393
|
decoder: utils.LevelDBDecoder,
|
|
1308
1394
|
key_prefix: KeyPrefix,
|
|
1309
1395
|
base_offset: int = 0,
|
|
1310
|
-
) ->
|
|
1311
|
-
BlobEntryKey,
|
|
1312
|
-
DatabaseMetaDataKey,
|
|
1313
|
-
ExistsEntryKey,
|
|
1314
|
-
GlobalMetaDataKey,
|
|
1315
|
-
IndexDataKey,
|
|
1316
|
-
ObjectStoreDataKey,
|
|
1317
|
-
]:
|
|
1396
|
+
) -> BaseIndexedDBKey:
|
|
1318
1397
|
"""Decodes the IndexedDB key."""
|
|
1319
1398
|
key_type = key_prefix.GetKeyPrefixType()
|
|
1320
1399
|
key_class = cls.METADATA_TYPE_TO_CLASS.get(key_type)
|
|
@@ -1322,7 +1401,7 @@ class IndexedDbKey(BaseIndexedDBKey):
|
|
|
1322
1401
|
raise errors.ParserError("Unknown KeyPrefixType")
|
|
1323
1402
|
return key_class.FromDecoder(
|
|
1324
1403
|
decoder=decoder,
|
|
1325
|
-
key_prefix=key_prefix,
|
|
1404
|
+
key_prefix=key_prefix,
|
|
1326
1405
|
base_offset=base_offset,
|
|
1327
1406
|
)
|
|
1328
1407
|
|
|
@@ -1481,8 +1560,8 @@ class IndexedDBExternalObject(utils.FromDecoderMixin):
|
|
|
1481
1560
|
|
|
1482
1561
|
|
|
1483
1562
|
@dataclass
|
|
1484
|
-
class
|
|
1485
|
-
"""An IndexedDB Record.
|
|
1563
|
+
class ChromiumIndexedDBRecord:
|
|
1564
|
+
"""An IndexedDB Record parsed from LevelDB.
|
|
1486
1565
|
|
|
1487
1566
|
Attributes:
|
|
1488
1567
|
path: the source file path
|
|
@@ -1498,7 +1577,7 @@ class IndexedDBRecord:
|
|
|
1498
1577
|
object_store_id: the object store ID.
|
|
1499
1578
|
database_name: the name of the database, if available.
|
|
1500
1579
|
object_store_name: the name of the object store, if available.
|
|
1501
|
-
|
|
1580
|
+
blobs: the list of blob paths and contents or error message, if available.
|
|
1502
1581
|
raw_key: the raw key, if available.
|
|
1503
1582
|
raw_value: the raw value, if available.
|
|
1504
1583
|
"""
|
|
@@ -1515,15 +1594,19 @@ class IndexedDBRecord:
|
|
|
1515
1594
|
object_store_id: int
|
|
1516
1595
|
database_name: Optional[str] = None
|
|
1517
1596
|
object_store_name: Optional[str] = None
|
|
1518
|
-
|
|
1597
|
+
blobs: Optional[list[tuple[str, Optional[Any]]]] = None
|
|
1519
1598
|
raw_key: Optional[bytes] = None
|
|
1520
1599
|
raw_value: Optional[bytes] = None
|
|
1521
1600
|
|
|
1522
1601
|
@classmethod
|
|
1523
1602
|
def FromLevelDBRecord(
|
|
1524
|
-
cls,
|
|
1525
|
-
|
|
1526
|
-
|
|
1603
|
+
cls,
|
|
1604
|
+
db_record: record.LevelDBRecord,
|
|
1605
|
+
parse_value: bool = True,
|
|
1606
|
+
include_raw_data: bool = False,
|
|
1607
|
+
blob_folder_reader: Optional[BlobFolderReader] = None,
|
|
1608
|
+
) -> ChromiumIndexedDBRecord:
|
|
1609
|
+
"""Returns an ChromiumIndexedDBRecord from a ParsedInternalKey."""
|
|
1527
1610
|
idb_key = IndexedDbKey.FromBytes(
|
|
1528
1611
|
db_record.record.key, base_offset=db_record.record.offset
|
|
1529
1612
|
)
|
|
@@ -1533,14 +1616,30 @@ class IndexedDBRecord:
|
|
|
1533
1616
|
else:
|
|
1534
1617
|
idb_value = None
|
|
1535
1618
|
|
|
1619
|
+
blobs = []
|
|
1620
|
+
if isinstance(idb_value, IndexedDBExternalObject) and blob_folder_reader:
|
|
1621
|
+
for (
|
|
1622
|
+
blob_path_or_error,
|
|
1623
|
+
blob_data,
|
|
1624
|
+
) in blob_folder_reader.ReadBlobsFromExternalObjectEntries(
|
|
1625
|
+
idb_key.key_prefix.database_id, idb_value.entries
|
|
1626
|
+
):
|
|
1627
|
+
if blob_data:
|
|
1628
|
+
blob = blink.V8ScriptValueDecoder.FromBytes(blob_data)
|
|
1629
|
+
else:
|
|
1630
|
+
blob = None
|
|
1631
|
+
blobs.append((blob_path_or_error, blob))
|
|
1632
|
+
|
|
1536
1633
|
return cls(
|
|
1537
1634
|
path=db_record.path,
|
|
1538
1635
|
offset=db_record.record.offset,
|
|
1539
1636
|
key=idb_key,
|
|
1540
1637
|
value=idb_value,
|
|
1541
|
-
sequence_number=
|
|
1542
|
-
|
|
1543
|
-
|
|
1638
|
+
sequence_number=(
|
|
1639
|
+
db_record.record.sequence_number
|
|
1640
|
+
if hasattr(db_record.record, "sequence_number")
|
|
1641
|
+
else None
|
|
1642
|
+
),
|
|
1544
1643
|
type=db_record.record.record_type,
|
|
1545
1644
|
level=db_record.level,
|
|
1546
1645
|
recovered=db_record.recovered,
|
|
@@ -1548,19 +1647,28 @@ class IndexedDBRecord:
|
|
|
1548
1647
|
object_store_id=idb_key.key_prefix.object_store_id,
|
|
1549
1648
|
database_name=None,
|
|
1550
1649
|
object_store_name=None,
|
|
1551
|
-
|
|
1552
|
-
raw_key=db_record.record.key,
|
|
1553
|
-
raw_value=db_record.record.value,
|
|
1650
|
+
blobs=blobs,
|
|
1651
|
+
raw_key=db_record.record.key if include_raw_data else None,
|
|
1652
|
+
raw_value=db_record.record.value if include_raw_data else None,
|
|
1554
1653
|
)
|
|
1555
1654
|
|
|
1556
1655
|
@classmethod
|
|
1557
1656
|
def FromFile(
|
|
1558
|
-
cls,
|
|
1559
|
-
|
|
1560
|
-
|
|
1657
|
+
cls,
|
|
1658
|
+
file_path: pathlib.Path,
|
|
1659
|
+
parse_value: bool = True,
|
|
1660
|
+
include_raw_data: bool = False,
|
|
1661
|
+
blob_folder_reader: Optional[BlobFolderReader] = None,
|
|
1662
|
+
) -> Generator[ChromiumIndexedDBRecord, None, None]:
|
|
1663
|
+
"""Yields ChromiumIndexedDBRecord from a file."""
|
|
1561
1664
|
for db_record in record.LevelDBRecord.FromFile(file_path):
|
|
1562
1665
|
try:
|
|
1563
|
-
yield cls.FromLevelDBRecord(
|
|
1666
|
+
yield cls.FromLevelDBRecord(
|
|
1667
|
+
db_record,
|
|
1668
|
+
parse_value=parse_value,
|
|
1669
|
+
include_raw_data=include_raw_data,
|
|
1670
|
+
blob_folder_reader=blob_folder_reader,
|
|
1671
|
+
)
|
|
1564
1672
|
except (
|
|
1565
1673
|
errors.ParserError,
|
|
1566
1674
|
errors.DecoderError,
|
|
@@ -1577,6 +1685,88 @@ class IndexedDBRecord:
|
|
|
1577
1685
|
print(f"Traceback: {traceback.format_exc()}", file=sys.stderr)
|
|
1578
1686
|
|
|
1579
1687
|
|
|
1688
|
+
class BlobFolderReader:
|
|
1689
|
+
"""A blob folder reader for Chrome/Chromium.
|
|
1690
|
+
|
|
1691
|
+
Attributes:
|
|
1692
|
+
folder_name (str): the source blob folder.
|
|
1693
|
+
"""
|
|
1694
|
+
|
|
1695
|
+
def __init__(self, folder_name: pathlib.Path):
|
|
1696
|
+
"""Initializes the BlobFolderReader.
|
|
1697
|
+
|
|
1698
|
+
Args:
|
|
1699
|
+
folder_name: the source blob folder.
|
|
1700
|
+
|
|
1701
|
+
Raises:
|
|
1702
|
+
ValueError: if folder_name is None or not a directory.
|
|
1703
|
+
"""
|
|
1704
|
+
if not folder_name or not folder_name.is_dir():
|
|
1705
|
+
raise ValueError(f"{folder_name} is None or not a directory")
|
|
1706
|
+
self.folder_name = folder_name.absolute()
|
|
1707
|
+
|
|
1708
|
+
def ReadBlob(self, database_id: int, blob_id: int) -> tuple[str, bytes]:
|
|
1709
|
+
"""Reads a blob from the blob folder.
|
|
1710
|
+
|
|
1711
|
+
Args:
|
|
1712
|
+
database_id: the database id of the blob to read.
|
|
1713
|
+
blob_id: the blob id to read.
|
|
1714
|
+
|
|
1715
|
+
Returns:
|
|
1716
|
+
A tuple of the blob path and contents.
|
|
1717
|
+
|
|
1718
|
+
Raises:
|
|
1719
|
+
FileNotFoundError: if the database directory or blob folder or blob not
|
|
1720
|
+
found.
|
|
1721
|
+
"""
|
|
1722
|
+
directory_path = self.folder_name / f"{database_id:x}"
|
|
1723
|
+
if not directory_path.exists():
|
|
1724
|
+
raise FileNotFoundError(f"Database directory not found: {directory_path}")
|
|
1725
|
+
|
|
1726
|
+
blob_folder = directory_path / f"{(blob_id & 0xff00) >> 8:02x}"
|
|
1727
|
+
if not blob_folder.exists():
|
|
1728
|
+
raise FileNotFoundError(f"Blob folder not found: {blob_folder}")
|
|
1729
|
+
|
|
1730
|
+
blob_path = blob_folder / f"{blob_id:x}"
|
|
1731
|
+
if not blob_path.exists():
|
|
1732
|
+
raise FileNotFoundError(f"Blob ({blob_id}) not found: {blob_path}")
|
|
1733
|
+
|
|
1734
|
+
with open(blob_path, "rb") as f:
|
|
1735
|
+
return str(blob_path), f.read()
|
|
1736
|
+
|
|
1737
|
+
def ReadBlobsFromExternalObjectEntries(
|
|
1738
|
+
self, database_id: int, entries: list[ExternalObjectEntry]
|
|
1739
|
+
) -> Generator[tuple[str, Optional[bytes]], None, None]:
|
|
1740
|
+
"""Reads blobs from the blob folder.
|
|
1741
|
+
|
|
1742
|
+
Args:
|
|
1743
|
+
database_id: the database id.
|
|
1744
|
+
entries: the external object entries.
|
|
1745
|
+
|
|
1746
|
+
Yields:
|
|
1747
|
+
A tuple of blob path and contents or if the blob is not found, an error
|
|
1748
|
+
message and None.
|
|
1749
|
+
"""
|
|
1750
|
+
for entry in entries:
|
|
1751
|
+
if (
|
|
1752
|
+
entry.object_type
|
|
1753
|
+
in (
|
|
1754
|
+
definitions.ExternalObjectType.BLOB,
|
|
1755
|
+
definitions.ExternalObjectType.FILE,
|
|
1756
|
+
)
|
|
1757
|
+
and entry.blob_number is not None
|
|
1758
|
+
):
|
|
1759
|
+
try:
|
|
1760
|
+
yield self.ReadBlob(database_id, entry.blob_number)
|
|
1761
|
+
except FileNotFoundError as err:
|
|
1762
|
+
error_message = (
|
|
1763
|
+
f"Blob not found for ExternalObjectEntry at offset {entry.offset}"
|
|
1764
|
+
f": {err}"
|
|
1765
|
+
)
|
|
1766
|
+
print(error_message, file=sys.stderr)
|
|
1767
|
+
yield error_message, None
|
|
1768
|
+
|
|
1769
|
+
|
|
1580
1770
|
class FolderReader:
|
|
1581
1771
|
"""A IndexedDB folder reader for Chrome/Chromium.
|
|
1582
1772
|
|
|
@@ -1595,15 +1785,25 @@ class FolderReader:
|
|
|
1595
1785
|
"""
|
|
1596
1786
|
if not folder_name or not folder_name.is_dir():
|
|
1597
1787
|
raise ValueError(f"{folder_name} is None or not a directory")
|
|
1598
|
-
self.folder_name = folder_name
|
|
1788
|
+
self.folder_name = folder_name.absolute()
|
|
1789
|
+
|
|
1790
|
+
# Locate the correponding blob folder. The folder_name should be
|
|
1791
|
+
# <origin>.leveldb and the blob folder should be <origin>.blob
|
|
1792
|
+
if str(self.folder_name).endswith(".leveldb"):
|
|
1793
|
+
self.blob_folder_reader = BlobFolderReader(
|
|
1794
|
+
pathlib.Path(str(self.folder_name).replace(".leveldb", ".blob"))
|
|
1795
|
+
)
|
|
1796
|
+
else:
|
|
1797
|
+
self.blob_folder_reader = None # type: ignore[assignment]
|
|
1599
1798
|
|
|
1600
1799
|
def GetRecords(
|
|
1601
1800
|
self,
|
|
1602
1801
|
use_manifest: bool = False,
|
|
1603
1802
|
use_sequence_number: bool = False,
|
|
1604
1803
|
parse_value: bool = True,
|
|
1605
|
-
|
|
1606
|
-
|
|
1804
|
+
include_raw_data: bool = False,
|
|
1805
|
+
) -> Generator[ChromiumIndexedDBRecord, None, None]:
|
|
1806
|
+
"""Yields ChromiumIndexedDBRecord.
|
|
1607
1807
|
|
|
1608
1808
|
Args:
|
|
1609
1809
|
use_manifest: True to use the current manifest in the folder as a means to
|
|
@@ -1613,15 +1813,18 @@ class FolderReader:
|
|
|
1613
1813
|
parse_value: True to parse values.
|
|
1614
1814
|
|
|
1615
1815
|
Yields:
|
|
1616
|
-
|
|
1816
|
+
ChromiumIndexedDBRecord.
|
|
1617
1817
|
"""
|
|
1618
1818
|
leveldb_folder_reader = record.FolderReader(self.folder_name)
|
|
1619
1819
|
for leveldb_record in leveldb_folder_reader.GetRecords(
|
|
1620
1820
|
use_manifest=use_manifest, use_sequence_number=use_sequence_number
|
|
1621
1821
|
):
|
|
1622
1822
|
try:
|
|
1623
|
-
yield
|
|
1624
|
-
leveldb_record,
|
|
1823
|
+
yield ChromiumIndexedDBRecord.FromLevelDBRecord(
|
|
1824
|
+
leveldb_record,
|
|
1825
|
+
parse_value=parse_value,
|
|
1826
|
+
include_raw_data=include_raw_data,
|
|
1827
|
+
blob_folder_reader=self.blob_folder_reader,
|
|
1625
1828
|
)
|
|
1626
1829
|
except (
|
|
1627
1830
|
errors.ParserError,
|