dissect.apfs 1.1.dev1__tar.gz → 1.1.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.dev1/dissect.apfs.egg-info → dissect_apfs-1.1.dev2}/PKG-INFO +1 -1
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/apfs.py +28 -2
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/nx_superblock.py +36 -4
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/omap.py +9 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2/dissect.apfs.egg-info}/PKG-INFO +1 -1
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect.apfs.egg-info/SOURCES.txt +1 -0
- dissect_apfs-1.1.dev2/tests/_data/corrupt.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/test_apfs.py +13 -1
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/COPYRIGHT +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/LICENSE +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/MANIFEST.in +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/README.md +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/__init__.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/c_apfs.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/c_apfs.pyi +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/cursor.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/exception.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/__init__.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/base.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/btree.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/btree_node.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/checkpoint_map.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/efi_jumpstart.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/er_recovery_block.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/er_state.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/fs.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/gbitmap.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/gbitmap_block.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/integrity_meta.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/keybag.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/nx_fusion_wbc.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/nx_fusion_wbc_list.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/nx_reap_list.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/nx_reaper.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/snap_meta_ext.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/spaceman.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/spaceman_bitmap.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/spaceman_cab.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/objects/spaceman_cib.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/stream.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect/apfs/util.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect.apfs.egg-info/dependency_links.txt +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect.apfs.egg-info/requires.txt +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/dissect.apfs.egg-info/top_level.txt +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/pyproject.toml +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/setup.cfg +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/__init__.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_data/case_insensitive.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_data/case_insensitive_beta.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_data/case_sensitive.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_data/case_sensitive_beta.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_data/encrypted.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_data/jhfs_converted.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_data/jhfs_encrypted.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_data/snapshot.bin.gz +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_docs/Makefile +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_docs/__init__.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_docs/conf.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/_docs/index.rst +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/conftest.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tests/test_exception.py +0 -0
- {dissect_apfs-1.1.dev1 → dissect_apfs-1.1.dev2}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.apfs
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.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
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
3
5
|
from typing import TYPE_CHECKING, BinaryIO
|
|
4
6
|
|
|
5
7
|
from dissect.apfs.c_apfs import c_apfs
|
|
@@ -11,6 +13,9 @@ if TYPE_CHECKING:
|
|
|
11
13
|
from dissect.apfs.objects.fs import FS
|
|
12
14
|
from dissect.apfs.objects.keybag import ContainerKeybag
|
|
13
15
|
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
log.setLevel(os.getenv("DISSECT_LOG_APFS", "CRITICAL"))
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
class APFS:
|
|
16
21
|
"""Container class for APFS operations.
|
|
@@ -24,8 +29,29 @@ class APFS:
|
|
|
24
29
|
self.fh.seek(0)
|
|
25
30
|
|
|
26
31
|
self.sb = NxSuperblock.from_block(self, 0, self.fh.read(c_apfs.NX_DEFAULT_BLOCK_SIZE))
|
|
27
|
-
self.
|
|
28
|
-
|
|
32
|
+
self.sb.check()
|
|
33
|
+
|
|
34
|
+
self.sbs = sorted(
|
|
35
|
+
[obj for obj in self.sb.checkpoint_objects if isinstance(obj, NxSuperblock)],
|
|
36
|
+
key=lambda obj: obj.xid,
|
|
37
|
+
reverse=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# TODO: Do more accurate checkpoint traversal
|
|
41
|
+
for sb in self.sbs:
|
|
42
|
+
try:
|
|
43
|
+
sb.check()
|
|
44
|
+
sb.compare(self.sb)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
log.debug("Skipping superblock xid=%d: %s", sb.xid, e)
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
if not sb.omap.is_valid():
|
|
50
|
+
log.debug("Skipping superblock xid=%d: invalid OMAP", sb.xid)
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
self.sb = sb
|
|
54
|
+
break
|
|
29
55
|
|
|
30
56
|
@property
|
|
31
57
|
def block_size(self) -> int:
|
|
@@ -29,8 +29,16 @@ class NxSuperblock(Object):
|
|
|
29
29
|
__struct__ = c_apfs.nx_superblock
|
|
30
30
|
object: c_apfs.nx_superblock
|
|
31
31
|
|
|
32
|
-
def
|
|
33
|
-
|
|
32
|
+
def check(self) -> None:
|
|
33
|
+
"""Check the validity of the superblock."""
|
|
34
|
+
if not self.is_valid():
|
|
35
|
+
raise Error("Invalid nx_superblock checksum")
|
|
36
|
+
|
|
37
|
+
if self.type != c_apfs.OBJECT_TYPE.NX_SUPERBLOCK:
|
|
38
|
+
raise Error("Invalid nx_superblock type")
|
|
39
|
+
|
|
40
|
+
if not self.is_ephemeral:
|
|
41
|
+
raise Error("Invalid nx_superblock storage type")
|
|
34
42
|
|
|
35
43
|
if self.object.nx_magic.to_bytes(4, "big") != c_apfs.NX_MAGIC:
|
|
36
44
|
raise Error(
|
|
@@ -38,6 +46,24 @@ class NxSuperblock(Object):
|
|
|
38
46
|
f"(expected {c_apfs.NX_MAGIC!r}, got {self.object.nx_magic.to_bytes(4, 'big')!r})"
|
|
39
47
|
)
|
|
40
48
|
|
|
49
|
+
def compare(self, other: NxSuperblock) -> None:
|
|
50
|
+
"""Compare this superblock to another superblock."""
|
|
51
|
+
if self.header.o_xid < other.header.o_xid:
|
|
52
|
+
raise Error("Lower xid than other superblock")
|
|
53
|
+
|
|
54
|
+
for attr in (
|
|
55
|
+
"nx_uuid",
|
|
56
|
+
"nx_fusion_uuid",
|
|
57
|
+
"nx_block_size",
|
|
58
|
+
"nx_block_count",
|
|
59
|
+
"nx_xp_desc_blocks",
|
|
60
|
+
"nx_xp_data_blocks",
|
|
61
|
+
"nx_xp_desc_base",
|
|
62
|
+
"nx_xp_data_base",
|
|
63
|
+
):
|
|
64
|
+
if getattr(self.object, attr) != getattr(other.object, attr):
|
|
65
|
+
raise Error(f"Mismatch on {attr}")
|
|
66
|
+
|
|
41
67
|
@cached_property
|
|
42
68
|
def block_size(self) -> int:
|
|
43
69
|
"""The block size of the container."""
|
|
@@ -66,14 +92,20 @@ class NxSuperblock(Object):
|
|
|
66
92
|
@cached_property
|
|
67
93
|
def checkpoint_objects(self) -> list[CheckpointMap | NxSuperblock]:
|
|
68
94
|
"""All checkpoint objects in the container."""
|
|
69
|
-
|
|
95
|
+
# TODO: Rework this a bit to be more accurate
|
|
96
|
+
return list(
|
|
97
|
+
_read_checkpoint_objects(self.container, self.object.nx_xp_desc_base, self.object.nx_xp_desc_blocks)
|
|
98
|
+
)
|
|
70
99
|
|
|
71
100
|
@cached_property
|
|
72
101
|
def ephemeral_objects(self) -> dict[int, Object]:
|
|
73
102
|
"""All ephemeral objects in the container."""
|
|
103
|
+
# TODO: I don't think this is correct
|
|
74
104
|
return {
|
|
75
105
|
obj.oid: obj
|
|
76
|
-
for obj in _read_checkpoint_objects(
|
|
106
|
+
for obj in _read_checkpoint_objects(
|
|
107
|
+
self.container, self.object.nx_xp_data_base, self.object.nx_xp_data_blocks
|
|
108
|
+
)
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
@cached_property
|
|
@@ -20,6 +20,15 @@ class ObjectMap(Object):
|
|
|
20
20
|
|
|
21
21
|
self.lookup = lru_cache(128)(self.lookup)
|
|
22
22
|
|
|
23
|
+
def is_valid(self) -> bool:
|
|
24
|
+
return (
|
|
25
|
+
super().is_valid()
|
|
26
|
+
and self.type == c_apfs.OBJECT_TYPE.OMAP
|
|
27
|
+
and self.subtype == 0
|
|
28
|
+
and self.is_physical
|
|
29
|
+
and self.oid == self.address
|
|
30
|
+
)
|
|
31
|
+
|
|
23
32
|
@cached_property
|
|
24
33
|
def btree(self) -> BTree:
|
|
25
34
|
"""The B-tree of the object map."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.apfs
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.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
|
|
@@ -49,6 +49,7 @@ tests/_data/case_insensitive.bin.gz
|
|
|
49
49
|
tests/_data/case_insensitive_beta.bin.gz
|
|
50
50
|
tests/_data/case_sensitive.bin.gz
|
|
51
51
|
tests/_data/case_sensitive_beta.bin.gz
|
|
52
|
+
tests/_data/corrupt.bin.gz
|
|
52
53
|
tests/_data/encrypted.bin.gz
|
|
53
54
|
tests/_data/jhfs_converted.bin.gz
|
|
54
55
|
tests/_data/jhfs_encrypted.bin.gz
|
|
Binary file
|
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from dissect.apfs.apfs import APFS
|
|
9
|
+
from dissect.apfs.apfs import APFS, log
|
|
10
10
|
from dissect.apfs.c_apfs import c_apfs
|
|
11
11
|
from tests.conftest import absolute_path
|
|
12
12
|
|
|
@@ -357,3 +357,15 @@ def test_snapshots() -> None:
|
|
|
357
357
|
for i, snapshot in enumerate(volume.snapshots):
|
|
358
358
|
assert snapshot.name == f"Snapshot {i}"
|
|
359
359
|
assert snapshot.open().get("file").open().read() == f"Snapshot {i}\n".encode()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_corrupt_checkpoints(caplog: pytest.LogCaptureFixture) -> None:
|
|
363
|
+
"""Test APFS volumes with corrupt checkpoints."""
|
|
364
|
+
with gzip.open(absolute_path("_data/corrupt.bin.gz"), "rb") as fh, caplog.at_level("DEBUG", log.name):
|
|
365
|
+
container = APFS(fh)
|
|
366
|
+
|
|
367
|
+
assert container.sb.xid == 302
|
|
368
|
+
assert len(container.volumes) == 1
|
|
369
|
+
|
|
370
|
+
assert caplog.messages[0] == "Skipping superblock xid=304: invalid OMAP"
|
|
371
|
+
assert caplog.messages[1] == "Skipping superblock xid=303: Invalid nx_superblock checksum"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|