dissect.ntfs 3.13.dev1__tar.gz → 3.14.dev1__tar.gz
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.
- {dissect_ntfs-3.13.dev1/dissect.ntfs.egg-info → dissect_ntfs-3.14.dev1}/PKG-INFO +2 -2
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/attr.py +33 -33
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/c_ntfs.py +3 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/index.py +12 -13
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/mft.py +26 -23
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/ntfs.py +13 -8
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/secure.py +10 -7
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/stream.py +2 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/usnjrnl.py +11 -11
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/util.py +14 -14
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1/dissect.ntfs.egg-info}/PKG-INFO +2 -2
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/pyproject.toml +48 -5
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/conftest.py +11 -6
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/test_attr.py +2 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/test_exceptions.py +3 -1
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/test_index.py +2 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/test_mft.py +2 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/test_ntfs.py +2 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/test_secure.py +3 -1
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/test_usnjrnl.py +2 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/test_util.py +10 -8
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tox.ini +4 -17
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/COPYRIGHT +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/LICENSE +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/MANIFEST.in +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/README.md +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/__init__.py +2 -2
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect/ntfs/exceptions.py +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect.ntfs.egg-info/SOURCES.txt +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect.ntfs.egg-info/dependency_links.txt +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect.ntfs.egg-info/requires.txt +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect.ntfs.egg-info/top_level.txt +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/setup.cfg +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/__init__.py +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/data/boot_2m.bin.gz +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/data/mft.bin.gz +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/data/ntfs.bin.gz +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/data/ntfs_fragmented_mft.csv.gz +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/data/sds.bin.gz +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/data/sds_complex.bin.gz +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/docs/Makefile +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/docs/conf.py +0 -0
- {dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/tests/docs/index.rst +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: dissect.ntfs
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.14.dev1
|
|
4
4
|
Summary: A Dissect module implementing a parser for the NTFS file system, used by the Windows operating system
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License: Affero General Public License v3
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
-
from
|
|
5
|
-
from typing import TYPE_CHECKING, Any, BinaryIO, Iterator, Optional
|
|
4
|
+
from typing import TYPE_CHECKING, Any, BinaryIO
|
|
6
5
|
|
|
7
6
|
from dissect.util.stream import RangeStream, RunlistStream
|
|
8
7
|
from dissect.util.ts import wintimestamp
|
|
@@ -18,6 +17,9 @@ from dissect.ntfs.exceptions import MftNotAvailableError, VolumeNotAvailableErro
|
|
|
18
17
|
from dissect.ntfs.util import ensure_volume, get_full_path, ts_to_ns
|
|
19
18
|
|
|
20
19
|
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Iterator
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
21
23
|
from dissect.ntfs.mft import MftRecord
|
|
22
24
|
|
|
23
25
|
|
|
@@ -31,9 +33,9 @@ class Attribute:
|
|
|
31
33
|
header: The AttributeHeader for this Attribute.
|
|
32
34
|
"""
|
|
33
35
|
|
|
34
|
-
__slots__ = ("
|
|
36
|
+
__slots__ = ("attribute", "header", "record")
|
|
35
37
|
|
|
36
|
-
def __init__(self, header: AttributeHeader, record:
|
|
38
|
+
def __init__(self, header: AttributeHeader, record: MftRecord | None = None):
|
|
37
39
|
self.header = header
|
|
38
40
|
self.record = record
|
|
39
41
|
self.attribute = None
|
|
@@ -52,7 +54,7 @@ class Attribute:
|
|
|
52
54
|
return f"<${self.header.type.name} name={self.header.name}>"
|
|
53
55
|
|
|
54
56
|
@classmethod
|
|
55
|
-
def from_fh(cls, fh: BinaryIO, record:
|
|
57
|
+
def from_fh(cls, fh: BinaryIO, record: MftRecord | None = None) -> Attribute:
|
|
56
58
|
"""Parse an attribute from a file-like object.
|
|
57
59
|
|
|
58
60
|
Args:
|
|
@@ -62,7 +64,7 @@ class Attribute:
|
|
|
62
64
|
return cls(AttributeHeader(fh, 0, record), record)
|
|
63
65
|
|
|
64
66
|
@classmethod
|
|
65
|
-
def from_bytes(cls, data: bytes, record:
|
|
67
|
+
def from_bytes(cls, data: bytes, record: MftRecord | None = None) -> Attribute:
|
|
66
68
|
"""Parse an attribute from bytes.
|
|
67
69
|
|
|
68
70
|
Args:
|
|
@@ -120,9 +122,9 @@ class AttributeHeader:
|
|
|
120
122
|
offset: The offset in the file-like object to parse an attribute header from.
|
|
121
123
|
"""
|
|
122
124
|
|
|
123
|
-
__slots__ = ("
|
|
125
|
+
__slots__ = ("fh", "header", "offset", "record")
|
|
124
126
|
|
|
125
|
-
def __init__(self, fh: BinaryIO, offset: int, record:
|
|
127
|
+
def __init__(self, fh: BinaryIO, offset: int, record: MftRecord | None = None):
|
|
126
128
|
self.fh = fh
|
|
127
129
|
self.offset = offset
|
|
128
130
|
self.record = record
|
|
@@ -134,7 +136,7 @@ class AttributeHeader:
|
|
|
134
136
|
return f"<${self.type.name} size={self.size}>"
|
|
135
137
|
|
|
136
138
|
@classmethod
|
|
137
|
-
def from_bytes(cls, data: bytes, record:
|
|
139
|
+
def from_bytes(cls, data: bytes, record: MftRecord | None = None) -> AttributeHeader:
|
|
138
140
|
"""Parse an attribute header from bytes.
|
|
139
141
|
|
|
140
142
|
Args:
|
|
@@ -175,22 +177,22 @@ class AttributeHeader:
|
|
|
175
177
|
return self.header.Form.Resident.ValueLength if self.resident else self.header.Form.Nonresident.FileSize
|
|
176
178
|
|
|
177
179
|
@property
|
|
178
|
-
def allocated_size(self) ->
|
|
180
|
+
def allocated_size(self) -> int | None:
|
|
179
181
|
"""Return the allocated size if non-resident, else None."""
|
|
180
182
|
return self.header.Form.Nonresident.AllocatedLength if not self.resident else None
|
|
181
183
|
|
|
182
184
|
@property
|
|
183
|
-
def lowest_vcn(self) ->
|
|
185
|
+
def lowest_vcn(self) -> int | None:
|
|
184
186
|
"""Return the lowest VCN if non-resident, else None."""
|
|
185
187
|
return self.header.Form.Nonresident.LowestVcn if not self.resident else None
|
|
186
188
|
|
|
187
189
|
@property
|
|
188
|
-
def highest_vcn(self) ->
|
|
190
|
+
def highest_vcn(self) -> int | None:
|
|
189
191
|
"""Return the highest VCN if non-resident, else None."""
|
|
190
192
|
return self.header.Form.Nonresident.HighestVcn if not self.resident else None
|
|
191
193
|
|
|
192
194
|
@property
|
|
193
|
-
def compression_unit(self) ->
|
|
195
|
+
def compression_unit(self) -> int | None:
|
|
194
196
|
"""Return the compression unit if non-resident, else None."""
|
|
195
197
|
return self.header.Form.Nonresident.CompressionUnit if not self.resident else None
|
|
196
198
|
|
|
@@ -242,16 +244,16 @@ class AttributeHeader:
|
|
|
242
244
|
self.offset + self.header.Form.Resident.ValueOffset,
|
|
243
245
|
self.size,
|
|
244
246
|
)
|
|
245
|
-
else:
|
|
246
|
-
ntfs = self.record.ntfs if self.record else None
|
|
247
|
-
ensure_volume(ntfs)
|
|
248
247
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
248
|
+
ntfs = self.record.ntfs if self.record else None
|
|
249
|
+
ensure_volume(ntfs)
|
|
250
|
+
|
|
251
|
+
return RunlistStream(
|
|
252
|
+
ntfs.fh,
|
|
253
|
+
self.dataruns(),
|
|
254
|
+
self.size,
|
|
255
|
+
ntfs.cluster_size,
|
|
256
|
+
)
|
|
255
257
|
|
|
256
258
|
def data(self) -> bytes:
|
|
257
259
|
"""Read and return all the data of this attribute.
|
|
@@ -272,13 +274,11 @@ class AttributeRecord:
|
|
|
272
274
|
|
|
273
275
|
__slots__ = ("record",)
|
|
274
276
|
|
|
275
|
-
def __init__(self, fh: BinaryIO, record:
|
|
277
|
+
def __init__(self, fh: BinaryIO, record: MftRecord | None = None):
|
|
276
278
|
self.record = record
|
|
277
279
|
|
|
278
280
|
@classmethod
|
|
279
|
-
def from_fh(
|
|
280
|
-
cls, fh: BinaryIO, attr_type: ATTRIBUTE_TYPE_CODE, record: Optional[MftRecord] = None
|
|
281
|
-
) -> AttributeRecord:
|
|
281
|
+
def from_fh(cls, fh: BinaryIO, attr_type: ATTRIBUTE_TYPE_CODE, record: MftRecord | None = None) -> AttributeRecord:
|
|
282
282
|
"""Parse an attribute from a file-like object.
|
|
283
283
|
|
|
284
284
|
Selects a more specific :class:`AttributeRecord` class if one is available for the given attribute type.
|
|
@@ -296,7 +296,7 @@ class AttributeList(AttributeRecord):
|
|
|
296
296
|
|
|
297
297
|
__slots__ = ("entries",)
|
|
298
298
|
|
|
299
|
-
def __init__(self, fh: BinaryIO, record:
|
|
299
|
+
def __init__(self, fh: BinaryIO, record: MftRecord | None = None):
|
|
300
300
|
super().__init__(fh, record)
|
|
301
301
|
|
|
302
302
|
offset = 0
|
|
@@ -343,7 +343,7 @@ class StandardInformation(AttributeRecord):
|
|
|
343
343
|
|
|
344
344
|
__slots__ = ("attr",)
|
|
345
345
|
|
|
346
|
-
def __init__(self, fh: BinaryIO, record:
|
|
346
|
+
def __init__(self, fh: BinaryIO, record: MftRecord | None = None):
|
|
347
347
|
super().__init__(fh, record)
|
|
348
348
|
# Data can be less than the _STANDARD_INFORMATION structure size, so pad remaining fields with null bytes
|
|
349
349
|
data = fh.read().ljust(len(c_ntfs._STANDARD_INFORMATION), b"\x00")
|
|
@@ -413,7 +413,7 @@ class FileName(AttributeRecord):
|
|
|
413
413
|
|
|
414
414
|
__slots__ = ("attr",)
|
|
415
415
|
|
|
416
|
-
def __init__(self, fh: BinaryIO, record:
|
|
416
|
+
def __init__(self, fh: BinaryIO, record: MftRecord | None = None):
|
|
417
417
|
super().__init__(fh, record)
|
|
418
418
|
data = fh.read().ljust(len(c_ntfs.STANDARD_INFORMATION_EX), b"\x00")
|
|
419
419
|
self.attr = c_ntfs._FILE_NAME(data)
|
|
@@ -489,9 +489,9 @@ class FileName(AttributeRecord):
|
|
|
489
489
|
class ReparsePoint(AttributeRecord):
|
|
490
490
|
"""Specific :class:`AttributeRecord` parser for ``$REPARSE_POINT``."""
|
|
491
491
|
|
|
492
|
-
__slots__ = ("attr", "
|
|
492
|
+
__slots__ = ("attr", "buffer", "tag_header")
|
|
493
493
|
|
|
494
|
-
def __init__(self, fh: BinaryIO, record:
|
|
494
|
+
def __init__(self, fh: BinaryIO, record: MftRecord | None = None):
|
|
495
495
|
super().__init__(fh, record)
|
|
496
496
|
self.attr = c_ntfs._REPARSE_DATA_BUFFER(fh)
|
|
497
497
|
data = io.BytesIO(fh.read(self.attr.ReparseDataLength))
|
|
@@ -512,7 +512,7 @@ class ReparsePoint(AttributeRecord):
|
|
|
512
512
|
return self.attr.ReparseTag
|
|
513
513
|
|
|
514
514
|
@property
|
|
515
|
-
def substitute_name(self) ->
|
|
515
|
+
def substitute_name(self) -> str | None:
|
|
516
516
|
if not self.tag_header:
|
|
517
517
|
return None
|
|
518
518
|
|
|
@@ -521,7 +521,7 @@ class ReparsePoint(AttributeRecord):
|
|
|
521
521
|
return self.buffer[offset : offset + length].decode("utf-16-le")
|
|
522
522
|
|
|
523
523
|
@property
|
|
524
|
-
def print_name(self) ->
|
|
524
|
+
def print_name(self) -> str | None:
|
|
525
525
|
if not self.tag_header:
|
|
526
526
|
return None
|
|
527
527
|
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import io
|
|
4
4
|
from enum import Enum, auto
|
|
5
5
|
from functools import cached_property, lru_cache
|
|
6
|
-
from typing import TYPE_CHECKING, Any, BinaryIO, Callable
|
|
6
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Callable
|
|
7
7
|
|
|
8
8
|
from dissect.ntfs.attr import AttributeRecord
|
|
9
9
|
from dissect.ntfs.c_ntfs import (
|
|
@@ -24,6 +24,8 @@ from dissect.ntfs.exceptions import (
|
|
|
24
24
|
from dissect.ntfs.util import apply_fixup
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Iterator
|
|
28
|
+
|
|
27
29
|
from dissect.ntfs.mft import MftRecord
|
|
28
30
|
|
|
29
31
|
|
|
@@ -79,7 +81,7 @@ class Index:
|
|
|
79
81
|
return IndexBuffer(self, self._index_stream, vcn << self._vcn_size_shift, self.root.bytes_per_index_buffer)
|
|
80
82
|
|
|
81
83
|
def search(
|
|
82
|
-
self, value: Any, exact: bool = True, cmp:
|
|
84
|
+
self, value: Any, exact: bool = True, cmp: Callable[[IndexEntry, Any], Match] | None = None
|
|
83
85
|
) -> IndexEntry:
|
|
84
86
|
"""Perform a binary search on this index.
|
|
85
87
|
|
|
@@ -114,8 +116,7 @@ class Index:
|
|
|
114
116
|
entry = _bsearch(entries, search_value, cmp)
|
|
115
117
|
if not entry.is_node or (not entry.is_end and cmp(entry, search_value) == Match.Equal):
|
|
116
118
|
break
|
|
117
|
-
|
|
118
|
-
entries = list(self.index_buffer(entry.node_vcn).entries())
|
|
119
|
+
entries = list(self.index_buffer(entry.node_vcn).entries())
|
|
119
120
|
|
|
120
121
|
if exact and (entry.is_end or cmp(entry, search_value) != Match.Equal):
|
|
121
122
|
raise KeyError(f"Value not found: {value}")
|
|
@@ -216,7 +217,7 @@ class IndexBuffer:
|
|
|
216
217
|
buf = fh.read(size)
|
|
217
218
|
|
|
218
219
|
if len(buf) != size:
|
|
219
|
-
raise EOFError
|
|
220
|
+
raise EOFError
|
|
220
221
|
|
|
221
222
|
if buf[:4] != b"INDX":
|
|
222
223
|
raise BrokenIndexError("Broken INDX header")
|
|
@@ -264,7 +265,7 @@ class IndexEntry:
|
|
|
264
265
|
"""
|
|
265
266
|
record = self.index.record
|
|
266
267
|
if not record or not record.ntfs or not record.ntfs.mft:
|
|
267
|
-
raise MftNotAvailableError
|
|
268
|
+
raise MftNotAvailableError
|
|
268
269
|
|
|
269
270
|
return record.ntfs.mft.get(segment_reference(self.header.FileReference))
|
|
270
271
|
|
|
@@ -284,7 +285,7 @@ class IndexEntry:
|
|
|
284
285
|
return self.buf[offset : offset + self.header.DataLength]
|
|
285
286
|
|
|
286
287
|
@cached_property
|
|
287
|
-
def attribute(self) ->
|
|
288
|
+
def attribute(self) -> AttributeRecord | None:
|
|
288
289
|
"""Return the :class:`dissect.ntfs.attr.AttributeRecord` of the attribute contained in this entry."""
|
|
289
290
|
if self.key_length and self.index.root.attribute_type:
|
|
290
291
|
return AttributeRecord.from_fh(
|
|
@@ -366,10 +367,9 @@ def _cmp_filename(entry: IndexEntry, value: str) -> Match:
|
|
|
366
367
|
|
|
367
368
|
if value < test_value:
|
|
368
369
|
return Match.Less
|
|
369
|
-
|
|
370
|
+
if value == test_value:
|
|
370
371
|
return Match.Equal
|
|
371
|
-
|
|
372
|
-
return Match.Greater
|
|
372
|
+
return Match.Greater
|
|
373
373
|
|
|
374
374
|
|
|
375
375
|
def _cmp_ulong(entry: IndexEntry, value: int) -> Match:
|
|
@@ -380,7 +380,6 @@ def _cmp_ulong(entry: IndexEntry, value: int) -> Match:
|
|
|
380
380
|
|
|
381
381
|
if value < test_value:
|
|
382
382
|
return Match.Less
|
|
383
|
-
|
|
383
|
+
if value == test_value:
|
|
384
384
|
return Match.Equal
|
|
385
|
-
|
|
386
|
-
return Match.Greater
|
|
385
|
+
return Match.Greater
|
|
@@ -4,7 +4,7 @@ import ntpath
|
|
|
4
4
|
from functools import cached_property, lru_cache
|
|
5
5
|
from io import BytesIO
|
|
6
6
|
from operator import itemgetter
|
|
7
|
-
from typing import TYPE_CHECKING,
|
|
7
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
8
8
|
|
|
9
9
|
from dissect.ntfs.attr import Attribute, AttributeHeader
|
|
10
10
|
from dissect.ntfs.c_ntfs import (
|
|
@@ -30,6 +30,8 @@ from dissect.ntfs.index import Index, IndexEntry
|
|
|
30
30
|
from dissect.ntfs.util import AttributeCollection, AttributeMap, apply_fixup
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Iterator
|
|
34
|
+
|
|
33
35
|
from dissect.ntfs.ntfs import NTFS
|
|
34
36
|
|
|
35
37
|
|
|
@@ -41,13 +43,13 @@ class Mft:
|
|
|
41
43
|
ntfs: An optional NTFS class instance.
|
|
42
44
|
"""
|
|
43
45
|
|
|
44
|
-
def __init__(self, fh: BinaryIO, ntfs:
|
|
46
|
+
def __init__(self, fh: BinaryIO, ntfs: NTFS | None = None):
|
|
45
47
|
self.fh = fh
|
|
46
48
|
self.ntfs = ntfs
|
|
47
49
|
|
|
48
50
|
self.get = lru_cache(4096)(self.get)
|
|
49
51
|
|
|
50
|
-
def __call__(self, ref, *args, **kwargs) -> MftRecord:
|
|
52
|
+
def __call__(self, ref: int | str | c_ntfs._MFT_SEGMENT_REFERENCE, *args, **kwargs) -> MftRecord:
|
|
51
53
|
return self.get(ref, *args, **kwargs)
|
|
52
54
|
|
|
53
55
|
@cached_property
|
|
@@ -55,7 +57,7 @@ class Mft:
|
|
|
55
57
|
"""Return the root directory MFT record."""
|
|
56
58
|
return self.get(FILE_NUMBER_ROOT)
|
|
57
59
|
|
|
58
|
-
def _get_path(self, path: str, root:
|
|
60
|
+
def _get_path(self, path: str, root: MftRecord | None = None) -> MftRecord:
|
|
59
61
|
"""Resolve a file path to the correct MFT record.
|
|
60
62
|
|
|
61
63
|
Args:
|
|
@@ -89,7 +91,7 @@ class Mft:
|
|
|
89
91
|
|
|
90
92
|
return node
|
|
91
93
|
|
|
92
|
-
def get(self, ref:
|
|
94
|
+
def get(self, ref: int | str | c_ntfs._MFT_SEGMENT_REFERENCE, root: MftRecord | None = None) -> MftRecord:
|
|
93
95
|
"""Retrieve an MFT record using a variety of methods.
|
|
94
96
|
|
|
95
97
|
Supported references are:
|
|
@@ -113,10 +115,11 @@ class Mft:
|
|
|
113
115
|
record = MftRecord.from_fh(self.fh, ref * record_size, ntfs=self.ntfs)
|
|
114
116
|
record.segment = ref
|
|
115
117
|
return record
|
|
116
|
-
|
|
118
|
+
|
|
119
|
+
if isinstance(ref, str):
|
|
117
120
|
return self._get_path(ref, root)
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
|
|
122
|
+
raise TypeError(f"Invalid MFT reference: {ref!r}")
|
|
120
123
|
|
|
121
124
|
def segments(self, start: int = 0, end: int = -1) -> Iterator[MftRecord]:
|
|
122
125
|
"""Yield all valid MFT records, regardless if they're allocated or not.
|
|
@@ -134,7 +137,7 @@ class Mft:
|
|
|
134
137
|
for segment in range(start, end + step, step):
|
|
135
138
|
try:
|
|
136
139
|
yield self.get(segment)
|
|
137
|
-
except Error:
|
|
140
|
+
except Error: # noqa: PERF203
|
|
138
141
|
continue
|
|
139
142
|
except EOFError:
|
|
140
143
|
break
|
|
@@ -147,16 +150,16 @@ class MftRecord:
|
|
|
147
150
|
"""
|
|
148
151
|
|
|
149
152
|
def __init__(self):
|
|
150
|
-
self.ntfs:
|
|
151
|
-
self.segment:
|
|
152
|
-
self.offset:
|
|
153
|
-
self.data:
|
|
154
|
-
self.header:
|
|
153
|
+
self.ntfs: NTFS | None = None
|
|
154
|
+
self.segment: int | None = None
|
|
155
|
+
self.offset: int | None = None
|
|
156
|
+
self.data: bytes | None = None
|
|
157
|
+
self.header: c_ntfs._FILE_RECORD_SEGMENT_HEADER | None = None
|
|
155
158
|
|
|
156
159
|
def __repr__(self) -> str:
|
|
157
160
|
return f"<MftRecord {self.segment}#{self.header.SequenceNumber}>"
|
|
158
161
|
|
|
159
|
-
def __eq__(self, other:
|
|
162
|
+
def __eq__(self, other: object) -> bool:
|
|
160
163
|
if isinstance(other, MftRecord):
|
|
161
164
|
return self.segment == other.segment and self.header.SequenceNumber == other.header.SequenceNumber
|
|
162
165
|
return False
|
|
@@ -164,7 +167,7 @@ class MftRecord:
|
|
|
164
167
|
__hash__ = object.__hash__
|
|
165
168
|
|
|
166
169
|
@classmethod
|
|
167
|
-
def from_fh(cls, fh: BinaryIO, offset: int, ntfs:
|
|
170
|
+
def from_fh(cls, fh: BinaryIO, offset: int, ntfs: NTFS | None = None) -> MftRecord:
|
|
168
171
|
"""Parse an MFT record from a file-like object.
|
|
169
172
|
|
|
170
173
|
Args:
|
|
@@ -182,7 +185,7 @@ class MftRecord:
|
|
|
182
185
|
return obj
|
|
183
186
|
|
|
184
187
|
@classmethod
|
|
185
|
-
def from_bytes(cls, data: bytes, ntfs:
|
|
188
|
+
def from_bytes(cls, data: bytes, ntfs: NTFS | None = None) -> MftRecord:
|
|
186
189
|
"""Parse an MFT record from bytes.
|
|
187
190
|
|
|
188
191
|
Args:
|
|
@@ -212,7 +215,7 @@ class MftRecord:
|
|
|
212
215
|
MftNotAvailableError: If no MFT is available.
|
|
213
216
|
"""
|
|
214
217
|
if not self.ntfs or not self.ntfs.mft:
|
|
215
|
-
raise MftNotAvailableError
|
|
218
|
+
raise MftNotAvailableError
|
|
216
219
|
return self.ntfs.mft.get(path, root=self)
|
|
217
220
|
|
|
218
221
|
@cached_property
|
|
@@ -264,7 +267,7 @@ class MftRecord:
|
|
|
264
267
|
return any(attr.header.resident for attr in self.attributes[ATTRIBUTE_TYPE_CODE.DATA])
|
|
265
268
|
|
|
266
269
|
@cached_property
|
|
267
|
-
def filename(self) ->
|
|
270
|
+
def filename(self) -> str | None:
|
|
268
271
|
"""Return the first file name, or ``None`` if this record has no file names."""
|
|
269
272
|
filenames = self.filenames()
|
|
270
273
|
return filenames[0] if filenames else None
|
|
@@ -282,7 +285,7 @@ class MftRecord:
|
|
|
282
285
|
result.append((attr.flags, attr.file_name))
|
|
283
286
|
return [item[1] for item in sorted(result, key=itemgetter(0))]
|
|
284
287
|
|
|
285
|
-
def full_path(self, ignore_dos: bool = False) ->
|
|
288
|
+
def full_path(self, ignore_dos: bool = False) -> str | None:
|
|
286
289
|
"""Return the first full path, or ``None`` if this record has no file names.
|
|
287
290
|
|
|
288
291
|
Args:
|
|
@@ -353,7 +356,7 @@ class MftRecord:
|
|
|
353
356
|
raise NotAReparsePointError(f"{self!r} is not a reparse point")
|
|
354
357
|
|
|
355
358
|
if not self.ntfs or not self.ntfs.mft:
|
|
356
|
-
raise MftNotAvailableError
|
|
359
|
+
raise MftNotAvailableError
|
|
357
360
|
|
|
358
361
|
reparse_point = self.attributes[ATTRIBUTE_TYPE_CODE.REPARSE_POINT]
|
|
359
362
|
|
|
@@ -442,7 +445,7 @@ class MftRecord:
|
|
|
442
445
|
"""
|
|
443
446
|
return Index(self, name)
|
|
444
447
|
|
|
445
|
-
def iterdir(self, dereference: bool = False, ignore_dos: bool = False) -> Iterator[
|
|
448
|
+
def iterdir(self, dereference: bool = False, ignore_dos: bool = False) -> Iterator[IndexEntry | MftRecord]:
|
|
446
449
|
"""Yield directory entries of this record.
|
|
447
450
|
|
|
448
451
|
Args:
|
|
@@ -460,7 +463,7 @@ class MftRecord:
|
|
|
460
463
|
continue
|
|
461
464
|
yield entry.dereference() if dereference else entry
|
|
462
465
|
|
|
463
|
-
def listdir(self, dereference: bool = False, ignore_dos: bool = False) -> dict[str,
|
|
466
|
+
def listdir(self, dereference: bool = False, ignore_dos: bool = False) -> dict[str, IndexEntry | MftRecord]:
|
|
464
467
|
"""Return a dictionary of the directory entries of this record.
|
|
465
468
|
|
|
466
469
|
Args:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from functools import cached_property
|
|
2
|
-
from typing import
|
|
4
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
3
5
|
|
|
4
6
|
from dissect.ntfs.c_ntfs import (
|
|
5
7
|
ATTRIBUTE_TYPE_CODE,
|
|
@@ -17,6 +19,9 @@ from dissect.ntfs.mft import Mft, MftRecord
|
|
|
17
19
|
from dissect.ntfs.secure import Secure
|
|
18
20
|
from dissect.ntfs.usnjrnl import UsnJrnl
|
|
19
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Iterator
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
class NTFS:
|
|
22
27
|
"""Implementation for Microsoft NTFS.
|
|
@@ -36,11 +41,11 @@ class NTFS:
|
|
|
36
41
|
|
|
37
42
|
def __init__(
|
|
38
43
|
self,
|
|
39
|
-
fh:
|
|
40
|
-
boot:
|
|
41
|
-
mft:
|
|
42
|
-
usnjrnl:
|
|
43
|
-
sds:
|
|
44
|
+
fh: BinaryIO | None = None,
|
|
45
|
+
boot: BinaryIO | None = None,
|
|
46
|
+
mft: BinaryIO | None = None,
|
|
47
|
+
usnjrnl: BinaryIO | None = None,
|
|
48
|
+
sds: BinaryIO | None = None,
|
|
44
49
|
):
|
|
45
50
|
self.fh = fh
|
|
46
51
|
|
|
@@ -130,11 +135,11 @@ class NTFS:
|
|
|
130
135
|
pass
|
|
131
136
|
|
|
132
137
|
@cached_property
|
|
133
|
-
def serial(self) ->
|
|
138
|
+
def serial(self) -> int | None:
|
|
134
139
|
return self.boot_sector.SerialNumber if self.boot_sector else None
|
|
135
140
|
|
|
136
141
|
@cached_property
|
|
137
|
-
def volume_name(self) ->
|
|
142
|
+
def volume_name(self) -> str | None:
|
|
138
143
|
if not self.mft:
|
|
139
144
|
return None
|
|
140
145
|
|
|
@@ -2,13 +2,17 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
4
|
from functools import lru_cache
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
8
|
from dissect.util.sid import read_sid
|
|
9
9
|
|
|
10
10
|
from dissect.ntfs.c_ntfs import ACE_OBJECT_FLAGS, ACE_TYPE, c_ntfs
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
|
|
15
|
+
from dissect.ntfs.mft import MftRecord
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
class Secure:
|
|
@@ -21,7 +25,7 @@ class Secure:
|
|
|
21
25
|
sds: A file-like object of the ``$SDS`` stream, used when opening from separate system files.
|
|
22
26
|
"""
|
|
23
27
|
|
|
24
|
-
def __init__(self, record: MftRecord = None, sds: BinaryIO = None):
|
|
28
|
+
def __init__(self, record: MftRecord = None, sds: BinaryIO | None = None):
|
|
25
29
|
self.record = record
|
|
26
30
|
self.sds = None
|
|
27
31
|
self.sii = None
|
|
@@ -187,18 +191,17 @@ class ACE:
|
|
|
187
191
|
def __repr__(self) -> str:
|
|
188
192
|
if self.is_standard_ace:
|
|
189
193
|
return f"<{self.header.AceType.name} mask=0x{self.mask:x} sid={self.sid}>"
|
|
190
|
-
|
|
194
|
+
if self.is_compound_ace:
|
|
191
195
|
return (
|
|
192
196
|
f"<{self.header.AceType.name} mask=0x{self.mask:x} type={self.compound_type.name}"
|
|
193
197
|
f" server_sid={self.server_sid} client_sid={self.sid}>"
|
|
194
198
|
)
|
|
195
|
-
|
|
199
|
+
if self.is_object_ace:
|
|
196
200
|
return (
|
|
197
201
|
f"<{self.header.AceType.name} mask=0x{self.mask:x} flags={self.flags} object_type={self.object_type}"
|
|
198
202
|
f" inherited_object_type={self.inherited_object_type} sid={self.sid}>"
|
|
199
203
|
)
|
|
200
|
-
|
|
201
|
-
return f"<ACE type={self.header.AceType} flags={self.header.AceFlags} size={self.header.AceSize}>"
|
|
204
|
+
return f"<ACE type={self.header.AceType} flags={self.header.AceFlags} size={self.header.AceSize}>"
|
|
202
205
|
|
|
203
206
|
@property
|
|
204
207
|
def type(self) -> ACE_TYPE:
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from datetime import datetime
|
|
4
3
|
from functools import cached_property
|
|
5
|
-
from typing import TYPE_CHECKING, Any, BinaryIO
|
|
4
|
+
from typing import TYPE_CHECKING, Any, BinaryIO
|
|
6
5
|
|
|
7
6
|
from dissect.util.stream import RunlistStream
|
|
8
7
|
from dissect.util.ts import wintimestamp
|
|
9
8
|
|
|
10
9
|
from dissect.ntfs.c_ntfs import USN_PAGE_SIZE, c_ntfs, segment_reference
|
|
11
10
|
from dissect.ntfs.exceptions import Error
|
|
12
|
-
from dissect.ntfs.mft import MftRecord
|
|
13
11
|
from dissect.ntfs.util import ts_to_ns
|
|
14
12
|
|
|
15
13
|
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
from dissect.ntfs.mft import MftRecord
|
|
16
18
|
from dissect.ntfs.ntfs import NTFS
|
|
17
19
|
|
|
18
20
|
|
|
@@ -24,7 +26,7 @@ class UsnJrnl:
|
|
|
24
26
|
ntfs: An optional :class:`~dissect.ntfs.ntfs.NTFS` class instance, used for resolving file paths.
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
|
-
def __init__(self, fh: BinaryIO, ntfs:
|
|
29
|
+
def __init__(self, fh: BinaryIO, ntfs: NTFS | None = None):
|
|
28
30
|
self.fh = fh
|
|
29
31
|
self.ntfs = ntfs
|
|
30
32
|
|
|
@@ -105,13 +107,13 @@ class UsnRecord:
|
|
|
105
107
|
return getattr(self.record, attr)
|
|
106
108
|
|
|
107
109
|
@cached_property
|
|
108
|
-
def file(self) ->
|
|
110
|
+
def file(self) -> MftRecord | None:
|
|
109
111
|
if self.usnjrnl.ntfs and self.usnjrnl.ntfs.mft:
|
|
110
112
|
return self.usnjrnl.ntfs.mft(self.record.FileReferenceNumber)
|
|
111
113
|
return None
|
|
112
114
|
|
|
113
115
|
@cached_property
|
|
114
|
-
def parent(self) ->
|
|
116
|
+
def parent(self) -> MftRecord | None:
|
|
115
117
|
if self.usnjrnl.ntfs and self.usnjrnl.ntfs.mft:
|
|
116
118
|
return self.usnjrnl.ntfs.mft(self.record.ParentFileReferenceNumber)
|
|
117
119
|
return None
|
|
@@ -133,12 +135,10 @@ class UsnRecord:
|
|
|
133
135
|
|
|
134
136
|
ref = segment_reference(self.record.ParentFileReferenceNumber)
|
|
135
137
|
if parent is None:
|
|
136
|
-
parent_path =
|
|
137
|
-
f"<unavailable_reference_0x{ref:x}" f"#{self.record.ParentFileReferenceNumber.SequenceNumber}>"
|
|
138
|
-
)
|
|
138
|
+
parent_path = f"<unavailable_reference_0x{ref:x}#{self.record.ParentFileReferenceNumber.SequenceNumber}>"
|
|
139
139
|
elif parent.header.SequenceNumber == self.record.ParentFileReferenceNumber.SequenceNumber:
|
|
140
140
|
parent_path = parent.full_path()
|
|
141
141
|
else:
|
|
142
|
-
parent_path = f"<broken_reference_0x{ref:x}
|
|
142
|
+
parent_path = f"<broken_reference_0x{ref:x}#{self.record.ParentFileReferenceNumber.SequenceNumber}>"
|
|
143
143
|
|
|
144
|
-
return "\\
|
|
144
|
+
return f"{parent_path}\\{self.filename}"
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import struct
|
|
4
4
|
from collections import UserDict
|
|
5
|
-
from typing import TYPE_CHECKING, Any, BinaryIO
|
|
5
|
+
from typing import TYPE_CHECKING, Any, BinaryIO
|
|
6
6
|
|
|
7
7
|
from dissect.cstruct import Enum
|
|
8
8
|
from dissect.util.stream import RunlistStream
|
|
@@ -44,12 +44,12 @@ class AttributeMap(UserDict):
|
|
|
44
44
|
|
|
45
45
|
return super().__getattribute__(attr)
|
|
46
46
|
|
|
47
|
-
def __getitem__(self, item:
|
|
47
|
+
def __getitem__(self, item: ATTRIBUTE_TYPE_CODE | int) -> AttributeCollection:
|
|
48
48
|
if isinstance(item, Enum):
|
|
49
49
|
item = item.value
|
|
50
50
|
return self.data.get(item, AttributeCollection())
|
|
51
51
|
|
|
52
|
-
def __contains__(self, key:
|
|
52
|
+
def __contains__(self, key: ATTRIBUTE_TYPE_CODE | int) -> bool:
|
|
53
53
|
if isinstance(key, Enum):
|
|
54
54
|
key = key.value
|
|
55
55
|
return super().__contains__(key)
|
|
@@ -132,13 +132,13 @@ class AttributeCollection(list):
|
|
|
132
132
|
ntfs.cluster_size,
|
|
133
133
|
attrs[0].header.compression_unit,
|
|
134
134
|
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
|
|
136
|
+
return RunlistStream(
|
|
137
|
+
ntfs.fh,
|
|
138
|
+
runs,
|
|
139
|
+
size,
|
|
140
|
+
ntfs.cluster_size,
|
|
141
|
+
)
|
|
142
142
|
|
|
143
143
|
def size(self, allocated: bool = False) -> int:
|
|
144
144
|
"""Retrieve the data stream size for this list of attributes.
|
|
@@ -169,7 +169,7 @@ class AttributeCollection(list):
|
|
|
169
169
|
def _get_stream_attrs(self) -> list[Attribute]:
|
|
170
170
|
return sorted((attr for attr in self if not attr.header.resident), key=lambda attr: attr.header.lowest_vcn)
|
|
171
171
|
|
|
172
|
-
def _get_dataruns(self, attrs:
|
|
172
|
+
def _get_dataruns(self, attrs: list[Attribute] | None = None) -> list[tuple[int, int]]:
|
|
173
173
|
attrs = attrs or self._get_stream_attrs()
|
|
174
174
|
|
|
175
175
|
runs = []
|
|
@@ -232,10 +232,10 @@ def ensure_volume(ntfs: NTFS) -> None:
|
|
|
232
232
|
VolumeNotAvailableError: If a volume is not available.
|
|
233
233
|
"""
|
|
234
234
|
if not ntfs or not ntfs.fh:
|
|
235
|
-
raise VolumeNotAvailableError
|
|
235
|
+
raise VolumeNotAvailableError
|
|
236
236
|
|
|
237
237
|
|
|
238
|
-
def get_full_path(mft: Mft, name: str, parent: c_ntfs._MFT_SEGMENT_REFERENCE, seen: set[str] = None) -> str:
|
|
238
|
+
def get_full_path(mft: Mft, name: str, parent: c_ntfs._MFT_SEGMENT_REFERENCE, seen: set[str] | None = None) -> str:
|
|
239
239
|
"""Walk up parent file references to construct a full path.
|
|
240
240
|
|
|
241
241
|
Args:
|
|
@@ -264,7 +264,7 @@ def get_full_path(mft: Mft, name: str, parent: c_ntfs._MFT_SEGMENT_REFERENCE, se
|
|
|
264
264
|
try:
|
|
265
265
|
record = mft.get(parent_ref)
|
|
266
266
|
if not record.filename:
|
|
267
|
-
raise FilenameNotAvailableError("No filename")
|
|
267
|
+
raise FilenameNotAvailableError("No filename") # noqa: TRY301
|
|
268
268
|
|
|
269
269
|
if record.header.SequenceNumber != parent.SequenceNumber:
|
|
270
270
|
path.append(f"<broken_reference_0x{parent_ref:x}#{parent.SequenceNumber}>")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: dissect.ntfs
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.14.dev1
|
|
4
4
|
Summary: A Dissect module implementing a parser for the NTFS file system, used by the Windows operating system
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License: Affero General Public License v3
|
|
@@ -41,13 +41,56 @@ dev = [
|
|
|
41
41
|
"dissect.util>=3.0.dev,<4.0.dev",
|
|
42
42
|
]
|
|
43
43
|
|
|
44
|
-
[tool.
|
|
44
|
+
[tool.ruff]
|
|
45
45
|
line-length = 120
|
|
46
|
+
required-version = ">=0.9.0"
|
|
46
47
|
|
|
47
|
-
[tool.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
[tool.ruff.format]
|
|
49
|
+
docstring-code-format = true
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
select = [
|
|
53
|
+
"F",
|
|
54
|
+
"E",
|
|
55
|
+
"W",
|
|
56
|
+
"I",
|
|
57
|
+
"UP",
|
|
58
|
+
"YTT",
|
|
59
|
+
"ANN",
|
|
60
|
+
"B",
|
|
61
|
+
"C4",
|
|
62
|
+
"DTZ",
|
|
63
|
+
"T10",
|
|
64
|
+
"FA",
|
|
65
|
+
"ISC",
|
|
66
|
+
"G",
|
|
67
|
+
"INP",
|
|
68
|
+
"PIE",
|
|
69
|
+
"PYI",
|
|
70
|
+
"PT",
|
|
71
|
+
"Q",
|
|
72
|
+
"RSE",
|
|
73
|
+
"RET",
|
|
74
|
+
"SLOT",
|
|
75
|
+
"SIM",
|
|
76
|
+
"TID",
|
|
77
|
+
"TCH",
|
|
78
|
+
"PTH",
|
|
79
|
+
"PLC",
|
|
80
|
+
"TRY",
|
|
81
|
+
"FLY",
|
|
82
|
+
"PERF",
|
|
83
|
+
"FURB",
|
|
84
|
+
"RUF",
|
|
85
|
+
]
|
|
86
|
+
ignore = ["E203", "B904", "UP024", "ANN002", "ANN003", "ANN204", "ANN401", "SIM105", "TRY003"]
|
|
87
|
+
|
|
88
|
+
[tool.ruff.lint.per-file-ignores]
|
|
89
|
+
"tests/docs/**" = ["INP001"]
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint.isort]
|
|
92
|
+
known-first-party = ["dissect.ntfs"]
|
|
93
|
+
known-third-party = ["dissect"]
|
|
51
94
|
|
|
52
95
|
[tool.setuptools]
|
|
53
96
|
license-files = ["LICENSE", "COPYRIGHT"]
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import csv
|
|
2
4
|
import gzip
|
|
3
5
|
import io
|
|
4
|
-
import
|
|
5
|
-
from typing import
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
6
8
|
|
|
7
9
|
import pytest
|
|
8
10
|
from dissect.util.stream import MappingStream
|
|
9
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
|
|
10
15
|
|
|
11
|
-
def absolute_path(filename: str) ->
|
|
12
|
-
return
|
|
16
|
+
def absolute_path(filename: str) -> Path:
|
|
17
|
+
return Path(__file__).parent / filename
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
def open_file_gz(name: str, mode: str = "rb") -> Iterator[BinaryIO]:
|
|
@@ -43,7 +48,7 @@ def boot_2m_bin() -> Iterator[BinaryIO]:
|
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
@pytest.fixture
|
|
46
|
-
def ntfs_fragmented_mft_fh() ->
|
|
51
|
+
def ntfs_fragmented_mft_fh() -> BinaryIO:
|
|
47
52
|
# Test data from https://github.com/msuhanov/ntfs-samples
|
|
48
53
|
# This is from the file ntfs_extremely_fragmented_mft.raw which has, as the name implies, a heavily fragmented MFT
|
|
49
54
|
# The entire file is way too large, so only take just enough data that we actually need to make dissect.ntfs happy
|
|
@@ -55,4 +60,4 @@ def ntfs_fragmented_mft_fh() -> Iterator[BinaryIO]:
|
|
|
55
60
|
buf = bytes.fromhex(data)
|
|
56
61
|
stream.add(int(offset), len(buf), io.BytesIO(buf), 0)
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
return stream
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
|
|
3
5
|
from dissect.ntfs import exceptions
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
@pytest.mark.parametrize(
|
|
7
|
-
"exc, std",
|
|
9
|
+
("exc", "std"),
|
|
8
10
|
[
|
|
9
11
|
(exceptions.FileNotFoundError, FileNotFoundError),
|
|
10
12
|
(exceptions.IsADirectoryError, IsADirectoryError),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from typing import BinaryIO
|
|
2
4
|
|
|
3
5
|
import pytest
|
|
@@ -58,5 +60,5 @@ def test_secure_complex_acl(sds_complex_bin: BinaryIO) -> None:
|
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def test_secure_fail() -> None:
|
|
61
|
-
with pytest.raises(ValueError):
|
|
63
|
+
with pytest.raises(ValueError, match="Either record or SDS stream is required"):
|
|
62
64
|
Secure()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from dissect.ntfs.attr import Attribute
|
|
2
4
|
from dissect.ntfs.c_ntfs import ATTRIBUTE_TYPE_CODE
|
|
3
5
|
from dissect.ntfs.util import AttributeMap, apply_fixup
|
|
@@ -7,7 +9,7 @@ def test_fixup() -> None:
|
|
|
7
9
|
buf = bytearray(
|
|
8
10
|
b"FILE\x30\x00"
|
|
9
11
|
+ (b"\x00" * 42)
|
|
10
|
-
+ b"\x02\x00\
|
|
12
|
+
+ b"\x02\x00\xff\x00\xfe\x00"
|
|
11
13
|
+ (b"\x00" * 456)
|
|
12
14
|
+ b"\x02\x00"
|
|
13
15
|
+ (b"\x00" * 510)
|
|
@@ -15,13 +17,13 @@ def test_fixup() -> None:
|
|
|
15
17
|
)
|
|
16
18
|
fixed = apply_fixup(buf)
|
|
17
19
|
|
|
18
|
-
assert fixed[510:512] == b"\
|
|
19
|
-
assert fixed[1022:1024] == b"\
|
|
20
|
+
assert fixed[510:512] == b"\xff\x00"
|
|
21
|
+
assert fixed[1022:1024] == b"\xfe\x00"
|
|
20
22
|
|
|
21
23
|
buf = bytearray(
|
|
22
24
|
b"FILE\x30\x00"
|
|
23
25
|
+ (b"\x00" * 42)
|
|
24
|
-
+ b"\x02\x00\
|
|
26
|
+
+ b"\x02\x00\xff\x00\xfe\x00\xfd\x00\xfc\x00"
|
|
25
27
|
+ (b"\x00" * 452)
|
|
26
28
|
+ b"\x02\x00"
|
|
27
29
|
+ (b"\x00" * 510)
|
|
@@ -33,10 +35,10 @@ def test_fixup() -> None:
|
|
|
33
35
|
)
|
|
34
36
|
fixed = apply_fixup(buf)
|
|
35
37
|
|
|
36
|
-
assert fixed[510:512] == b"\
|
|
37
|
-
assert fixed[1022:1024] == b"\
|
|
38
|
-
assert fixed[1534:1536] == b"\
|
|
39
|
-
assert fixed[2046:2048] == b"\
|
|
38
|
+
assert fixed[510:512] == b"\xff\x00"
|
|
39
|
+
assert fixed[1022:1024] == b"\xfe\x00"
|
|
40
|
+
assert fixed[1534:1536] == b"\xfd\x00"
|
|
41
|
+
assert fixed[2046:2048] == b"\xfc\x00"
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def test_attribute_map() -> None:
|
|
@@ -32,32 +32,19 @@ commands =
|
|
|
32
32
|
[testenv:fix]
|
|
33
33
|
package = skip
|
|
34
34
|
deps =
|
|
35
|
-
|
|
36
|
-
isort==5.11.4
|
|
35
|
+
ruff==0.9.2
|
|
37
36
|
commands =
|
|
38
|
-
|
|
39
|
-
isort dissect tests
|
|
37
|
+
ruff format dissect tests
|
|
40
38
|
|
|
41
39
|
[testenv:lint]
|
|
42
40
|
package = skip
|
|
43
41
|
deps =
|
|
44
|
-
|
|
45
|
-
flake8
|
|
46
|
-
flake8-black
|
|
47
|
-
flake8-isort
|
|
48
|
-
isort==5.11.4
|
|
42
|
+
ruff==0.9.2
|
|
49
43
|
vermin
|
|
50
44
|
commands =
|
|
51
|
-
|
|
45
|
+
ruff check dissect tests
|
|
52
46
|
vermin -t=3.9- --no-tips --lint dissect tests
|
|
53
47
|
|
|
54
|
-
[flake8]
|
|
55
|
-
max-line-length = 120
|
|
56
|
-
extend-ignore =
|
|
57
|
-
# See https://github.com/PyCQA/pycodestyle/issues/373
|
|
58
|
-
E203,
|
|
59
|
-
statistics = True
|
|
60
|
-
|
|
61
48
|
[testenv:docs-build]
|
|
62
49
|
allowlist_externals = make
|
|
63
50
|
deps =
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -10,6 +10,8 @@ __all__ = [
|
|
|
10
10
|
"ACE",
|
|
11
11
|
"ACL",
|
|
12
12
|
"ATTRIBUTE_TYPE_CODE",
|
|
13
|
+
"NTFS",
|
|
14
|
+
"NTFS_SIGNATURE",
|
|
13
15
|
"Attribute",
|
|
14
16
|
"AttributeHeader",
|
|
15
17
|
"AttributeRecord",
|
|
@@ -17,8 +19,6 @@ __all__ = [
|
|
|
17
19
|
"IndexEntry",
|
|
18
20
|
"Mft",
|
|
19
21
|
"MftRecord",
|
|
20
|
-
"NTFS",
|
|
21
|
-
"NTFS_SIGNATURE",
|
|
22
22
|
"Secure",
|
|
23
23
|
"SecurityDescriptor",
|
|
24
24
|
"UsnJrnl",
|
|
File without changes
|
|
File without changes
|
{dissect_ntfs-3.13.dev1 → dissect_ntfs-3.14.dev1}/dissect.ntfs.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|