dfindexeddb 20240501__py3-none-any.whl → 20241031__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 (28) hide show
  1. dfindexeddb/indexeddb/chromium/blink.py +5 -0
  2. dfindexeddb/indexeddb/chromium/record.py +11 -6
  3. dfindexeddb/indexeddb/chromium/v8.py +30 -10
  4. dfindexeddb/indexeddb/cli.py +52 -9
  5. dfindexeddb/indexeddb/firefox/definitions.py +143 -0
  6. dfindexeddb/indexeddb/firefox/gecko.py +600 -0
  7. dfindexeddb/indexeddb/firefox/record.py +180 -0
  8. dfindexeddb/indexeddb/safari/webkit.py +52 -20
  9. dfindexeddb/indexeddb/types.py +71 -0
  10. dfindexeddb/leveldb/cli.py +69 -6
  11. dfindexeddb/leveldb/descriptor.py +2 -1
  12. dfindexeddb/leveldb/log.py +9 -3
  13. dfindexeddb/leveldb/plugins/__init__.py +17 -0
  14. dfindexeddb/leveldb/plugins/chrome_notifications.py +135 -0
  15. dfindexeddb/leveldb/plugins/interface.py +36 -0
  16. dfindexeddb/leveldb/plugins/manager.py +75 -0
  17. dfindexeddb/leveldb/plugins/notification_database_data_pb2.py +38 -0
  18. dfindexeddb/leveldb/record.py +33 -1
  19. dfindexeddb/utils.py +35 -1
  20. dfindexeddb/version.py +1 -1
  21. {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/METADATA +42 -15
  22. dfindexeddb-20241031.dist-info/RECORD +41 -0
  23. {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/WHEEL +1 -1
  24. dfindexeddb-20240501.dist-info/RECORD +0 -32
  25. {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/AUTHORS +0 -0
  26. {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/LICENSE +0 -0
  27. {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/entry_points.txt +0 -0
  28. {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,180 @@
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
+ """Firefox IndexedDB records."""
16
+ from dataclasses import dataclass
17
+ import sqlite3
18
+ import sys
19
+ import traceback
20
+ from typing import Any, Generator, Optional
21
+
22
+ from dfindexeddb import errors
23
+ from dfindexeddb.indexeddb.firefox import gecko
24
+
25
+
26
+ @dataclass
27
+ class FirefoxObjectStoreInfo:
28
+ """A FireFox ObjectStoreInfo.
29
+
30
+ Attributes:
31
+ id: the object store ID.
32
+ name: the object store name.
33
+ key_path: the object store key path.
34
+ auto_inc: the current auto-increment value.
35
+ database_name: the database name from the database table.
36
+ """
37
+ id: int
38
+ name: str
39
+ key_path: str
40
+ auto_inc: int
41
+ database_name: str
42
+
43
+
44
+ @dataclass
45
+ class FirefoxIndexedDBRecord:
46
+ """A Firefox IndexedDBRecord.
47
+
48
+ Attributes:
49
+ key: the parsed key.
50
+ value: the parsed value.
51
+ file_ids: the file identifiers.
52
+ object_store_id: the object store id.
53
+ object_store_name: the object store name from the object_store table.
54
+ database_name: the IndexedDB database name from the database table.
55
+ """
56
+ key: Any
57
+ value: Any
58
+ file_ids: Optional[str]
59
+ object_store_id: int
60
+ object_store_name: str
61
+ database_name: str
62
+
63
+
64
+ class FileReader:
65
+ """A reader for Firefox IndexedDB sqlite3 files.
66
+
67
+ Attributes:
68
+ database_name: the database name.
69
+ origin: the database origin.
70
+ metadata_version: the metadata version.
71
+ last_vacuum_time: the last vacuum time.
72
+ last_analyze_time: the last analyze time.
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 name, origin, version, last_vacuum_time, last_analyze_time '
86
+ 'FROM database')
87
+ result = cursor.fetchone()
88
+ self.database_name = result[0]
89
+ self.origin = result[1]
90
+ self.metadata_version = result[2]
91
+ self.last_vacuum_time = result[3]
92
+ self.last_analyze_time = result[4]
93
+
94
+ def _ParseKey(self, key: bytes) -> Any:
95
+ """Parses a key."""
96
+ try:
97
+ return gecko.IDBKey.FromBytes(key)
98
+ except errors.ParserError as e:
99
+ print('failed to parse', key, file=sys.stderr)
100
+ traceback.print_exception(type(e), e, e.__traceback__)
101
+ return key
102
+
103
+ def _ParseValue(self, value: bytes) -> Any:
104
+ """Parses a value."""
105
+ try:
106
+ return gecko.JSStructuredCloneDecoder.FromBytes(value)
107
+ except errors.ParserError as err:
108
+ print('failed to parse', value, file=sys.stderr)
109
+ traceback.print_exception(type(err), err, err.__traceback__)
110
+ return value
111
+
112
+ def ObjectStores(self) -> Generator[FirefoxObjectStoreInfo, None, None]:
113
+ """Returns the Object Store information from the IndexedDB database.
114
+
115
+ Yields:
116
+ FirefoxObjectStoreInfo instances.
117
+ """
118
+ with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
119
+ cursor = conn.execute(
120
+ 'SELECT id, auto_increment, name, key_path FROM object_store')
121
+ results = cursor.fetchall()
122
+ for result in results:
123
+ yield FirefoxObjectStoreInfo(
124
+ id=result[0],
125
+ name=result[2],
126
+ key_path=result[3],
127
+ auto_inc=result[1],
128
+ database_name=self.database_name)
129
+
130
+ def RecordsByObjectStoreId(
131
+ self,
132
+ object_store_id: int
133
+ ) -> Generator[FirefoxIndexedDBRecord, None, None]:
134
+ """Returns FirefoxIndexedDBRecords by a given object store id.
135
+
136
+ Args:
137
+ object_store_id: the object store id.
138
+ """
139
+ with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
140
+ conn.text_factory = bytes
141
+ cursor = conn.execute(
142
+ 'SELECT od.key, od.data, od.object_store_id, od.file_ids, os.name '
143
+ 'FROM object_data od '
144
+ 'JOIN object_store os ON od.object_store_id == os.id '
145
+ 'WHERE os.id = ? ORDER BY od.key', (object_store_id, ))
146
+ for row in cursor:
147
+ key = self._ParseKey(row[0])
148
+ if row[3]:
149
+ value = row[1]
150
+ else:
151
+ value = self._ParseValue(row[1])
152
+ yield FirefoxIndexedDBRecord(
153
+ key=key,
154
+ value=value,
155
+ object_store_id=row[2],
156
+ file_ids=row[3],
157
+ object_store_name=row[4].decode('utf-8'),
158
+ database_name=self.database_name)
159
+
160
+ def Records(self) -> Generator[FirefoxIndexedDBRecord, None, None]:
161
+ """Returns FirefoxIndexedDBRecords from the database."""
162
+ with sqlite3.connect(f'file:{self.filename}?mode=ro', uri=True) as conn:
163
+ conn.text_factory = bytes
164
+ cursor = conn.execute(
165
+ 'SELECT od.key, od.data, od.object_store_id, od.file_ids, os.name '
166
+ 'FROM object_data od '
167
+ 'JOIN object_store os ON od.object_store_id == os.id')
168
+ for row in cursor:
169
+ key = self._ParseKey(row[0])
170
+ if row[3]:
171
+ value = row[1]
172
+ else:
173
+ value = self._ParseValue(row[1])
174
+ yield FirefoxIndexedDBRecord(
175
+ key=key,
176
+ value=value,
177
+ object_store_id=row[2],
178
+ file_ids=row[3],
179
+ object_store_name=row[4].decode('utf-8'),
180
+ database_name=self.database_name)
@@ -82,15 +82,22 @@ class FileList:
82
82
  files: List[FileData]
83
83
 
84
84
 
85
- class JSArray(list):
86
- """A parsed JavaScript array.
85
+ class JSArray:
86
+ """A parsed Javascript array.
87
87
 
88
- This is a wrapper around a standard Python list to allow assigning arbitrary
89
- properties as is possible in the JavaScript equivalent.
88
+ A Javascript array behaves like a Python list but allows assigning arbitrary
89
+ properties. The array is stored in the attribute __array__.
90
90
  """
91
+ def __init__(self):
92
+ self.__array__ = []
93
+
94
+ def Append(self, element: Any):
95
+ """Appends a new element to the array."""
96
+ self.__array__.append(element)
91
97
 
92
98
  def __repr__(self):
93
- array_entries = ", ".join([str(entry) for entry in list(self)])
99
+ array_entries = ", ".join(
100
+ [str(entry) for entry in list(self.__array__)])
94
101
  properties = ", ".join(
95
102
  f'{key}: {value}' for key, value in self.properties.items())
96
103
  return f'[{array_entries}, {properties}]'
@@ -100,6 +107,11 @@ class JSArray(list):
100
107
  """Returns the object properties."""
101
108
  return self.__dict__
102
109
 
110
+ def __eq__(self, other: JSArray):
111
+ return (
112
+ self.__array__ == other.__array__
113
+ and self.properties == other.properties)
114
+
103
115
  def __contains__(self, item):
104
116
  return item in self.__dict__
105
117
 
@@ -107,24 +119,36 @@ class JSArray(list):
107
119
  return self.__dict__[name]
108
120
 
109
121
 
110
- class JSSet(set):
111
- """A parsed JavaScript set.
122
+ class JSSet:
123
+ """A parsed Javascript set.
112
124
 
113
- This is a wrapper around a standard Python set to allow assigning arbitrary
114
- properties as is possible in the JavaScript equivalent.
125
+ A Javascript set behaves like a Python set but allows assigning arbitrary
126
+ properties. The array is stored in the attribute __set__.
115
127
  """
128
+ def __init__(self):
129
+ self.__set__ = set()
130
+
131
+ def Add(self, element: Any):
132
+ """Adds a new element to the set."""
133
+ self.__set__.add(element)
116
134
 
117
135
  def __repr__(self):
118
- set_entries = ", ".join([str(entry) for entry in list(self)])
136
+ array_entries = ", ".join(
137
+ [str(entry) for entry in list(self.__set__)])
119
138
  properties = ", ".join(
120
139
  f'{key}: {value}' for key, value in self.properties.items())
121
- return f'[{set_entries}, {properties}]'
140
+ return f'[{array_entries}, {properties}]'
122
141
 
123
142
  @property
124
143
  def properties(self) -> Dict[str, Any]:
125
144
  """Returns the object properties."""
126
145
  return self.__dict__
127
146
 
147
+ def __eq__(self, other: JSSet):
148
+ return (
149
+ self.__set__ == other.__set__
150
+ and self.properties == other.properties)
151
+
128
152
  def __contains__(self, item):
129
153
  return item in self.__dict__
130
154
 
@@ -290,10 +314,11 @@ class SerializedScriptValueDecoder():
290
314
  """
291
315
  _, length = self.decoder.DecodeUint32()
292
316
  array = JSArray()
317
+ self.object_pool.append(array)
293
318
  for _ in range(length):
294
319
  _, _ = self.decoder.DecodeUint32()
295
320
  _, value = self.DecodeValue()
296
- array.append(value)
321
+ array.Append(value)
297
322
 
298
323
  offset, terminator_tag = self.decoder.DecodeUint32()
299
324
  if terminator_tag != definitions.TerminatorTag:
@@ -314,13 +339,13 @@ class SerializedScriptValueDecoder():
314
339
  """Decodes an Object value."""
315
340
  tag = self.PeekTag()
316
341
  js_object = {}
342
+ self.object_pool.append(js_object)
317
343
  while tag != definitions.TerminatorTag:
318
344
  name = self.DecodeStringData()
319
345
  _, value = self.DecodeValue()
320
346
  js_object[name] = value
321
347
  tag = self.PeekTag()
322
348
  _ = self.decoder.DecodeUint32()
323
- self.object_pool.append(js_object)
324
349
  return js_object
325
350
 
326
351
  def DecodeStringData(self) -> str:
@@ -342,11 +367,11 @@ class SerializedScriptValueDecoder():
342
367
 
343
368
  if peeked_tag == definitions.StringPoolTag:
344
369
  _ = self.decoder.DecodeUint32()
345
- if len(self.constant_pool) < 0xff:
370
+ if len(self.constant_pool) <= 0xff:
346
371
  _, cp_index = self.decoder.DecodeUint8()
347
- elif len(self.constant_pool) < 0xffff:
372
+ elif len(self.constant_pool) <= 0xffff:
348
373
  _, cp_index = self.decoder.DecodeUint16()
349
- elif len(self.constant_pool) < 0xffffffff:
374
+ elif len(self.constant_pool) <= 0xffffffff:
350
375
  _, cp_index = self.decoder.DecodeUint32()
351
376
  else:
352
377
  raise errors.ParserError('Unexpected constant pool size value.')
@@ -450,6 +475,7 @@ class SerializedScriptValueDecoder():
450
475
  """Decodes a Map value."""
451
476
  tag = self.PeekSerializationTag()
452
477
  js_map = {} # TODO: make this into a JSMap (like JSArray/JSSet)
478
+ self.object_pool.append(js_map)
453
479
 
454
480
  while tag != definitions.SerializationTag.NON_MAP_PROPERTIES:
455
481
  _, key = self.DecodeValue()
@@ -468,17 +494,17 @@ class SerializedScriptValueDecoder():
468
494
  pool_tag = self.PeekTag()
469
495
 
470
496
  _, tag = self.decoder.DecodeUint32()
471
-
472
497
  return js_map
473
498
 
474
499
  def DecodeSetData(self) -> JSSet:
475
500
  """Decodes a SetData value."""
476
501
  tag = self.PeekSerializationTag()
477
502
  js_set = JSSet()
503
+ self.object_pool.append(js_set)
478
504
 
479
505
  while tag != definitions.SerializationTag.NON_SET_PROPERTIES:
480
506
  _, key = self.DecodeValue()
481
- js_set.add(key)
507
+ js_set.Add(key)
482
508
  tag = self.PeekSerializationTag()
483
509
 
484
510
  # consume the NonSetPropertiesTag
@@ -540,8 +566,13 @@ class SerializedScriptValueDecoder():
540
566
 
541
567
  def DecodeObjectReference(self) -> Any:
542
568
  """Decodes an ObjectReference value."""
543
- _, object_ref = self.decoder.DecodeUint8()
544
- return self.object_pool[object_ref - 1]
569
+ if len(self.object_pool) < 0xFF:
570
+ _, object_ref = self.decoder.DecodeUint8()
571
+ elif len(self.object_pool) < 0xFFFF:
572
+ _, object_ref = self.decoder.DecodeUint16()
573
+ else: # if len(self.object_pool) < 0xFFFFFFFF:
574
+ _, object_ref = self.decoder.DecodeUint32()
575
+ return self.object_pool[object_ref]
545
576
 
546
577
  def DecodeArrayBufferView(self) -> ArrayBufferView:
547
578
  """Decodes an ArrayBufferView value.
@@ -641,6 +672,7 @@ class SerializedScriptValueDecoder():
641
672
  value = self.DecodeArrayBuffer()
642
673
  elif tag == definitions.SerializationTag.ARRAY_BUFFER_VIEW:
643
674
  value = self.DecodeArrayBufferView()
675
+ self.object_pool.append(value)
644
676
  elif tag == definitions.SerializationTag.ARRAY_BUFFER_TRANSFER:
645
677
  value = self.DecodeArrayBufferTransfer()
646
678
  elif tag == definitions.SerializationTag.TRUE_OBJECT:
@@ -0,0 +1,71 @@
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
+ """Types for indexeddb."""
16
+ from __future__ import annotations
17
+
18
+ import dataclasses
19
+ from typing import Any, Dict, List, Set
20
+
21
+
22
+ @dataclasses.dataclass
23
+ class JSArray:
24
+ """A parsed Javascript array.
25
+
26
+ A JavaScript array behaves like a Python list but allows assigning arbitrary
27
+ properties. The array is stored in the attribute __array__.
28
+
29
+ Attributes:
30
+ values: the array values.
31
+ properties: the array properties.
32
+ """
33
+ values: List[Any] = dataclasses.field(default_factory=list)
34
+ properties: Dict[Any, Any] = dataclasses.field(default_factory=dict)
35
+
36
+
37
+ @dataclasses.dataclass
38
+ class JSSet:
39
+ """A parsed JavaScript set.
40
+
41
+ A JavaScript set behaves like a Python set but allows assigning arbitrary
42
+ properties. The array is stored in the attribute __set__.
43
+
44
+ Attributes:
45
+ values: the set values.
46
+ properties: the set properties.
47
+ """
48
+ values: Set[Any] = dataclasses.field(default_factory=set)
49
+ properties: Dict[Any, Any] = dataclasses.field(default_factory=dict)
50
+
51
+
52
+ @dataclasses.dataclass
53
+ class Null:
54
+ """A parsed JavaScript Null."""
55
+
56
+
57
+ @dataclasses.dataclass
58
+ class RegExp:
59
+ """A parsed JavaScript RegExp.
60
+
61
+ Attributes:
62
+ pattern: the pattern.
63
+ flags: the flags.
64
+ """
65
+ pattern: str
66
+ flags: int
67
+
68
+
69
+ @dataclasses.dataclass
70
+ class Undefined:
71
+ """A JavaScript undef."""
@@ -19,11 +19,13 @@ from datetime import datetime
19
19
  import json
20
20
  import pathlib
21
21
 
22
+ from dfindexeddb import utils
22
23
  from dfindexeddb import version
23
24
  from dfindexeddb.leveldb import descriptor
24
25
  from dfindexeddb.leveldb import ldb
25
26
  from dfindexeddb.leveldb import log
26
27
  from dfindexeddb.leveldb import record
28
+ from dfindexeddb.leveldb.plugins import manager
27
29
 
28
30
 
29
31
  _VALID_PRINTABLE_CHARACTERS = (
@@ -37,7 +39,7 @@ class Encoder(json.JSONEncoder):
37
39
  def default(self, o):
38
40
  """Returns a serializable object for o."""
39
41
  if dataclasses.is_dataclass(o):
40
- o_dict = dataclasses.asdict(o)
42
+ o_dict = utils.asdict(o)
41
43
  return o_dict
42
44
  if isinstance(o, bytes):
43
45
  out = []
@@ -66,13 +68,39 @@ def _Output(structure, output):
66
68
 
67
69
  def DbCommand(args):
68
70
  """The CLI for processing leveldb folders."""
71
+ if args.plugin and args.plugin == 'list':
72
+ for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
73
+ print(plugin)
74
+ return
75
+
76
+ if args.plugin:
77
+ plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
78
+ else:
79
+ plugin_class = None
80
+
69
81
  for leveldb_record in record.FolderReader(
70
- args.source).GetRecords(use_manifest=args.use_manifest):
71
- _Output(leveldb_record, output=args.output)
82
+ args.source).GetRecords(
83
+ use_manifest=args.use_manifest,
84
+ use_sequence_number=args.use_sequence_number):
85
+ if plugin_class:
86
+ plugin_record = plugin_class.FromLevelDBRecord(leveldb_record)
87
+ _Output(plugin_record, output=args.output)
88
+ else:
89
+ _Output(leveldb_record, output=args.output)
72
90
 
73
91
 
74
92
  def LdbCommand(args):
75
93
  """The CLI for processing ldb files."""
94
+ if args.plugin and args.plugin == 'list':
95
+ for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
96
+ print(plugin)
97
+ return
98
+
99
+ if args.plugin:
100
+ plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
101
+ else:
102
+ plugin_class = None
103
+
76
104
  ldb_file = ldb.FileReader(args.source)
77
105
 
78
106
  if args.structure_type == 'blocks':
@@ -83,7 +111,11 @@ def LdbCommand(args):
83
111
  elif args.structure_type == 'records' or not args.structure_type:
84
112
  # Prints key value record information.
85
113
  for key_value_record in ldb_file.GetKeyValueRecords():
86
- _Output(key_value_record, output=args.output)
114
+ if plugin_class:
115
+ plugin_record = plugin_class.FromKeyValueRecord(key_value_record)
116
+ _Output(plugin_record, output=args.output)
117
+ else:
118
+ _Output(key_value_record, output=args.output)
87
119
 
88
120
  else:
89
121
  print(f'{args.structure_type} is not supported for ldb files.')
@@ -91,6 +123,16 @@ def LdbCommand(args):
91
123
 
92
124
  def LogCommand(args):
93
125
  """The CLI for processing log files."""
126
+ if args.plugin and args.plugin == 'list':
127
+ for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
128
+ print(plugin)
129
+ return
130
+
131
+ if args.plugin:
132
+ plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
133
+ else:
134
+ plugin_class = None
135
+
94
136
  log_file = log.FileReader(args.source)
95
137
 
96
138
  if args.structure_type == 'blocks':
@@ -112,7 +154,11 @@ def LogCommand(args):
112
154
  or not args.structure_type):
113
155
  # Prints key value record information.
114
156
  for internal_key_record in log_file.GetParsedInternalKeys():
115
- _Output(internal_key_record, output=args.output)
157
+ if plugin_class:
158
+ plugin_record = plugin_class.FromKeyValueRecord(internal_key_record)
159
+ _Output(plugin_record, output=args.output)
160
+ else:
161
+ _Output(internal_key_record, output=args.output)
116
162
 
117
163
  else:
118
164
  print(f'{args.structure_type} is not supported for log files.')
@@ -144,6 +190,7 @@ def DescriptorCommand(args):
144
190
  else:
145
191
  print(f'{args.structure_type} is not supported for descriptor files.')
146
192
 
193
+
147
194
  def App():
148
195
  """The CLI app entrypoint for parsing leveldb files."""
149
196
  parser = argparse.ArgumentParser(
@@ -160,10 +207,17 @@ def App():
160
207
  required=True,
161
208
  type=pathlib.Path,
162
209
  help='The source leveldb directory')
163
- parser_db.add_argument(
210
+ recover_group = parser_db.add_mutually_exclusive_group()
211
+ recover_group.add_argument(
164
212
  '--use_manifest',
165
213
  action='store_true',
166
214
  help='Use manifest file to determine active/deleted records.')
215
+ recover_group.add_argument(
216
+ '--use_sequence_number',
217
+ action='store_true',
218
+ help=(
219
+ 'Use sequence number and file offset to determine active/deleted '
220
+ 'records.'))
167
221
  parser_db.add_argument(
168
222
  '-o',
169
223
  '--output',
@@ -173,6 +227,9 @@ def App():
173
227
  'repr'],
174
228
  default='json',
175
229
  help='Output format. Default is json')
230
+ parser_db.add_argument(
231
+ '--plugin',
232
+ help='Use plugin to parse records.')
176
233
  parser_db.set_defaults(func=DbCommand)
177
234
 
178
235
  parser_log = subparsers.add_parser(
@@ -191,6 +248,9 @@ def App():
191
248
  'repr'],
192
249
  default='json',
193
250
  help='Output format. Default is json')
251
+ parser_log.add_argument(
252
+ '--plugin',
253
+ help='Use plugin to parse records.')
194
254
  parser_log.add_argument(
195
255
  '-t',
196
256
  '--structure_type',
@@ -218,6 +278,9 @@ def App():
218
278
  'repr'],
219
279
  default='json',
220
280
  help='Output format. Default is json')
281
+ parser_ldb.add_argument(
282
+ '--plugin',
283
+ help='Use plugin to parse records.')
221
284
  parser_ldb.add_argument(
222
285
  '-t',
223
286
  '--structure_type',
@@ -384,5 +384,6 @@ class FileReader:
384
384
 
385
385
  def GetLatestVersion(self) -> LevelDBVersion:
386
386
  """Returns the latest LevelDBVersion instance."""
387
- *_, latest = self.GetVersions()
387
+ for version in self.GetVersions():
388
+ latest = version
388
389
  return latest
@@ -152,7 +152,7 @@ class PhysicalRecord(utils.FromDecoderMixin):
152
152
  @classmethod
153
153
  def FromDecoder(
154
154
  cls, decoder: utils.LevelDBDecoder, base_offset: int = 0
155
- ) -> PhysicalRecord:
155
+ ) -> Optional[PhysicalRecord]:
156
156
  """Decodes a PhysicalRecord from the current position of a LevelDBDecoder.
157
157
 
158
158
  Args:
@@ -161,11 +161,13 @@ class PhysicalRecord(utils.FromDecoderMixin):
161
161
  read from.
162
162
 
163
163
  Returns:
164
- A PhysicalRecord.
164
+ A PhysicalRecord or None if the parsed header is 0.
165
165
  """
166
166
  offset, checksum = decoder.DecodeUint32()
167
167
  _, length = decoder.DecodeUint16()
168
168
  _, record_type_byte = decoder.DecodeUint8()
169
+ if checksum == 0 or length == 0 or record_type_byte == 0:
170
+ return None
169
171
  try:
170
172
  record_type = definitions.LogFilePhysicalRecordType(record_type_byte)
171
173
  except ValueError as error:
@@ -206,7 +208,11 @@ class Block:
206
208
  buffer_length = len(self.data)
207
209
 
208
210
  while buffer.tell() + PhysicalRecord.PHYSICAL_HEADER_LENGTH < buffer_length:
209
- yield PhysicalRecord.FromStream(buffer, base_offset=self.offset)
211
+ record = PhysicalRecord.FromStream(buffer, base_offset=self.offset)
212
+ if record:
213
+ yield record
214
+ else:
215
+ return
210
216
 
211
217
  @classmethod
212
218
  def FromStream(cls, stream: BinaryIO) -> Optional[Block]:
@@ -0,0 +1,17 @@
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
+ """Leveldb Plugin module."""
16
+
17
+ from dfindexeddb.leveldb.plugins import chrome_notifications