Skip to content

Commit

Permalink
Changed APFS container back-end to support apfs{GUID} alias #739 (#740)
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimmetz authored May 27, 2023
1 parent 38f8979 commit 9e76951
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 119 deletions.
32 changes: 0 additions & 32 deletions dfvfs/lib/apfs_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,6 @@
"""Helper functions for Apple File System (APFS) support."""


_APFS_LOCATION_PREFIX = '/apfs'
_APFS_LOCATION_PREFIX_LENGTH = len(_APFS_LOCATION_PREFIX)


def APFSContainerPathSpecGetVolumeIndex(path_spec):
"""Retrieves the volume index from the path specification.
Args:
path_spec (PathSpec): path specification.
Returns:
int: volume index or None if the index cannot be determined.
"""
volume_index = getattr(path_spec, 'volume_index', None)
if volume_index is not None:
return volume_index

location = getattr(path_spec, 'location', None)
if location is None or not location.startswith(_APFS_LOCATION_PREFIX):
return None

try:
volume_index = int(location[_APFS_LOCATION_PREFIX_LENGTH:], 10) - 1
except (TypeError, ValueError):
volume_index = None

if volume_index is None or volume_index < 0 or volume_index > 99:
volume_index = None

return volume_index


def APFSUnlockVolume(fsapfs_volume, path_spec, key_chain):
"""Unlocks an APFS volume using the path specification.
Expand Down
29 changes: 12 additions & 17 deletions dfvfs/vfs/apfs_container_directory.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
"""The APFS container directory implementation."""

from dfvfs.lib import apfs_helper
from dfvfs.path import apfs_container_path_spec
from dfvfs.vfs import directory

Expand All @@ -19,19 +18,15 @@ def _EntriesGenerator(self):
APFSContainerPathSpec: a path specification.
"""
# Only the virtual root file has directory entries.
volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(
self.path_spec)
if volume_index is not None:
return

location = getattr(self.path_spec, 'location', None)
if location is None or location != self._file_system.LOCATION_ROOT:
return

fsapfs_container = self._file_system.GetAPFSContainer()

for volume_index in range(0, fsapfs_container.number_of_volumes):
apfs_volume_index = volume_index + 1
yield apfs_container_path_spec.APFSContainerPathSpec(
location=f'/apfs{apfs_volume_index:d}', parent=self.path_spec.parent,
volume_index=volume_index)
volume_index = self._file_system.GetVolumeIndexByPathSpec(self.path_spec)
if volume_index is None:
location = getattr(self.path_spec, 'location', None)
if location and location == self._file_system.LOCATION_ROOT:
fsapfs_container = self._file_system.GetAPFSContainer()

for volume_index in range(0, fsapfs_container.number_of_volumes):
apfs_volume_index = volume_index + 1
apfs_volume_location = f'/apfs{apfs_volume_index:d}'
yield apfs_container_path_spec.APFSContainerPathSpec(
location=apfs_volume_location, parent=self.path_spec.parent,
volume_index=volume_index)
28 changes: 14 additions & 14 deletions dfvfs/vfs/apfs_container_file_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class APFSContainerFileEntry(file_entry.FileEntry):

def __init__(
self, resolver_context, file_system, path_spec, is_root=False,
is_virtual=False):
is_virtual=False, volume_index=None):
"""Initializes a file entry.
Args:
Expand All @@ -27,6 +27,7 @@ def __init__(
of the corresponding file system.
is_virtual (Optional[bool]): True if the file entry is a virtual file
entry emulated by the corresponding file system.
volume_index (Optional[int]): volume index or None.
Raises:
BackEndError: when the fsapfs volume is missing in a non-virtual
Expand All @@ -42,6 +43,7 @@ def __init__(
is_virtual=is_virtual)
self._name = None
self._fsapfs_volume = fsapfs_volume
self._volume_index = volume_index

if self._is_virtual:
self.entry_type = definitions.FILE_ENTRY_TYPE_DIRECTORY
Expand Down Expand Up @@ -80,17 +82,17 @@ def _GetSubFileEntries(self):
def name(self):
"""str: name of the file entry, which does not include the full path."""
if self._name is None:
location = getattr(self.path_spec, 'location', None)
if location is not None:
self._name = self._file_system.BasenamePath(location)
self._name = ''

# Prefer generating the name seeing that the APFS container back-end
# supports aliases, such as "/apfs1" and "/apfs{UUID}".
if self._volume_index is not None:
apfs_volume_index = self._volume_index + 1
self._name = f'apfs{apfs_volume_index:d}'
else:
volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(
self.path_spec)
if volume_index is not None:
apfs_volume_index = volume_index + 1
self._name = f'apfs{apfs_volume_index:d}'
else:
self._name = ''
location = getattr(self.path_spec, 'location', None)
if location is not None:
self._name = self._file_system.BasenamePath(location)

return self._name

Expand Down Expand Up @@ -122,9 +124,7 @@ def GetParentFileEntry(self):
Returns:
APFSContainerFileEntry: parent file entry or None if not available.
"""
volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(
self.path_spec)
if volume_index is None:
if self._volume_index is None:
return None

return self._file_system.GetRootFileEntry()
Expand Down
48 changes: 43 additions & 5 deletions dfvfs/vfs/apfs_container_file_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import pyfsapfs

from dfvfs.lib import apfs_helper
from dfvfs.lib import definitions
from dfvfs.lib import errors
from dfvfs.path import apfs_container_path_spec
Expand All @@ -17,6 +16,8 @@ class APFSContainerFileSystem(file_system.FileSystem):

TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS_CONTAINER

_APFS_LOCATION_PREFIX = '/apfs'

def __init__(self, resolver_context, path_spec):
"""Initializes an APFS container file system.
Expand Down Expand Up @@ -72,7 +73,7 @@ def FileEntryExistsByPathSpec(self, path_spec):
Returns:
bool: True if the file entry exists.
"""
volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
volume_index = self.GetVolumeIndexByPathSpec(path_spec)

# The virtual root file has no corresponding volume index but
# should have a location.
Expand All @@ -99,7 +100,7 @@ def GetAPFSVolumeByPathSpec(self, path_spec):
Returns:
pyfsapfs.volume: an APFS volume or None if not available.
"""
volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
volume_index = self.GetVolumeIndexByPathSpec(path_spec)
if volume_index is None:
return None

Expand All @@ -114,7 +115,7 @@ def GetFileEntryByPathSpec(self, path_spec):
Returns:
APFSContainerFileEntry: a file entry or None if not exists.
"""
volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
volume_index = self.GetVolumeIndexByPathSpec(path_spec)

# The virtual root file has no corresponding volume index but
# should have a location.
Expand All @@ -132,7 +133,7 @@ def GetFileEntryByPathSpec(self, path_spec):
return None

return apfs_container_file_entry.APFSContainerFileEntry(
self._resolver_context, self, path_spec)
self._resolver_context, self, path_spec, volume_index=volume_index)

def GetRootFileEntry(self):
"""Retrieves the root file entry.
Expand All @@ -143,3 +144,40 @@ def GetRootFileEntry(self):
path_spec = apfs_container_path_spec.APFSContainerPathSpec(
location=self.LOCATION_ROOT, parent=self._path_spec.parent)
return self.GetFileEntryByPathSpec(path_spec)

def GetVolumeIndexByPathSpec(self, path_spec):
"""Retrieves the volume index for a path specification.
Args:
path_spec (PathSpec): path specification.
Returns:
int: volume index or None if the index cannot be determined.
"""
volume_index = getattr(path_spec, 'volume_index', None)
if volume_index is not None:
return volume_index

location = getattr(path_spec, 'location', None)
if location is None or location[:5] != self._APFS_LOCATION_PREFIX:
return None

volume_index = None

if len(location) > 6 and location[5] == '{' and location[-1] == '}':
for index, fsapfs_volume in enumerate(
self._fsapfs_container.volumes):
if fsapfs_volume.identifier == location[6:-1]:
volume_index = index
break

else:
try:
volume_index = int(location[5:], 10) - 1
except (TypeError, ValueError):
pass

if volume_index is None or volume_index < 0 or volume_index > 99:
volume_index = None

return volume_index
50 changes: 0 additions & 50 deletions tests/lib/apfs_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

from dfvfs.lib import apfs_helper
from dfvfs.lib import definitions
from dfvfs.path import apfs_container_path_spec
from dfvfs.path import factory as path_spec_factory
from dfvfs.path import fake_path_spec
from dfvfs.resolver import context
from dfvfs.resolver import resolver

Expand All @@ -20,54 +18,6 @@ class APFSContainerHelperTest(shared_test_lib.BaseTestCase):

_APFS_PASSWORD = 'apfs-TEST'

def testAPFSContainerPathSpecGetVolumeIndex(self):
"""Tests the APFSContainerPathSpecGetVolumeIndex function."""
test_fake_path_spec = fake_path_spec.FakePathSpec(location='/')

path_spec = apfs_container_path_spec.APFSContainerPathSpec(
parent=test_fake_path_spec)

self.assertIsNotNone(path_spec)

volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
self.assertIsNone(volume_index)

path_spec = apfs_container_path_spec.APFSContainerPathSpec(
location='/apfs2', parent=test_fake_path_spec)

self.assertIsNotNone(path_spec)

volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
self.assertEqual(volume_index, 1)

path_spec = apfs_container_path_spec.APFSContainerPathSpec(
volume_index=1, parent=test_fake_path_spec)

self.assertIsNotNone(path_spec)

volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
self.assertEqual(volume_index, 1)

path_spec = apfs_container_path_spec.APFSContainerPathSpec(
location='/apfs2', volume_index=1, parent=test_fake_path_spec)

self.assertIsNotNone(path_spec)

volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
self.assertEqual(volume_index, 1)

path_spec = apfs_container_path_spec.APFSContainerPathSpec(
location='/apfs', parent=test_fake_path_spec)

volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
self.assertIsNone(volume_index)

path_spec = apfs_container_path_spec.APFSContainerPathSpec(
location='/apfs101', parent=test_fake_path_spec)

volume_index = apfs_helper.APFSContainerPathSpecGetVolumeIndex(path_spec)
self.assertIsNone(volume_index)

def testAPFSUnlockVolumeOnAPFS(self):
"""Tests the APFSUnlockVolume function on an APFS image."""
resolver_context = context.Context()
Expand Down
Loading

0 comments on commit 9e76951

Please sign in to comment.