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.
Files changed (63) hide show
  1. {dissect_apfs-1.1.dev2/dissect.apfs.egg-info → dissect_apfs-1.2.dev2}/PKG-INFO +1 -1
  2. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/__init__.py +2 -0
  3. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/c_apfs.py +2 -0
  4. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/exception.py +3 -0
  5. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/fs.py +9 -9
  6. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/keybag.py +0 -1
  7. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/stream.py +14 -10
  8. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2/dissect.apfs.egg-info}/PKG-INFO +1 -1
  9. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect.apfs.egg-info/SOURCES.txt +1 -0
  10. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/pyproject.toml +20 -2
  11. dissect_apfs-1.2.dev2/tests/_data/large.bin.gz +0 -0
  12. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_docs/conf.py +2 -0
  13. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/test_apfs.py +35 -0
  14. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/COPYRIGHT +0 -0
  15. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/LICENSE +0 -0
  16. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/MANIFEST.in +0 -0
  17. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/README.md +0 -0
  18. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/apfs.py +0 -0
  19. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/c_apfs.pyi +0 -0
  20. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/cursor.py +0 -0
  21. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/__init__.py +0 -0
  22. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/base.py +0 -0
  23. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/btree.py +0 -0
  24. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/btree_node.py +0 -0
  25. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/checkpoint_map.py +0 -0
  26. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/efi_jumpstart.py +0 -0
  27. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/er_recovery_block.py +0 -0
  28. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/er_state.py +0 -0
  29. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/gbitmap.py +0 -0
  30. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/gbitmap_block.py +0 -0
  31. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/integrity_meta.py +0 -0
  32. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_fusion_wbc.py +0 -0
  33. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_fusion_wbc_list.py +0 -0
  34. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_reap_list.py +0 -0
  35. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_reaper.py +0 -0
  36. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/nx_superblock.py +0 -0
  37. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/omap.py +0 -0
  38. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/snap_meta_ext.py +0 -0
  39. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/spaceman.py +0 -0
  40. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/spaceman_bitmap.py +0 -0
  41. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/spaceman_cab.py +0 -0
  42. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/objects/spaceman_cib.py +0 -0
  43. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect/apfs/util.py +0 -0
  44. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect.apfs.egg-info/dependency_links.txt +0 -0
  45. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect.apfs.egg-info/requires.txt +0 -0
  46. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/dissect.apfs.egg-info/top_level.txt +0 -0
  47. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/setup.cfg +0 -0
  48. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/__init__.py +0 -0
  49. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/case_insensitive.bin.gz +0 -0
  50. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/case_insensitive_beta.bin.gz +0 -0
  51. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/case_sensitive.bin.gz +0 -0
  52. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/case_sensitive_beta.bin.gz +0 -0
  53. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/corrupt.bin.gz +0 -0
  54. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/encrypted.bin.gz +0 -0
  55. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/jhfs_converted.bin.gz +0 -0
  56. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/jhfs_encrypted.bin.gz +0 -0
  57. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_data/snapshot.bin.gz +0 -0
  58. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_docs/Makefile +0 -0
  59. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_docs/__init__.py +0 -0
  60. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/_docs/index.rst +0 -0
  61. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/conftest.py +0 -0
  62. {dissect_apfs-1.1.dev2 → dissect_apfs-1.2.dev2}/tests/test_exception.py +0 -0
  63. {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.1.dev2
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
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dissect.apfs.apfs import APFS
2
4
  from dissect.apfs.exception import (
3
5
  Error,
@@ -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 = """
@@ -1,3 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+
1
4
  class Error(Exception):
2
5
  pass
3
6
 
@@ -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 << 12)
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 << 12)
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 << 12)
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 << 12)
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 << 12)
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 << 12)
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 << 12)
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 << 12)
831
+ return stat.S_ISWHT(self.type)
832
832
 
833
833
 
834
834
  class XAttr:
@@ -292,7 +292,6 @@ def _create_cipher(key: bytes, iv: bytes = b"\x00" * 16, mode: str = "cbc") -> A
292
292
 
293
293
  Dynamic based on the available crypto module.
294
294
  """
295
-
296
295
  if HAS_PYSTANDALONE:
297
296
  key_size = len(key)
298
297
  if key_size not in (32, 24, 16):
@@ -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
- block = self.volume.container._read_block(physical_address, extent_length // self.align)
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
- block = self.volume._cipher.decrypt(block, crypto_id * self.volume.container.sectors_per_block)
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 += min(extent_length, length)
110
- length -= min(extent_length, 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.1.dev2
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
@@ -53,6 +53,7 @@ tests/_data/corrupt.bin.gz
53
53
  tests/_data/encrypted.bin.gz
54
54
  tests/_data/jhfs_converted.bin.gz
55
55
  tests/_data/jhfs_encrypted.bin.gz
56
+ tests/_data/large.bin.gz
56
57
  tests/_data/snapshot.bin.gz
57
58
  tests/_docs/Makefile
58
59
  tests/_docs/__init__.py
@@ -99,7 +99,7 @@ select = [
99
99
  "SLOT",
100
100
  "SIM",
101
101
  "TID",
102
- "TCH",
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 = ["E203", "B904", "UP024", "ANN002", "ANN003", "ANN204", "ANN401", "SIM105", "TRY003"]
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.*"]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  project = "dissect.apfs"
2
4
 
3
5
  extensions = [
@@ -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