dissect.apfs 1.1.dev2__tar.gz → 1.2.dev2__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_apfs-1.1.dev2/dissect.apfs.egg-info → dissect_apfs-1.2.dev2}/PKG-INFO +1 -1
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/__init__.py +2 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/c_apfs.py +2 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/exception.py +3 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/fs.py +9 -9
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/keybag.py +0 -1
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/stream.py +14 -10
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2/dissect.apfs.egg-info}/PKG-INFO +1 -1
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect.apfs.egg-info/SOURCES.txt +1 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/pyproject.toml +20 -2
- dissect_apfs-1.2.dev2/tests/_data/large.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_docs/conf.py +2 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/test_apfs.py +35 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/COPYRIGHT +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/LICENSE +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/MANIFEST.in +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/README.md +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/apfs.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/c_apfs.pyi +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/cursor.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/__init__.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/base.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/btree.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/btree_node.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/checkpoint_map.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/efi_jumpstart.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/er_recovery_block.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/er_state.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/gbitmap.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/gbitmap_block.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/integrity_meta.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_fusion_wbc.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_fusion_wbc_list.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_reap_list.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_reaper.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_superblock.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/omap.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/snap_meta_ext.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/spaceman.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/spaceman_bitmap.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/spaceman_cab.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/spaceman_cib.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/util.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect.apfs.egg-info/dependency_links.txt +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect.apfs.egg-info/requires.txt +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect.apfs.egg-info/top_level.txt +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/setup.cfg +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/__init__.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/case_insensitive.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/case_insensitive_beta.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/case_sensitive.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/case_sensitive_beta.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/corrupt.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/encrypted.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/jhfs_converted.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/jhfs_encrypted.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/snapshot.bin.gz +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_docs/Makefile +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_docs/__init__.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_docs/index.rst +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/conftest.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/test_exception.py +0 -0
- {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.apfs
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.dev2
|
|
4
4
|
Summary: A Dissect module implementing a parser for the APFS file system, a commonly used Apple file system
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# - https://developer.apple.com/support/downloads/Apple-File-System-Reference.pdf
|
|
3
3
|
# - https://github.com/sgan81/apfs-fuse
|
|
4
4
|
# - https://github.com/linux-apfs/linux-apfs-rw
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
from dissect.cstruct import cstruct
|
|
6
8
|
|
|
7
9
|
apfs_def = """
|
|
@@ -792,27 +792,27 @@ class DirectoryEntry:
|
|
|
792
792
|
@cached_property
|
|
793
793
|
def type(self) -> int:
|
|
794
794
|
"""The file type of this directory entry."""
|
|
795
|
-
return self.value.flags & c_apfs.DREC_TYPE_MASK << 12
|
|
795
|
+
return (self.value.flags & c_apfs.DREC_TYPE_MASK) << 12
|
|
796
796
|
|
|
797
797
|
def is_dir(self) -> bool:
|
|
798
798
|
"""Return whether this directory entry is a directory."""
|
|
799
|
-
return stat.S_ISDIR(self.type
|
|
799
|
+
return stat.S_ISDIR(self.type)
|
|
800
800
|
|
|
801
801
|
def is_file(self) -> bool:
|
|
802
802
|
"""Return whether this directory entry is a regular file."""
|
|
803
|
-
return stat.S_ISREG(self.type
|
|
803
|
+
return stat.S_ISREG(self.type)
|
|
804
804
|
|
|
805
805
|
def is_symlink(self) -> bool:
|
|
806
806
|
"""Return whether this directory entry is a symbolic link."""
|
|
807
|
-
return stat.S_ISLNK(self.type
|
|
807
|
+
return stat.S_ISLNK(self.type)
|
|
808
808
|
|
|
809
809
|
def is_block_device(self) -> bool:
|
|
810
810
|
"""Return whether this directory entry is a block device."""
|
|
811
|
-
return stat.S_ISBLK(self.type
|
|
811
|
+
return stat.S_ISBLK(self.type)
|
|
812
812
|
|
|
813
813
|
def is_character_device(self) -> bool:
|
|
814
814
|
"""Return whether this directory entry is a character device."""
|
|
815
|
-
return stat.S_ISCHR(self.type
|
|
815
|
+
return stat.S_ISCHR(self.type)
|
|
816
816
|
|
|
817
817
|
def is_device(self) -> bool:
|
|
818
818
|
"""Return whether this directory entry is a device (block or character)."""
|
|
@@ -820,15 +820,15 @@ class DirectoryEntry:
|
|
|
820
820
|
|
|
821
821
|
def is_fifo(self) -> bool:
|
|
822
822
|
"""Return whether this directory entry is a FIFO."""
|
|
823
|
-
return stat.S_ISFIFO(self.type
|
|
823
|
+
return stat.S_ISFIFO(self.type)
|
|
824
824
|
|
|
825
825
|
def is_socket(self) -> bool:
|
|
826
826
|
"""Return whether this directory entry is a socket."""
|
|
827
|
-
return stat.S_ISSOCK(self.type
|
|
827
|
+
return stat.S_ISSOCK(self.type)
|
|
828
828
|
|
|
829
829
|
def is_whiteout(self) -> bool:
|
|
830
830
|
"""Return whether this directory entry is a whiteout."""
|
|
831
|
-
return stat.S_ISWHT(self.type
|
|
831
|
+
return stat.S_ISWHT(self.type)
|
|
832
832
|
|
|
833
833
|
|
|
834
834
|
class XAttr:
|
|
@@ -88,26 +88,30 @@ class FileStream(AlignedStream):
|
|
|
88
88
|
|
|
89
89
|
while length:
|
|
90
90
|
logical_address, physical_address, extent_length, crypto_id = self._lookup(offset)
|
|
91
|
-
|
|
91
|
+
|
|
92
|
+
offset_in_extent = offset - logical_address
|
|
93
|
+
if offset_in_extent >= extent_length:
|
|
94
|
+
raise Error(
|
|
95
|
+
f"Offset {offset:#x} is out of bounds for extent ({logical_address:#x}, {extent_length:#x})"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
block_in_extent = offset_in_extent // self.align
|
|
99
|
+
read_length = min(extent_length - offset_in_extent, length)
|
|
100
|
+
block = self.volume.container._read_block(physical_address + block_in_extent, read_length // self.align)
|
|
92
101
|
|
|
93
102
|
if self.volume.is_encrypted:
|
|
94
103
|
if not self.volume._cipher:
|
|
95
104
|
raise Error("Volume is encrypted, unlock it first")
|
|
96
105
|
|
|
97
106
|
if self.volume.is_onekey:
|
|
98
|
-
|
|
107
|
+
sector = (crypto_id + block_in_extent) * self.volume.container.sectors_per_block
|
|
108
|
+
block = self.volume._cipher.decrypt(block, sector)
|
|
99
109
|
else:
|
|
100
110
|
raise Error("Multi-key encryption is not supported yet")
|
|
101
111
|
|
|
102
|
-
if offset_in_extent := offset - logical_address:
|
|
103
|
-
block = block[offset_in_extent:]
|
|
104
|
-
|
|
105
|
-
if length < len(block):
|
|
106
|
-
block = block[:length]
|
|
107
|
-
|
|
108
112
|
result.append(block)
|
|
109
|
-
offset +=
|
|
110
|
-
length -=
|
|
113
|
+
offset += read_length
|
|
114
|
+
length -= read_length
|
|
111
115
|
|
|
112
116
|
return b"".join(result)
|
|
113
117
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.apfs
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.dev2
|
|
4
4
|
Summary: A Dissect module implementing a parser for the APFS file system, a commonly used Apple file system
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -99,7 +99,7 @@ select = [
|
|
|
99
99
|
"SLOT",
|
|
100
100
|
"SIM",
|
|
101
101
|
"TID",
|
|
102
|
-
"
|
|
102
|
+
"TC",
|
|
103
103
|
"PTH",
|
|
104
104
|
"PLC",
|
|
105
105
|
"TRY",
|
|
@@ -107,8 +107,25 @@ select = [
|
|
|
107
107
|
"PERF",
|
|
108
108
|
"FURB",
|
|
109
109
|
"RUF",
|
|
110
|
+
"D"
|
|
110
111
|
]
|
|
111
|
-
ignore = [
|
|
112
|
+
ignore = [
|
|
113
|
+
"E203", "B904", "UP024", "ANN002", "ANN003", "ANN204", "ANN401", "SIM105", "TRY003", "PLC0415",
|
|
114
|
+
# Ignore some pydocstyle rules for now as they require a larger cleanup
|
|
115
|
+
"D1",
|
|
116
|
+
"D205",
|
|
117
|
+
"D301",
|
|
118
|
+
"D417",
|
|
119
|
+
# Seems bugged: https://github.com/astral-sh/ruff/issues/16824
|
|
120
|
+
"D402",
|
|
121
|
+
]
|
|
122
|
+
future-annotations = true
|
|
123
|
+
|
|
124
|
+
[tool.ruff.lint.pydocstyle]
|
|
125
|
+
convention = "google"
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.flake8-type-checking]
|
|
128
|
+
strict = true
|
|
112
129
|
|
|
113
130
|
[tool.ruff.lint.per-file-ignores]
|
|
114
131
|
"tests/_docs/**" = ["INP001"]
|
|
@@ -117,6 +134,7 @@ ignore = ["E203", "B904", "UP024", "ANN002", "ANN003", "ANN204", "ANN401", "SIM1
|
|
|
117
134
|
[tool.ruff.lint.isort]
|
|
118
135
|
known-first-party = ["dissect.apfs"]
|
|
119
136
|
known-third-party = ["dissect"]
|
|
137
|
+
required-imports = ["from __future__ import annotations"]
|
|
120
138
|
|
|
121
139
|
[tool.setuptools.packages.find]
|
|
122
140
|
include = ["dissect.*"]
|
|
Binary file
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import gzip
|
|
4
4
|
import hashlib
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
|
+
from unittest.mock import patch
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
|
|
@@ -36,6 +37,11 @@ def _assert_apfs_content(volume: FS, beta: bool) -> None:
|
|
|
36
37
|
]
|
|
37
38
|
)
|
|
38
39
|
|
|
40
|
+
# Test direntry parsing
|
|
41
|
+
assert node.listdir()["dir"].is_dir()
|
|
42
|
+
assert node.listdir()["hardlink"].is_file()
|
|
43
|
+
assert node.listdir()["symlink-dir"].is_symlink()
|
|
44
|
+
|
|
39
45
|
# Empty file
|
|
40
46
|
node = volume.get("empty")
|
|
41
47
|
assert node.name == "empty"
|
|
@@ -240,6 +246,9 @@ def _assert_apfs_content(volume: FS, beta: bool) -> None:
|
|
|
240
246
|
|
|
241
247
|
if ".HFS+ Private Directory Data\r" not in volume.get("/").listdir() and not beta:
|
|
242
248
|
# Special files
|
|
249
|
+
dirents = volume.get("dir").listdir()
|
|
250
|
+
assert dirents["blockdev"].is_block_device()
|
|
251
|
+
|
|
243
252
|
node = volume.get("dir/blockdev")
|
|
244
253
|
assert node.name == "blockdev"
|
|
245
254
|
assert node.is_block_device()
|
|
@@ -263,6 +272,8 @@ def _assert_apfs_content(volume: FS, beta: bool) -> None:
|
|
|
263
272
|
"chardev-svr4",
|
|
264
273
|
"chardev-ultrix",
|
|
265
274
|
]:
|
|
275
|
+
assert dirents[name].is_character_device()
|
|
276
|
+
|
|
266
277
|
node = volume.get(f"dir/{name}")
|
|
267
278
|
assert node.name == name
|
|
268
279
|
assert node.is_character_device()
|
|
@@ -369,3 +380,27 @@ def test_corrupt_checkpoints(caplog: pytest.LogCaptureFixture) -> None:
|
|
|
369
380
|
|
|
370
381
|
assert caplog.messages[0] == "Skipping superblock xid=304: invalid OMAP"
|
|
371
382
|
assert caplog.messages[1] == "Skipping superblock xid=303: Invalid nx_superblock checksum"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_large_extents() -> None:
|
|
386
|
+
"""Test APFS volumes with large extents."""
|
|
387
|
+
with gzip.open(absolute_path("_data/large.bin.gz"), "rb") as fh:
|
|
388
|
+
container = APFS(fh)
|
|
389
|
+
assert len(container.volumes) == 1
|
|
390
|
+
|
|
391
|
+
volume = container.volumes[0]
|
|
392
|
+
assert volume.name == "Large"
|
|
393
|
+
|
|
394
|
+
node = volume.get("yomomma.bin")
|
|
395
|
+
assert node.size == 512 * 1024 * 1024
|
|
396
|
+
|
|
397
|
+
fh = node.open()
|
|
398
|
+
|
|
399
|
+
# First extent is 128MiB
|
|
400
|
+
assert fh._lookup(0) == (0, 1070, 128 * 1024 * 1024, 0)
|
|
401
|
+
|
|
402
|
+
with patch.object(volume.container, "_read_block", wraps=volume.container._read_block) as mock_read_block:
|
|
403
|
+
assert fh.read(512) == b"\x67" * 512
|
|
404
|
+
|
|
405
|
+
# Check that we only read one block, not the entire 128MiB extent
|
|
406
|
+
mock_read_block.assert_called_once_with(1070, 1)
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|