dfindexeddb 20240417__py3-none-any.whl → 20240501__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/record.py +82 -4
- dfindexeddb/indexeddb/cli.py +70 -78
- dfindexeddb/indexeddb/safari/definitions.py +123 -0
- dfindexeddb/indexeddb/safari/record.py +238 -0
- dfindexeddb/indexeddb/safari/webkit.py +693 -0
- dfindexeddb/leveldb/cli.py +4 -8
- dfindexeddb/leveldb/record.py +180 -53
- dfindexeddb/version.py +1 -1
- {dfindexeddb-20240417.dist-info → dfindexeddb-20240501.dist-info}/METADATA +43 -70
- {dfindexeddb-20240417.dist-info → dfindexeddb-20240501.dist-info}/RECORD +15 -12
- {dfindexeddb-20240417.dist-info → dfindexeddb-20240501.dist-info}/AUTHORS +0 -0
- {dfindexeddb-20240417.dist-info → dfindexeddb-20240501.dist-info}/LICENSE +0 -0
- {dfindexeddb-20240417.dist-info → dfindexeddb-20240501.dist-info}/WHEEL +0 -0
- {dfindexeddb-20240417.dist-info → dfindexeddb-20240501.dist-info}/entry_points.txt +0 -0
- {dfindexeddb-20240417.dist-info → dfindexeddb-20240501.dist-info}/top_level.txt +0 -0
|
@@ -17,7 +17,11 @@ from __future__ import annotations
|
|
|
17
17
|
from dataclasses import dataclass, field
|
|
18
18
|
from datetime import datetime
|
|
19
19
|
import io
|
|
20
|
-
|
|
20
|
+
import pathlib
|
|
21
|
+
import sys
|
|
22
|
+
import traceback
|
|
23
|
+
from typing import Any, BinaryIO, Generator, Optional, Tuple, Type, TypeVar, \
|
|
24
|
+
Union
|
|
21
25
|
|
|
22
26
|
from dfindexeddb import errors
|
|
23
27
|
from dfindexeddb.indexeddb.chromium import blink
|
|
@@ -456,7 +460,7 @@ class MaxDatabaseIdKey(BaseIndexedDBKey):
|
|
|
456
460
|
cls, decoder: utils.LevelDBDecoder, key_prefix: KeyPrefix,
|
|
457
461
|
base_offset: int = 0
|
|
458
462
|
) -> MaxDatabaseIdKey:
|
|
459
|
-
"""Decodes the maximum
|
|
463
|
+
"""Decodes the maximum database key."""
|
|
460
464
|
offset, key_type = decoder.DecodeUint8()
|
|
461
465
|
if key_type != definitions.GlobalMetadataKeyType.MAX_DATABASE_ID:
|
|
462
466
|
raise errors.ParserError('Not a MaxDatabaseIdKey')
|
|
@@ -1331,12 +1335,14 @@ class IndexedDBRecord:
|
|
|
1331
1335
|
"""An IndexedDB Record.
|
|
1332
1336
|
|
|
1333
1337
|
Attributes:
|
|
1338
|
+
path: the source file path
|
|
1334
1339
|
offset: the offset of the record.
|
|
1335
1340
|
key: the key of the record.
|
|
1336
1341
|
value: the value of the record.
|
|
1337
1342
|
sequence_number: if available, the sequence number of the record.
|
|
1338
1343
|
type: the type of the record.
|
|
1339
|
-
level: the leveldb level, None
|
|
1344
|
+
level: the leveldb level, if applicable, None can indicate the record
|
|
1345
|
+
originated from a log file or the level could not be determined.
|
|
1340
1346
|
recovered: True if the record is a recovered record.
|
|
1341
1347
|
"""
|
|
1342
1348
|
path: str
|
|
@@ -1350,7 +1356,8 @@ class IndexedDBRecord:
|
|
|
1350
1356
|
|
|
1351
1357
|
@classmethod
|
|
1352
1358
|
def FromLevelDBRecord(
|
|
1353
|
-
cls,
|
|
1359
|
+
cls,
|
|
1360
|
+
db_record: record.LevelDBRecord
|
|
1354
1361
|
) -> IndexedDBRecord:
|
|
1355
1362
|
"""Returns an IndexedDBRecord from a ParsedInternalKey."""
|
|
1356
1363
|
idb_key = IndexedDbKey.FromBytes(
|
|
@@ -1366,3 +1373,74 @@ class IndexedDBRecord:
|
|
|
1366
1373
|
type=db_record.record.record_type,
|
|
1367
1374
|
level=db_record.level,
|
|
1368
1375
|
recovered=db_record.recovered)
|
|
1376
|
+
|
|
1377
|
+
@classmethod
|
|
1378
|
+
def FromFile(
|
|
1379
|
+
cls,
|
|
1380
|
+
file_path: pathlib.Path
|
|
1381
|
+
) -> Generator[IndexedDBRecord, None, None]:
|
|
1382
|
+
"""Yields IndexedDBRecords from a file."""
|
|
1383
|
+
for db_record in record.LevelDBRecord.FromFile(file_path):
|
|
1384
|
+
try:
|
|
1385
|
+
yield cls.FromLevelDBRecord(db_record)
|
|
1386
|
+
except(
|
|
1387
|
+
errors.ParserError,
|
|
1388
|
+
errors.DecoderError,
|
|
1389
|
+
NotImplementedError) as err:
|
|
1390
|
+
print((
|
|
1391
|
+
'Error parsing Indexeddb record: '
|
|
1392
|
+
f'{err} at offset {db_record.record.offset} in '
|
|
1393
|
+
f'{db_record.path}'),
|
|
1394
|
+
file=sys.stderr)
|
|
1395
|
+
print(f'Traceback: {traceback.format_exc()}', file=sys.stderr)
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
class FolderReader:
|
|
1399
|
+
"""A IndexedDB folder reader for Chrome/Chromium.
|
|
1400
|
+
|
|
1401
|
+
Attributes:
|
|
1402
|
+
foldername (str): the source LevelDB folder.
|
|
1403
|
+
"""
|
|
1404
|
+
|
|
1405
|
+
def __init__(self, foldername: pathlib.Path):
|
|
1406
|
+
"""Initializes the FileReader.
|
|
1407
|
+
|
|
1408
|
+
Args:
|
|
1409
|
+
foldername: the source IndexedDB folder.
|
|
1410
|
+
|
|
1411
|
+
Raises:
|
|
1412
|
+
ValueError: if foldername is None or not a directory.
|
|
1413
|
+
"""
|
|
1414
|
+
if not foldername or not foldername.is_dir():
|
|
1415
|
+
raise ValueError(f'{foldername} is None or not a directory')
|
|
1416
|
+
self.foldername = foldername
|
|
1417
|
+
|
|
1418
|
+
def GetRecords(
|
|
1419
|
+
self,
|
|
1420
|
+
use_manifest: bool = False
|
|
1421
|
+
) -> Generator[IndexedDBRecord, None, None]:
|
|
1422
|
+
"""Yield LevelDBRecords.
|
|
1423
|
+
|
|
1424
|
+
Args:
|
|
1425
|
+
use_manifest: True to use the current manifest in the folder as a means to
|
|
1426
|
+
find the active file set.
|
|
1427
|
+
|
|
1428
|
+
Yields:
|
|
1429
|
+
IndexedDBRecord.
|
|
1430
|
+
"""
|
|
1431
|
+
leveldb_folder_reader = record.FolderReader(self.foldername)
|
|
1432
|
+
for leveldb_record in leveldb_folder_reader.GetRecords(
|
|
1433
|
+
use_manifest=use_manifest):
|
|
1434
|
+
try:
|
|
1435
|
+
yield IndexedDBRecord.FromLevelDBRecord(
|
|
1436
|
+
leveldb_record)
|
|
1437
|
+
except(
|
|
1438
|
+
errors.ParserError,
|
|
1439
|
+
errors.DecoderError,
|
|
1440
|
+
NotImplementedError) as err:
|
|
1441
|
+
print((
|
|
1442
|
+
'Error parsing Indexeddb record: '
|
|
1443
|
+
f'{err} at offset {leveldb_record.record.offset} in '
|
|
1444
|
+
f'{leveldb_record.path}'),
|
|
1445
|
+
file=sys.stderr)
|
|
1446
|
+
print(f'Traceback: {traceback.format_exc()}', file=sys.stderr)
|
dfindexeddb/indexeddb/cli.py
CHANGED
|
@@ -19,14 +19,12 @@ import enum
|
|
|
19
19
|
from datetime import datetime
|
|
20
20
|
import json
|
|
21
21
|
import pathlib
|
|
22
|
-
import sys
|
|
23
|
-
import traceback
|
|
24
22
|
|
|
25
|
-
from dfindexeddb import errors
|
|
26
23
|
from dfindexeddb import version
|
|
27
|
-
from dfindexeddb.
|
|
24
|
+
from dfindexeddb.indexeddb.chromium import blink
|
|
28
25
|
from dfindexeddb.indexeddb.chromium import record as chromium_record
|
|
29
26
|
from dfindexeddb.indexeddb.chromium import v8
|
|
27
|
+
from dfindexeddb.indexeddb.safari import record as safari_record
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
_VALID_PRINTABLE_CHARACTERS = (
|
|
@@ -73,94 +71,79 @@ def _Output(structure, output):
|
|
|
73
71
|
print(structure)
|
|
74
72
|
|
|
75
73
|
|
|
74
|
+
def BlinkCommand(args):
|
|
75
|
+
"""The CLI for processing a file as a blink value."""
|
|
76
|
+
with open(args.source, 'rb') as fd:
|
|
77
|
+
buffer = fd.read()
|
|
78
|
+
blink_value = blink.V8ScriptValueDecoder.FromBytes(buffer)
|
|
79
|
+
_Output(blink_value, output=args.output)
|
|
80
|
+
|
|
81
|
+
|
|
76
82
|
def DbCommand(args):
|
|
77
|
-
"""The CLI for processing a directory as
|
|
78
|
-
if args.
|
|
79
|
-
for db_record in
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
errors.ParserError,
|
|
86
|
-
errors.DecoderError,
|
|
87
|
-
NotImplementedError) as err:
|
|
88
|
-
print((
|
|
89
|
-
f'Error parsing Indexeddb record {record.__class__.__name__}: {err}'
|
|
90
|
-
f' at offset {record.offset} in {db_record.path}'), file=sys.stderr)
|
|
91
|
-
print(f'Traceback: {traceback.format_exc()}', file=sys.stderr)
|
|
92
|
-
continue
|
|
93
|
-
_Output(idb_record, output=args.output)
|
|
94
|
-
else:
|
|
95
|
-
for db_record in leveldb_record.LevelDBRecord.FromDir(args.source):
|
|
96
|
-
record = db_record.record
|
|
97
|
-
try:
|
|
98
|
-
idb_record = chromium_record.IndexedDBRecord.FromLevelDBRecord(
|
|
99
|
-
db_record)
|
|
100
|
-
except(
|
|
101
|
-
errors.ParserError,
|
|
102
|
-
errors.DecoderError,
|
|
103
|
-
NotImplementedError) as err:
|
|
104
|
-
print((
|
|
105
|
-
f'Error parsing Indexeddb record {record.__class__.__name__}: {err}'
|
|
106
|
-
f' at offset {record.offset} in {db_record.path}'), file=sys.stderr)
|
|
107
|
-
print(f'Traceback: {traceback.format_exc()}', file=sys.stderr)
|
|
108
|
-
continue
|
|
109
|
-
_Output(idb_record, output=args.output)
|
|
83
|
+
"""The CLI for processing a directory as IndexedDB."""
|
|
84
|
+
if args.format in ('chrome', 'chromium'):
|
|
85
|
+
for db_record in chromium_record.FolderReader(
|
|
86
|
+
args.source).GetRecords(use_manifest=args.use_manifest):
|
|
87
|
+
_Output(db_record, output=args.output)
|
|
88
|
+
elif args.format == 'safari':
|
|
89
|
+
for db_record in safari_record.FileReader(args.source).Records():
|
|
90
|
+
_Output(db_record, output=args.output)
|
|
110
91
|
|
|
111
92
|
|
|
112
93
|
def LdbCommand(args):
|
|
113
|
-
"""The CLI for processing a
|
|
114
|
-
for db_record in
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
idb_record = chromium_record.IndexedDBRecord.FromLevelDBRecord(
|
|
118
|
-
db_record)
|
|
119
|
-
except(
|
|
120
|
-
errors.ParserError,
|
|
121
|
-
errors.DecoderError,
|
|
122
|
-
NotImplementedError) as err:
|
|
123
|
-
print(
|
|
124
|
-
(f'Error parsing Indexeddb record {record.__class__.__name__}: {err} '
|
|
125
|
-
f'at offset {record.offset} in {db_record.path}'), file=sys.stderr)
|
|
126
|
-
print(f'Traceback: {traceback.format_exc()}', file=sys.stderr)
|
|
127
|
-
continue
|
|
128
|
-
_Output(idb_record, output=args.output)
|
|
94
|
+
"""The CLI for processing a LevelDB table (.ldb) file as IndexedDB."""
|
|
95
|
+
for db_record in chromium_record.IndexedDBRecord.FromFile(args.source):
|
|
96
|
+
_Output(db_record, output=args.output)
|
|
129
97
|
|
|
130
98
|
|
|
131
99
|
def LogCommand(args):
|
|
132
|
-
"""The CLI for processing a
|
|
133
|
-
for db_record in
|
|
134
|
-
|
|
135
|
-
try:
|
|
136
|
-
idb_record = chromium_record.IndexedDBRecord.FromLevelDBRecord(
|
|
137
|
-
db_record)
|
|
138
|
-
except(
|
|
139
|
-
errors.ParserError,
|
|
140
|
-
errors.DecoderError,
|
|
141
|
-
NotImplementedError) as err:
|
|
142
|
-
print(
|
|
143
|
-
(f'Error parsing Indexeddb record {record.__class__.__name__}: {err} '
|
|
144
|
-
f'at offset {record.offset} in {db_record.path}'), file=sys.stderr)
|
|
145
|
-
print(f'Traceback: {traceback.format_exc()}', file=sys.stderr)
|
|
146
|
-
continue
|
|
147
|
-
_Output(idb_record, output=args.output)
|
|
100
|
+
"""The CLI for processing a LevelDB log file as IndexedDB."""
|
|
101
|
+
for db_record in chromium_record.IndexedDBRecord.FromFile(args.source):
|
|
102
|
+
_Output(db_record, output=args.output)
|
|
148
103
|
|
|
149
104
|
|
|
150
105
|
def App():
|
|
151
106
|
"""The CLI app entrypoint for dfindexeddb."""
|
|
152
107
|
parser = argparse.ArgumentParser(
|
|
153
108
|
prog='dfindexeddb',
|
|
154
|
-
description='A cli tool for parsing
|
|
109
|
+
description='A cli tool for parsing IndexedDB files',
|
|
155
110
|
epilog=f'Version {version.GetVersion()}')
|
|
156
111
|
|
|
157
112
|
subparsers = parser.add_subparsers()
|
|
158
113
|
|
|
114
|
+
parser_blink = subparsers.add_parser(
|
|
115
|
+
'blink', help='Parse a file as a blink value.')
|
|
116
|
+
parser_blink.add_argument(
|
|
117
|
+
'-s', '--source',
|
|
118
|
+
required=True,
|
|
119
|
+
type=pathlib.Path,
|
|
120
|
+
help=(
|
|
121
|
+
'The source file.'))
|
|
122
|
+
parser_blink.add_argument(
|
|
123
|
+
'-o',
|
|
124
|
+
'--output',
|
|
125
|
+
choices=[
|
|
126
|
+
'json',
|
|
127
|
+
'jsonl',
|
|
128
|
+
'repr'],
|
|
129
|
+
default='json',
|
|
130
|
+
help='Output format. Default is json')
|
|
131
|
+
parser_blink.set_defaults(func=BlinkCommand)
|
|
132
|
+
|
|
159
133
|
parser_db = subparsers.add_parser(
|
|
160
|
-
'db', help='Parse a directory as
|
|
134
|
+
'db', help='Parse a directory as IndexedDB.')
|
|
135
|
+
parser_db.add_argument(
|
|
136
|
+
'-s', '--source',
|
|
137
|
+
required=True,
|
|
138
|
+
type=pathlib.Path,
|
|
139
|
+
help=(
|
|
140
|
+
'The source IndexedDB folder (for chrome/chromium) '
|
|
141
|
+
'or file (for safari).'))
|
|
161
142
|
parser_db.add_argument(
|
|
162
|
-
'
|
|
163
|
-
|
|
143
|
+
'--format',
|
|
144
|
+
required=True,
|
|
145
|
+
choices=['chromium', 'chrome', 'safari'],
|
|
146
|
+
help='The type of IndexedDB to parse.')
|
|
164
147
|
parser_db.add_argument(
|
|
165
148
|
'--use_manifest',
|
|
166
149
|
action='store_true',
|
|
@@ -177,9 +160,12 @@ def App():
|
|
|
177
160
|
parser_db.set_defaults(func=DbCommand)
|
|
178
161
|
|
|
179
162
|
parser_ldb = subparsers.add_parser(
|
|
180
|
-
'ldb',
|
|
163
|
+
'ldb',
|
|
164
|
+
help='Parse a ldb file as IndexedDB.')
|
|
181
165
|
parser_ldb.add_argument(
|
|
182
|
-
'-s', '--source',
|
|
166
|
+
'-s', '--source',
|
|
167
|
+
required=True,
|
|
168
|
+
type=pathlib.Path,
|
|
183
169
|
help='The source .ldb file.')
|
|
184
170
|
parser_ldb.add_argument(
|
|
185
171
|
'-o',
|
|
@@ -193,9 +179,12 @@ def App():
|
|
|
193
179
|
parser_ldb.set_defaults(func=LdbCommand)
|
|
194
180
|
|
|
195
181
|
parser_log = subparsers.add_parser(
|
|
196
|
-
'log',
|
|
182
|
+
'log',
|
|
183
|
+
help='Parse a log file as IndexedDB.')
|
|
197
184
|
parser_log.add_argument(
|
|
198
|
-
'-s', '--source',
|
|
185
|
+
'-s', '--source',
|
|
186
|
+
required=True,
|
|
187
|
+
type=pathlib.Path,
|
|
199
188
|
help='The source .log file.')
|
|
200
189
|
parser_log.add_argument(
|
|
201
190
|
'-o',
|
|
@@ -209,4 +198,7 @@ def App():
|
|
|
209
198
|
parser_log.set_defaults(func=LogCommand)
|
|
210
199
|
|
|
211
200
|
args = parser.parse_args()
|
|
212
|
-
args
|
|
201
|
+
if hasattr(args, 'func'):
|
|
202
|
+
args.func(args)
|
|
203
|
+
else:
|
|
204
|
+
parser.print_help()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""Definitions for Webkit/Safari."""
|
|
16
|
+
from enum import IntEnum
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CurrentVersion = 0x0000000F # 15
|
|
20
|
+
TerminatorTag = 0xFFFFFFFF
|
|
21
|
+
StringPoolTag = 0xFFFFFFFE
|
|
22
|
+
NonIndexPropertiesTag = 0xFFFFFFFD
|
|
23
|
+
ImageDataPoolTag = 0xFFFFFFFE
|
|
24
|
+
StringDataIs8BitFlag = 0x80000000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SIDBKeyVersion = 0x00
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SIDBKeyType(IntEnum):
|
|
31
|
+
"""SIDBKeyType."""
|
|
32
|
+
MIN = 0x00
|
|
33
|
+
NUMBER = 0x20
|
|
34
|
+
DATE = 0x40
|
|
35
|
+
STRING = 0x60
|
|
36
|
+
BINARY = 0x80
|
|
37
|
+
ARRAY = 0xA0
|
|
38
|
+
MAX = 0xFF
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SerializationTag(IntEnum):
|
|
42
|
+
"""Database Metadata key types.
|
|
43
|
+
|
|
44
|
+
All tags are recorded as a single uint8_t.
|
|
45
|
+
"""
|
|
46
|
+
ARRAY = 1
|
|
47
|
+
OBJECT = 2
|
|
48
|
+
UNDEFINED = 3
|
|
49
|
+
NULL = 4
|
|
50
|
+
INT = 5
|
|
51
|
+
ZERO = 6
|
|
52
|
+
ONE = 7
|
|
53
|
+
FALSE = 8
|
|
54
|
+
TRUE = 9
|
|
55
|
+
DOUBLE = 10
|
|
56
|
+
DATE = 11
|
|
57
|
+
FILE = 12
|
|
58
|
+
FILE_LIST = 13
|
|
59
|
+
IMAGE_DATA = 14
|
|
60
|
+
BLOB = 15
|
|
61
|
+
STRING = 16
|
|
62
|
+
EMPTY_STRING = 17
|
|
63
|
+
REG_EXP = 18
|
|
64
|
+
OBJECT_REFERENCE = 19
|
|
65
|
+
MESSAGE_PORT_REFERENCE = 20
|
|
66
|
+
ARRAY_BUFFER = 21
|
|
67
|
+
ARRAY_BUFFER_VIEW = 22
|
|
68
|
+
ARRAY_BUFFER_TRANSFER = 23
|
|
69
|
+
TRUE_OBJECT = 24
|
|
70
|
+
FALSE_OBJECT = 25
|
|
71
|
+
STRING_OBJECT = 26
|
|
72
|
+
EMPTY_STRING_OBJECT = 27
|
|
73
|
+
NUMBER_OBJECT = 28
|
|
74
|
+
SET_OBJECT = 29
|
|
75
|
+
MAP_OBJECT = 30
|
|
76
|
+
NON_MAP_PROPERTIES = 31
|
|
77
|
+
NON_SET_PROPERTIES = 32
|
|
78
|
+
CRYPTO_KEY = 33
|
|
79
|
+
SHARED_ARRAY_BUFFER = 34
|
|
80
|
+
WASM_MODULE = 35
|
|
81
|
+
DOM_POINT_READONLY = 36
|
|
82
|
+
DOM_POINT = 37
|
|
83
|
+
DOM_RECT_READONLY = 38
|
|
84
|
+
DOM_RECT = 39
|
|
85
|
+
DOM_MATRIX_READONLY = 40
|
|
86
|
+
DOM_MATRIX = 41
|
|
87
|
+
DOM_QUAD = 42
|
|
88
|
+
IMAGE_BITMAP_TRANSFER = 43
|
|
89
|
+
RTC_CERTIFICATE = 44
|
|
90
|
+
IMAGE_BITMAP = 45
|
|
91
|
+
OFF_SCREEN_CANVAS_TRANSFER = 46
|
|
92
|
+
BIGINT = 47
|
|
93
|
+
BIGINT_OBJECT = 48
|
|
94
|
+
WASM_MEMORY = 49
|
|
95
|
+
RTC_DATA_CHANNEL_TRANSFER = 50
|
|
96
|
+
DOM_EXCEPTION = 51
|
|
97
|
+
WEB_CODECS_ENCODED_VIDEO_CHUNK = 52
|
|
98
|
+
WEB_CODECS_VIDEO_FRAME = 53
|
|
99
|
+
RESIZABLE_ARRAY_BUFFER = 54
|
|
100
|
+
ERROR_INSTANCE = 55
|
|
101
|
+
IN_MEMORY_OFFSCREEN_CANVAS = 56
|
|
102
|
+
IN_MEMORY_MESSAGE_PORT = 57
|
|
103
|
+
WEB_CODECS_ENCODED_AUDIO_CHUNK = 58
|
|
104
|
+
WEB_CODECS_AUDIO_DATA = 59
|
|
105
|
+
MEDIA_STREAM_TRACK = 60
|
|
106
|
+
MEDIA_SOURCE_HANDLE_TRANSFER = 61
|
|
107
|
+
ERROR = 255
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ArrayBufferViewSubtag(IntEnum):
|
|
111
|
+
"""ArrayBufferView sub tags."""
|
|
112
|
+
DATA_VIEW = 0
|
|
113
|
+
INT8_ARRAY = 1
|
|
114
|
+
UINT8_ARRAY = 2
|
|
115
|
+
UINT8_CLAMPED_ARRAY = 3
|
|
116
|
+
INT16_ARRAY = 4
|
|
117
|
+
UINT16_ARRAY = 5
|
|
118
|
+
INT32_ARRAY = 6
|
|
119
|
+
UINT32_ARRAY = 7
|
|
120
|
+
FLOAT32_ARRAY = 8
|
|
121
|
+
FLOAT64_ARRAY = 9
|
|
122
|
+
BIG_INT64_ARRAY = 10
|
|
123
|
+
BIG_UINT64_ARRAY = 11
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""Safari IndexedDB records."""
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
import plistlib
|
|
18
|
+
import sqlite3
|
|
19
|
+
import sys
|
|
20
|
+
import traceback
|
|
21
|
+
from typing import Any, Generator, Optional
|
|
22
|
+
|
|
23
|
+
from dfindexeddb import errors
|
|
24
|
+
from dfindexeddb.indexeddb.safari import webkit
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ObjectStoreInfo:
|
|
29
|
+
"""An ObjectStoreInfo.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
id: the object store ID.
|
|
33
|
+
name: the object store name.
|
|
34
|
+
key_path: the object store key path.
|
|
35
|
+
auto_inc: True if the object store uses auto incrementing IDs.
|
|
36
|
+
database_name: the database name from the IDBDatabaseInfo table.
|
|
37
|
+
"""
|
|
38
|
+
id: int
|
|
39
|
+
name: str
|
|
40
|
+
key_path: str
|
|
41
|
+
auto_inc: bool
|
|
42
|
+
database_name: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class IndexedDBRecord:
|
|
47
|
+
"""A Safari IndexedDBRecord.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
key: the parsed key.
|
|
51
|
+
value: the parsed value.
|
|
52
|
+
object_store_id: the object store id.
|
|
53
|
+
object_store_name: the object store name from the ObjectStoreInfo table.
|
|
54
|
+
database_name: the IndexedDB database name from the IDBDatabaseInfo table.
|
|
55
|
+
record_id: the record ID from the Record table.
|
|
56
|
+
"""
|
|
57
|
+
key: Any
|
|
58
|
+
value: Any
|
|
59
|
+
object_store_id: int
|
|
60
|
+
object_store_name: str
|
|
61
|
+
database_name: str
|
|
62
|
+
record_id: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class FileReader:
|
|
66
|
+
"""A reader for Safari IndexedDB sqlite3 files.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
database_name: the database name.
|
|
70
|
+
database_version: the database version.
|
|
71
|
+
metadata_version: the metadata version.
|
|
72
|
+
max_object_store_id: the maximum object store ID.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, filename: str):
|
|
76
|
+
"""Initializes the FileReader.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
filename: the IndexedDB filename.
|
|
80
|
+
"""
|
|
81
|
+
self.filename = filename
|
|
82
|
+
|
|
83
|
+
with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
|
|
84
|
+
cursor = conn.execute(
|
|
85
|
+
'SELECT value FROM IDBDatabaseInfo WHERE key = "DatabaseVersion"')
|
|
86
|
+
result = cursor.fetchone()
|
|
87
|
+
self.database_version = result[0]
|
|
88
|
+
|
|
89
|
+
cursor = conn.execute(
|
|
90
|
+
'SELECT value FROM IDBDatabaseInfo WHERE key = "MetadataVersion"')
|
|
91
|
+
result = cursor.fetchone()
|
|
92
|
+
self.metadata_version = result[0]
|
|
93
|
+
|
|
94
|
+
cursor = conn.execute(
|
|
95
|
+
'SELECT value FROM IDBDatabaseInfo WHERE key = "DatabaseName"')
|
|
96
|
+
result = cursor.fetchone()
|
|
97
|
+
self.database_name = result[0]
|
|
98
|
+
|
|
99
|
+
cursor = conn.execute(
|
|
100
|
+
'SELECT value FROM IDBDatabaseInfo WHERE key = "MaxObjectStoreID"')
|
|
101
|
+
result = cursor.fetchone()
|
|
102
|
+
self.max_object_store_id = result[0]
|
|
103
|
+
|
|
104
|
+
def ObjectStores(self) -> Generator[ObjectStoreInfo, None, None]:
|
|
105
|
+
"""Returns the Object Store information from the IndexedDB database.
|
|
106
|
+
|
|
107
|
+
Yields:
|
|
108
|
+
ObjectStoreInfo instances.
|
|
109
|
+
"""
|
|
110
|
+
with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
|
|
111
|
+
cursor = conn.execute(
|
|
112
|
+
'SELECT id, name, keypath, autoinc FROM ObjectStoreInfo')
|
|
113
|
+
results = cursor.fetchall()
|
|
114
|
+
for result in results:
|
|
115
|
+
key_path = plistlib.loads(result[2])
|
|
116
|
+
yield ObjectStoreInfo(
|
|
117
|
+
id=result[0],
|
|
118
|
+
name=result[1],
|
|
119
|
+
key_path=key_path,
|
|
120
|
+
auto_inc=result[3],
|
|
121
|
+
database_name=self.database_name)
|
|
122
|
+
|
|
123
|
+
def RecordById(self, record_id: int) -> Optional[IndexedDBRecord]:
|
|
124
|
+
"""Returns an IndexedDBRecord for the given record_id.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
the IndexedDBRecord or None if the record_id does not exist in the
|
|
128
|
+
database.
|
|
129
|
+
"""
|
|
130
|
+
with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
|
|
131
|
+
conn.text_factory = bytes
|
|
132
|
+
cursor = conn.execute(
|
|
133
|
+
'SELECT r.key, r.value, r.objectStoreID, o.name, r.recordID FROM '
|
|
134
|
+
'Records r '
|
|
135
|
+
'JOIN ObjectStoreInfo o ON r.objectStoreID == o.id '
|
|
136
|
+
'WHERE r.recordID = ?', (record_id, ))
|
|
137
|
+
row = cursor.fetchone()
|
|
138
|
+
if not row:
|
|
139
|
+
return None
|
|
140
|
+
key = webkit.IDBKeyData.FromBytes(row[0]).data
|
|
141
|
+
value = webkit.SerializedScriptValueDecoder.FromBytes(row[1])
|
|
142
|
+
return IndexedDBRecord(
|
|
143
|
+
key=key,
|
|
144
|
+
value=value,
|
|
145
|
+
object_store_id=row[2],
|
|
146
|
+
object_store_name=row[3].decode('utf-8'),
|
|
147
|
+
database_name=self.database_name,
|
|
148
|
+
record_id=row[4])
|
|
149
|
+
|
|
150
|
+
def RecordsByObjectStoreName(
|
|
151
|
+
self,
|
|
152
|
+
name: str
|
|
153
|
+
) -> Generator[IndexedDBRecord, None, None]:
|
|
154
|
+
"""Returns IndexedDBRecords for the given ObjectStore name.
|
|
155
|
+
|
|
156
|
+
Yields:
|
|
157
|
+
IndexedDBRecord instances.
|
|
158
|
+
"""
|
|
159
|
+
with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
|
|
160
|
+
conn.text_factory = bytes
|
|
161
|
+
for row in conn.execute(
|
|
162
|
+
'SELECT r.key, r.value, r.objectStoreID, o.name, r.recordID FROM '
|
|
163
|
+
'Records r '
|
|
164
|
+
'JOIN ObjectStoreInfo o ON r.objectStoreID == o.id '
|
|
165
|
+
'WHERE o.name = ?', (name, )):
|
|
166
|
+
key = webkit.IDBKeyData.FromBytes(row[0]).data
|
|
167
|
+
value = webkit.SerializedScriptValueDecoder.FromBytes(row[1])
|
|
168
|
+
yield IndexedDBRecord(
|
|
169
|
+
key=key,
|
|
170
|
+
value=value,
|
|
171
|
+
object_store_id=row[2],
|
|
172
|
+
object_store_name=row[3].decode('utf-8'),
|
|
173
|
+
database_name=self.database_name,
|
|
174
|
+
record_id=row[4])
|
|
175
|
+
|
|
176
|
+
def RecordsByObjectStoreId(
|
|
177
|
+
self,
|
|
178
|
+
object_store_id: int
|
|
179
|
+
) -> Generator[IndexedDBRecord, None, None]:
|
|
180
|
+
"""Returns IndexedDBRecords for the given ObjectStore id.
|
|
181
|
+
|
|
182
|
+
Yields:
|
|
183
|
+
IndexedDBRecord instances.
|
|
184
|
+
"""
|
|
185
|
+
with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
|
|
186
|
+
conn.text_factory = bytes
|
|
187
|
+
cursor = conn.execute(
|
|
188
|
+
'SELECT r.key, r.value, r.objectStoreID, o.name, r.recordID '
|
|
189
|
+
'FROM Records r '
|
|
190
|
+
'JOIN ObjectStoreInfo o ON r.objectStoreID == o.id '
|
|
191
|
+
'WHERE o.id = ?', (object_store_id, ))
|
|
192
|
+
for row in cursor:
|
|
193
|
+
key = webkit.IDBKeyData.FromBytes(row[0]).data
|
|
194
|
+
value = webkit.SerializedScriptValueDecoder.FromBytes(row[1])
|
|
195
|
+
yield IndexedDBRecord(
|
|
196
|
+
key=key,
|
|
197
|
+
value=value,
|
|
198
|
+
object_store_id=row[2],
|
|
199
|
+
object_store_name=row[3].decode('utf-8'),
|
|
200
|
+
database_name=self.database_name,
|
|
201
|
+
record_id=row[4])
|
|
202
|
+
|
|
203
|
+
def Records(self) -> Generator[IndexedDBRecord, None, None]:
|
|
204
|
+
"""Returns all the IndexedDBRecords."""
|
|
205
|
+
with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
|
|
206
|
+
conn.text_factory = bytes
|
|
207
|
+
cursor = conn.execute(
|
|
208
|
+
'SELECT r.key, r.value, r.objectStoreID, o.name, r.recordID '
|
|
209
|
+
'FROM Records r '
|
|
210
|
+
'JOIN ObjectStoreInfo o ON r.objectStoreID == o.id')
|
|
211
|
+
for row in cursor:
|
|
212
|
+
try:
|
|
213
|
+
key = webkit.IDBKeyData.FromBytes(row[0]).data
|
|
214
|
+
except(
|
|
215
|
+
errors.ParserError,
|
|
216
|
+
errors.DecoderError,
|
|
217
|
+
NotImplementedError) as err:
|
|
218
|
+
print(
|
|
219
|
+
f'Error parsing IndexedDB key: {err}', file=sys.stderr)
|
|
220
|
+
print(f'Traceback: {traceback.format_exc()}', file=sys.stderr)
|
|
221
|
+
continue
|
|
222
|
+
try:
|
|
223
|
+
value = webkit.SerializedScriptValueDecoder.FromBytes(row[1])
|
|
224
|
+
except(
|
|
225
|
+
errors.ParserError,
|
|
226
|
+
errors.DecoderError,
|
|
227
|
+
NotImplementedError) as err:
|
|
228
|
+
print(
|
|
229
|
+
f'Error parsing IndexedDB value: {err}', file=sys.stderr)
|
|
230
|
+
print(f'Traceback: {traceback.format_exc()}', file=sys.stderr)
|
|
231
|
+
continue
|
|
232
|
+
yield IndexedDBRecord(
|
|
233
|
+
key=key,
|
|
234
|
+
value=value,
|
|
235
|
+
object_store_id=row[2],
|
|
236
|
+
object_store_name=row[3].decode('utf-8'),
|
|
237
|
+
database_name=self.database_name,
|
|
238
|
+
record_id=row[4])
|