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.
- dfindexeddb/indexeddb/chromium/blink.py +5 -0
- dfindexeddb/indexeddb/chromium/record.py +11 -6
- dfindexeddb/indexeddb/chromium/v8.py +30 -10
- dfindexeddb/indexeddb/cli.py +52 -9
- dfindexeddb/indexeddb/firefox/definitions.py +143 -0
- dfindexeddb/indexeddb/firefox/gecko.py +600 -0
- dfindexeddb/indexeddb/firefox/record.py +180 -0
- dfindexeddb/indexeddb/safari/webkit.py +52 -20
- dfindexeddb/indexeddb/types.py +71 -0
- dfindexeddb/leveldb/cli.py +69 -6
- dfindexeddb/leveldb/descriptor.py +2 -1
- dfindexeddb/leveldb/log.py +9 -3
- dfindexeddb/leveldb/plugins/__init__.py +17 -0
- dfindexeddb/leveldb/plugins/chrome_notifications.py +135 -0
- dfindexeddb/leveldb/plugins/interface.py +36 -0
- dfindexeddb/leveldb/plugins/manager.py +75 -0
- dfindexeddb/leveldb/plugins/notification_database_data_pb2.py +38 -0
- dfindexeddb/leveldb/record.py +33 -1
- dfindexeddb/utils.py +35 -1
- dfindexeddb/version.py +1 -1
- {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/METADATA +42 -15
- dfindexeddb-20241031.dist-info/RECORD +41 -0
- {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/WHEEL +1 -1
- dfindexeddb-20240501.dist-info/RECORD +0 -32
- {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/AUTHORS +0 -0
- {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/LICENSE +0 -0
- {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/entry_points.txt +0 -0
- {dfindexeddb-20240501.dist-info → dfindexeddb-20241031.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
"""Parser plugin for Chrome Notifications."""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# pytype: disable=import-error
|
|
25
|
+
from dfdatetime import webkit_time
|
|
26
|
+
from dfindexeddb.leveldb.plugins import notification_database_data_pb2 as \
|
|
27
|
+
notification_pb2
|
|
28
|
+
# pytype: enable=import-error
|
|
29
|
+
_has_import_dependencies = True
|
|
30
|
+
except ImportError as err:
|
|
31
|
+
_has_import_dependencies = False
|
|
32
|
+
logging.warning((
|
|
33
|
+
'Could not import dependencies for '
|
|
34
|
+
'leveldb.plugins.chrome_notifications: %s'), err)
|
|
35
|
+
|
|
36
|
+
from dfindexeddb.indexeddb.chromium import blink
|
|
37
|
+
from dfindexeddb.leveldb.plugins import interface
|
|
38
|
+
from dfindexeddb.leveldb.plugins import manager
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclasses.dataclass
|
|
42
|
+
class ChromeNotificationRecord(interface.LeveldbPlugin):
|
|
43
|
+
"""Chrome notification record."""
|
|
44
|
+
src_file: Optional[str] = None
|
|
45
|
+
offset: Optional[int] = None
|
|
46
|
+
key: Optional[str] = None
|
|
47
|
+
sequence_number: Optional[int] = None
|
|
48
|
+
type: Optional[int] = None
|
|
49
|
+
origin: Optional[str] = None
|
|
50
|
+
service_worker_registration_id: Optional[int] = None
|
|
51
|
+
notification_title: Optional[str] = None
|
|
52
|
+
notification_direction: Optional[str] = None
|
|
53
|
+
notification_lang: Optional[str] = None
|
|
54
|
+
notification_body: Optional[str] = None
|
|
55
|
+
notification_tag: Optional[str] = None
|
|
56
|
+
notification_icon: Optional[str] = None
|
|
57
|
+
notification_silent: Optional[bool] = None
|
|
58
|
+
notification_data: Optional[str] = None
|
|
59
|
+
notification_require_interaction: Optional[bool] = None
|
|
60
|
+
notification_time: Optional[str] = None
|
|
61
|
+
notification_renotify: Optional[bool] = None
|
|
62
|
+
notification_badge: Optional[str] = None
|
|
63
|
+
notification_image: Optional[str] = None
|
|
64
|
+
notification_id: Optional[str] = None
|
|
65
|
+
replaced_existing_notification: Optional[bool] = None
|
|
66
|
+
num_clicks: Optional[int] = None
|
|
67
|
+
num_action_button_clicks: Optional[int] = None
|
|
68
|
+
creation_time: Optional[str] = None
|
|
69
|
+
closed_reason: Optional[str] = None
|
|
70
|
+
has_triggered: Optional[bool] = None
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def FromKeyValueRecord(
|
|
74
|
+
cls,
|
|
75
|
+
ldb_record
|
|
76
|
+
) -> ChromeNotificationRecord:
|
|
77
|
+
record = cls()
|
|
78
|
+
record.offset = ldb_record.offset
|
|
79
|
+
record.key = ldb_record.key.decode()
|
|
80
|
+
record.sequence_number = ldb_record.sequence_number
|
|
81
|
+
record.type = ldb_record.record_type
|
|
82
|
+
|
|
83
|
+
if not ldb_record.value:
|
|
84
|
+
return record
|
|
85
|
+
|
|
86
|
+
# pylint: disable-next=no-member,line-too-long
|
|
87
|
+
notification_proto = notification_pb2.NotificationDatabaseDataProto() # pytype: disable=module-attr
|
|
88
|
+
notification_proto.ParseFromString(ldb_record.value)
|
|
89
|
+
|
|
90
|
+
record.origin = notification_proto.origin
|
|
91
|
+
record.service_worker_registration_id = (
|
|
92
|
+
notification_proto.service_worker_registration_id)
|
|
93
|
+
record.notification_title = notification_proto.notification_data.title
|
|
94
|
+
record.notification_direction = (
|
|
95
|
+
notification_proto.notification_data.direction)
|
|
96
|
+
record.notification_lang = notification_proto.notification_data.lang
|
|
97
|
+
record.notification_body = notification_proto.notification_data.body
|
|
98
|
+
record.notification_tag = notification_proto.notification_data.tag
|
|
99
|
+
record.notification_icon = notification_proto.notification_data.icon
|
|
100
|
+
record.notification_silent = notification_proto.notification_data.silent
|
|
101
|
+
record.notification_data = notification_proto.notification_data.data
|
|
102
|
+
record.notification_require_interaction = (
|
|
103
|
+
notification_proto.notification_data.require_interaction)
|
|
104
|
+
record.notification_time = webkit_time.WebKitTime(
|
|
105
|
+
timestamp=notification_proto.notification_data.timestamp
|
|
106
|
+
).CopyToDateTimeString()
|
|
107
|
+
record.notification_renotify = notification_proto.notification_data.renotify
|
|
108
|
+
record.notification_badge = notification_proto.notification_data.badge
|
|
109
|
+
record.notification_image = notification_proto.notification_data.image
|
|
110
|
+
record.notification_id = notification_proto.notification_id
|
|
111
|
+
record.replaced_existing_notification = (
|
|
112
|
+
notification_proto.replaced_existing_notification)
|
|
113
|
+
record.num_clicks = notification_proto.num_clicks
|
|
114
|
+
record.num_action_button_clicks = (
|
|
115
|
+
notification_proto.num_action_button_clicks)
|
|
116
|
+
record.creation_time = webkit_time.WebKitTime(
|
|
117
|
+
timestamp=notification_proto.creation_time_millis
|
|
118
|
+
).CopyToDateTimeString()
|
|
119
|
+
record.closed_reason = notification_proto.closed_reason
|
|
120
|
+
record.has_triggered = notification_proto.has_triggered
|
|
121
|
+
|
|
122
|
+
if not notification_proto.notification_data.data:
|
|
123
|
+
return record
|
|
124
|
+
|
|
125
|
+
notification_data = blink.V8ScriptValueDecoder(
|
|
126
|
+
raw_data=notification_proto.notification_data.data).Deserialize()
|
|
127
|
+
record.notification_data = notification_data
|
|
128
|
+
|
|
129
|
+
return record
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# check if dependencies are in existence..
|
|
133
|
+
|
|
134
|
+
if _has_import_dependencies:
|
|
135
|
+
manager.PluginManager.RegisterPlugin(ChromeNotificationRecord)
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
"""Interface for leveldb plugins."""
|
|
16
|
+
from typing import Any, Union
|
|
17
|
+
|
|
18
|
+
from dfindexeddb.leveldb import record
|
|
19
|
+
from dfindexeddb.leveldb import ldb
|
|
20
|
+
from dfindexeddb.leveldb import log
|
|
21
|
+
|
|
22
|
+
class LeveldbPlugin:
|
|
23
|
+
"""The base leveldb plugin class."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def FromLevelDBRecord(cls,
|
|
27
|
+
ldb_record: record.LevelDBRecord) -> Any:
|
|
28
|
+
"""Parses a leveldb record."""
|
|
29
|
+
parsed_record = cls.FromKeyValueRecord(ldb_record.record)
|
|
30
|
+
ldb_record.record = parsed_record
|
|
31
|
+
return ldb_record
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def FromKeyValueRecord(
|
|
35
|
+
cls, ldb_record: Union[ldb.KeyValueRecord, log.ParsedInternalKey]) -> Any:
|
|
36
|
+
"""Parses a leveldb key value record."""
|
|
@@ -0,0 +1,75 @@
|
|
|
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 manager."""
|
|
16
|
+
from typing import Type
|
|
17
|
+
|
|
18
|
+
from dfindexeddb.leveldb.plugins import interface
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LeveldbPluginManager:
|
|
22
|
+
"""The leveldb plugin manager."""
|
|
23
|
+
|
|
24
|
+
_class_registry = {}
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def GetPlugins(cls):
|
|
28
|
+
"""Retrieves the registered leveldb plugins.
|
|
29
|
+
|
|
30
|
+
Yields:
|
|
31
|
+
tuple: containing:
|
|
32
|
+
str: the name of the leveldb plugin.
|
|
33
|
+
class: the plugin class.
|
|
34
|
+
"""
|
|
35
|
+
yield from cls._class_registry.items()
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def GetPlugin(cls, plugin_name: str) -> interface.LeveldbPlugin:
|
|
39
|
+
"""Retrieves a class object of a specific plugin.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
plugin_name: name of the plugin.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
the LeveldbPlugin class.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
KeyError: if the plugin is not found/registered in the manager.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
return cls._class_registry[plugin_name]
|
|
52
|
+
except KeyError:
|
|
53
|
+
raise KeyError(f'Plugin not found: {plugin_name}')
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def RegisterPlugin(cls, plugin_class: Type[interface.LeveldbPlugin]):
|
|
57
|
+
"""Registers a leveldb plugin.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
plugin_class (class): the plugin class to register.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
KeyError: if class is already set for the corresponding name.
|
|
64
|
+
"""
|
|
65
|
+
plugin_name = plugin_class.__name__
|
|
66
|
+
if plugin_name in cls._class_registry:
|
|
67
|
+
raise KeyError(f'Plugin already registered {plugin_name}')
|
|
68
|
+
cls._class_registry[plugin_name] = plugin_class
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def ClearPlugins(cls):
|
|
72
|
+
"""Clears all plugin registrations."""
|
|
73
|
+
cls._class_registry = {}
|
|
74
|
+
|
|
75
|
+
PluginManager = LeveldbPluginManager()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: notification_database_data.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.1
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# pylint: skip-file
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n notification_database_data.proto\"\xff\t\n\x1dNotificationDatabaseDataProto\x12\"\n\x1apersistent_notification_id\x18\x01 \x01(\x03\x12\x17\n\x0fnotification_id\x18\x05 \x01(\t\x12\x0e\n\x06origin\x18\x02 \x01(\t\x12&\n\x1eservice_worker_registration_id\x18\x03 \x01(\x03\x12&\n\x1ereplaced_existing_notification\x18\x06 \x01(\x08\x12\x12\n\nnum_clicks\x18\x07 \x01(\x05\x12 \n\x18num_action_button_clicks\x18\x08 \x01(\x05\x12\x1c\n\x14\x63reation_time_millis\x18\t \x01(\x03\x12%\n\x1dtime_until_first_click_millis\x18\n \x01(\x03\x12$\n\x1ctime_until_last_click_millis\x18\x0b \x01(\x03\x12\x1f\n\x17time_until_close_millis\x18\x0c \x01(\x03\x12\x42\n\rclosed_reason\x18\r \x01(\x0e\x32+.NotificationDatabaseDataProto.ClosedReason\x12J\n\x11notification_data\x18\x04 \x01(\x0b\x32/.NotificationDatabaseDataProto.NotificationData\x12\x15\n\rhas_triggered\x18\x0e \x01(\x08\x1a\xba\x01\n\x12NotificationAction\x12\x0e\n\x06\x61\x63tion\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0c\n\x04icon\x18\x03 \x01(\t\x12\x44\n\x04type\x18\x04 \x01(\x0e\x32\x36.NotificationDatabaseDataProto.NotificationAction.Type\x12\x13\n\x0bplaceholder\x18\x05 \x01(\t\"\x1c\n\x04Type\x12\n\n\x06\x42UTTON\x10\x00\x12\x08\n\x04TEXT\x10\x01\x1a\xe4\x03\n\x10NotificationData\x12\r\n\x05title\x18\x01 \x01(\t\x12L\n\tdirection\x18\x02 \x01(\x0e\x32\x39.NotificationDatabaseDataProto.NotificationData.Direction\x12\x0c\n\x04lang\x18\x03 \x01(\t\x12\x0c\n\x04\x62ody\x18\x04 \x01(\t\x12\x0b\n\x03tag\x18\x05 \x01(\t\x12\r\n\x05image\x18\x0f \x01(\t\x12\x0c\n\x04icon\x18\x06 \x01(\t\x12\r\n\x05\x62\x61\x64ge\x18\x0e \x01(\t\x12\x1d\n\x11vibration_pattern\x18\t \x03(\x05\x42\x02\x10\x01\x12\x11\n\ttimestamp\x18\x0c \x01(\x03\x12\x10\n\x08renotify\x18\r \x01(\x08\x12\x0e\n\x06silent\x18\x07 \x01(\x08\x12\x1b\n\x13require_interaction\x18\x0b \x01(\x08\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\x12\x42\n\x07\x61\x63tions\x18\n \x03(\x0b\x32\x31.NotificationDatabaseDataProto.NotificationAction\x12\x1e\n\x16show_trigger_timestamp\x18\x10 \x01(\x03\";\n\tDirection\x12\x11\n\rLEFT_TO_RIGHT\x10\x00\x12\x11\n\rRIGHT_TO_LEFT\x10\x01\x12\x08\n\x04\x41UTO\x10\x02\"4\n\x0c\x43losedReason\x12\x08\n\x04USER\x10\x00\x12\r\n\tDEVELOPER\x10\x01\x12\x0b\n\x07UNKNOWN\x10\x02')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'notification_database_data_pb2', _globals)
|
|
22
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
|
+
DESCRIPTOR._options = None
|
|
24
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA'].fields_by_name['vibration_pattern']._options = None
|
|
25
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA'].fields_by_name['vibration_pattern']._serialized_options = b'\020\001'
|
|
26
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO']._serialized_start=37
|
|
27
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO']._serialized_end=1316
|
|
28
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION']._serialized_start=589
|
|
29
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION']._serialized_end=775
|
|
30
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION_TYPE']._serialized_start=747
|
|
31
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION_TYPE']._serialized_end=775
|
|
32
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA']._serialized_start=778
|
|
33
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA']._serialized_end=1262
|
|
34
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA_DIRECTION']._serialized_start=1203
|
|
35
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA_DIRECTION']._serialized_end=1262
|
|
36
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_CLOSEDREASON']._serialized_start=1264
|
|
37
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_CLOSEDREASON']._serialized_end=1316
|
|
38
|
+
# @@protoc_insertion_point(module_scope)
|
dfindexeddb/leveldb/record.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""A module for records from LevelDB files."""
|
|
16
16
|
from __future__ import annotations
|
|
17
|
+
from collections import defaultdict
|
|
17
18
|
import dataclasses
|
|
18
19
|
import pathlib
|
|
19
20
|
import re
|
|
@@ -297,9 +298,38 @@ class FolderReader:
|
|
|
297
298
|
record.recovered = True
|
|
298
299
|
yield record
|
|
299
300
|
|
|
301
|
+
def _RecordsBySequenceNumber(self) -> Generator[LevelDBRecord, None, None]:
|
|
302
|
+
"""Yields LevelDBRecords using the sequence number and file offset.
|
|
303
|
+
|
|
304
|
+
Yields:
|
|
305
|
+
LevelDBRecords.
|
|
306
|
+
"""
|
|
307
|
+
unsorted_records = defaultdict(list)
|
|
308
|
+
|
|
309
|
+
for filename in self.foldername.iterdir():
|
|
310
|
+
for leveldb_record in LevelDBRecord.FromFile(filename):
|
|
311
|
+
if leveldb_record:
|
|
312
|
+
unsorted_records[leveldb_record.record.key].append(leveldb_record)
|
|
313
|
+
for _, unsorted_records in unsorted_records.items():
|
|
314
|
+
num_unsorted_records = len(unsorted_records)
|
|
315
|
+
if num_unsorted_records == 1:
|
|
316
|
+
unsorted_records[0].recovered = False
|
|
317
|
+
yield unsorted_records[0]
|
|
318
|
+
else:
|
|
319
|
+
for i, record in enumerate(sorted(
|
|
320
|
+
unsorted_records, key=lambda x: (
|
|
321
|
+
x.record.sequence_number, x.record.offset)),
|
|
322
|
+
start=1):
|
|
323
|
+
if i == num_unsorted_records:
|
|
324
|
+
record.recovered = False
|
|
325
|
+
else:
|
|
326
|
+
record.recovered = True
|
|
327
|
+
yield record
|
|
328
|
+
|
|
300
329
|
def GetRecords(
|
|
301
330
|
self,
|
|
302
|
-
use_manifest: bool = False
|
|
331
|
+
use_manifest: bool = False,
|
|
332
|
+
use_sequence_number: bool = False
|
|
303
333
|
) -> Generator[LevelDBRecord, None, None]:
|
|
304
334
|
"""Yield LevelDBRecords.
|
|
305
335
|
|
|
@@ -312,6 +342,8 @@ class FolderReader:
|
|
|
312
342
|
"""
|
|
313
343
|
if use_manifest:
|
|
314
344
|
yield from self._RecordsByManifest()
|
|
345
|
+
elif use_sequence_number:
|
|
346
|
+
yield from self._RecordsBySequenceNumber()
|
|
315
347
|
else:
|
|
316
348
|
for filename in self.foldername.iterdir():
|
|
317
349
|
yield from LevelDBRecord.FromFile(filename)
|
dfindexeddb/utils.py
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Utilities for dfindexeddb."""
|
|
16
16
|
from __future__ import annotations
|
|
17
|
+
import copy
|
|
18
|
+
import dataclasses
|
|
17
19
|
import io
|
|
18
20
|
import os
|
|
19
21
|
import struct
|
|
@@ -211,7 +213,7 @@ T = TypeVar('T')
|
|
|
211
213
|
|
|
212
214
|
|
|
213
215
|
class FromDecoderMixin:
|
|
214
|
-
"""A mixin for parsing dataclass attributes using a
|
|
216
|
+
"""A mixin for parsing dataclass attributes using a StreamDecoder."""
|
|
215
217
|
|
|
216
218
|
@classmethod
|
|
217
219
|
def FromDecoder(
|
|
@@ -259,3 +261,35 @@ class FromDecoderMixin:
|
|
|
259
261
|
"""
|
|
260
262
|
stream = io.BytesIO(raw_data)
|
|
261
263
|
return cls.FromStream(stream=stream, base_offset=base_offset)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def asdict(obj, *, dict_factory=dict): # pylint: disable=invalid-name
|
|
267
|
+
"""Custom implementation of the asdict dataclasses method to include the
|
|
268
|
+
class name under the __type__ attribute name.
|
|
269
|
+
"""
|
|
270
|
+
if not dataclasses.is_dataclass(obj):
|
|
271
|
+
raise TypeError("asdict() should be called on dataclass instances")
|
|
272
|
+
return _asdict_inner(obj, dict_factory)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _asdict_inner(obj, dict_factory):
|
|
276
|
+
"""Custom implementation of the _asdict_inner dataclasses method."""
|
|
277
|
+
if dataclasses.is_dataclass(obj):
|
|
278
|
+
result = [('__type__', obj.__class__.__name__)]
|
|
279
|
+
for f in dataclasses.fields(obj):
|
|
280
|
+
value = _asdict_inner(getattr(obj, f.name), dict_factory)
|
|
281
|
+
result.append((f.name, value))
|
|
282
|
+
return dict_factory(result)
|
|
283
|
+
|
|
284
|
+
if isinstance(obj, tuple) and hasattr(obj, '_fields'):
|
|
285
|
+
return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj])
|
|
286
|
+
|
|
287
|
+
if isinstance(obj, (list, tuple)):
|
|
288
|
+
return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
|
|
289
|
+
|
|
290
|
+
if isinstance(obj, dict):
|
|
291
|
+
return type(obj)((_asdict_inner(k, dict_factory),
|
|
292
|
+
_asdict_inner(v, dict_factory))
|
|
293
|
+
for k, v in obj.items())
|
|
294
|
+
|
|
295
|
+
return copy.deepcopy(obj)
|
dfindexeddb/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dfindexeddb
|
|
3
|
-
Version:
|
|
3
|
+
Version: 20241031
|
|
4
4
|
Summary: dfindexeddb is an experimental Python tool for performing digital forensic analysis of IndexedDB and leveldb files.
|
|
5
5
|
Author-email: Syd Pleno <sydp@google.com>
|
|
6
6
|
Maintainer-email: dfIndexeddb Developers <dfindexeddb-dev@googlegroups.com>
|
|
@@ -219,6 +219,9 @@ License-File: LICENSE
|
|
|
219
219
|
License-File: AUTHORS
|
|
220
220
|
Requires-Dist: python-snappy ==0.6.1
|
|
221
221
|
Requires-Dist: zstd ==1.5.5.1
|
|
222
|
+
Provides-Extra: plugins
|
|
223
|
+
Requires-Dist: protobuf ; extra == 'plugins'
|
|
224
|
+
Requires-Dist: dfdatetime ; extra == 'plugins'
|
|
222
225
|
|
|
223
226
|
# dfIndexeddb
|
|
224
227
|
|
|
@@ -227,8 +230,7 @@ analysis of IndexedDB and LevelDB files.
|
|
|
227
230
|
|
|
228
231
|
It parses LevelDB, IndexedDB and JavaScript structures from these files without
|
|
229
232
|
requiring native libraries. (Note: only a subset of IndexedDB key types and
|
|
230
|
-
JavaScript types for Safari and Chromium-based browsers are currently supported.
|
|
231
|
-
Firefox is under development).
|
|
233
|
+
JavaScript types for Firefox, Safari and Chromium-based browsers are currently supported).
|
|
232
234
|
|
|
233
235
|
The content of IndexedDB files is dependent on what a web application stores
|
|
234
236
|
locally/offline using the web browser's
|
|
@@ -255,6 +257,12 @@ include:
|
|
|
255
257
|
$ pip install dfindexeddb
|
|
256
258
|
```
|
|
257
259
|
|
|
260
|
+
To also install the dependencies for leveldb/indexeddb plugins, run
|
|
261
|
+
```
|
|
262
|
+
$ pip install 'dfindexeddb[plugins]'
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
|
|
258
266
|
## Installation from source
|
|
259
267
|
|
|
260
268
|
1. [Linux] Install the snappy compression development package
|
|
@@ -273,6 +281,11 @@ include:
|
|
|
273
281
|
$ pip install .
|
|
274
282
|
```
|
|
275
283
|
|
|
284
|
+
To also install the dependencies for leveldb/indexeddb plugins, run
|
|
285
|
+
```
|
|
286
|
+
$ pip install '.[plugins]'
|
|
287
|
+
```
|
|
288
|
+
|
|
276
289
|
## Usage
|
|
277
290
|
|
|
278
291
|
Two CLI tools for parsing IndexedDB/LevelDB files are available after
|
|
@@ -299,6 +312,13 @@ options:
|
|
|
299
312
|
|
|
300
313
|
#### Examples:
|
|
301
314
|
|
|
315
|
+
To parse IndexedDB records from an sqlite file for Firefox and output the
|
|
316
|
+
results as JSON, use the following command:
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
dfindexeddb db -s SOURCE --format firefox -o json
|
|
320
|
+
```
|
|
321
|
+
|
|
302
322
|
To parse IndexedDB records from an sqlite file for Safari and output the
|
|
303
323
|
results as JSON-L, use the following command:
|
|
304
324
|
|
|
@@ -359,7 +379,15 @@ options:
|
|
|
359
379
|
To parse records from a LevelDB folder, use the following command:
|
|
360
380
|
|
|
361
381
|
```
|
|
362
|
-
|
|
382
|
+
dfleveldb db -s SOURCE
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
To parse records from a LevelDB folder, and use the sequence number to
|
|
386
|
+
determine recovered records and output as JSON, use the
|
|
387
|
+
following command:
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
dfleveldb db -s SOURCE --use_sequence_number
|
|
363
391
|
```
|
|
364
392
|
|
|
365
393
|
To parse blocks / physical records/ write batches / internal key records from a
|
|
@@ -383,15 +411,14 @@ following command:
|
|
|
383
411
|
|
|
384
412
|
```
|
|
385
413
|
$ dfleveldb descriptor -s SOURCE [-o {json,jsonl,repr}] [-t {blocks,physical_records,versionedit} | -v]
|
|
386
|
-
|
|
387
|
-
options:
|
|
388
|
-
-h, --help show this help message and exit
|
|
389
|
-
-s SOURCE, --source SOURCE
|
|
390
|
-
The source leveldb file
|
|
391
|
-
-o {json,jsonl,repr}, --output {json,jsonl,repr}
|
|
392
|
-
Output format. Default is json
|
|
393
|
-
-t {blocks,physical_records,versionedit}, --structure_type {blocks,physical_records,versionedit}
|
|
394
|
-
Parses the specified structure. Default is versionedit.
|
|
395
|
-
-v, --version_history
|
|
396
|
-
Parses the leveldb version history.
|
|
397
414
|
```
|
|
415
|
+
|
|
416
|
+
#### Plugins
|
|
417
|
+
|
|
418
|
+
To apply a plugin parser for a leveldb file/folder, add the
|
|
419
|
+
`--plugin [Plugin Name]` argument. Currently, there is support for the
|
|
420
|
+
following artifacts:
|
|
421
|
+
|
|
422
|
+
| Plugin Name | Artifact Name |
|
|
423
|
+
| -------- | ------- |
|
|
424
|
+
| `ChromeNotificationRecord` | Chrome/Chromium Notifications |
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
dfindexeddb/__init__.py,sha256=KPYL9__l8od6_OyDfGRTgaJ6iy_fqIgZ-dS2S-e3Rac,599
|
|
2
|
+
dfindexeddb/errors.py,sha256=PNpwyf_lrPc4TE77oAakX3mu5D_YcP3f80wq8Y1LkvY,749
|
|
3
|
+
dfindexeddb/utils.py,sha256=g-uqQzT_iKM7PPEIuSCNkQG2ltwpnLpRA_dPtrgVzzc,9997
|
|
4
|
+
dfindexeddb/version.py,sha256=UAMCa51yvzp9xG8y5eagDkYAfzVLED94Kp7hTfVBSSI,751
|
|
5
|
+
dfindexeddb/indexeddb/__init__.py,sha256=kExXSVBCTKCD5BZJkdMfUMqGksH-DMJxP2_lI0gq-BE,575
|
|
6
|
+
dfindexeddb/indexeddb/cli.py,sha256=O07_DpVeGtYx2V-jUJF5Oc3OftM2FPLSwNdS45MLCdo,7435
|
|
7
|
+
dfindexeddb/indexeddb/types.py,sha256=cIXmShUbbXJMSbXkmthxGGrpIF9fWr3Ypfl6ckGoSBU,1892
|
|
8
|
+
dfindexeddb/indexeddb/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
dfindexeddb/indexeddb/chromium/__init__.py,sha256=kExXSVBCTKCD5BZJkdMfUMqGksH-DMJxP2_lI0gq-BE,575
|
|
10
|
+
dfindexeddb/indexeddb/chromium/blink.py,sha256=kwhPfzcWOOxYyXUWfV6f4grQwXzS2ABFaNVMIVhol3c,32268
|
|
11
|
+
dfindexeddb/indexeddb/chromium/definitions.py,sha256=1a-AmHVZ95uDB6se_fdarwJR8q0tFMQNh2xrZ2-VxN8,8739
|
|
12
|
+
dfindexeddb/indexeddb/chromium/record.py,sha256=LIuTwwQeQbn6CBXdo0AZZHounOWcnXRg6W082yxmNBo,47578
|
|
13
|
+
dfindexeddb/indexeddb/chromium/v8.py,sha256=tzkKhx0S53HLCNMCls4GCFXrRCjjrgkFSwL3mbNQsjg,22656
|
|
14
|
+
dfindexeddb/indexeddb/firefox/__init__.py,sha256=kExXSVBCTKCD5BZJkdMfUMqGksH-DMJxP2_lI0gq-BE,575
|
|
15
|
+
dfindexeddb/indexeddb/firefox/definitions.py,sha256=xkvlYaaFR2IDQBGJGnrhVIOUce6VuMq-kWXe2CLX3Aw,4306
|
|
16
|
+
dfindexeddb/indexeddb/firefox/gecko.py,sha256=m6-tGHOFnND-XG4C-9o1Atxo4BkHTMhFWCGqW2vFVPk,19423
|
|
17
|
+
dfindexeddb/indexeddb/firefox/record.py,sha256=yB7dYiwzCx1c67Sf6ViMSX51SCrcgU8OBUaVYczqTik,5766
|
|
18
|
+
dfindexeddb/indexeddb/safari/__init__.py,sha256=kExXSVBCTKCD5BZJkdMfUMqGksH-DMJxP2_lI0gq-BE,575
|
|
19
|
+
dfindexeddb/indexeddb/safari/definitions.py,sha256=nW8MmYx9Ob86W4pxm4QD4Xvr5QjoV34-U7wDhm2GIr0,2779
|
|
20
|
+
dfindexeddb/indexeddb/safari/record.py,sha256=bzoMSgpXs2SsEOKHjVh9tkJDZtzGkQByq3G5dK_Yd7Q,8010
|
|
21
|
+
dfindexeddb/indexeddb/safari/webkit.py,sha256=LHaSLOGr74dzGblrqC_RVYD6GCftzGP-p0oWujZ3l1c,22592
|
|
22
|
+
dfindexeddb/leveldb/__init__.py,sha256=KPYL9__l8od6_OyDfGRTgaJ6iy_fqIgZ-dS2S-e3Rac,599
|
|
23
|
+
dfindexeddb/leveldb/cli.py,sha256=e2C94FSP28dh83FWQXD5N44ymUDwkfFeX0Tfk9YLCTo,9913
|
|
24
|
+
dfindexeddb/leveldb/definitions.py,sha256=lPW_kjc47vyoGOoEWfgWvKcpGbN-0h7XXwCeMoFmYKk,1486
|
|
25
|
+
dfindexeddb/leveldb/descriptor.py,sha256=BgWO-sEqT2zhPu9oEplTa8O_szpgU2N4QfDZeroTcx0,12237
|
|
26
|
+
dfindexeddb/leveldb/ldb.py,sha256=mN-M7PLtE_VLZCbCbzRgjkSezbMUhgDjgWgPgIxJ1jM,8087
|
|
27
|
+
dfindexeddb/leveldb/log.py,sha256=ofw0r2f_3Ll5oHzssvp61nmjhIPdt3tmb9UeNiGLHXk,9401
|
|
28
|
+
dfindexeddb/leveldb/record.py,sha256=j7ZnU6VDVcYVpJRGFRb5Sr2edhC3aGp3U0kPNcoZgng,11912
|
|
29
|
+
dfindexeddb/leveldb/utils.py,sha256=RgEEZ7Z35m3CcOUypAiViQSzKjBgSXZ3aeJhQjY3H9w,3748
|
|
30
|
+
dfindexeddb/leveldb/plugins/__init__.py,sha256=RoC6tRkq8FhqIaFs6jwu1fao_qaSvlSfIFxQVjWregI,690
|
|
31
|
+
dfindexeddb/leveldb/plugins/chrome_notifications.py,sha256=-dyb_AJbUPE2wPJg_Y1Ns5CMtg4udi9Fqo5WKh6f3Z4,5354
|
|
32
|
+
dfindexeddb/leveldb/plugins/interface.py,sha256=QlNEvVvU8K9ChE2kblM97cOvXwvmCh9NuSf2b6WwezQ,1257
|
|
33
|
+
dfindexeddb/leveldb/plugins/manager.py,sha256=jisYyks3OQQQUVACoGcWN81UCGQEa537YvYL7v3CiFs,2139
|
|
34
|
+
dfindexeddb/leveldb/plugins/notification_database_data_pb2.py,sha256=DCPZHbyq2szLgrBprOKrJKycKJma8Z_SnAQM6Jx9bZg,4389
|
|
35
|
+
dfindexeddb-20241031.dist-info/AUTHORS,sha256=QbvjbAom57fpEkekkCVFUj0B9KUMGraR510aUMBC-PE,286
|
|
36
|
+
dfindexeddb-20241031.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
37
|
+
dfindexeddb-20241031.dist-info/METADATA,sha256=0Wi-Bd_2GyLC3ssYcDQuPKiFK6s98gOgir-1QuYfTPo,18972
|
|
38
|
+
dfindexeddb-20241031.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
39
|
+
dfindexeddb-20241031.dist-info/entry_points.txt,sha256=WG9YNLZ9lBx4Q9QF6wS4dZdZfADT3Zs4_-MV5TcA0ls,102
|
|
40
|
+
dfindexeddb-20241031.dist-info/top_level.txt,sha256=X9OTaub1c8S_JJ7g-f8JdkhhdiZ4x1j4eni1hdUCwE4,12
|
|
41
|
+
dfindexeddb-20241031.dist-info/RECORD,,
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
dfindexeddb/__init__.py,sha256=KPYL9__l8od6_OyDfGRTgaJ6iy_fqIgZ-dS2S-e3Rac,599
|
|
2
|
-
dfindexeddb/errors.py,sha256=PNpwyf_lrPc4TE77oAakX3mu5D_YcP3f80wq8Y1LkvY,749
|
|
3
|
-
dfindexeddb/utils.py,sha256=pV2blFnMxDwk3kBRK6UVji66ctkYpm6wfH9p0jCC7Nk,8797
|
|
4
|
-
dfindexeddb/version.py,sha256=w3jTfQPKTCKVn_YxB-eYjw2WM5lN7n0625noN-CZErc,751
|
|
5
|
-
dfindexeddb/indexeddb/__init__.py,sha256=kExXSVBCTKCD5BZJkdMfUMqGksH-DMJxP2_lI0gq-BE,575
|
|
6
|
-
dfindexeddb/indexeddb/cli.py,sha256=MsVqxIhZWXoiBEDbysO9gWztsUkZ8cwFMCYmgSZViLw,5980
|
|
7
|
-
dfindexeddb/indexeddb/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
dfindexeddb/indexeddb/chromium/__init__.py,sha256=kExXSVBCTKCD5BZJkdMfUMqGksH-DMJxP2_lI0gq-BE,575
|
|
9
|
-
dfindexeddb/indexeddb/chromium/blink.py,sha256=Sa5BvDkuwXg2IPc4iUDkNA9cbQKpMnXd3bPpNWSPG0I,32122
|
|
10
|
-
dfindexeddb/indexeddb/chromium/definitions.py,sha256=1a-AmHVZ95uDB6se_fdarwJR8q0tFMQNh2xrZ2-VxN8,8739
|
|
11
|
-
dfindexeddb/indexeddb/chromium/record.py,sha256=kdItbJC5Mo6xWW71QylWvvKxtS4yYJ4BGc_ABHaHIiM,47366
|
|
12
|
-
dfindexeddb/indexeddb/chromium/v8.py,sha256=NsbMgA6nRcAfdLg6CFwWadwsDS6TJ95-4MrgphaTuLw,22102
|
|
13
|
-
dfindexeddb/indexeddb/firefox/__init__.py,sha256=kExXSVBCTKCD5BZJkdMfUMqGksH-DMJxP2_lI0gq-BE,575
|
|
14
|
-
dfindexeddb/indexeddb/safari/__init__.py,sha256=kExXSVBCTKCD5BZJkdMfUMqGksH-DMJxP2_lI0gq-BE,575
|
|
15
|
-
dfindexeddb/indexeddb/safari/definitions.py,sha256=nW8MmYx9Ob86W4pxm4QD4Xvr5QjoV34-U7wDhm2GIr0,2779
|
|
16
|
-
dfindexeddb/indexeddb/safari/record.py,sha256=bzoMSgpXs2SsEOKHjVh9tkJDZtzGkQByq3G5dK_Yd7Q,8010
|
|
17
|
-
dfindexeddb/indexeddb/safari/webkit.py,sha256=eQeKXOcVdCaZKM4xekw1d3iLTynXc_e-GCT_x9iSJNs,21595
|
|
18
|
-
dfindexeddb/leveldb/__init__.py,sha256=KPYL9__l8od6_OyDfGRTgaJ6iy_fqIgZ-dS2S-e3Rac,599
|
|
19
|
-
dfindexeddb/leveldb/cli.py,sha256=H1QmbZ2jQ75LqsxPaiOjflljDtYGCSxtsDI3mEf7BNU,7982
|
|
20
|
-
dfindexeddb/leveldb/definitions.py,sha256=lPW_kjc47vyoGOoEWfgWvKcpGbN-0h7XXwCeMoFmYKk,1486
|
|
21
|
-
dfindexeddb/leveldb/descriptor.py,sha256=WR3irG16oIE6VbaP9UPnzOD3KlHR8GYFnoeG6ySJUzU,12211
|
|
22
|
-
dfindexeddb/leveldb/ldb.py,sha256=mN-M7PLtE_VLZCbCbzRgjkSezbMUhgDjgWgPgIxJ1jM,8087
|
|
23
|
-
dfindexeddb/leveldb/log.py,sha256=QeH8oESOPEZUjANGiDRSmXZa2SuoKlPFBJY7SxTV1lg,9209
|
|
24
|
-
dfindexeddb/leveldb/record.py,sha256=_xmeEBXz5nimqteOMAf0zYoi8wDLgG27F4BhLk6N21Q,10747
|
|
25
|
-
dfindexeddb/leveldb/utils.py,sha256=RgEEZ7Z35m3CcOUypAiViQSzKjBgSXZ3aeJhQjY3H9w,3748
|
|
26
|
-
dfindexeddb-20240501.dist-info/AUTHORS,sha256=QbvjbAom57fpEkekkCVFUj0B9KUMGraR510aUMBC-PE,286
|
|
27
|
-
dfindexeddb-20240501.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
28
|
-
dfindexeddb-20240501.dist-info/METADATA,sha256=Ejikzq8iS6BHrSroiBS4A_xR7O4GJnwVsmyoMF25jUM,18498
|
|
29
|
-
dfindexeddb-20240501.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
30
|
-
dfindexeddb-20240501.dist-info/entry_points.txt,sha256=WG9YNLZ9lBx4Q9QF6wS4dZdZfADT3Zs4_-MV5TcA0ls,102
|
|
31
|
-
dfindexeddb-20240501.dist-info/top_level.txt,sha256=X9OTaub1c8S_JJ7g-f8JdkhhdiZ4x1j4eni1hdUCwE4,12
|
|
32
|
-
dfindexeddb-20240501.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|