dissect.database 1.1.dev15__py3-none-any.whl → 1.2.dev2__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.
@@ -2,8 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from dissect.database.ese.btree import BTree
6
- from dissect.database.ese.exception import KeyNotFoundError, NoNeighbourPageError
5
+ from dissect.database.ese.exception import KeyNotFoundError
7
6
  from dissect.database.ese.record import Record
8
7
 
9
8
  if TYPE_CHECKING:
@@ -11,13 +10,14 @@ if TYPE_CHECKING:
11
10
 
12
11
  from typing_extensions import Self
13
12
 
13
+ from dissect.database.ese.ese import ESE
14
14
  from dissect.database.ese.index import Index
15
- from dissect.database.ese.page import Node
15
+ from dissect.database.ese.page import Node, Page
16
16
  from dissect.database.ese.util import RecordValue
17
17
 
18
18
 
19
19
  class Cursor:
20
- """A simple cursor implementation for searching the ESE indexes.
20
+ """A simple cursor implementation for searching the ESE indexes on their records.
21
21
 
22
22
  Args:
23
23
  index: The :class:`~dissect.database.ese.index.Index` to create the cursor for.
@@ -28,10 +28,13 @@ class Cursor:
28
28
  self.table = index.table
29
29
  self.db = index.db
30
30
 
31
- self._primary = BTree(self.db, index.root)
32
- self._secondary = None if index.is_primary else BTree(self.db, self.table.root)
31
+ self._primary = RawCursor(self.db, index.root)
32
+ self._secondary = None if index.is_primary else RawCursor(self.db, self.table.root)
33
33
 
34
34
  def __iter__(self) -> Iterator[Record]:
35
+ if self._primary._page.is_branch:
36
+ self._primary.first()
37
+
35
38
  record = self.record()
36
39
  while record is not None:
37
40
  yield record
@@ -44,9 +47,8 @@ class Cursor:
44
47
  A :class:`~dissect.database.ese.page.Node` object of the current node.
45
48
  """
46
49
  node = self._primary.node()
47
- if self._secondary:
48
- self._secondary.reset()
49
- node = self._secondary.search(node.data.tobytes(), exact=True)
50
+ if self._secondary is not None:
51
+ node = self._secondary.search(node.data.tobytes(), exact=True).node()
50
52
  return node
51
53
 
52
54
  def record(self) -> Record:
@@ -67,30 +69,22 @@ class Cursor:
67
69
  def next(self) -> Record | None:
68
70
  """Move the cursor to the next record and return it.
69
71
 
70
- Can move the cursor to the next page as a side effect.
71
-
72
72
  Returns:
73
73
  A :class:`~dissect.database.ese.record.Record` object of the next record.
74
74
  """
75
- try:
76
- self._primary.next()
77
- except NoNeighbourPageError:
78
- return None
79
- return self.record()
75
+ if self._primary.next():
76
+ return self.record()
77
+ return None
80
78
 
81
79
  def prev(self) -> Record | None:
82
80
  """Move the cursor to the previous node and return it.
83
81
 
84
- Can move the cursor to the previous page as a side effect.
85
-
86
82
  Returns:
87
83
  A :class:`~dissect.database.ese.record.Record` object of the previous record.
88
84
  """
89
- try:
90
- self._primary.prev()
91
- except NoNeighbourPageError:
92
- return None
93
- return self.record()
85
+ if self._primary.prev():
86
+ return self.record()
87
+ return None
94
88
 
95
89
  def make_key(self, *args: RecordValue, **kwargs: RecordValue) -> bytes:
96
90
  """Generate a key for this index from the given values.
@@ -137,7 +131,7 @@ class Cursor:
137
131
  exact: If ``True``, search for an exact match. If ``False``, sets the cursor on the
138
132
  next record that is greater than or equal to the key.
139
133
  """
140
- self._primary.search(key, exact)
134
+ self._primary.search(key, exact=exact)
141
135
  return self.record()
142
136
 
143
137
  def seek(self, *args: RecordValue, **kwargs: RecordValue) -> Self:
@@ -189,18 +183,6 @@ class Cursor:
189
183
  return
190
184
 
191
185
  current_key = self._primary.node().key
192
-
193
- # Check if we need to move the cursor back to find the first record
194
- while True:
195
- if current_key != self._primary.node().key:
196
- self._primary.next()
197
- break
198
-
199
- try:
200
- self._primary.prev()
201
- except NoNeighbourPageError:
202
- break
203
-
204
186
  while True:
205
187
  # Entries with the same indexed columns are guaranteed to be adjacent
206
188
  if current_key != self._primary.node().key:
@@ -224,7 +206,228 @@ class Cursor:
224
206
  else:
225
207
  yield record
226
208
 
227
- try:
228
- self._primary.next()
229
- except NoNeighbourPageError:
209
+ if not self._primary.next():
230
210
  break
211
+
212
+
213
+ class RawCursor:
214
+ """A simple cursor implementation for searching the ESE B+Trees on their raw nodes.
215
+
216
+ Args:
217
+ db: An instance of :class:`~dissect.database.ese.ese.ESE`.
218
+ root: The page to open the raw cursor on.
219
+ """
220
+
221
+ def __init__(self, db: ESE, root: Page | int):
222
+ self.db = db
223
+ self.root = db.page(root) if isinstance(root, int) else root
224
+
225
+ self._page = self.root
226
+ self._idx = 0
227
+
228
+ # Stack of (page, idx, stack[:]) for traversing back up the tree when doing in-order traversal
229
+ self._stack = []
230
+
231
+ @property
232
+ def state(self) -> tuple[Page, int, list[tuple[Page, int]]]:
233
+ """Get the current cursor state."""
234
+ return self._page, self._idx, self._stack[:]
235
+
236
+ @state.setter
237
+ def state(self, value: tuple[Page, int, list[tuple[Page, int]]]) -> None:
238
+ """Set the current cursor state."""
239
+ self._page, self._idx, self._stack = value[0], value[1], value[2][:]
240
+
241
+ def reset(self) -> Self:
242
+ """Reset the cursor to the root of the B+Tree."""
243
+ self._page = self.root
244
+ self._idx = 0
245
+ self._stack = []
246
+
247
+ return self
248
+
249
+ def node(self) -> Node:
250
+ """Return the node the cursor is currently on.
251
+
252
+ Returns:
253
+ A :class:`~dissect.database.ese.page.Node` object of the current node.
254
+ """
255
+ return self._page.node(self._idx)
256
+
257
+ def first(self) -> bool:
258
+ """Move the cursor to the first leaf node in the B+Tree."""
259
+ self.reset()
260
+ while self._page.is_branch and self._page.node_count > 0:
261
+ self.push()
262
+
263
+ return self._page.node_count != 0
264
+
265
+ def last(self) -> bool:
266
+ """Move the cursor to the last leaf node in the B+Tree."""
267
+ self.reset()
268
+ while self._page.is_branch and self._page.node_count > 0:
269
+ self._idx = self._page.node_count - 1
270
+ self.push()
271
+
272
+ self._idx = self._page.node_count - 1
273
+ return self._page.node_count != 0
274
+
275
+ def next(self) -> bool:
276
+ """Move the cursor to the next leaf node."""
277
+ if self._page.is_branch:
278
+ # Treat as if we were at the first node
279
+ self.first()
280
+ return self._page.node_count != 0
281
+
282
+ if self._idx + 1 < self._page.node_count:
283
+ self._idx += 1
284
+ elif self._stack:
285
+ # End of current page, traverse to the next leaf page
286
+
287
+ # First pop until we find a page with unvisited nodes
288
+ while self._idx + 1 >= self._page.node_count:
289
+ if not self._stack:
290
+ return False
291
+ self.pop()
292
+
293
+ self._idx += 1
294
+
295
+ # Then push down to the next page
296
+ while self._page.is_branch:
297
+ self.push()
298
+ else:
299
+ return False
300
+
301
+ return True
302
+
303
+ def prev(self) -> bool:
304
+ """Move the cursor to the previous leaf node."""
305
+ if self._page.is_branch:
306
+ # Treat as if we were at the last node
307
+ self.last()
308
+ return self._page.node_count != 0
309
+
310
+ if self._idx - 1 >= 0:
311
+ self._idx -= 1
312
+ elif self._stack:
313
+ # Start of current page, traverse to the previous leaf page
314
+
315
+ # First pop until we find a page with unvisited nodes
316
+ while self._idx - 1 < 0:
317
+ if not self._stack:
318
+ # Start of B+Tree reached
319
+ return False
320
+ self.pop()
321
+
322
+ self._idx -= 1
323
+
324
+ # Then push down to the rightmost leaf
325
+ while self._page.is_branch:
326
+ self._idx = self._page.node_count - 1
327
+ self.push()
328
+ else:
329
+ # Start of B+Tree reached
330
+ return False
331
+
332
+ return True
333
+
334
+ def push(self) -> Self:
335
+ """Push down to the child page at the current index."""
336
+ child_page = self.db.page(self._page.node(self._idx).child)
337
+
338
+ self._stack.append((self._page, self._idx))
339
+ self._page = child_page
340
+ self._idx = 0
341
+
342
+ return self
343
+
344
+ def pop(self) -> Self:
345
+ """Pop back to the parent page."""
346
+ if not self._stack:
347
+ raise IndexError("Cannot pop from an empty stack")
348
+
349
+ self._page, self._idx = self._stack.pop()
350
+
351
+ return self
352
+
353
+ def walk(self) -> Iterator[Node]:
354
+ """Walk the B+Tree in order, yielding nodes."""
355
+ if self.first():
356
+ yield self.node()
357
+
358
+ while self.next():
359
+ yield self.node()
360
+
361
+ def search(self, key: bytes, *, exact: bool = True) -> Self:
362
+ """Search the tree for the given ``key``.
363
+
364
+ Moves the cursor to the matching node, or on the last node that is less than the requested key.
365
+
366
+ Args:
367
+ key: The key to search for.
368
+ exact: Whether to only return successfully on an exact match.
369
+
370
+ Raises:
371
+ KeyNotFoundError: If an ``exact`` match was requested but not found.
372
+ """
373
+ self.reset()
374
+
375
+ while self._page.is_branch:
376
+ self._idx = find_node(self._page, key, exact=False)
377
+ self.push()
378
+
379
+ self._idx = find_node(self._page, key, exact=exact)
380
+ if self._idx >= self._page.node_count or self._idx == -1:
381
+ raise KeyNotFoundError(f"Key not found: {key!r}")
382
+
383
+ return self
384
+
385
+
386
+ def find_node(page: Page, key: bytes, *, exact: bool) -> int:
387
+ """Search a page for a node matching the given key.
388
+
389
+ Referencing Extensible-Storage-Engine source, they bail out early if they find an exact match.
390
+ However, we prefer to always find the _first_ node that is greater than or equal to the key,
391
+ so we can handle cases where there are duplicate index keys. This is important for "range" searches
392
+ where we want to find all keys matching a certain prefix, and not end up somewhere in the middle of the range.
393
+
394
+ Args:
395
+ page: The page to search.
396
+ key: The key to search.
397
+ exact: Whether to only return successfully on an exact match.
398
+
399
+ Returns:
400
+ The node number of the first node that's greater than or equal to the key, or the last node on the page if
401
+ the key is larger than all nodes. If ``exact`` is ``True`` and an exact match is not found, returns -1.
402
+ """
403
+ if page.node_count == 0:
404
+ return -1
405
+
406
+ lo, hi = 0, page.node_count - 1
407
+
408
+ node = None
409
+ while lo < hi:
410
+ mid = (lo + hi) // 2
411
+ node = page.node(mid)
412
+
413
+ # It turns out that the way BTree keys are compared matches 1:1 with how Python compares bytes
414
+ # First compare data, then length
415
+ if key > node.key:
416
+ lo = mid + 1
417
+ else:
418
+ hi = mid
419
+
420
+ # Final comparison on the last node
421
+ node = page.node(lo)
422
+
423
+ if key == node.key:
424
+ if page.is_branch:
425
+ # If there's an exact match on a key on a branch page, the actual leaf nodes are in the next branch
426
+ # Page keys for branch pages appear to be non-inclusive upper bounds
427
+ lo = min(lo + 1, page.node_count - 1)
428
+
429
+ # key != node.key
430
+ elif exact:
431
+ return -1
432
+
433
+ return lo
@@ -114,10 +114,17 @@ class DataTable:
114
114
  yield (obj := stack.pop())
115
115
  stack.extend(obj.children())
116
116
 
117
- def iter(self) -> Iterator[Object]:
118
- """Iterate over all objects in the NTDS database."""
117
+ def iter(self, raw: bool = False) -> Iterator[Object]:
118
+ """Iterate over all objects in the NTDS database.
119
+
120
+ Args:
121
+ raw: Whether to return base :class:`Object` instances without upcasting to more specific types
122
+ based on the objectClass.
123
+ """
124
+ from_record = Object if raw else Object.from_record
125
+
119
126
  for record in self.table.records():
120
- yield Object.from_record(self.db, record)
127
+ yield from_record(self.db, record)
121
128
 
122
129
  def get(self, dnt: int) -> Object:
123
130
  """Retrieve an object by its Directory Number Tag (DNT) value.
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import fnmatch
3
4
  import logging
4
- from typing import TYPE_CHECKING, Any
5
+ import re
6
+ from typing import TYPE_CHECKING
5
7
 
6
8
  from dissect.util.ldap import LogicalOperator, SearchFilter
7
9
 
@@ -33,30 +35,25 @@ class Query:
33
35
  """
34
36
  yield from self._process_query(self._filter)
35
37
 
36
- def _process_query(self, filter: SearchFilter, records: list[Record] | None = None) -> Iterator[Record]:
38
+ def _process_query(self, filter: SearchFilter, records: Iterator[Record] | None = None) -> Iterator[Record]:
37
39
  """Process LDAP query recursively, handling nested logical operations.
38
40
 
39
41
  Args:
40
42
  filter: The LDAP search filter to process.
41
- records: Optional list of records to filter instead of querying the database.
43
+ records: Optional iterable of records to filter instead of querying the database.
42
44
 
43
45
  Yields:
44
46
  Records matching the search filter.
45
47
  """
46
- if not filter.is_nested():
47
- if records is None:
48
- try:
49
- yield from self._query_database(filter)
50
- except IndexError:
51
- log.debug("No records found for filter: %s", filter)
52
- else:
53
- yield from self._filter_records(filter, records)
54
- return
55
-
56
- if filter.operator == LogicalOperator.AND:
57
- yield from self._process_and_operation(filter, records)
58
- elif filter.operator == LogicalOperator.OR:
59
- yield from self._process_or_operation(filter, records)
48
+ if filter.is_nested():
49
+ if filter.operator == LogicalOperator.AND:
50
+ yield from self._process_and_operation(filter, records)
51
+ elif filter.operator == LogicalOperator.OR:
52
+ yield from self._process_or_operation(filter, records)
53
+ elif records is not None:
54
+ yield from self._filter_records(filter, records)
55
+ else:
56
+ yield from self._query_database(filter)
60
57
 
61
58
  def _query_database(self, filter: SearchFilter) -> Iterator[Record]:
62
59
  """Execute a simple LDAP filter against the database.
@@ -73,25 +70,33 @@ class Query:
73
70
 
74
71
  # Get the database index for this attribute
75
72
  if (index := self.db.data.table.find_index([schema.column])) is None:
76
- raise ValueError(f"Index for attribute {schema.column!r} not found in the NTDS database")
77
-
78
- if "*" in filter.value:
79
- # Handle wildcard searches differently
80
- if filter.value.endswith("*"):
81
- yield from _process_wildcard_tail(index, filter.value)
82
- else:
83
- raise NotImplementedError("Wildcards in the middle or start of the value are not yet supported")
73
+ # If no index is available, we have to scan the entire table
74
+ log.debug("No index found for attribute %s (%s), scanning entire table", filter.attribute, schema.column)
75
+ yield from self._filter_records(filter, self.db.data.table.records())
84
76
  else:
85
- # Exact match query
86
- encoded_value = encode_value(self.db, schema, filter.value)
87
- yield from index.cursor().find_all(**{schema.column: encoded_value})
77
+ if "*" in filter.value:
78
+ # Handle wildcard searches differently
79
+ if filter.value.endswith("*"):
80
+ yield from _process_wildcard_tail(index, filter.value)
81
+ else:
82
+ # For more complex wildcard patterns, we need to scan the index and apply the filter
83
+ log.debug(
84
+ "Complex wildcard search for attribute %s (%s), scanning entire index",
85
+ filter.attribute,
86
+ schema.column,
87
+ )
88
+ yield from self._filter_records(filter, index.cursor())
89
+ else:
90
+ # Exact match query
91
+ encoded_value = encode_value(self.db, schema, filter.value)
92
+ yield from index.cursor().find_all(**{schema.column: encoded_value})
88
93
 
89
- def _process_and_operation(self, filter: SearchFilter, records: list[Record] | None) -> Iterator[Record]:
94
+ def _process_and_operation(self, filter: SearchFilter, records: Iterator[Record] | None) -> Iterator[Record]:
90
95
  """Process AND logical operation.
91
96
 
92
97
  Args:
93
98
  filter: The LDAP search filter with AND operator.
94
- records: Optional list of records to filter.
99
+ records: Optional iterable of records to filter.
95
100
 
96
101
  Yields:
97
102
  Records matching all conditions in the AND operation.
@@ -102,19 +107,19 @@ class Query:
102
107
  else:
103
108
  # Use the first child as base query, then filter with remaining children
104
109
  base_query, *remaining_children = filter.children
105
- records_to_process = list(self._process_query(base_query))
110
+ records_to_process = self._process_query(base_query)
106
111
  children_to_check = remaining_children
107
112
 
108
113
  for record in records_to_process:
109
114
  if all(any(self._process_query(child, records=[record])) for child in children_to_check):
110
115
  yield record
111
116
 
112
- def _process_or_operation(self, filter: SearchFilter, records: list[Record] | None) -> Iterator[Record]:
117
+ def _process_or_operation(self, filter: SearchFilter, records: Iterator[Record] | None) -> Iterator[Record]:
113
118
  """Process OR logical operation.
114
119
 
115
120
  Args:
116
121
  filter: The LDAP search filter with OR operator.
117
- records: Optional list of records to filter.
122
+ records: Optional iterable of records to filter.
118
123
 
119
124
  Yields:
120
125
  Records matching any condition in the OR operation.
@@ -122,12 +127,12 @@ class Query:
122
127
  for child in filter.children:
123
128
  yield from self._process_query(child, records=records)
124
129
 
125
- def _filter_records(self, filter: SearchFilter, records: list[Record]) -> Iterator[Record]:
126
- """Filter a list of records against a simple LDAP filter.
130
+ def _filter_records(self, filter: SearchFilter, records: Iterator[Record]) -> Iterator[Record]:
131
+ """Filter an iterable of records against a simple LDAP filter.
127
132
 
128
133
  Args:
129
134
  filter: The LDAP search filter to apply.
130
- records: The list of records to filter.
135
+ records: The iterable of records to filter.
131
136
 
132
137
  Yields:
133
138
  Records that match the filter criteria.
@@ -136,14 +141,26 @@ class Query:
136
141
  return
137
142
 
138
143
  encoded_value = encode_value(self.db, schema, filter.value)
144
+ re_encoded_value = None
139
145
 
140
- has_wildcard = "*" in filter.value
141
- wildcard_prefix = filter.value.replace("*", "").lower() if has_wildcard else None
146
+ has_wildcard = "*" in filter.value and isinstance(encoded_value, str)
147
+ if has_wildcard:
148
+ re_encoded_value = re.compile(fnmatch.translate(encoded_value), re.IGNORECASE)
142
149
 
143
150
  for record in records:
144
151
  record_value = record.get(schema.column)
145
152
 
146
- if _value_matches_filter(record_value, encoded_value, has_wildcard, wildcard_prefix):
153
+ if isinstance(record_value, list):
154
+ # Currently assume that we can only search for single values, not lists
155
+ if has_wildcard and record_value and isinstance(record_value[0], str):
156
+ if any(re_encoded_value.match(rv) for rv in record_value):
157
+ yield record
158
+ elif encoded_value in record_value:
159
+ yield record
160
+
161
+ elif (
162
+ has_wildcard and isinstance(record_value, str) and re_encoded_value.match(record_value)
163
+ ) or record_value == encoded_value:
147
164
  yield record
148
165
 
149
166
 
@@ -174,26 +191,6 @@ def _process_wildcard_tail(index: Index, filter_value: str) -> Iterator[Record]:
174
191
  record = cursor.next()
175
192
 
176
193
 
177
- def _value_matches_filter(
178
- record_value: Any, encoded_value: Any, has_wildcard: bool, wildcard_prefix: str | None
179
- ) -> bool:
180
- """Return whether a record value matches the filter criteria.
181
-
182
- Args:
183
- record_value: The value from the database record.
184
- encoded_value: The encoded filter value to match against.
185
- has_wildcard: Whether the filter contains wildcard characters.
186
- wildcard_prefix: The prefix to match for wildcard searches.
187
- """
188
- if isinstance(record_value, list):
189
- return encoded_value in record_value
190
-
191
- if has_wildcard and wildcard_prefix and isinstance(record_value, str):
192
- return record_value.lower().startswith(wildcard_prefix)
193
-
194
- return encoded_value == record_value
195
-
196
-
197
194
  def _increment_last_char(value: str) -> str:
198
195
  """Increment the last character in a string to find the next lexicographically sortable key.
199
196
 
@@ -49,6 +49,9 @@ class Page:
49
49
  self._node_cls = LeafNode if self.is_leaf else BranchNode
50
50
  self._node_cache = {}
51
51
 
52
+ def __repr__(self) -> str:
53
+ return f"<Page num={self.num:d} flags={self.flags.name} nodes={self.node_count}>"
54
+
52
55
  @cached_property
53
56
  def is_small_page(self) -> bool:
54
57
  return self.db.has_small_pages
@@ -123,7 +126,7 @@ class Page:
123
126
  IndexError: If the node number is out of bounds.
124
127
  """
125
128
  if num < 0 or num > self.node_count - 1:
126
- raise IndexError(f"Node number exceeds boundaries: 0-{self.node_count - 1}")
129
+ raise IndexError(f"Node number exceeds boundaries 0-{self.node_count - 1}: {num}")
127
130
 
128
131
  if num not in self._node_cache:
129
132
  self._node_cache[num] = self._node_cls(self.tag(num + 1))
@@ -161,9 +164,6 @@ class Page:
161
164
  if self.is_root and leaf and leaf.tag.page.next_page:
162
165
  yield from db.page(leaf.tag.page.next_page).iter_leaf_nodes()
163
166
 
164
- def __repr__(self) -> str:
165
- return f"<Page num={self.num:d}>"
166
-
167
167
 
168
168
  class Tag:
169
169
  """A tag is the "physical" data entry of a page.
@@ -5,14 +5,13 @@ from functools import cached_property
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from dissect.database.ese import compression
8
- from dissect.database.ese.btree import BTree
9
8
  from dissect.database.ese.c_ese import (
10
9
  CODEPAGE,
11
10
  FIELDFLAG,
12
11
  SYSOBJ,
13
12
  JET_coltyp,
14
13
  )
15
- from dissect.database.ese.exception import NoNeighbourPageError
14
+ from dissect.database.ese.cursor import RawCursor
16
15
  from dissect.database.ese.index import Index
17
16
  from dissect.database.ese.record import Record
18
17
  from dissect.database.ese.util import COLUMN_TYPE_MAP, ColumnType, RecordValue
@@ -189,19 +188,16 @@ class Table:
189
188
  key: The lookup key for the long value.
190
189
  """
191
190
  rkey = key[::-1]
192
- btree = BTree(self.db, self.lv_page)
193
- header = btree.search(rkey)
191
+ cursor = RawCursor(self.db, self.lv_page)
192
+ header = cursor.search(rkey).node()
194
193
 
195
194
  _, size = struct.unpack("<2I", header.data)
196
195
  chunks = []
197
196
  chunk_offsets = []
198
197
 
199
- while True:
200
- try:
201
- node = btree.next()
202
- if not node.key.startswith(rkey):
203
- break
204
- except NoNeighbourPageError:
198
+ while cursor.next():
199
+ node = cursor.node()
200
+ if not node.key.startswith(rkey):
205
201
  break
206
202
 
207
203
  chunks.append(node.data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.database
3
- Version: 1.1.dev15
3
+ Version: 1.2.dev2
4
4
  Summary: A Dissect module implementing parsers for various database formats, including Berkeley DB, Microsofts Extensible Storage Engine (ESE) and SQLite3
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: Apache-2.0
@@ -22,13 +22,13 @@ Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  License-File: COPYRIGHT
24
24
  Requires-Dist: dissect.cstruct<5,>=4
25
- Requires-Dist: dissect.util<4,>=3.24.dev1
25
+ Requires-Dist: dissect.util<4,>=3.24
26
26
  Provides-Extra: full
27
27
  Requires-Dist: pycryptodome; extra == "full"
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: dissect.database[full]; extra == "dev"
30
30
  Requires-Dist: dissect.cstruct<5.0.dev,>=4.0.dev; extra == "dev"
31
- Requires-Dist: dissect.util<4.0.dev,>=3.24.dev; extra == "dev"
31
+ Requires-Dist: dissect.util<4.0.dev,>=3.24; extra == "dev"
32
32
  Dynamic: license-file
33
33
 
34
34
  # dissect.database
@@ -9,19 +9,18 @@ dissect/database/bsd/tools/c_rpm.py,sha256=yI9JIerMRPCsO-QZRXzFmY4sAo1fvdwYvLL0S
9
9
  dissect/database/bsd/tools/c_rpm.pyi,sha256=a22dZ8eMGPnj9rsKWjT7ol82Q6OmEcTgWsLNBbBJ3IM,15116
10
10
  dissect/database/bsd/tools/rpm.py,sha256=Wpmhfr4w4_TTFpp94o1RCcjPkwrHxyebIUz-P03r3OE,7699
11
11
  dissect/database/ese/__init__.py,sha256=aF5V41QUz6KMUtzBQ1Mak3PlK-TbidbPxRNRnngNtLc,559
12
- dissect/database/ese/btree.py,sha256=HkunjHheh_rd71i3EckksSoiJgSQO19DtvtDfY2KVg8,5491
13
12
  dissect/database/ese/c_ese.py,sha256=kcn2GHTQ_fijY1CEX4gtNAUeTvu5feR-ya4C2_BzTpk,24643
14
13
  dissect/database/ese/c_ese.pyi,sha256=v-h3If5kUxS8HN8G23m6AHbIQUoGHQ2XZih4zurxWTM,18874
15
14
  dissect/database/ese/compression.py,sha256=lLrriyvVjUuDEibUl_ECYVfvqmeU9ijKIaBnx38m98Q,2027
16
- dissect/database/ese/cursor.py,sha256=hdv4WlIbytmCaAqhlyH61gUxa3fx35tSXmz9XyvIkx0,7794
15
+ dissect/database/ese/cursor.py,sha256=ju-mUewhW22bVW0MMdsaKolKCiKnztRcyU0nJw5bNOg,14346
17
16
  dissect/database/ese/ese.py,sha256=cFW2KYXoZ6ca6S-XzziwHEjXN22aP9VWtG2EaIgssFI,3500
18
17
  dissect/database/ese/exception.py,sha256=09xbltclWIBsGbk8Ry8RlKG-OwGSQvyLKsgj0d98T0k,210
19
18
  dissect/database/ese/index.py,sha256=dYngAWmmX7X8lkP2_q3vgBm1woXYfwtTIOsWORmM2Q0,9859
20
19
  dissect/database/ese/lcmapstring.py,sha256=ni_S4xXGCsJg9AIRVTyVWK0JoZbVzL8ocZ1eacnl9cQ,7004
21
- dissect/database/ese/page.py,sha256=3YWSPhlJqAZHk8Cauqy4oq56RDK777ZwgDFI5RyBewM,8271
20
+ dissect/database/ese/page.py,sha256=TbR_pGBKCaYag1SBAYtcQp8EnGOw3QRp3tLMNeuVdCM,8325
22
21
  dissect/database/ese/record.py,sha256=DKBwkyuszsuUaBgu3iD41cidmw4GEpaoNNKPhI0Tnxg,19723
23
22
  dissect/database/ese/sorting_table.py,sha256=NsBchm4-XxyDKZrdt2M_wMuGELUQed5osKbv1RxmIbk,819383
24
- dissect/database/ese/table.py,sha256=dPb_eQuk72dPLS3IcVSl2glFg1S74v8XWYIc3CRjHs8,13439
23
+ dissect/database/ese/table.py,sha256=kIgUp2VuUOO_irmpGpP9uepgYdmoL2agfeClnYNpk0Q,13311
25
24
  dissect/database/ese/util.py,sha256=5ZWurLGHahG4LL4iXpYCK9kbMLr53dVh18f1Ox9YHlA,3044
26
25
  dissect/database/ese/ntds/__init__.py,sha256=Fu6_i_7fAlDfJoOgbpsVlPxM4CXpDCNtXmgzNzkmesY,264
27
26
  dissect/database/ese/ntds/c_ds.py,sha256=ZaWdEcpJPJrRjiJFhi1s5KlOIVj0yoix4XAh-D31uLU,3055
@@ -30,10 +29,10 @@ dissect/database/ese/ntds/c_pek.py,sha256=YU_rRpOEf8uB8OE6bGb25KIGm9e-yrn0tTItRC
30
29
  dissect/database/ese/ntds/c_pek.pyi,sha256=lHfhvHWABT1vLmknu66phOXsylgwzGIWVdbqDIZU5Qw,4683
31
30
  dissect/database/ese/ntds/c_sd.py,sha256=DYICZsWCBzj0OtvhO6vhzzVjK9YP6tzBCsgKgr0pc-k,4294
32
31
  dissect/database/ese/ntds/c_sd.pyi,sha256=7717Y0EBVu37Liu26rqsDWkLIdSCqWn9KK9svtniLqY,5279
33
- dissect/database/ese/ntds/database.py,sha256=LCm9OLgqS6MUzQu97wXVwJODu8MVH147Sq94bomb0s8,15942
32
+ dissect/database/ese/ntds/database.py,sha256=rKHdbQObLp9vSV2gquGZ_RBnIY5Y-2hACIe_atTvxbY,16189
34
33
  dissect/database/ese/ntds/ntds.py,sha256=UKJnfHrnqEoHnO_HS-ircUtjkjpO9vmLmLjRMW9u6Tk,3548
35
34
  dissect/database/ese/ntds/pek.py,sha256=BEmxO175T8QkGVvFQLN9MI9uDCcK4jztuZetbwbbYqU,4154
36
- dissect/database/ese/ntds/query.py,sha256=DkgzivfVwg7696Egeb-5g-0lirdO7qkFb71SAqZjj4Q,7709
35
+ dissect/database/ese/ntds/query.py,sha256=orTuXH5jXYUVUVL6PtD9rOcdfCKHjOS-D8RzUMEl0sk,7972
37
36
  dissect/database/ese/ntds/schema.py,sha256=EbJYKvixpw7rUZgUxxNFjCvzYLufkwNVVGWI-qSGT1A,16660
38
37
  dissect/database/ese/ntds/sd.py,sha256=Y-oYnJPcLMDB_4X8TLEGtt-n_nC4HLA0WqIS8qYAwAs,5995
39
38
  dissect/database/ese/ntds/util.py,sha256=mx_b-_mIINR1Mn01rH7bR4CeY0gl0gnkD2gjeMeiAU8,18348
@@ -159,10 +158,10 @@ dissect/database/sqlite3/encryption/__init__.py,sha256=kJdFWXD9Z_O_QipC-_A9dlVfR
159
158
  dissect/database/sqlite3/encryption/sqlcipher/__init__.py,sha256=kJdFWXD9Z_O_QipC-_A9dlVfR6AOPSOoT8WBhpFbSsE,238
160
159
  dissect/database/sqlite3/encryption/sqlcipher/exception.py,sha256=GKNtzcnAKlWkvjLluruA8LfzCwjRRWubibbH8WM9l2o,121
161
160
  dissect/database/sqlite3/encryption/sqlcipher/sqlcipher.py,sha256=YK6pTzbRl_c7ura5nY_vUAdM28s7KhNj6LMxCR-AkAY,11220
162
- dissect_database-1.1.dev15.dist-info/licenses/COPYRIGHT,sha256=pFH-OBYz6Xj23UB0Odz5IhoTR8nsTbJQNlCRV_wMaiE,317
163
- dissect_database-1.1.dev15.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
164
- dissect_database-1.1.dev15.dist-info/METADATA,sha256=UCK_HKbTdzS4zEI1KedpjriiUxclKQHqjsX1XlExGBM,5550
165
- dissect_database-1.1.dev15.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
166
- dissect_database-1.1.dev15.dist-info/entry_points.txt,sha256=ZVVKj3Nzjkgm1kBXGWyGNVUJzTbmVgivv9lgFcuLkpk,343
167
- dissect_database-1.1.dev15.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
168
- dissect_database-1.1.dev15.dist-info/RECORD,,
161
+ dissect_database-1.2.dev2.dist-info/licenses/COPYRIGHT,sha256=pFH-OBYz6Xj23UB0Odz5IhoTR8nsTbJQNlCRV_wMaiE,317
162
+ dissect_database-1.2.dev2.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
163
+ dissect_database-1.2.dev2.dist-info/METADATA,sha256=tv1ywwLkuTmIaWFVJCymTIPYDDeUe7AqXi_H-LjFW5k,5540
164
+ dissect_database-1.2.dev2.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
165
+ dissect_database-1.2.dev2.dist-info/entry_points.txt,sha256=ZVVKj3Nzjkgm1kBXGWyGNVUJzTbmVgivv9lgFcuLkpk,343
166
+ dissect_database-1.2.dev2.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
167
+ dissect_database-1.2.dev2.dist-info/RECORD,,
@@ -1,177 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from dissect.database.ese.exception import KeyNotFoundError, NoNeighbourPageError
6
-
7
- if TYPE_CHECKING:
8
- from dissect.database.ese.ese import ESE
9
- from dissect.database.ese.page import Node, Page
10
-
11
-
12
- class BTree:
13
- """A simple implementation for searching the ESE B+Trees.
14
-
15
- This is a stateful interactive class that moves an internal cursor to a position within the BTree.
16
-
17
- Args:
18
- db: An instance of :class:`~dissect.database.ese.ese.ESE`.
19
- page: The page to open the :class:`BTree` on.
20
- """
21
-
22
- def __init__(self, db: ESE, root: int | Page):
23
- self.db = db
24
-
25
- if isinstance(root, int):
26
- page_num = root
27
- root = db.page(page_num)
28
- else:
29
- page_num = root.num
30
-
31
- self.root = root
32
-
33
- self._page = root
34
- self._page_num = page_num
35
- self._node_num = 0
36
-
37
- def reset(self) -> None:
38
- """Reset the internal state to the root of the BTree."""
39
- self._page = self.root
40
- self._page_num = self._page.num
41
- self._node_num = 0
42
-
43
- def node(self) -> Node:
44
- """Return the node the BTree is currently on.
45
-
46
- Returns:
47
- A :class:`~dissect.database.ese.page.Node` object of the current node.
48
- """
49
- return self._page.node(self._node_num)
50
-
51
- def next(self) -> Node:
52
- """Move the BTree to the next node and return it.
53
-
54
- Can move the BTree to the next page as a side effect.
55
-
56
- Returns:
57
- A :class:`~dissect.database.ese.page.Node` object of the next node.
58
- """
59
- if self._node_num + 1 > self._page.node_count - 1:
60
- self.next_page()
61
- else:
62
- self._node_num += 1
63
-
64
- return self.node()
65
-
66
- def next_page(self) -> None:
67
- """Move the BTree to the next page in the tree.
68
-
69
- Raises:
70
- NoNeighbourPageError: If the current page has no next page.
71
- """
72
- if self._page.next_page:
73
- self._page = self.db.page(self._page.next_page)
74
- self._node_num = 0
75
- else:
76
- raise NoNeighbourPageError(f"{self._page} has no next page")
77
-
78
- def prev(self) -> Node:
79
- """Move the BTree to the previous node and return it.
80
-
81
- Can move the BTree to the previous page as a side effect.
82
-
83
- Returns:
84
- A :class:`~dissect.database.ese.page.Node` object of the previous node.
85
- """
86
- if self._node_num - 1 < 0:
87
- self.prev_page()
88
- else:
89
- self._node_num -= 1
90
-
91
- return self.node()
92
-
93
- def prev_page(self) -> None:
94
- """Move the BTree to the previous page in the tree.
95
-
96
- Raises:
97
- NoNeighbourPageError: If the current page has no previous page.
98
- """
99
- if self._page.previous_page:
100
- self._page = self.db.page(self._page.previous_page)
101
- self._node_num = self._page.node_count - 1
102
- else:
103
- raise NoNeighbourPageError(f"{self._page} has no previous page")
104
-
105
- def search(self, key: bytes, exact: bool = True) -> Node:
106
- """Search the tree for the given ``key``.
107
-
108
- Moves the BTree to the matching node, or on the last node that is less than the requested key.
109
-
110
- Args:
111
- key: The key to search for.
112
- exact: Whether to only return successfully on an exact match.
113
-
114
- Raises:
115
- KeyNotFoundError: If an ``exact`` match was requested but not found.
116
- """
117
- page = self._page
118
- while True:
119
- num = find_node(page, key)
120
- node = page.node(num)
121
-
122
- if page.is_branch:
123
- page = self.db.page(node.child)
124
- else:
125
- self._page = page
126
- self._page_num = page.num
127
- self._node_num = node.num
128
- break
129
-
130
- if exact and key != node.key:
131
- raise KeyNotFoundError(f"Can't find key: {key}")
132
-
133
- return self.node()
134
-
135
-
136
- def find_node(page: Page, key: bytes) -> int:
137
- """Search a page for a node matching ``key``.
138
-
139
- Referencing Extensible-Storage-Engine source, they bail out early if they find an exact match.
140
- However, we prefer to always find the _first_ node that is greater than or equal to the key,
141
- so we can handle cases where there are duplicate index keys. This is important for "range" searches
142
- where we want to find all keys matching a certain prefix, and not end up somewhere in the middle of the range.
143
-
144
- Args:
145
- page: The page to search.
146
- key: The key to search.
147
-
148
- Returns:
149
- The node number of the first node that's greater than or equal to the key.
150
- """
151
- lo, hi = 0, page.node_count - 1
152
- res = 0
153
-
154
- node = None
155
- while lo < hi:
156
- mid = (lo + hi) // 2
157
- node = page.node(mid)
158
-
159
- # It turns out that the way BTree keys are compared matches 1:1 with how Python compares bytes
160
- # First compare data, then length
161
- res = (key < node.key) - (key > node.key)
162
-
163
- if res < 0:
164
- lo = mid + 1
165
- else:
166
- hi = mid
167
-
168
- # Final comparison on the last node
169
- node = page.node(lo)
170
- res = (key < node.key) - (key > node.key)
171
-
172
- if page.is_branch and res == 0:
173
- # If there's an exact match on a key on a branch page, the actual leaf nodes are in the next branch
174
- # Page keys for branch pages appear to be non-inclusive upper bounds
175
- lo = min(lo + 1, page.node_count - 1)
176
-
177
- return lo