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.
Files changed (34) hide show
  1. dfindexeddb/indexeddb/chromium/blink.py +116 -74
  2. dfindexeddb/indexeddb/chromium/definitions.py +240 -125
  3. dfindexeddb/indexeddb/chromium/record.py +651 -346
  4. dfindexeddb/indexeddb/chromium/sqlite.py +362 -0
  5. dfindexeddb/indexeddb/chromium/v8.py +100 -78
  6. dfindexeddb/indexeddb/cli.py +282 -121
  7. dfindexeddb/indexeddb/firefox/definitions.py +7 -4
  8. dfindexeddb/indexeddb/firefox/gecko.py +98 -74
  9. dfindexeddb/indexeddb/firefox/record.py +78 -26
  10. dfindexeddb/indexeddb/safari/definitions.py +5 -3
  11. dfindexeddb/indexeddb/safari/record.py +86 -53
  12. dfindexeddb/indexeddb/safari/webkit.py +85 -71
  13. dfindexeddb/indexeddb/types.py +4 -1
  14. dfindexeddb/leveldb/cli.py +146 -138
  15. dfindexeddb/leveldb/definitions.py +6 -2
  16. dfindexeddb/leveldb/descriptor.py +70 -56
  17. dfindexeddb/leveldb/ldb.py +39 -33
  18. dfindexeddb/leveldb/log.py +41 -30
  19. dfindexeddb/leveldb/plugins/chrome_notifications.py +30 -18
  20. dfindexeddb/leveldb/plugins/interface.py +5 -6
  21. dfindexeddb/leveldb/plugins/manager.py +10 -9
  22. dfindexeddb/leveldb/record.py +71 -62
  23. dfindexeddb/leveldb/utils.py +105 -13
  24. dfindexeddb/utils.py +36 -31
  25. dfindexeddb/version.py +2 -2
  26. dfindexeddb-20260205.dist-info/METADATA +171 -0
  27. dfindexeddb-20260205.dist-info/RECORD +41 -0
  28. {dfindexeddb-20241105.dist-info → dfindexeddb-20260205.dist-info}/WHEEL +1 -1
  29. dfindexeddb-20241105.dist-info/AUTHORS +0 -12
  30. dfindexeddb-20241105.dist-info/METADATA +0 -424
  31. dfindexeddb-20241105.dist-info/RECORD +0 -41
  32. {dfindexeddb-20241105.dist-info → dfindexeddb-20260205.dist-info}/entry_points.txt +0 -0
  33. {dfindexeddb-20241105.dist-info → dfindexeddb-20260205.dist-info/licenses}/LICENSE +0 -0
  34. {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
+ )