dfindexeddb 20240519__py3-none-any.whl → 20241105__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.
@@ -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)
@@ -16,15 +16,15 @@
16
16
  from enum import IntEnum
17
17
 
18
18
 
19
- CurrentVersion = 0x0000000F # 15
20
- TerminatorTag = 0xFFFFFFFF
21
- StringPoolTag = 0xFFFFFFFE
22
- NonIndexPropertiesTag = 0xFFFFFFFD
23
- ImageDataPoolTag = 0xFFFFFFFE
24
- StringDataIs8BitFlag = 0x80000000
19
+ CURRENT_VERSION = 0x0000000F # 15
20
+ TERMINATOR_TAG = 0xFFFFFFFF
21
+ STRING_POOL_TAG = 0xFFFFFFFE
22
+ NON_INDEX_PROPERTIES_TAG = 0xFFFFFFFD
23
+ IMAGE_DATA_POOL_TAG = 0xFFFFFFFE
24
+ STRING_DATA_IS_8BIT_FLAG = 0x80000000
25
25
 
26
26
 
27
- SIDBKeyVersion = 0x00
27
+ SIDB_KEY_VERSION = 0x00
28
28
 
29
29
 
30
30
  class SIDBKeyType(IntEnum):
@@ -23,6 +23,7 @@ from typing import Any, Dict, List, Tuple, Union
23
23
 
24
24
  from dfindexeddb import errors
25
25
  from dfindexeddb import utils
26
+ from dfindexeddb.indexeddb import types
26
27
  from dfindexeddb.indexeddb.safari import definitions
27
28
 
28
29
 
@@ -82,78 +83,6 @@ class FileList:
82
83
  files: List[FileData]
83
84
 
84
85
 
85
- class JSArray(list):
86
- """A parsed JavaScript array.
87
-
88
- This is a wrapper around a standard Python list to allow assigning arbitrary
89
- properties as is possible in the JavaScript equivalent.
90
- """
91
-
92
- def __repr__(self):
93
- array_entries = ", ".join([str(entry) for entry in list(self)])
94
- properties = ", ".join(
95
- f'{key}: {value}' for key, value in self.properties.items())
96
- return f'[{array_entries}, {properties}]'
97
-
98
- @property
99
- def properties(self) -> Dict[str, Any]:
100
- """Returns the object properties."""
101
- return self.__dict__
102
-
103
- def __contains__(self, item):
104
- return item in self.__dict__
105
-
106
- def __getitem__(self, name):
107
- return self.__dict__[name]
108
-
109
-
110
- class JSSet(set):
111
- """A parsed JavaScript set.
112
-
113
- This is a wrapper around a standard Python set to allow assigning arbitrary
114
- properties as is possible in the JavaScript equivalent.
115
- """
116
-
117
- def __repr__(self):
118
- set_entries = ", ".join([str(entry) for entry in list(self)])
119
- properties = ", ".join(
120
- f'{key}: {value}' for key, value in self.properties.items())
121
- return f'[{set_entries}, {properties}]'
122
-
123
- @property
124
- def properties(self) -> Dict[str, Any]:
125
- """Returns the object properties."""
126
- return self.__dict__
127
-
128
- def __contains__(self, item):
129
- return item in self.__dict__
130
-
131
- def __getitem__(self, name):
132
- return self.__dict__[name]
133
-
134
-
135
- @dataclass
136
- class Null:
137
- """A parsed JavaScript Null."""
138
-
139
-
140
- @dataclass
141
- class RegExp:
142
- """A parsed JavaScript RegExp.
143
-
144
- Attributes:
145
- pattern: the pattern.
146
- flags: the flags.
147
- """
148
- pattern: str
149
- flags: str
150
-
151
-
152
- @dataclass(frozen=True)
153
- class Undefined:
154
- """A parsed JavaScript undef."""
155
-
156
-
157
86
  @dataclass
158
87
  class IDBKeyData(utils.FromDecoderMixin):
159
88
  """An IDBKeyData.
@@ -169,7 +98,10 @@ class IDBKeyData(utils.FromDecoderMixin):
169
98
 
170
99
  @classmethod
171
100
  def FromDecoder(
172
- cls, decoder: utils.StreamDecoder, base_offset: int = 0) -> IDBKeyData:
101
+ cls,
102
+ decoder: utils.StreamDecoder,
103
+ base_offset: int = 0
104
+ ) -> IDBKeyData:
173
105
  """Decodes an IDBKeyData from the current position of decoder.
174
106
 
175
107
  Refer to IDBSerialization.cpp for the encoding scheme.
@@ -211,7 +143,7 @@ class IDBKeyData(utils.FromDecoderMixin):
211
143
  return data
212
144
 
213
145
  offset, version_header = decoder.DecodeUint8()
214
- if version_header != definitions.SIDBKeyVersion:
146
+ if version_header != definitions.SIDB_KEY_VERSION:
215
147
  raise errors.ParserError('SIDBKeyVersion not found.')
216
148
 
217
149
  _, raw_key_type = decoder.DecodeUint8()
@@ -279,7 +211,7 @@ class SerializedScriptValueDecoder():
279
211
  raise errors.ParserError(
280
212
  f'Invalid terminal {terminal_byte} at offset {offset}') from error
281
213
 
282
- def DecodeArray(self) -> JSArray:
214
+ def DecodeArray(self) -> types.JSArray:
283
215
  """Decodes an Array value.
284
216
 
285
217
  Returns:
@@ -289,25 +221,25 @@ class SerializedScriptValueDecoder():
289
221
  ParserError if an invalid Terminator tag was found.
290
222
  """
291
223
  _, length = self.decoder.DecodeUint32()
292
- array = JSArray()
224
+ array = types.JSArray()
293
225
  self.object_pool.append(array)
294
226
  for _ in range(length):
295
227
  _, _ = self.decoder.DecodeUint32()
296
228
  _, value = self.DecodeValue()
297
- array.append(value)
229
+ array.values.append(value)
298
230
 
299
231
  offset, terminator_tag = self.decoder.DecodeUint32()
300
- if terminator_tag != definitions.TerminatorTag:
232
+ if terminator_tag != definitions.TERMINATOR_TAG:
301
233
  raise errors.ParserError(f'Terminator tag not found at offset {offset}.')
302
234
 
303
235
  offset, tag = self.decoder.DecodeUint32()
304
- if tag == definitions.NonIndexPropertiesTag:
305
- while tag != definitions.TerminatorTag:
236
+ if tag == definitions.NON_INDEX_PROPERTIES_TAG:
237
+ while tag != definitions.TERMINATOR_TAG:
306
238
  name = self.DecodeStringData()
307
239
  _, value = self.DecodeValue()
308
240
  _, tag = self.decoder.DecodeUint32()
309
241
  array.properties[name] = value
310
- elif tag != definitions.TerminatorTag:
242
+ elif tag != definitions.TERMINATOR_TAG:
311
243
  raise errors.ParserError(f'Terminator tag not found at offset {offset}.')
312
244
  return array
313
245
 
@@ -316,7 +248,7 @@ class SerializedScriptValueDecoder():
316
248
  tag = self.PeekTag()
317
249
  js_object = {}
318
250
  self.object_pool.append(js_object)
319
- while tag != definitions.TerminatorTag:
251
+ while tag != definitions.TERMINATOR_TAG:
320
252
  name = self.DecodeStringData()
321
253
  _, value = self.DecodeValue()
322
254
  js_object[name] = value
@@ -338,10 +270,10 @@ class SerializedScriptValueDecoder():
338
270
  * unable to to decode a buffer as utf-16-le.
339
271
  """
340
272
  peeked_tag = self.PeekTag()
341
- if peeked_tag == definitions.TerminatorTag:
273
+ if peeked_tag == definitions.TERMINATOR_TAG:
342
274
  raise errors.ParserError('Unexpected TerminatorTag found')
343
275
 
344
- if peeked_tag == definitions.StringPoolTag:
276
+ if peeked_tag == definitions.STRING_POOL_TAG:
345
277
  _ = self.decoder.DecodeUint32()
346
278
  if len(self.constant_pool) <= 0xff:
347
279
  _, cp_index = self.decoder.DecodeUint8()
@@ -354,11 +286,11 @@ class SerializedScriptValueDecoder():
354
286
  return self.constant_pool[cp_index]
355
287
 
356
288
  _, length_with_8bit_flag = self.decoder.DecodeUint32()
357
- if length_with_8bit_flag == definitions.TerminatorTag:
289
+ if length_with_8bit_flag == definitions.TERMINATOR_TAG:
358
290
  raise errors.ParserError('Disallowed string length found.')
359
291
 
360
292
  length = length_with_8bit_flag & 0x7FFFFFFF
361
- is_8bit = length_with_8bit_flag & definitions.StringDataIs8BitFlag
293
+ is_8bit = length_with_8bit_flag & definitions.STRING_DATA_IS_8BIT_FLAG
362
294
 
363
295
  if is_8bit:
364
296
  _, characters = self.decoder.ReadBytes(length)
@@ -367,9 +299,10 @@ class SerializedScriptValueDecoder():
367
299
  _, characters = self.decoder.ReadBytes(2*length)
368
300
  try:
369
301
  value = characters.decode('utf-16-le')
370
- except UnicodeDecodeError:
302
+ except UnicodeDecodeError as exc:
371
303
  raise errors.ParserError(
372
- f'Unable to decode {len(characters)} characters as utf-16-le')
304
+ f'Unable to decode {len(characters)} characters as utf-16-le'
305
+ ) from exc
373
306
  self.constant_pool.append(value)
374
307
  return value
375
308
 
@@ -441,11 +374,11 @@ class SerializedScriptValueDecoder():
441
374
  'memory_cost': memory_cost
442
375
  }
443
376
 
444
- def DecodeRegExp(self) -> RegExp:
377
+ def DecodeRegExp(self) -> types.RegExp:
445
378
  """Decodes a RegExp value."""
446
379
  pattern = self.DecodeStringData()
447
380
  flags = self.DecodeStringData()
448
- return RegExp(pattern=pattern, flags=flags)
381
+ return types.RegExp(pattern=pattern, flags=flags)
449
382
 
450
383
  def DecodeMapData(self) -> dict:
451
384
  """Decodes a Map value."""
@@ -463,7 +396,7 @@ class SerializedScriptValueDecoder():
463
396
  _, tag = self.DecodeSerializationTag()
464
397
 
465
398
  pool_tag = self.PeekTag()
466
- while pool_tag != definitions.TerminatorTag:
399
+ while pool_tag != definitions.TERMINATOR_TAG:
467
400
  name = self.DecodeStringData()
468
401
  value = self.DecodeValue()
469
402
  js_map[name] = value
@@ -472,22 +405,22 @@ class SerializedScriptValueDecoder():
472
405
  _, tag = self.decoder.DecodeUint32()
473
406
  return js_map
474
407
 
475
- def DecodeSetData(self) -> JSSet:
408
+ def DecodeSetData(self) -> types.JSSet:
476
409
  """Decodes a SetData value."""
477
410
  tag = self.PeekSerializationTag()
478
- js_set = JSSet()
411
+ js_set = types.JSSet()
479
412
  self.object_pool.append(js_set)
480
413
 
481
414
  while tag != definitions.SerializationTag.NON_SET_PROPERTIES:
482
415
  _, key = self.DecodeValue()
483
- js_set.add(key)
416
+ js_set.values.add(key)
484
417
  tag = self.PeekSerializationTag()
485
418
 
486
419
  # consume the NonSetPropertiesTag
487
420
  _, tag = self.DecodeSerializationTag()
488
421
 
489
422
  pool_tag = self.PeekTag()
490
- while pool_tag != definitions.TerminatorTag:
423
+ while pool_tag != definitions.TERMINATOR_TAG:
491
424
  name = self.DecodeStringData()
492
425
  value = self.DecodeValue()
493
426
  js_set.properties[name] = value
@@ -590,7 +523,7 @@ class SerializedScriptValueDecoder():
590
523
  ParserError when CurrentVersion is not found.
591
524
  """
592
525
  _, current_version = self.decoder.DecodeUint32()
593
- if current_version != definitions.CurrentVersion:
526
+ if current_version != definitions.CURRENT_VERSION:
594
527
  raise errors.ParserError(
595
528
  f'{current_version} is not the expected CurrentVersion')
596
529
  _, value = self.DecodeValue()
@@ -611,9 +544,9 @@ class SerializedScriptValueDecoder():
611
544
  elif tag == definitions.SerializationTag.OBJECT:
612
545
  value = self.DecodeObject()
613
546
  elif tag == definitions.SerializationTag.UNDEFINED:
614
- value = Undefined()
547
+ value = types.Undefined()
615
548
  elif tag == definitions.SerializationTag.NULL:
616
- value = Null()
549
+ value = types.Null()
617
550
  elif tag == definitions.SerializationTag.INT:
618
551
  _, value = self.decoder.DecodeInt32()
619
552
  elif tag == definitions.SerializationTag.ZERO:
@@ -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: str
67
+
68
+
69
+ @dataclasses.dataclass
70
+ class Undefined:
71
+ """A JavaScript undef."""
@@ -203,10 +203,11 @@ def App():
203
203
  parser_db = subparsers.add_parser(
204
204
  'db', help='Parse a directory as leveldb.')
205
205
  parser_db.add_argument(
206
- '-s', '--source',
206
+ '-s',
207
+ '--source',
207
208
  required=True,
208
209
  type=pathlib.Path,
209
- help='The source leveldb directory')
210
+ help='The source leveldb directory.')
210
211
  recover_group = parser_db.add_mutually_exclusive_group()
211
212
  recover_group.add_argument(
212
213
  '--use_manifest',
@@ -226,7 +227,7 @@ def App():
226
227
  'jsonl',
227
228
  'repr'],
228
229
  default='json',
229
- help='Output format. Default is json')
230
+ help='Output format. Default is json.')
230
231
  parser_db.add_argument(
231
232
  '--plugin',
232
233
  help='Use plugin to parse records.')
@@ -235,10 +236,11 @@ def App():
235
236
  parser_log = subparsers.add_parser(
236
237
  'log', help='Parse a leveldb log file.')
237
238
  parser_log.add_argument(
238
- '-s', '--source',
239
+ '-s',
240
+ '--source',
239
241
  required=True,
240
242
  type=pathlib.Path,
241
- help='The source leveldb file')
243
+ help='The source leveldb file.')
242
244
  parser_log.add_argument(
243
245
  '-o',
244
246
  '--output',
@@ -247,7 +249,7 @@ def App():
247
249
  'jsonl',
248
250
  'repr'],
249
251
  default='json',
250
- help='Output format. Default is json')
252
+ help='Output format. Default is json.')
251
253
  parser_log.add_argument(
252
254
  '--plugin',
253
255
  help='Use plugin to parse records.')
@@ -265,7 +267,8 @@ def App():
265
267
  parser_ldb = subparsers.add_parser(
266
268
  'ldb', help='Parse a leveldb table (.ldb) file.')
267
269
  parser_ldb.add_argument(
268
- '-s', '--source',
270
+ '-s',
271
+ '--source',
269
272
  required=True,
270
273
  type=pathlib.Path,
271
274
  help='The source leveldb file')
@@ -277,7 +280,7 @@ def App():
277
280
  'jsonl',
278
281
  'repr'],
279
282
  default='json',
280
- help='Output format. Default is json')
283
+ help='Output format. Default is json.')
281
284
  parser_ldb.add_argument(
282
285
  '--plugin',
283
286
  help='Use plugin to parse records.')
@@ -293,7 +296,8 @@ def App():
293
296
  parser_descriptor = subparsers.add_parser(
294
297
  'descriptor', help='Parse a leveldb descriptor (MANIFEST) file.')
295
298
  parser_descriptor.add_argument(
296
- '-s', '--source',
299
+ '-s',
300
+ '--source',
297
301
  required=True,
298
302
  type=pathlib.Path,
299
303
  help='The source leveldb file')
@@ -305,13 +309,16 @@ def App():
305
309
  'jsonl',
306
310
  'repr'],
307
311
  default='json',
308
- help='Output format. Default is json')
312
+ help='Output format. Default is json.')
309
313
  db_group = parser_descriptor.add_mutually_exclusive_group()
310
314
  db_group.add_argument(
311
315
  '-t',
312
316
  '--structure_type',
313
317
  choices=[
314
- 'blocks', 'physical_records', 'versionedit'],
318
+ 'blocks',
319
+ 'physical_records',
320
+ 'versionedit'
321
+ ],
315
322
  help='Parses the specified structure. Default is versionedit.')
316
323
  db_group.add_argument(
317
324
  '-v',