dissect.apfs 0.1.dev2__tar.gz → 1.0.1__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.
Files changed (62) hide show
  1. {dissect_apfs-0.1.dev2/dissect.apfs.egg-info → dissect_apfs-1.0.1}/PKG-INFO +2 -2
  2. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/apfs.py +28 -2
  3. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/nx_superblock.py +36 -4
  4. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/omap.py +9 -0
  5. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1/dissect.apfs.egg-info}/PKG-INFO +2 -2
  6. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect.apfs.egg-info/SOURCES.txt +1 -0
  7. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect.apfs.egg-info/requires.txt +1 -1
  8. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/pyproject.toml +1 -2
  9. dissect_apfs-1.0.1/tests/_data/corrupt.bin.gz +0 -0
  10. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/test_apfs.py +13 -1
  11. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/COPYRIGHT +0 -0
  12. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/LICENSE +0 -0
  13. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/MANIFEST.in +0 -0
  14. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/README.md +0 -0
  15. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/__init__.py +0 -0
  16. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/c_apfs.py +0 -0
  17. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/c_apfs.pyi +0 -0
  18. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/cursor.py +0 -0
  19. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/exception.py +0 -0
  20. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/__init__.py +0 -0
  21. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/base.py +0 -0
  22. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/btree.py +0 -0
  23. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/btree_node.py +0 -0
  24. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/checkpoint_map.py +0 -0
  25. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/efi_jumpstart.py +0 -0
  26. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/er_recovery_block.py +0 -0
  27. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/er_state.py +0 -0
  28. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/fs.py +0 -0
  29. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/gbitmap.py +0 -0
  30. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/gbitmap_block.py +0 -0
  31. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/integrity_meta.py +0 -0
  32. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/keybag.py +0 -0
  33. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/nx_fusion_wbc.py +0 -0
  34. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/nx_fusion_wbc_list.py +0 -0
  35. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/nx_reap_list.py +0 -0
  36. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/nx_reaper.py +0 -0
  37. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/snap_meta_ext.py +0 -0
  38. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/spaceman.py +0 -0
  39. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/spaceman_bitmap.py +0 -0
  40. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/spaceman_cab.py +0 -0
  41. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/objects/spaceman_cib.py +0 -0
  42. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/stream.py +0 -0
  43. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect/apfs/util.py +0 -0
  44. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect.apfs.egg-info/dependency_links.txt +0 -0
  45. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/dissect.apfs.egg-info/top_level.txt +0 -0
  46. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/setup.cfg +0 -0
  47. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/__init__.py +0 -0
  48. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_data/case_insensitive.bin.gz +0 -0
  49. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_data/case_insensitive_beta.bin.gz +0 -0
  50. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_data/case_sensitive.bin.gz +0 -0
  51. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_data/case_sensitive_beta.bin.gz +0 -0
  52. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_data/encrypted.bin.gz +0 -0
  53. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_data/jhfs_converted.bin.gz +0 -0
  54. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_data/jhfs_encrypted.bin.gz +0 -0
  55. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_data/snapshot.bin.gz +0 -0
  56. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_docs/Makefile +0 -0
  57. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_docs/__init__.py +0 -0
  58. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_docs/conf.py +0 -0
  59. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/_docs/index.rst +0 -0
  60. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/conftest.py +0 -0
  61. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tests/test_exception.py +0 -0
  62. {dissect_apfs-0.1.dev2 → dissect_apfs-1.0.1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.apfs
3
- Version: 0.1.dev2
3
+ Version: 1.0.1
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
@@ -23,7 +23,7 @@ License-File: LICENSE
23
23
  License-File: COPYRIGHT
24
24
  Requires-Dist: dissect.cstruct<5,>=4
25
25
  Requires-Dist: dissect.fve<5,>=4.2
26
- Requires-Dist: dissect.util<4,>=3.23.dev
26
+ Requires-Dist: dissect.util<4,>=3.23
27
27
  Requires-Dist: asn1crypto
28
28
  Requires-Dist: pycryptodome
29
29
  Provides-Extra: dev
@@ -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.sbs = [self.sb] + [obj for obj in self.sb.checkpoint_objects if isinstance(obj, NxSuperblock)]
28
- self.sb = sorted(self.sbs, key=lambda obj: obj.xid)[-1]
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 __init__(self, *args, **kwargs):
33
- super().__init__(*args, **kwargs)
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
- return list(_read_checkpoint_objects(self.container, self.object.nx_xp_desc_base, self.object.nx_xp_desc_len))
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(self.container, self.object.nx_xp_data_base, self.object.nx_xp_data_len)
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: 0.1.dev2
3
+ Version: 1.0.1
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
@@ -23,7 +23,7 @@ License-File: LICENSE
23
23
  License-File: COPYRIGHT
24
24
  Requires-Dist: dissect.cstruct<5,>=4
25
25
  Requires-Dist: dissect.fve<5,>=4.2
26
- Requires-Dist: dissect.util<4,>=3.23.dev
26
+ Requires-Dist: dissect.util<4,>=3.23
27
27
  Requires-Dist: asn1crypto
28
28
  Requires-Dist: pycryptodome
29
29
  Provides-Extra: dev
@@ -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
@@ -1,6 +1,6 @@
1
1
  dissect.cstruct<5,>=4
2
2
  dissect.fve<5,>=4.2
3
- dissect.util<4,>=3.23.dev
3
+ dissect.util<4,>=3.23
4
4
  asn1crypto
5
5
  pycryptodome
6
6
 
@@ -27,7 +27,7 @@ classifiers = [
27
27
  dependencies = [
28
28
  "dissect.cstruct>=4,<5",
29
29
  "dissect.fve>=4.2,<5",
30
- "dissect.util>=3.23.dev,<4", # TODO: update on release!
30
+ "dissect.util>=3.23,<4",
31
31
  "asn1crypto",
32
32
  "pycryptodome",
33
33
  ]
@@ -122,4 +122,3 @@ known-third-party = ["dissect"]
122
122
  include = ["dissect.*"]
123
123
 
124
124
  [tool.setuptools_scm]
125
- local_scheme = "no-local-version"
@@ -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