dfindexeddb 20241105__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/blink.py +116 -74
- dfindexeddb/indexeddb/chromium/definitions.py +240 -125
- dfindexeddb/indexeddb/chromium/record.py +651 -346
- dfindexeddb/indexeddb/chromium/sqlite.py +362 -0
- dfindexeddb/indexeddb/chromium/v8.py +100 -78
- dfindexeddb/indexeddb/cli.py +282 -121
- dfindexeddb/indexeddb/firefox/definitions.py +7 -4
- dfindexeddb/indexeddb/firefox/gecko.py +98 -74
- dfindexeddb/indexeddb/firefox/record.py +78 -26
- dfindexeddb/indexeddb/safari/definitions.py +5 -3
- dfindexeddb/indexeddb/safari/record.py +86 -53
- dfindexeddb/indexeddb/safari/webkit.py +85 -71
- dfindexeddb/indexeddb/types.py +4 -1
- dfindexeddb/leveldb/cli.py +146 -138
- dfindexeddb/leveldb/definitions.py +6 -2
- dfindexeddb/leveldb/descriptor.py +70 -56
- dfindexeddb/leveldb/ldb.py +39 -33
- dfindexeddb/leveldb/log.py +41 -30
- dfindexeddb/leveldb/plugins/chrome_notifications.py +30 -18
- dfindexeddb/leveldb/plugins/interface.py +5 -6
- dfindexeddb/leveldb/plugins/manager.py +10 -9
- dfindexeddb/leveldb/record.py +71 -62
- dfindexeddb/leveldb/utils.py +105 -13
- dfindexeddb/utils.py +36 -31
- dfindexeddb/version.py +2 -2
- dfindexeddb-20260205.dist-info/METADATA +171 -0
- dfindexeddb-20260205.dist-info/RECORD +41 -0
- {dfindexeddb-20241105.dist-info → dfindexeddb-20260205.dist-info}/WHEEL +1 -1
- dfindexeddb-20241105.dist-info/AUTHORS +0 -12
- dfindexeddb-20241105.dist-info/METADATA +0 -424
- dfindexeddb-20241105.dist-info/RECORD +0 -41
- {dfindexeddb-20241105.dist-info → dfindexeddb-20260205.dist-info}/entry_points.txt +0 -0
- {dfindexeddb-20241105.dist-info → dfindexeddb-20260205.dist-info/licenses}/LICENSE +0 -0
- {dfindexeddb-20241105.dist-info → dfindexeddb-20260205.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2026 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
|
+
"""Chromium IndexedDB records encoded in sqlite3 databases."""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sqlite3
|
|
19
|
+
from typing import Any, Generator, Optional
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
import snappy
|
|
23
|
+
import zstd
|
|
24
|
+
|
|
25
|
+
from dfindexeddb.indexeddb.chromium import blink
|
|
26
|
+
from dfindexeddb.indexeddb.chromium import definitions
|
|
27
|
+
from dfindexeddb.indexeddb.chromium import record
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ChromiumIndexedDBRecord:
|
|
32
|
+
"""Chromium IndexedDB record parsed from sqlite3 database.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
row_id: the row ID.
|
|
36
|
+
object_store_id: the object store ID.
|
|
37
|
+
compression_type: the compression type.
|
|
38
|
+
key: the key.
|
|
39
|
+
value: the value.
|
|
40
|
+
has_blobs: whether the record has blobs.
|
|
41
|
+
raw_key: the raw key.
|
|
42
|
+
raw_value: the raw value.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
row_id: int
|
|
46
|
+
object_store_id: int
|
|
47
|
+
compression_type: int
|
|
48
|
+
key: Any
|
|
49
|
+
value: Any
|
|
50
|
+
has_blobs: bool
|
|
51
|
+
raw_key: Optional[bytes]
|
|
52
|
+
raw_value: Optional[bytes]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ChromiumObjectStoreInfo:
|
|
57
|
+
"""Chromium IndexedDB object store info parsed from sqlite3 database.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
id: the object store ID.
|
|
61
|
+
name: the object store name.
|
|
62
|
+
key_path: the object store key path.
|
|
63
|
+
auto_increment: whether the object store is auto increment.
|
|
64
|
+
key_generator_current_number: the current number of the key generator.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
id: int
|
|
68
|
+
name: str
|
|
69
|
+
key_path: str
|
|
70
|
+
auto_increment: int
|
|
71
|
+
key_generator_current_number: int
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ChromiumBlobInfo:
|
|
76
|
+
"""Chromium IndexedDB blob info parsed from sqlite3 database.
|
|
77
|
+
|
|
78
|
+
Attributes:
|
|
79
|
+
row_id: the blob row ID.
|
|
80
|
+
object_type: the object type.
|
|
81
|
+
mime_type: the mime type.
|
|
82
|
+
size_bytes: the total size in bytes.
|
|
83
|
+
file_name: the file name (only for files).
|
|
84
|
+
number_of_chunks: the number of chunks including the initial one.
|
|
85
|
+
blob_data: the blob data.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
row_id: int
|
|
89
|
+
object_type: int
|
|
90
|
+
mime_type: Optional[str]
|
|
91
|
+
size_bytes: int
|
|
92
|
+
file_name: Optional[str]
|
|
93
|
+
number_of_chunks: int
|
|
94
|
+
blob_data: bytes
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DatabaseReader:
|
|
98
|
+
"""A reader for Chromium IndexedDB sqlite3 files."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, filename: str):
|
|
101
|
+
"""Initializes the reader.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
filename: the path to the sqlite3 file.
|
|
105
|
+
"""
|
|
106
|
+
self._filename = filename
|
|
107
|
+
|
|
108
|
+
def ObjectStores(self) -> Generator[ChromiumObjectStoreInfo, None, None]:
|
|
109
|
+
"""Yields object stores."""
|
|
110
|
+
with sqlite3.connect(f"file:{self._filename}?mode=ro", uri=True) as conn:
|
|
111
|
+
cursor = conn.cursor()
|
|
112
|
+
cursor.execute(definitions.SQL_OBJECT_STORES_QUERY)
|
|
113
|
+
for row in cursor:
|
|
114
|
+
yield ChromiumObjectStoreInfo(
|
|
115
|
+
id=row[0],
|
|
116
|
+
name=row[1].decode("utf-16-le"),
|
|
117
|
+
key_path=row[2],
|
|
118
|
+
auto_increment=row[3],
|
|
119
|
+
key_generator_current_number=row[4],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _GetLegacyBlobPath(self, blob_id: int) -> str:
|
|
123
|
+
"""Gets the path to a legacy blob file.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
blob_id: the blob ID.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The path to the legacy blob file.
|
|
130
|
+
"""
|
|
131
|
+
base, ext = os.path.splitext(self._filename)
|
|
132
|
+
db_dir = f"{base}_{ext}"
|
|
133
|
+
return os.path.join(db_dir, f"{blob_id:x}")
|
|
134
|
+
|
|
135
|
+
def LoadLegacyBlobData(self, blob_id: int) -> bytes:
|
|
136
|
+
"""Loads legacy blob data from disk.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
blob_id: the blob ID.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The blob data.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
FileNotFoundError: if the legacy blob file is not found.
|
|
146
|
+
"""
|
|
147
|
+
blob_path = self._GetLegacyBlobPath(blob_id)
|
|
148
|
+
if os.path.exists(blob_path):
|
|
149
|
+
with open(blob_path, "rb") as f:
|
|
150
|
+
return f.read()
|
|
151
|
+
raise FileNotFoundError(f"Legacy blob file not found: {blob_path}")
|
|
152
|
+
|
|
153
|
+
def LoadBlobDataForRecordId(
|
|
154
|
+
self, row_id: int
|
|
155
|
+
) -> Generator[ChromiumBlobInfo, None, None]:
|
|
156
|
+
"""Loads blob data for a given record row ID.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
row_id: the record row ID.
|
|
160
|
+
|
|
161
|
+
Yields:
|
|
162
|
+
ChromiumBlobInfo objects.
|
|
163
|
+
"""
|
|
164
|
+
with sqlite3.connect(f"file:{self._filename}?mode=ro", uri=True) as conn:
|
|
165
|
+
conn.row_factory = sqlite3.Row
|
|
166
|
+
cursor = conn.cursor()
|
|
167
|
+
|
|
168
|
+
# Note this is a UNION query between the blob and overflow_blob_chunks
|
|
169
|
+
# table. The chunk_index = 0 for the row from the 'blobs' table.
|
|
170
|
+
cursor.execute(definitions.SQL_BLOB_DATA_QUERY, (row_id, row_id))
|
|
171
|
+
|
|
172
|
+
current_blob_id = None
|
|
173
|
+
current_blob_data = bytearray()
|
|
174
|
+
current_record: Optional[sqlite3.Row] = None
|
|
175
|
+
total_number_of_chunks = 0
|
|
176
|
+
|
|
177
|
+
for blob_row in cursor:
|
|
178
|
+
blob_id = blob_row["row_id"]
|
|
179
|
+
|
|
180
|
+
if blob_id != current_blob_id:
|
|
181
|
+
if current_record is not None:
|
|
182
|
+
yield ChromiumBlobInfo(
|
|
183
|
+
row_id=current_record["row_id"],
|
|
184
|
+
object_type=current_record["object_type"],
|
|
185
|
+
mime_type=current_record["mime_type"],
|
|
186
|
+
size_bytes=current_record["size_bytes"],
|
|
187
|
+
file_name=current_record["file_name"],
|
|
188
|
+
number_of_chunks=total_number_of_chunks,
|
|
189
|
+
blob_data=bytes(current_blob_data),
|
|
190
|
+
)
|
|
191
|
+
current_blob_id = blob_id
|
|
192
|
+
current_blob_data = bytearray()
|
|
193
|
+
current_record = blob_row
|
|
194
|
+
total_number_of_chunks = 0
|
|
195
|
+
|
|
196
|
+
if blob_row["chunk_index"] == 0 and blob_row["bytes"] is None:
|
|
197
|
+
current_blob_data.extend(self.LoadLegacyBlobData(blob_id))
|
|
198
|
+
total_number_of_chunks += 1
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
if blob_row["bytes"]:
|
|
202
|
+
current_blob_data.extend(blob_row["bytes"])
|
|
203
|
+
total_number_of_chunks += 1
|
|
204
|
+
|
|
205
|
+
if current_record is not None:
|
|
206
|
+
yield ChromiumBlobInfo(
|
|
207
|
+
row_id=current_record["row_id"],
|
|
208
|
+
object_type=current_record["object_type"],
|
|
209
|
+
mime_type=current_record["mime_type"],
|
|
210
|
+
size_bytes=current_record["size_bytes"],
|
|
211
|
+
file_name=current_record["file_name"],
|
|
212
|
+
number_of_chunks=total_number_of_chunks,
|
|
213
|
+
blob_data=bytes(current_blob_data),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _EnumerateCursor(
|
|
217
|
+
self,
|
|
218
|
+
cursor: sqlite3.Cursor,
|
|
219
|
+
include_raw_data: bool = False,
|
|
220
|
+
parse_key: bool = True,
|
|
221
|
+
parse_value: bool = True,
|
|
222
|
+
load_blobs: bool = True,
|
|
223
|
+
) -> Generator[ChromiumIndexedDBRecord, None, None]:
|
|
224
|
+
"""Yields ChromiumIndexedDBRecord records from a sqlite3 cursor.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
cursor: the sqlite3 cursor.
|
|
228
|
+
include_raw_data: whether to include the raw data.
|
|
229
|
+
parse_key: whether to parse the key.
|
|
230
|
+
parse_value: whether to parse the value.
|
|
231
|
+
load_blobs: whether to load the record blobs.
|
|
232
|
+
|
|
233
|
+
Yields:
|
|
234
|
+
ChromiumIndexedDBRecord records.
|
|
235
|
+
"""
|
|
236
|
+
for row in cursor:
|
|
237
|
+
row_id = row[0]
|
|
238
|
+
object_store_id = row[1]
|
|
239
|
+
compression_type = definitions.DatabaseCompressionType(row[2])
|
|
240
|
+
raw_key = row[3]
|
|
241
|
+
raw_value = row[4]
|
|
242
|
+
has_blobs = bool(row[5])
|
|
243
|
+
|
|
244
|
+
key, value = None, None
|
|
245
|
+
if parse_key and raw_key:
|
|
246
|
+
key = record.SortableIDBKey.FromBytes(raw_data=raw_key, base_offset=0)
|
|
247
|
+
|
|
248
|
+
if parse_value and raw_value:
|
|
249
|
+
if compression_type == definitions.DatabaseCompressionType.UNCOMPRESSED:
|
|
250
|
+
value = blink.V8ScriptValueDecoder.FromBytes(raw_value)
|
|
251
|
+
elif compression_type == definitions.DatabaseCompressionType.ZSTD:
|
|
252
|
+
value = blink.V8ScriptValueDecoder.FromBytes(
|
|
253
|
+
zstd.decompress(raw_value)
|
|
254
|
+
)
|
|
255
|
+
elif compression_type == definitions.DatabaseCompressionType.SNAPPY:
|
|
256
|
+
value = blink.V8ScriptValueDecoder.FromBytes(
|
|
257
|
+
snappy.decompress(raw_value)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if load_blobs and raw_value is None:
|
|
261
|
+
if not has_blobs:
|
|
262
|
+
raise ValueError("Raw value is None but has_blobs is not set")
|
|
263
|
+
value = []
|
|
264
|
+
for blob in self.LoadBlobDataForRecordId(row_id):
|
|
265
|
+
blob.blob_data = blink.V8ScriptValueDecoder.FromBytes(blob.blob_data)
|
|
266
|
+
value.append(blob)
|
|
267
|
+
|
|
268
|
+
yield ChromiumIndexedDBRecord(
|
|
269
|
+
row_id=row_id,
|
|
270
|
+
object_store_id=object_store_id,
|
|
271
|
+
compression_type=compression_type,
|
|
272
|
+
key=key,
|
|
273
|
+
value=value,
|
|
274
|
+
has_blobs=has_blobs,
|
|
275
|
+
raw_key=raw_key if include_raw_data else None,
|
|
276
|
+
raw_value=raw_value if include_raw_data else None,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def RecordsByObjectStoreId(
|
|
280
|
+
self,
|
|
281
|
+
object_store_id: int,
|
|
282
|
+
include_raw_data: bool = False,
|
|
283
|
+
parse_key: bool = True,
|
|
284
|
+
parse_value: bool = True,
|
|
285
|
+
load_blobs: bool = True,
|
|
286
|
+
) -> Generator[ChromiumIndexedDBRecord, None, None]:
|
|
287
|
+
"""Yields ChromiumIndexedDBRecord records for a given object store ID.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
object_store_id: the object store ID.
|
|
291
|
+
include_raw_data: whether to include the raw data.
|
|
292
|
+
parse_key: whether to parse the key.
|
|
293
|
+
parse_value: whether to parse the value.
|
|
294
|
+
load_blobs: whether to load the record blobs.
|
|
295
|
+
|
|
296
|
+
Yields:
|
|
297
|
+
ChromiumIndexedDBRecord records.
|
|
298
|
+
"""
|
|
299
|
+
with sqlite3.connect(f"file:{self._filename}?mode=ro", uri=True) as conn:
|
|
300
|
+
conn.row_factory = sqlite3.Row
|
|
301
|
+
cursor = conn.cursor()
|
|
302
|
+
cursor.execute(definitions.SQL_RECORDS_BY_ID_QUERY, (object_store_id,))
|
|
303
|
+
yield from self._EnumerateCursor(
|
|
304
|
+
cursor, include_raw_data, parse_key, parse_value, load_blobs
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def RecordsByObjectStoreName(
|
|
308
|
+
self,
|
|
309
|
+
object_store_name: str,
|
|
310
|
+
include_raw_data: bool = False,
|
|
311
|
+
parse_key: bool = True,
|
|
312
|
+
parse_value: bool = True,
|
|
313
|
+
load_blobs: bool = True,
|
|
314
|
+
) -> Generator[ChromiumIndexedDBRecord, None, None]:
|
|
315
|
+
"""Yields ChromiumIndexedDBRecord records for a given object store name.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
object_store_name: the object store name.
|
|
319
|
+
include_raw_data: whether to include the raw data.
|
|
320
|
+
parse_key: whether to parse the key.
|
|
321
|
+
parse_value: whether to parse the value.
|
|
322
|
+
load_blobs: whether to load the record blobs.
|
|
323
|
+
|
|
324
|
+
Yields:
|
|
325
|
+
ChromiumIndexedDBRecord records.
|
|
326
|
+
"""
|
|
327
|
+
with sqlite3.connect(f"file:{self._filename}?mode=ro", uri=True) as conn:
|
|
328
|
+
conn.row_factory = sqlite3.Row
|
|
329
|
+
cursor = conn.cursor()
|
|
330
|
+
cursor.execute(
|
|
331
|
+
definitions.SQL_RECORDS_BY_NAME_QUERY,
|
|
332
|
+
(object_store_name.encode("utf-16-le"),),
|
|
333
|
+
)
|
|
334
|
+
yield from self._EnumerateCursor(
|
|
335
|
+
cursor, include_raw_data, parse_key, parse_value, load_blobs
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def Records(
|
|
339
|
+
self,
|
|
340
|
+
include_raw_data: bool = False,
|
|
341
|
+
parse_key: bool = True,
|
|
342
|
+
parse_value: bool = True,
|
|
343
|
+
load_blobs: bool = True,
|
|
344
|
+
) -> Generator[ChromiumIndexedDBRecord, None, None]:
|
|
345
|
+
"""Yields ChromiumIndexedDBRecord records from all object stores.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
include_raw_data: whether to include the raw data.
|
|
349
|
+
parse_key: whether to parse the key.
|
|
350
|
+
parse_value: whether to parse the value.
|
|
351
|
+
load_blobs: whether to load the record blobs.
|
|
352
|
+
|
|
353
|
+
Yields:
|
|
354
|
+
ChromiumIndexedDBRecord records.
|
|
355
|
+
"""
|
|
356
|
+
with sqlite3.connect(f"file:{self._filename}?mode=ro", uri=True) as conn:
|
|
357
|
+
conn.row_factory = sqlite3.Row
|
|
358
|
+
cursor = conn.cursor()
|
|
359
|
+
cursor.execute(definitions.SQL_RECORDS_QUERY)
|
|
360
|
+
yield from self._EnumerateCursor(
|
|
361
|
+
cursor, include_raw_data, parse_key, parse_value, load_blobs
|
|
362
|
+
)
|