diff --git a/.pylintrc b/.pylintrc index 0bd6276c..97a8fcaa 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code -extension-pkg-whitelist=pybde,pyewf,pyfsntfs,pyfvde,pyfwnt,pyqcow,pysigscan,pysmdev,pysmraw,pytsk3,pyvhdi,pyvmdk,pyvshadow,pyvslvm +extension-pkg-whitelist=pybde,pyewf,pyfsapfs,pyfsntfs,pyfvde,pyfwnt,pyqcow,pysigscan,pysmdev,pysmraw,pytsk3,pyvhdi,pyvmdk,pyvshadow,pyvslvm # Add files or directories to the blacklist. They should be base names, not # paths. diff --git a/appveyor.yml b/appveyor.yml index 71f346d1..d438ffc3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,11 +26,11 @@ install: - cmd: if [%TARGET%]==[python27] ( mkdir dependencies && set PYTHONPATH=..\l2tdevtools && - "%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type %MACHINE_TYPE% --msi-targetdir "%PYTHON%" --track dev PyYAML dfdatetime dtfabric funcsigs libbde libewf libfsntfs libfvde libfwnt libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvshadow libvslvm mock pbr pycrypto pysqlite pytsk3 six ) + "%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type %MACHINE_TYPE% --msi-targetdir "%PYTHON%" --track dev PyYAML dfdatetime dtfabric funcsigs libbde libewf libfsapfs libfsntfs libfvde libfwnt libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvshadow libvslvm mock pbr pycrypto pysqlite pytsk3 six ) - cmd: if [%TARGET%]==[python36] ( mkdir dependencies && set PYTHONPATH=..\l2tdevtools && - "%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type %MACHINE_TYPE% --msi-targetdir "%PYTHON%" --track dev PyYAML dfdatetime dtfabric funcsigs libbde libewf libfsntfs libfvde libfwnt libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvshadow libvslvm mock pbr pycrypto pytsk3 six ) + "%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type %MACHINE_TYPE% --msi-targetdir "%PYTHON%" --track dev PyYAML dfdatetime dtfabric funcsigs libbde libewf libfsapfs libfsntfs libfvde libfwnt libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvshadow libvslvm mock pbr pycrypto pytsk3 six ) build: off diff --git a/config/dpkg/control b/config/dpkg/control index 8ea20cf3..92eff89f 100644 --- a/config/dpkg/control +++ b/config/dpkg/control @@ -10,7 +10,7 @@ Homepage: https://github.com/log2timeline/dfvfs Package: python-dfvfs Architecture: all -Depends: libbde-python (>= 20140531), libewf-python (>= 20131210), libfsntfs-python (>= 20151130), libfvde-python (>= 20160719), libfwnt-python (>= 20160418), libqcow-python (>= 20131204), libsigscan-python (>= 20150627), libsmdev-python (>= 20140529), libsmraw-python (>= 20140612), libvhdi-python (>= 20131210), libvmdk-python (>= 20140421), libvshadow-python (>= 20160109), libvslvm-python (>= 20160109), python-backports.lzma, python-crypto (>= 2.6), python-dfdatetime (>= 20180324), python-dtfabric (>= 20170524), python-pysqlite2, python-pytsk3 (>= 20160721), python-yaml (>= 3.10), ${python:Depends}, ${misc:Depends} +Depends: libbde-python (>= 20140531), libewf-python (>= 20131210), libfsapfs-python (>= 20181110), libfsntfs-python (>= 20151130), libfvde-python (>= 20160719), libfwnt-python (>= 20160418), libqcow-python (>= 20131204), libsigscan-python (>= 20150627), libsmdev-python (>= 20140529), libsmraw-python (>= 20140612), libvhdi-python (>= 20131210), libvmdk-python (>= 20140421), libvshadow-python (>= 20160109), libvslvm-python (>= 20160109), python-backports.lzma, python-crypto (>= 2.6), python-dfdatetime (>= 20181025), python-dtfabric (>= 20170524), python-pysqlite2, python-pytsk3 (>= 20160721), python-yaml (>= 3.10), ${python:Depends}, ${misc:Depends} Description: Python 2 module of dfVFS dfVFS, or Digital Forensics Virtual File System, provides read-only access to file-system objects from various storage media types and file formats. The goal @@ -20,7 +20,7 @@ Description: Python 2 module of dfVFS Package: python3-dfvfs Architecture: all -Depends: libbde-python3 (>= 20140531), libewf-python3 (>= 20131210), libfsntfs-python3 (>= 20151130), libfvde-python3 (>= 20160719), libfwnt-python3 (>= 20160418), libqcow-python3 (>= 20131204), libsigscan-python3 (>= 20150627), libsmdev-python3 (>= 20140529), libsmraw-python3 (>= 20140612), libvhdi-python3 (>= 20131210), libvmdk-python3 (>= 20140421), libvshadow-python3 (>= 20160109), libvslvm-python3 (>= 20160109), python3-crypto (>= 2.6), python3-dfdatetime (>= 20180324), python3-dtfabric (>= 20170524), python3-pytsk3 (>= 20160721), python3-yaml (>= 3.10), ${python3:Depends}, ${misc:Depends} +Depends: libbde-python3 (>= 20140531), libewf-python3 (>= 20131210), libfsapfs-python3 (>= 20181110), libfsntfs-python3 (>= 20151130), libfvde-python3 (>= 20160719), libfwnt-python3 (>= 20160418), libqcow-python3 (>= 20131204), libsigscan-python3 (>= 20150627), libsmdev-python3 (>= 20140529), libsmraw-python3 (>= 20140612), libvhdi-python3 (>= 20131210), libvmdk-python3 (>= 20140421), libvshadow-python3 (>= 20160109), libvslvm-python3 (>= 20160109), python3-crypto (>= 2.6), python3-dfdatetime (>= 20181025), python3-dtfabric (>= 20170524), python3-pytsk3 (>= 20160721), python3-yaml (>= 3.10), ${python3:Depends}, ${misc:Depends} Description: Python 3 module of dfVFS dfVFS, or Digital Forensics Virtual File System, provides read-only access to file-system objects from various storage media types and file formats. The goal diff --git a/config/linux/gift_copr_install.sh b/config/linux/gift_copr_install.sh index c7853128..76c88e55 100644 --- a/config/linux/gift_copr_install.sh +++ b/config/linux/gift_copr_install.sh @@ -11,6 +11,7 @@ set -e PYTHON2_DEPENDENCIES="PyYAML libbde-python libewf-python + libfsapfs-python2 libfsntfs-python libfvde-python libfwnt-python @@ -40,6 +41,8 @@ DEBUG_DEPENDENCIES="libbde-debuginfo libbde-python-debuginfo libewf-debuginfo libewf-python-debuginfo + libfsapfs-debuginfo + libfsapfs-python2-debuginfo libfsntfs-debuginfo libfsntfs-python-debuginfo libfvde-debuginfo diff --git a/config/linux/gift_ppa_install.sh b/config/linux/gift_ppa_install.sh index 1ae481cd..c5102242 100755 --- a/config/linux/gift_ppa_install.sh +++ b/config/linux/gift_ppa_install.sh @@ -10,6 +10,7 @@ set -e # This should not include packages only required for testing or development. PYTHON2_DEPENDENCIES="libbde-python libewf-python + libfsapfs-python libfsntfs-python libfvde-python libfwnt-python @@ -40,6 +41,8 @@ DEBUG_DEPENDENCIES="libbde-dbg libbde-python-dbg libewf-dbg libewf-python-dbg + libfsapfs-dbg + libfsapfs-python-dbg libfsntfs-dbg libfsntfs-python-dbg libfvde-dbg diff --git a/config/linux/gift_ppa_install_py3.sh b/config/linux/gift_ppa_install_py3.sh index 6039c230..74316cf8 100644 --- a/config/linux/gift_ppa_install_py3.sh +++ b/config/linux/gift_ppa_install_py3.sh @@ -10,6 +10,7 @@ set -e # This should not include packages only required for testing or development. PYTHON3_DEPENDENCIES="libbde-python3 libewf-python3 + libfsapfs-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 @@ -38,6 +39,8 @@ DEBUG_DEPENDENCIES="libbde-dbg libbde-python3-dbg libewf-dbg libewf-python3-dbg + libfsapfs-dbg + libfsapfs-python3-dbg libfsntfs-dbg libfsntfs-python3-dbg libfvde-dbg diff --git a/config/travis/install.sh b/config/travis/install.sh index 92402e0e..3a19b0df 100755 --- a/config/travis/install.sh +++ b/config/travis/install.sh @@ -5,15 +5,15 @@ # This file is generated by l2tdevtools update-dependencies.py any dependency # related changes should be made in dependencies.ini. -L2TBINARIES_DEPENDENCIES="PyYAML backports.lzma dfdatetime dtfabric libbde libewf libfsntfs libfvde libfwnt libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvshadow libvslvm pycrypto pysqlite pytsk3"; +L2TBINARIES_DEPENDENCIES="PyYAML backports.lzma dfdatetime dtfabric libbde libewf libfsapfs libfsntfs libfvde libfwnt libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvshadow libvslvm pycrypto pysqlite pytsk3"; L2TBINARIES_TEST_DEPENDENCIES="funcsigs mock pbr six"; -PYTHON2_DEPENDENCIES="libbde-python libewf-python libfsntfs-python libfvde-python libfwnt-python libqcow-python libsigscan-python libsmdev-python libsmraw-python libvhdi-python libvmdk-python libvshadow-python libvslvm-python python-backports.lzma python-crypto python-dfdatetime python-dtfabric python-pysqlite2 python-pytsk3 python-yaml"; +PYTHON2_DEPENDENCIES="libbde-python libewf-python libfsapfs-python libfsntfs-python libfvde-python libfwnt-python libqcow-python libsigscan-python libsmdev-python libsmraw-python libvhdi-python libvmdk-python libvshadow-python libvslvm-python python-backports.lzma python-crypto python-dfdatetime python-dtfabric python-pysqlite2 python-pytsk3 python-yaml"; PYTHON2_TEST_DEPENDENCIES="python-coverage python-mock python-tox"; -PYTHON3_DEPENDENCIES="libbde-python3 libewf-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvshadow-python3 libvslvm-python3 python3-crypto python3-dfdatetime python3-dtfabric python3-pytsk3 python3-yaml"; +PYTHON3_DEPENDENCIES="libbde-python3 libewf-python3 libfsapfs-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvshadow-python3 libvslvm-python3 python3-crypto python3-dfdatetime python3-dtfabric python3-pytsk3 python3-yaml"; PYTHON3_TEST_DEPENDENCIES="python3-mock python3-setuptools python3-tox"; diff --git a/dependencies.ini b/dependencies.ini index 87fb7f8a..04b912db 100644 --- a/dependencies.ini +++ b/dependencies.ini @@ -8,7 +8,7 @@ version_property: __version__ [dfdatetime] dpkg_name: python-dfdatetime -minimum_version: 20180324 +minimum_version: 20181025 rpm_name: python-dfdatetime version_property: __version__ @@ -43,6 +43,14 @@ pypi_name: libewf-python rpm_name: libewf-python version_property: get_version() +[pyfsapfs] +dpkg_name: libfsapfs-python +l2tbinaries_name: libfsapfs +minimum_version: 20181110 +pypi_name: libfsapfs-python +rpm_name: libfsapfs-python2 +version_property: get_version() + [pyfsntfs] dpkg_name: libfsntfs-python l2tbinaries_name: libfsntfs diff --git a/dfvfs/file_io/apfs_file_io.py b/dfvfs/file_io/apfs_file_io.py new file mode 100644 index 00000000..69025d92 --- /dev/null +++ b/dfvfs/file_io/apfs_file_io.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +"""The APFS file-like object implementation.""" + +from __future__ import unicode_literals + +import os + +from dfvfs.file_io import file_io +from dfvfs.resolver import resolver + + +class APFSFile(file_io.FileIO): + """File-like object using pyfsapfs.""" + + def __init__(self, resolver_context): + """Initializes a file-like object. + + Args: + resolver_context (Context): resolver context. + """ + super(APFSFile, self).__init__(resolver_context) + self._file_system = None + self._fsapfs_data_stream = None + self._fsapfs_file_entry = None + + def _Close(self): + """Closes the file-like object.""" + self._fsapfs_data_stream = None + self._fsapfs_file_entry = None + + self._file_system.Close() + self._file_system = None + + def _Open(self, path_spec=None, mode='rb'): + """Opens the file-like object defined by path specification. + + Args: + path_spec (PathSpec): path specification. + mode (Optional[str]): file access mode. + + Raises: + AccessError: if the access to open the file was denied. + IOError: if the file-like object could not be opened. + OSError: if the file-like object could not be opened. + PathSpecError: if the path specification is incorrect. + ValueError: if the path specification is invalid. + """ + if not path_spec: + raise ValueError('Missing path specification.') + + data_stream = getattr(path_spec, 'data_stream', None) + + self._file_system = resolver.Resolver.OpenFileSystem( + path_spec, resolver_context=self._resolver_context) + + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + if not file_entry: + raise IOError('Unable to open file entry.') + + fsapfs_data_stream = None + fsapfs_file_entry = file_entry.GetAPFSFileEntry() + if not fsapfs_file_entry: + raise IOError('Unable to open APFS file entry.') + + if data_stream: + fsapfs_data_stream = fsapfs_file_entry.get_alternate_data_stream_by_name( + data_stream) + if not fsapfs_data_stream: + raise IOError('Unable to open data stream: {0:s}.'.format( + data_stream)) + + self._fsapfs_data_stream = fsapfs_data_stream + self._fsapfs_file_entry = fsapfs_file_entry + + # Note: that the following functions do not follow the style guide + # because they are part of the file-like object interface. + # pylint: disable=invalid-name + + def read(self, size=None): + """Reads a byte string from the file-like object at the current offset. + + The function will read a byte string of the specified size or + all of the remaining data if no size was specified. + + Args: + size (Optional[int]): number of bytes to read, where None is all + remaining data. + + Returns: + bytes: data read. + + Raises: + IOError: if the read failed. + OSError: if the read failed. + """ + if not self._is_open: + raise IOError('Not opened.') + + if self._fsapfs_data_stream: + return self._fsapfs_data_stream.read(size=size) + return self._fsapfs_file_entry.read(size=size) + + def seek(self, offset, whence=os.SEEK_SET): + """Seeks to an offset within the file-like object. + + Args: + offset (int): offset to seek to. + whence (Optional(int)): value that indicates whether offset is an absolute + or relative position within the file. + + Raises: + IOError: if the seek failed. + OSError: if the seek failed. + """ + if not self._is_open: + raise IOError('Not opened.') + + if self._fsapfs_data_stream: + self._fsapfs_data_stream.seek(offset, whence) + else: + self._fsapfs_file_entry.seek(offset, whence) + + def get_offset(self): + """Retrieves the current offset into the file-like object. + + Return: + int: current offset into the file-like object. + + Raises: + IOError: if the file-like object has not been opened. + OSError: if the file-like object has not been opened. + """ + if not self._is_open: + raise IOError('Not opened.') + + if self._fsapfs_data_stream: + return self._fsapfs_data_stream.get_offset() + return self._fsapfs_file_entry.get_offset() + + def get_size(self): + """Retrieves the size of the file-like object. + + Returns: + int: size of the file-like object data. + + Raises: + IOError: if the file-like object has not been opened. + OSError: if the file-like object has not been opened. + """ + if not self._is_open: + raise IOError('Not opened.') + + if self._fsapfs_data_stream: + return self._fsapfs_data_stream.get_size() + return self._fsapfs_file_entry.get_size() diff --git a/dfvfs/file_io/file_object_io.py b/dfvfs/file_io/file_object_io.py index 79c089bd..243d89cd 100644 --- a/dfvfs/file_io/file_object_io.py +++ b/dfvfs/file_io/file_object_io.py @@ -65,6 +65,7 @@ def _Open(self, path_spec=None, mode='rb'): if not self._file_object: raise IOError('Unable to open missing file-like object.') + # pylint: disable=redundant-returns-doc @abc.abstractmethod def _OpenFileObject(self, path_spec): """Opens the file-like object defined by path specification. diff --git a/dfvfs/lib/apfs_container.py b/dfvfs/lib/apfs_container.py new file mode 100644 index 00000000..1aef7a41 --- /dev/null +++ b/dfvfs/lib/apfs_container.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +"""Helper functions for APFS container support.""" + +from __future__ import unicode_literals + + +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'): + return None + + try: + volume_index = int(location[5:], 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 diff --git a/dfvfs/lib/definitions.py b/dfvfs/lib/definitions.py index 89a0c31a..fc3d6259 100644 --- a/dfvfs/lib/definitions.py +++ b/dfvfs/lib/definitions.py @@ -29,6 +29,8 @@ ENCRYPTION_MODE_OFB = 'ofb' # The type indicator definitions. +TYPE_INDICATOR_APFS = 'APFS' +TYPE_INDICATOR_APFS_CONTAINER = 'APFS_CONTAINER' TYPE_INDICATOR_BDE = 'BDE' TYPE_INDICATOR_BZIP2 = 'BZIP2' TYPE_INDICATOR_COMPRESSED_STREAM = 'COMPRESSED_STREAM' @@ -60,6 +62,7 @@ TYPE_INDICATOR_FVDE]) FILE_SYSTEM_TYPE_INDICATORS = frozenset([ + TYPE_INDICATOR_APFS, TYPE_INDICATOR_NTFS, TYPE_INDICATOR_TSK]) @@ -71,6 +74,7 @@ TYPE_INDICATOR_VMDK]) VOLUME_SYSTEM_TYPE_INDICATORS = frozenset([ + TYPE_INDICATOR_APFS_CONTAINER, TYPE_INDICATOR_LVM, TYPE_INDICATOR_TSK_PARTITION, TYPE_INDICATOR_VSHADOW]) @@ -91,6 +95,7 @@ FILE_ENTRY_TYPE_LINK = 'link' FILE_ENTRY_TYPE_SOCKET = 'socket' FILE_ENTRY_TYPE_PIPE = 'pipe' +FILE_ENTRY_TYPE_WHITEOUT = 'whiteout' # The format category definitions. FORMAT_CATEGORY_UNDEFINED = 0 diff --git a/dfvfs/lib/vshadow.py b/dfvfs/lib/vshadow.py index 424d0323..492fc362 100644 --- a/dfvfs/lib/vshadow.py +++ b/dfvfs/lib/vshadow.py @@ -24,7 +24,7 @@ def VShadowPathSpecGetStoreIndex(path_spec): store_index = None try: store_index = int(location[4:], 10) - 1 - except ValueError: + except (TypeError, ValueError): pass if store_index is None or store_index < 0: diff --git a/dfvfs/path/__init__.py b/dfvfs/path/__init__.py index 7cc88d02..30f79977 100644 --- a/dfvfs/path/__init__.py +++ b/dfvfs/path/__init__.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Imports for path specification factory.""" +from dfvfs.path import apfs_container_path_spec +from dfvfs.path import apfs_path_spec from dfvfs.path import bde_path_spec from dfvfs.path import compressed_stream_path_spec from dfvfs.path import cpio_path_spec diff --git a/dfvfs/path/apfs_container_path_spec.py b/dfvfs/path/apfs_container_path_spec.py new file mode 100644 index 00000000..d00edc3e --- /dev/null +++ b/dfvfs/path/apfs_container_path_spec.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +"""The APFS container path specification implementation.""" + +from __future__ import unicode_literals + +from dfvfs.lib import definitions +from dfvfs.path import factory +from dfvfs.path import path_spec + + +class APFSContainerPathSpec(path_spec.PathSpec): + """APFS container path specification implementation. + + Attributes: + location (str): location. + volume_index (int): volume index. + """ + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS_CONTAINER + + def __init__( + self, location=None, parent=None, volume_index=None, **kwargs): + """Initializes a path specification. + + Note that an APFS container path specification must have a parent. + + Args: + location (Optional[str]): location. + parent (Optional[PathSpec]): parent path specification. + volume_index (Optional[int]): index of the volume within the container. + + Raises: + ValueError: when parent is not set. + """ + if not parent: + raise ValueError('Missing parent value.') + + super(APFSContainerPathSpec, self).__init__(parent=parent, **kwargs) + self.location = location + self.volume_index = volume_index + + @property + def comparable(self): + """str: comparable representation of the path specification.""" + string_parts = [] + + if self.location is not None: + string_parts.append('location: {0:s}'.format(self.location)) + if self.volume_index is not None: + string_parts.append('volume index: {0:d}'.format(self.volume_index)) + + return self._GetComparable(sub_comparable_string=', '.join(string_parts)) + + +factory.Factory.RegisterPathSpec(APFSContainerPathSpec) diff --git a/dfvfs/path/apfs_path_spec.py b/dfvfs/path/apfs_path_spec.py new file mode 100644 index 00000000..b32cd913 --- /dev/null +++ b/dfvfs/path/apfs_path_spec.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +"""The APFS path specification implementation.""" + +from __future__ import unicode_literals + +from dfvfs.lib import definitions +from dfvfs.path import factory +from dfvfs.path import path_spec + + +class APFSPathSpec(path_spec.PathSpec): + """APFS path specification implementation. + + Attributes: + identifier (int): identifier. + location (str): location. + """ + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS + + def __init__( + self, identifier=None, location=None, parent=None, **kwargs): + """Initializes a path specification. + + Note that an APFS path specification must have a parent. + + Args: + identifier (Optional[int]): identifier. + location (Optional[str]): location. + parent (Optional[PathSpec]): parent path specification. + + Raises: + ValueError: when parent or both identifier and location are not set. + """ + if (not identifier and not location) or not parent: + raise ValueError('Missing identifier and location, or parent value.') + + super(APFSPathSpec, self).__init__(parent=parent, **kwargs) + self.identifier = identifier + self.location = location + + @property + def comparable(self): + """str: comparable representation of the path specification.""" + string_parts = [] + + if self.identifier is not None: + string_parts.append('identifier: {0:d}'.format(self.identifier)) + if self.location is not None: + string_parts.append('location: {0:s}'.format(self.location)) + + return self._GetComparable(sub_comparable_string=', '.join(string_parts)) + + +factory.Factory.RegisterPathSpec(APFSPathSpec) diff --git a/dfvfs/resolver_helpers/__init__.py b/dfvfs/resolver_helpers/__init__.py index 8617041c..8f1061a4 100644 --- a/dfvfs/resolver_helpers/__init__.py +++ b/dfvfs/resolver_helpers/__init__.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- """Imports for the path specification resolver.""" +from dfvfs.resolver_helpers import apfs_container_resolver_helper +from dfvfs.resolver_helpers import apfs_resolver_helper + try: from dfvfs.resolver_helpers import bde_resolver_helper except ImportError: diff --git a/dfvfs/resolver_helpers/apfs_container_resolver_helper.py b/dfvfs/resolver_helpers/apfs_container_resolver_helper.py new file mode 100644 index 00000000..f8913d0d --- /dev/null +++ b/dfvfs/resolver_helpers/apfs_container_resolver_helper.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""The APFS container path specification resolver helper implementation.""" + +from __future__ import unicode_literals + +from dfvfs.lib import definitions +from dfvfs.resolver_helpers import manager +from dfvfs.resolver_helpers import resolver_helper +from dfvfs.vfs import apfs_container_file_system + + +class APFSContainerResolverHelper(resolver_helper.ResolverHelper): + """APFS container resolver helper.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS_CONTAINER + + def NewFileSystem(self, resolver_context): + """Creates a new file system object. + + Args: + resolver_context (Context): resolver context. + + Returns: + APFSContainerFileSystem: file system. + """ + return apfs_container_file_system.APFSContainerFileSystem(resolver_context) + + +manager.ResolverHelperManager.RegisterHelper(APFSContainerResolverHelper()) diff --git a/dfvfs/resolver_helpers/apfs_resolver_helper.py b/dfvfs/resolver_helpers/apfs_resolver_helper.py new file mode 100644 index 00000000..d91f02d6 --- /dev/null +++ b/dfvfs/resolver_helpers/apfs_resolver_helper.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""The APFS path specification resolver helper implementation.""" + +from __future__ import unicode_literals + +from dfvfs.file_io import apfs_file_io +from dfvfs.lib import definitions +from dfvfs.resolver_helpers import manager +from dfvfs.resolver_helpers import resolver_helper +from dfvfs.vfs import apfs_file_system + + +class APFSResolverHelper(resolver_helper.ResolverHelper): + """APFS resolver helper.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS + + def NewFileObject(self, resolver_context): + """Creates a new file-like object. + + Args: + resolver_context (Context): resolver context. + + Returns: + FileIO: file-like object. + """ + return apfs_file_io.APFSFile(resolver_context) + + def NewFileSystem(self, resolver_context): + """Creates a new file system object. + + Args: + resolver_context (Context): resolver context. + + Returns: + FileSystem: file system. + """ + return apfs_file_system.APFSFileSystem(resolver_context) + + +manager.ResolverHelperManager.RegisterHelper(APFSResolverHelper()) diff --git a/dfvfs/vfs/apfs_container_file_entry.py b/dfvfs/vfs/apfs_container_file_entry.py new file mode 100644 index 00000000..3fb1b497 --- /dev/null +++ b/dfvfs/vfs/apfs_container_file_entry.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +"""The APFS container file entry implementation.""" + +from __future__ import unicode_literals + +from dfvfs.lib import apfs_container +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import apfs_container_path_spec +from dfvfs.vfs import file_entry + + +class APFSContainerDirectory(file_entry.Directory): + """File system directory that uses pyfsapfs.""" + + def _EntriesGenerator(self): + """Retrieves directory entries. + + Since a directory can contain a vast number of entries using + a generator is more memory efficient. + + Yields: + APFSContainerPathSpec: a path specification. + """ + # Only the virtual root file has directory entries. + volume_index = apfs_container.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): + yield apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs{0:d}'.format(volume_index + 1), + volume_index=volume_index, parent=self.path_spec.parent) + + +class APFSContainerFileEntry(file_entry.FileEntry): + """File system file entry that uses pyfsapfs.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS_CONTAINER + + def __init__( + self, resolver_context, file_system, path_spec, is_root=False, + is_virtual=False): + """Initializes a file entry. + + Args: + resolver_context (Context): resolver context. + file_system (FileSystem): file system. + path_spec (PathSpec): path specification. + is_root (Optional[bool]): True if the file entry is the root file entry + 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. + + Raises: + BackEndError: when the fsapfs volume is missing in a non-virtual + file entry. + """ + fsapfs_volume = file_system.GetAPFSVolumeByPathSpec(path_spec) + if not is_virtual and fsapfs_volume is None: + raise errors.BackEndError( + 'Missing fsapfs volume in non-virtual file entry.') + + super(APFSContainerFileEntry, self).__init__( + resolver_context, file_system, path_spec, is_root=is_root, + is_virtual=is_virtual) + self._name = None + self._fsapfs_volume = fsapfs_volume + + if self._is_virtual: + self.entry_type = definitions.FILE_ENTRY_TYPE_DIRECTORY + else: + self.entry_type = definitions.FILE_ENTRY_TYPE_FILE + + def _GetDirectory(self): + """Retrieves a directory. + + Returns: + APFSContainerDirectory: a directory or None if not available. + """ + if self.entry_type != definitions.FILE_ENTRY_TYPE_DIRECTORY: + return None + + return APFSContainerDirectory(self._file_system, self.path_spec) + + def _GetStat(self): + """Retrieves information about the file entry. + + Returns: + VFSStat: a stat object. + """ + stat_object = super(APFSContainerFileEntry, self)._GetStat() + + if self._fsapfs_volume is not None: + # File data stat information. + # TODO: implement volume size. + # stat_object.size = self._fsapfs_volume.size + pass + + # Ownership and permissions stat information. + + # File entry type stat information. + + # The root file entry is virtual and should have type directory. + return stat_object + + def _GetSubFileEntries(self): + """Retrieves a sub file entries generator. + + Yields: + APFSContainerFileEntry: a sub file entry. + """ + if self._directory is None: + self._directory = self._GetDirectory() + + if self._directory: + for path_spec in self._directory.entries: + yield APFSContainerFileEntry( + self._resolver_context, self._file_system, path_spec) + + # TODO: expose date and time values. + + @property + 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) + else: + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex( + self.path_spec) + if volume_index is not None: + self._name = 'apfs{0:d}'.format(volume_index + 1) + else: + self._name = '' + return self._name + + @property + def sub_file_entries(self): + """generator[APFSContainerFileEntry]: sub file entries.""" + return self._GetSubFileEntries() + + def GetAPFSVolume(self): + """Retrieves an APFS volume. + + Returns: + pyfsapfs.volume: an APFS volume or None if not available. + """ + return self._fsapfs_volume + + def GetParentFileEntry(self): + """Retrieves the parent file entry. + + Returns: + APFSContainerFileEntry: parent file entry or None if not available. + """ + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex( + self.path_spec) + if volume_index is None: + return None + + return self._file_system.GetRootFileEntry() diff --git a/dfvfs/vfs/apfs_container_file_system.py b/dfvfs/vfs/apfs_container_file_system.py new file mode 100644 index 00000000..15c27616 --- /dev/null +++ b/dfvfs/vfs/apfs_container_file_system.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +"""The APFS container file system implementation.""" + +from __future__ import unicode_literals + +import pyfsapfs + +from dfvfs.lib import apfs_container +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import apfs_container_path_spec +from dfvfs.resolver import resolver +from dfvfs.vfs import apfs_container_file_entry +from dfvfs.vfs import file_system + + +class APFSContainerFileSystem(file_system.FileSystem): + """APFS container file system using pyfsapfs.""" + + LOCATION_ROOT = '/' + TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS_CONTAINER + + def __init__(self, resolver_context): + """Initializes an APFS container file system. + + Args: + resolver_context (resolver.Context): resolver context. + """ + super(APFSContainerFileSystem, self).__init__(resolver_context) + self._file_object = None + self._fsapfs_container = None + + def _Close(self): + """Closes the file system. + + Raises: + IOError: if the close failed. + """ + self._fsapfs_container = None + + self._file_object.close() + self._file_object = None + + def _Open(self, path_spec, mode='rb'): + """Opens the file system defined by path specification. + + Args: + path_spec (PathSpec): a path specification. + mode (Optional[str])): file access mode. The default is 'rb' read-only + binary. + + Raises: + AccessError: if the access to open the file was denied. + IOError: if the file system object could not be opened. + PathSpecError: if the path specification is incorrect. + ValueError: if the path specification is invalid. + """ + if not path_spec.HasParent(): + raise errors.PathSpecError( + 'Unsupported path specification without parent.') + + file_object = resolver.Resolver.OpenFileObject( + path_spec.parent, resolver_context=self._resolver_context) + + try: + fsapfs_container = pyfsapfs.container() + fsapfs_container.open_file_object(file_object) + except: + file_object.close() + raise + + self._file_object = file_object + self._fsapfs_container = fsapfs_container + + def FileEntryExistsByPathSpec(self, path_spec): + """Determines if a file entry for a path specification exists. + + Args: + path_spec (PathSpec): a path specification. + + Returns: + bool: True if the file entry exists. + """ + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex( + path_spec) + + # The virtual root file has not corresponding volume index but + # should have a location. + if volume_index is None: + location = getattr(path_spec, 'location', None) + return location is not None and location == self.LOCATION_ROOT + + return 0 <= volume_index < self._fsapfs_container.number_of_volumes + + def GetAPFSContainer(self): + """Retrieves the APFS container. + + Returns: + pyfsapfs.container: the APFS container. + """ + return self._fsapfs_container + + def GetAPFSVolumeByPathSpec(self, path_spec): + """Retrieves an APFS volume for a path specification. + + Args: + path_spec (PathSpec): path specification. + + Returns: + pyfsapfs.volume: an APFS volume or None if not available. + """ + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex( + path_spec) + if volume_index is None: + return None + + return self._fsapfs_container.get_volume(volume_index) + + def GetFileEntryByPathSpec(self, path_spec): + """Retrieves a file entry for a path specification. + + Args: + path_spec (PathSpec): a path specification. + + Returns: + APFSContainerFileEntry: a file entry or None if not exists. + """ + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex( + path_spec) + + # The virtual root file has not corresponding volume index but + # should have a location. + if volume_index is None: + location = getattr(path_spec, 'location', None) + if location is None or location != self.LOCATION_ROOT: + return None + + return apfs_container_file_entry.APFSContainerFileEntry( + self._resolver_context, self, path_spec, is_root=True, + is_virtual=True) + + if (volume_index < 0 or + volume_index >= self._fsapfs_container.number_of_volumes): + return None + + return apfs_container_file_entry.APFSContainerFileEntry( + self._resolver_context, self, path_spec) + + def GetRootFileEntry(self): + """Retrieves the root file entry. + + Returns: + APFSContainerFileEntry: a file entry. + """ + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location=self.LOCATION_ROOT, parent=self._path_spec.parent) + return self.GetFileEntryByPathSpec(path_spec) diff --git a/dfvfs/vfs/apfs_file_entry.py b/dfvfs/vfs/apfs_file_entry.py new file mode 100644 index 00000000..ae9fdfda --- /dev/null +++ b/dfvfs/vfs/apfs_file_entry.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +"""The APFS file entry implementation.""" + +from __future__ import unicode_literals + +from dfdatetime import apfs_time as dfdatetime_apfs_time + +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import apfs_path_spec +from dfvfs.vfs import file_entry + + +class APFSDirectory(file_entry.Directory): + """File system directory that uses pyfsapfs.""" + + def _EntriesGenerator(self): + """Retrieves directory entries. + + Since a directory can contain a vast number of entries using + a generator is more memory efficient. + + Yields: + APFSPathSpec: APFS path specification. + """ + try: + fsapfs_file_entry = self._file_system.GetAPFSFileEntryByPathSpec( + self.path_spec) + except errors.PathSpecError: + return + + location = getattr(self.path_spec, 'location', None) + + for fsapfs_sub_file_entry in fsapfs_file_entry.sub_file_entries: + directory_entry = fsapfs_sub_file_entry.name + + if location == self._file_system.PATH_SEPARATOR: + directory_entry = self._file_system.JoinPath([directory_entry]) + else: + directory_entry = self._file_system.JoinPath([ + location, directory_entry]) + + yield apfs_path_spec.APFSPathSpec( + identifier=fsapfs_sub_file_entry.identifier, location=directory_entry, + parent=self.path_spec.parent) + + +class APFSFileEntry(file_entry.FileEntry): + """File system file entry that uses pyfsapfs.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS + + # Mappings of APFS file types to dfVFS file entry types. + _ENTRY_TYPES = { + 0x1000: definitions.FILE_ENTRY_TYPE_PIPE, + 0x2000: definitions.FILE_ENTRY_TYPE_DEVICE, + 0x4000: definitions.FILE_ENTRY_TYPE_DIRECTORY, + 0x6000: definitions.FILE_ENTRY_TYPE_DEVICE, + 0x8000: definitions.FILE_ENTRY_TYPE_FILE, + 0xa000: definitions.FILE_ENTRY_TYPE_LINK, + 0xc000: definitions.FILE_ENTRY_TYPE_SOCKET, + 0xe000: definitions.FILE_ENTRY_TYPE_WHITEOUT} + + def __init__( + self, resolver_context, file_system, path_spec, fsapfs_file_entry=None, + is_root=False, is_virtual=False): + """Initializes a file entry. + + Args: + resolver_context (Context): resolver context. + file_system (FileSystem): file system. + path_spec (PathSpec): path specification. + fsapfs_file_entry (Optional[pyfsapfs.file_entry]): APFS file entry. + is_root (Optional[bool]): True if the file entry is the root file entry + 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. + + Raises: + BackEndError: if the pyfsapfs file entry is missing. + """ + if not fsapfs_file_entry: + fsapfs_file_entry = file_system.GetAPFSFileEntryByPathSpec(path_spec) + if not fsapfs_file_entry: + raise errors.BackEndError('Missing pyfsapfs file entry.') + + super(APFSFileEntry, self).__init__( + resolver_context, file_system, path_spec, is_root=is_root, + is_virtual=is_virtual) + self._fsapfs_file_entry = fsapfs_file_entry + + self.entry_type = self._ENTRY_TYPES.get( + fsapfs_file_entry.file_mode & 0xf000, None) + + def _GetDirectory(self): + """Retrieves a directory. + + Returns: + APFSDirectory: directory or None if not available. + """ + if self._fsapfs_file_entry.number_of_sub_file_entries <= 0: + return None + + return APFSDirectory(self._file_system, self.path_spec) + + def _GetLink(self): + """Retrieves the link. + + Returns: + str: path of the linked file. + """ + if self._link is None: + self._link = '' + if self.entry_type != definitions.FILE_ENTRY_TYPE_LINK: + return self._link + + link = self._fsapfs_file_entry.symbolic_link_target + if link and link[0] != self._file_system.PATH_SEPARATOR: + # TODO: make link absolute. + self._link = '/{0:s}'.format(link) + + return self._link + + def _GetStat(self): + """Retrieves information about the file entry. + + Returns: + VFSStat: a stat object. + """ + stat_object = super(APFSFileEntry, self)._GetStat() + + # File data stat information. + stat_object.size = self._fsapfs_file_entry.size + + # Ownership and permissions stat information. + stat_object.mode = self._fsapfs_file_entry.file_mode & 0x0fff + stat_object.uid = self._fsapfs_file_entry.owner_identifier + stat_object.gid = self._fsapfs_file_entry.group_identifier + + # File entry type stat information. + stat_object.type = self.entry_type + + # Other stat information. + stat_object.ino = self._fsapfs_file_entry.identifier + stat_object.fs_type = 'APFS' + + stat_object.is_allocated = True + + return stat_object + + def _GetSubFileEntries(self): + """Retrieves a sub file entries generator. + + Yields: + APFSFileEntry: a sub file entry. + """ + if self._directory is None: + self._directory = self._GetDirectory() + + if self._directory: + for path_spec in self._directory.entries: + yield APFSFileEntry( + self._resolver_context, self._file_system, path_spec) + + @property + def access_time(self): + """dfdatetime.DateTimeValues: access time or None if not available.""" + timestamp = self._fsapfs_file_entry.get_access_time_as_integer() + return dfdatetime_apfs_time.APFSTime(timestamp=timestamp) + + @property + def change_time(self): + """dfdatetime.DateTimeValues: change time or None if not available.""" + timestamp = self._fsapfs_file_entry.get_inode_change_time_as_integer() + return dfdatetime_apfs_time.APFSTime(timestamp=timestamp) + + @property + def creation_time(self): + """dfdatetime.DateTimeValues: creation time or None if not available.""" + timestamp = self._fsapfs_file_entry.get_creation_time_as_integer() + return dfdatetime_apfs_time.APFSTime(timestamp=timestamp) + + @property + def name(self): + """str: name of the file entry, which does not include the full path.""" + # The root directory file name is typically 'root', dfVFS however uses ''. + if self._is_root: + return '' + + return self._fsapfs_file_entry.name + + @property + def modification_time(self): + """dfdatetime.DateTimeValues: modification time or None if not available.""" + timestamp = self._fsapfs_file_entry.get_modification_time_as_integer() + return dfdatetime_apfs_time.APFSTime(timestamp=timestamp) + + def GetAPFSFileEntry(self): + """Retrieves the APFS file entry. + + Returns: + pyfsapfs.file_entry: APFS file entry. + """ + return self._fsapfs_file_entry + + def GetLinkedFileEntry(self): + """Retrieves the linked file entry, e.g. for a symbolic link. + + Returns: + APFSFileEntry: linked file entry or None if not available. + """ + link = self._GetLink() + if not link: + return None + + # TODO: is there a way to determine the identifier here? + link_identifier = None + + parent_path_spec = getattr(self.path_spec, 'parent', None) + path_spec = apfs_path_spec.APFSPathSpec( + location=link, parent=parent_path_spec) + + is_root = bool( + link == self._file_system.LOCATION_ROOT or + link_identifier == self._file_system.ROOT_DIRECTORY_IDENTIFIER) + + return APFSFileEntry( + self._resolver_context, self._file_system, path_spec, is_root=is_root) + + def GetParentFileEntry(self): + """Retrieves the parent file entry. + + Returns: + APFSFileEntry: parent file entry or None if not available. + """ + parent_location = None + + location = getattr(self.path_spec, 'location', None) + if location is not None: + parent_location = self._file_system.DirnamePath(location) + if parent_location == '': + parent_location = self._file_system.PATH_SEPARATOR + + parent_identifier = self._fsapfs_file_entry.parent_identifier + if parent_identifier is None: + return None + + parent_path_spec = getattr(self.path_spec, 'parent', None) + path_spec = apfs_path_spec.APFSPathSpec( + location=parent_location, identifier=parent_identifier, + parent=parent_path_spec) + + is_root = bool( + parent_location == self._file_system.LOCATION_ROOT or + parent_identifier == self._file_system.ROOT_DIRECTORY_IDENTIFIER) + + return APFSFileEntry( + self._resolver_context, self._file_system, path_spec, is_root=is_root) diff --git a/dfvfs/vfs/apfs_file_system.py b/dfvfs/vfs/apfs_file_system.py new file mode 100644 index 00000000..8e14965f --- /dev/null +++ b/dfvfs/vfs/apfs_file_system.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +"""The APFS file system implementation.""" + +from __future__ import unicode_literals + +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import apfs_path_spec +from dfvfs.resolver import resolver +from dfvfs.vfs import file_system +from dfvfs.vfs import apfs_file_entry + + +class APFSFileSystem(file_system.FileSystem): + """File system that uses pyfsapfs.""" + + ROOT_DIRECTORY_IDENTIFIER = 2 + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_APFS + + def __init__(self, resolver_context): + """Initializes an APFS file system. + + Args: + resolver_context (Context): resolver context. + """ + super(APFSFileSystem, self).__init__(resolver_context) + self._fsapfs_volume = None + + def _Close(self): + """Closes the file system. + + Raises: + IOError: if the close failed. + """ + self._fsapfs_volume = None + + def _Open(self, path_spec, mode='rb'): + """Opens the file system defined by path specification. + + Args: + path_spec (PathSpec): path specification. + mode (Optional[str]): file access mode. + + Raises: + AccessError: if the access to open the file was denied. + IOError: if the file system object could not be opened. + OSError: if the file system object could not be opened. + PathSpecError: if the path specification is incorrect. + ValueError: if the path specification is invalid. + """ + if not path_spec.HasParent(): + raise errors.PathSpecError( + 'Unsupported path specification without parent.') + + if path_spec.parent.type_indicator != ( + definitions.TYPE_INDICATOR_APFS_CONTAINER): + raise errors.PathSpecError( + 'Unsupported path specification not type APFS container.') + + apfs_file_system = resolver.Resolver.OpenFileSystem( + path_spec.parent, resolver_context=self._resolver_context) + + fsapfs_volume = apfs_file_system.GetAPFSVolumeByPathSpec(path_spec.parent) + if not fsapfs_volume: + raise IOError('Unable to open APFS volume') + + self._fsapfs_volume = fsapfs_volume + + def FileEntryExistsByPathSpec(self, path_spec): + """Determines if a file entry for a path specification exists. + + Args: + path_spec (PathSpec): path specification. + + Returns: + bool: True if the file entry exists. + + Raises: + BackEndError: if the file entry cannot be opened. + """ + # Opening a file by identifier is faster than opening a file by location. + fsapfs_file_entry = None + location = getattr(path_spec, 'location', None) + identifier = getattr(path_spec, 'identifier', None) + + try: + if identifier is not None: + fsapfs_file_entry = self._fsapfs_volume.get_file_entry_by_identifier( + identifier) + elif location is not None: + fsapfs_file_entry = self._fsapfs_volume.get_file_entry_by_path(location) + + except IOError as exception: + raise errors.BackEndError(exception) + + return fsapfs_file_entry is not None + + def GetFileEntryByPathSpec(self, path_spec): + """Retrieves a file entry for a path specification. + + Args: + path_spec (PathSpec): path specification. + + Returns: + APFSFileEntry: file entry or None if not available. + + Raises: + BackEndError: if the file entry cannot be opened. + """ + # Opening a file by identifier is faster than opening a file by location. + fsapfs_file_entry = None + location = getattr(path_spec, 'location', None) + identifier = getattr(path_spec, 'identifier', None) + + if (location == self.LOCATION_ROOT or + identifier == self.ROOT_DIRECTORY_IDENTIFIER): + fsapfs_file_entry = self._fsapfs_volume.get_root_directory() + return apfs_file_entry.APFSFileEntry( + self._resolver_context, self, path_spec, + fsapfs_file_entry=fsapfs_file_entry, is_root=True) + + try: + if identifier is not None: + fsapfs_file_entry = self._fsapfs_volume.get_file_entry_by_identifier( + identifier) + elif location is not None: + fsapfs_file_entry = self._fsapfs_volume.get_file_entry_by_path(location) + + except IOError as exception: + raise errors.BackEndError(exception) + + if fsapfs_file_entry is None: + return None + + return apfs_file_entry.APFSFileEntry( + self._resolver_context, self, path_spec, + fsapfs_file_entry=fsapfs_file_entry) + + def GetAPFSFileEntryByPathSpec(self, path_spec): + """Retrieves the APFS file entry for a path specification. + + Args: + path_spec (PathSpec): a path specification. + + Returns: + pyfsapfs.file_entry: file entry. + + Raises: + PathSpecError: if the path specification is missing location and + identifier. + """ + # Opening a file by identifier is faster than opening a file by location. + location = getattr(path_spec, 'location', None) + identifier = getattr(path_spec, 'identifier', None) + + if identifier is not None: + fsapfs_file_entry = self._fsapfs_volume.get_file_entry_by_identifier( + identifier) + elif location is not None: + fsapfs_file_entry = self._fsapfs_volume.get_file_entry_by_path(location) + else: + raise errors.PathSpecError( + 'Path specification missing location and identifier.') + + return fsapfs_file_entry + + def GetRootFileEntry(self): + """Retrieves the root file entry. + + Returns: + APFSFileEntry: file entry. + """ + path_spec = apfs_path_spec.APFSPathSpec( + location=self.LOCATION_ROOT, identifier=self.ROOT_DIRECTORY_IDENTIFIER, + parent=self._path_spec.parent) + return self.GetFileEntryByPathSpec(path_spec) diff --git a/dfvfs/vfs/ntfs_file_system.py b/dfvfs/vfs/ntfs_file_system.py index 8cca6cdf..aacb6ac8 100644 --- a/dfvfs/vfs/ntfs_file_system.py +++ b/dfvfs/vfs/ntfs_file_system.py @@ -62,9 +62,10 @@ def _Open(self, path_spec, mode='rb'): raise errors.PathSpecError( 'Unsupported path specification without parent.') + file_object = resolver.Resolver.OpenFileObject( + path_spec.parent, resolver_context=self._resolver_context) + try: - file_object = resolver.Resolver.OpenFileObject( - path_spec.parent, resolver_context=self._resolver_context) fsnfts_volume = pyfsntfs.volume() fsnfts_volume.open_file_object(file_object) except: diff --git a/requirements.txt b/requirements.txt index bfc306ee..ae7c7c31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,11 @@ pip >= 7.0.0 PyYAML >= 3.10 backports.lzma ; python_version < '3.0' -dfdatetime >= 20180324 +dfdatetime >= 20181025 dtfabric >= 20170524 libbde-python >= 20140531 libewf-python >= 20131210 +libfsapfs-python >= 20181110 libfsntfs-python >= 20151130 libfvde-python >= 20160719 libfwnt-python >= 20160418 diff --git a/setup.cfg b/setup.cfg index b46dd69f..c0e2755e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ build_requires = python-setuptools requires = PyYAML >= 3.10 libbde-python >= 20140531 libewf-python >= 20131210 + libfsapfs-python2 >= 20181110 libfsntfs-python >= 20151130 libfvde-python >= 20160719 libfwnt-python >= 20160418 @@ -30,7 +31,7 @@ requires = PyYAML >= 3.10 libvslvm-python >= 20160109 python-backports-lzma python-crypto >= 2.6 - python-dfdatetime >= 20180324 + python-dfdatetime >= 20181025 python-dtfabric >= 20170524 python-pysqlite python-pytsk3 >= 20160721 diff --git a/test_data/apfs.dmg b/test_data/apfs.dmg new file mode 100644 index 00000000..f16f6f76 Binary files /dev/null and b/test_data/apfs.dmg differ diff --git a/test_data/apfs_encrypted.dmg b/test_data/apfs_encrypted.dmg new file mode 100644 index 00000000..7a51a5e5 Binary files /dev/null and b/test_data/apfs_encrypted.dmg differ diff --git a/tests/file_io/apfs_file_io.py b/tests/file_io/apfs_file_io.py new file mode 100644 index 00000000..9135c524 --- /dev/null +++ b/tests/file_io/apfs_file_io.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the file-like object implementation using pyfsapfs.""" + +from __future__ import unicode_literals + +import os +import unittest + +from dfvfs.file_io import apfs_file_io +from dfvfs.lib import errors +from dfvfs.path import apfs_container_path_spec +from dfvfs.path import apfs_path_spec +from dfvfs.path import os_path_spec +from dfvfs.path import raw_path_spec +from dfvfs.path import tsk_partition_path_spec +from dfvfs.resolver import context + +from tests import test_lib as shared_test_lib +from tests.file_io import test_lib + + +@shared_test_lib.skipUnlessHasTestFile(['apfs.dmg']) +class APFSFileTest(test_lib.ImageFileTestCase): + """The unit test for the APFS file-like object.""" + + _IDENTIFIER_ANOTHER_FILE = 21 + _IDENTIFIER_PASSWORDS_TXT = 19 + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + super(APFSFileTest, self).setUp() + self._resolver_context = context.Context() + test_file = self._GetTestFilePath(['apfs.dmg']) + path_spec = os_path_spec.OSPathSpec(location=test_file) + path_spec = raw_path_spec.RawPathSpec(parent=path_spec) + partition_path_spec = tsk_partition_path_spec.TSKPartitionPathSpec( + location='/p1', parent=path_spec) + self._apfs_container_path_spec = ( + apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs1', parent=partition_path_spec)) + + def testOpenCloseIdentifier(self): + """Test the open and close functionality using an identifier.""" + path_spec = apfs_path_spec.APFSPathSpec( + identifier=self._IDENTIFIER_PASSWORDS_TXT, + parent=self._apfs_container_path_spec) + file_object = apfs_file_io.APFSFile(self._resolver_context) + + file_object.open(path_spec=path_spec) + self.assertEqual(file_object.get_size(), 116) + file_object.close() + + # TODO: add a failing scenario. + + def testOpenCloseLocation(self): + """Test the open and close functionality using a location.""" + path_spec = apfs_path_spec.APFSPathSpec( + location='/passwords.txt', parent=self._apfs_container_path_spec) + file_object = apfs_file_io.APFSFile(self._resolver_context) + + file_object.open(path_spec=path_spec) + self.assertEqual(file_object.get_size(), 116) + file_object.close() + + # Try open with a path specification that has no parent. + path_spec.parent = None + + with self.assertRaises(errors.PathSpecError): + self._TestOpenCloseLocation(path_spec) + + def testSeek(self): + """Test the seek functionality.""" + path_spec = apfs_path_spec.APFSPathSpec( + location='/a_directory/another_file', + identifier=self._IDENTIFIER_ANOTHER_FILE, + parent=self._apfs_container_path_spec) + file_object = apfs_file_io.APFSFile(self._resolver_context) + + file_object.open(path_spec=path_spec) + self.assertEqual(file_object.get_size(), 22) + + file_object.seek(10) + self.assertEqual(file_object.read(5), b'other') + self.assertEqual(file_object.get_offset(), 15) + + file_object.seek(-10, os.SEEK_END) + self.assertEqual(file_object.read(5), b'her f') + + file_object.seek(2, os.SEEK_CUR) + self.assertEqual(file_object.read(2), b'e.') + + # Conforming to the POSIX seek the offset can exceed the file size + # but reading will result in no data being returned. + file_object.seek(300, os.SEEK_SET) + self.assertEqual(file_object.get_offset(), 300) + self.assertEqual(file_object.read(2), b'') + + with self.assertRaises(IOError): + file_object.seek(-10, os.SEEK_SET) + + # On error the offset should not change. + self.assertEqual(file_object.get_offset(), 300) + + with self.assertRaises(IOError): + file_object.seek(10, 5) + + # On error the offset should not change. + self.assertEqual(file_object.get_offset(), 300) + + file_object.close() + + def testRead(self): + """Test the read functionality.""" + path_spec = apfs_path_spec.APFSPathSpec( + location='/passwords.txt', identifier=self._IDENTIFIER_PASSWORDS_TXT, + parent=self._apfs_container_path_spec) + file_object = apfs_file_io.APFSFile(self._resolver_context) + + file_object.open(path_spec=path_spec) + read_buffer = file_object.read() + + expected_buffer = ( + b'place,user,password\n' + b'bank,joesmith,superrich\n' + b'alarm system,-,1234\n' + b'treasure chest,-,1111\n' + b'uber secret laire,admin,admin\n') + + self.assertEqual(read_buffer, expected_buffer) + + # TODO: add boundary scenarios. + + file_object.close() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/lib/apfs_container.py b/tests/lib/apfs_container.py new file mode 100644 index 00000000..5ea090ba --- /dev/null +++ b/tests/lib/apfs_container.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the helper functions for APFS container support.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.lib import apfs_container +from dfvfs.path import apfs_container_path_spec + +from tests.path import test_lib + + +class APFSContainerHelperTest(test_lib.PathSpecTestCase): + """Tests for the helper functions for APFS container support.""" + + def testAPFSContainerPathSpecGetVolumeIndex(self): + """Tests the APFSContainerPathSpecGetVolumeIndex function.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex(path_spec) + self.assertIsNone(volume_index) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs2', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex(path_spec) + self.assertEqual(volume_index, 1) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + volume_index=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex(path_spec) + self.assertEqual(volume_index, 1) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs2', volume_index=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex(path_spec) + self.assertEqual(volume_index, 1) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs', parent=self._path_spec) + + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex(path_spec) + self.assertIsNone(volume_index) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs101', parent=self._path_spec) + + volume_index = apfs_container.APFSContainerPathSpecGetVolumeIndex(path_spec) + self.assertIsNone(volume_index) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/path/apfs_container_path_spec.py b/tests/path/apfs_container_path_spec.py new file mode 100644 index 00000000..bf6427c7 --- /dev/null +++ b/tests/path/apfs_container_path_spec.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the APFS container path specification implementation.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.path import apfs_container_path_spec + +from tests.path import test_lib + + +class APFSContainerPathSpecTest(test_lib.PathSpecTestCase): + """Tests for the APFS container path specification implementation.""" + + def testInitialize(self): + """Tests the path specification initialization.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs2', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + volume_index=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs2', volume_index=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + with self.assertRaises(ValueError): + apfs_container_path_spec.APFSContainerPathSpec(parent=None) + + with self.assertRaises(ValueError): + apfs_container_path_spec.APFSContainerPathSpec( + parent=self._path_spec, bogus='BOGUS') + + def testComparable(self): + """Tests the path specification comparable property.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: APFS_CONTAINER', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs2', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: APFS_CONTAINER, location: /apfs2', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + volume_index=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: APFS_CONTAINER, volume index: 1', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs2', volume_index=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: APFS_CONTAINER, location: /apfs2, volume index: 1', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/path/apfs_path_spec.py b/tests/path/apfs_path_spec.py new file mode 100644 index 00000000..87730864 --- /dev/null +++ b/tests/path/apfs_path_spec.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the APFS path specification implementation.""" + +from __future__ import unicode_literals + +import unittest + +from tests.path import test_lib +from dfvfs.path import apfs_path_spec + + +class APFSPathSpecTest(test_lib.PathSpecTestCase): + """Tests for the APFS path specification implementation.""" + + def testInitialize(self): + """Tests the path specification initialization.""" + path_spec = apfs_path_spec.APFSPathSpec( + location='/test', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = apfs_path_spec.APFSPathSpec( + identifier=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = apfs_path_spec.APFSPathSpec( + location='/test', identifier=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + with self.assertRaises(ValueError): + apfs_path_spec.APFSPathSpec(location='/test', parent=None) + + with self.assertRaises(ValueError): + apfs_path_spec.APFSPathSpec(location=None, parent=self._path_spec) + + with self.assertRaises(ValueError): + apfs_path_spec.APFSPathSpec(identifier=None, parent=self._path_spec) + + with self.assertRaises(ValueError): + apfs_path_spec.APFSPathSpec( + location='/test', parent=self._path_spec, bogus='BOGUS') + + def testComparable(self): + """Tests the path specification comparable property.""" + path_spec = apfs_path_spec.APFSPathSpec( + location='/test', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: APFS, location: /test', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = apfs_path_spec.APFSPathSpec( + identifier=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: APFS, identifier: 1', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = apfs_path_spec.APFSPathSpec( + location='/test', identifier=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: APFS, identifier: 1, location: /test', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/apfs_container_file_entry.py b/tests/vfs/apfs_container_file_entry.py new file mode 100644 index 00000000..7abc3c4f --- /dev/null +++ b/tests/vfs/apfs_container_file_entry.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the APFS container file entry implementation using pyfsapfs.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.path import apfs_container_path_spec +from dfvfs.path import os_path_spec +from dfvfs.path import raw_path_spec +from dfvfs.path import tsk_partition_path_spec +from dfvfs.resolver import context +from dfvfs.vfs import apfs_container_file_entry +from dfvfs.vfs import apfs_container_file_system + +from tests import test_lib as shared_test_lib + + +@shared_test_lib.skipUnlessHasTestFile(['apfs.dmg']) +class APFSContainerFileEntryTest(shared_test_lib.BaseTestCase): + """APFS container file entry tests.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_file = self._GetTestFilePath(['apfs.dmg']) + path_spec = os_path_spec.OSPathSpec(location=test_file) + path_spec = raw_path_spec.RawPathSpec(parent=path_spec) + self._partition_path_spec = tsk_partition_path_spec.TSKPartitionPathSpec( + location='/p1', parent=path_spec) + self._apfs_container_path_spec = ( + apfs_container_path_spec.APFSContainerPathSpec( + location='/', parent=self._partition_path_spec)) + + self._file_system = apfs_container_file_system.APFSContainerFileSystem( + self._resolver_context) + self._file_system.Open(self._apfs_container_path_spec) + + def tearDown(self): + """Cleans up the needed objects used throughout the test.""" + self._file_system.Close() + + def testInitialize(self): + """Test the __init__ function.""" + file_entry = apfs_container_file_entry.APFSContainerFileEntry( + self._resolver_context, self._file_system, + self._apfs_container_path_spec, is_virtual=True) + + self.assertIsNotNone(file_entry) + + def testGetParentFileEntry(self): + """Tests the GetParentFileEntry function.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=0) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + parent_file_entry = file_entry.GetParentFileEntry() + self.assertIsNotNone(parent_file_entry) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/', parent=self._partition_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + parent_file_entry = file_entry.GetParentFileEntry() + self.assertIsNone(parent_file_entry) + + def testGetStat(self): + """Tests the GetStat function.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=0) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + stat_object = file_entry.GetStat() + + self.assertIsNotNone(stat_object) + self.assertEqual(stat_object.type, stat_object.TYPE_FILE) + + def testIsFunctions(self): + """Test the Is? functions.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=0) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertFalse(file_entry.IsRoot()) + self.assertFalse(file_entry.IsVirtual()) + self.assertTrue(file_entry.IsAllocated()) + + self.assertFalse(file_entry.IsDevice()) + self.assertFalse(file_entry.IsDirectory()) + self.assertTrue(file_entry.IsFile()) + self.assertFalse(file_entry.IsLink()) + self.assertFalse(file_entry.IsPipe()) + self.assertFalse(file_entry.IsSocket()) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/', parent=self._partition_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertTrue(file_entry.IsRoot()) + self.assertTrue(file_entry.IsVirtual()) + self.assertTrue(file_entry.IsAllocated()) + + self.assertFalse(file_entry.IsDevice()) + self.assertTrue(file_entry.IsDirectory()) + self.assertFalse(file_entry.IsFile()) + self.assertFalse(file_entry.IsLink()) + self.assertFalse(file_entry.IsPipe()) + self.assertFalse(file_entry.IsSocket()) + + def testSubFileEntries(self): + """Test the sub file entries iteration functionality.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/', parent=self._partition_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertEqual(file_entry.number_of_sub_file_entries, 1) + + expected_sub_file_entry_names = ['apfs1'] + + sub_file_entry_names = [] + for sub_file_entry in file_entry.sub_file_entries: + sub_file_entry_names.append(sub_file_entry.name) + + self.assertEqual( + len(sub_file_entry_names), len(expected_sub_file_entry_names)) + self.assertEqual( + sorted(sub_file_entry_names), sorted(expected_sub_file_entry_names)) + + def testDataStreams(self): + """Test the data streams functionality.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=0) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertEqual(file_entry.number_of_data_streams, 1) + + data_stream_names = [] + for data_stream in file_entry.data_streams: + data_stream_names.append(data_stream.name) + + self.assertEqual(data_stream_names, ['']) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/', parent=self._partition_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertEqual(file_entry.number_of_data_streams, 0) + + data_stream_names = [] + for data_stream in file_entry.data_streams: + data_stream_names.append(data_stream.name) + + self.assertEqual(data_stream_names, []) + + def testGetDataStream(self): + """Tests the GetDataStream function.""" + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=0) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + data_stream_name = '' + data_stream = file_entry.GetDataStream(data_stream_name) + self.assertIsNotNone(data_stream) + self.assertEqual(data_stream.name, data_stream_name) + + data_stream = file_entry.GetDataStream('bogus') + self.assertIsNone(data_stream) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/apfs_container_file_system.py b/tests/vfs/apfs_container_file_system.py new file mode 100644 index 00000000..2a941830 --- /dev/null +++ b/tests/vfs/apfs_container_file_system.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the APFS container file system implementation using pyfsapfs.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.path import apfs_container_path_spec +from dfvfs.path import os_path_spec +from dfvfs.path import raw_path_spec +from dfvfs.path import tsk_partition_path_spec +from dfvfs.resolver import context +from dfvfs.vfs import apfs_container_file_system + +from tests import test_lib as shared_test_lib + + +@shared_test_lib.skipUnlessHasTestFile(['apfs.dmg']) +class APFSContainerFileSystemTest(shared_test_lib.BaseTestCase): + """APFS container file system tests.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_file = self._GetTestFilePath(['apfs.dmg']) + path_spec = os_path_spec.OSPathSpec(location=test_file) + path_spec = raw_path_spec.RawPathSpec(parent=path_spec) + self._partition_path_spec = tsk_partition_path_spec.TSKPartitionPathSpec( + location='/p1', parent=path_spec) + self._apfs_container_path_spec = ( + apfs_container_path_spec.APFSContainerPathSpec( + location='/', parent=self._partition_path_spec)) + + def testOpenAndClose(self): + """Test the open and close functionality.""" + file_system = apfs_container_file_system.APFSContainerFileSystem( + self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._apfs_container_path_spec) + + file_system.Close() + + def testFileEntryExistsByPathSpec(self): + """Test the file entry exists by path specification functionality.""" + file_system = apfs_container_file_system.APFSContainerFileSystem( + self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._apfs_container_path_spec) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/', parent=self._partition_path_spec) + self.assertTrue(file_system.FileEntryExistsByPathSpec(path_spec)) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=0) + self.assertTrue(file_system.FileEntryExistsByPathSpec(path_spec)) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs1', parent=self._partition_path_spec) + self.assertTrue(file_system.FileEntryExistsByPathSpec(path_spec)) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=9) + self.assertFalse(file_system.FileEntryExistsByPathSpec(path_spec)) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs0', parent=self._partition_path_spec) + self.assertFalse(file_system.FileEntryExistsByPathSpec(path_spec)) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs9', parent=self._partition_path_spec) + self.assertFalse(file_system.FileEntryExistsByPathSpec(path_spec)) + + file_system.Close() + + def testGetFileEntryByPathSpec(self): + """Tests the GetFileEntryByPathSpec function.""" + file_system = apfs_container_file_system.APFSContainerFileSystem( + self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._apfs_container_path_spec) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/', parent=self._partition_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.name, '') + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=0) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.name, 'apfs1') + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs1', parent=self._partition_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.name, 'apfs1') + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + parent=self._partition_path_spec, volume_index=9) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNone(file_entry) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs0', parent=self._partition_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNone(file_entry) + + path_spec = apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs9', parent=self._partition_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNone(file_entry) + + file_system.Close() + + def testGetRootFileEntry(self): + """Test the get root file entry functionality.""" + file_system = apfs_container_file_system.APFSContainerFileSystem( + self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._apfs_container_path_spec) + + file_entry = file_system.GetRootFileEntry() + + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.name, '') + + file_system.Close() + + # TODO: add tests for GetAPFSVolumeByPathSpec function. + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/apfs_file_entry.py b/tests/vfs/apfs_file_entry.py new file mode 100644 index 00000000..c8f1a7b7 --- /dev/null +++ b/tests/vfs/apfs_file_entry.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the file entry implementation using pyfsapfs.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.path import apfs_path_spec +from dfvfs.path import apfs_container_path_spec +from dfvfs.path import os_path_spec +from dfvfs.path import raw_path_spec +from dfvfs.path import tsk_partition_path_spec +from dfvfs.resolver import context +from dfvfs.vfs import apfs_file_entry +from dfvfs.vfs import apfs_file_system + +from tests import test_lib as shared_test_lib + + +# TODO: add tests for APFSDirectory. + + +@shared_test_lib.skipUnlessHasTestFile(['apfs.dmg']) +class APFSFileEntryTest(shared_test_lib.BaseTestCase): + """Tests the APFS file entry.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_file = self._GetTestFilePath(['apfs.dmg']) + path_spec = os_path_spec.OSPathSpec(location=test_file) + path_spec = raw_path_spec.RawPathSpec(parent=path_spec) + partition_path_spec = tsk_partition_path_spec.TSKPartitionPathSpec( + location='/p1', parent=path_spec) + self._apfs_container_path_spec = ( + apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs1', parent=partition_path_spec)) + self._apfs_path_spec = apfs_path_spec.APFSPathSpec( + location='/', parent=self._apfs_container_path_spec) + + self._file_system = apfs_file_system.APFSFileSystem(self._resolver_context) + self._file_system.Open(self._apfs_path_spec) + + def tearDown(self): + """Cleans up the needed objects used throughout the test.""" + self._file_system.Close() + + def testInitialize(self): + """Tests the __init__ function.""" + file_entry = apfs_file_entry.APFSFileEntry( + self._resolver_context, self._file_system, self._apfs_path_spec) + + self.assertIsNotNone(file_entry) + + def testAccessTime(self): + """Test the access_time property.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertIsNotNone(file_entry.access_time) + + def testChangeTime(self): + """Test the change_time property.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertIsNotNone(file_entry.change_time) + + def testCreationTime(self): + """Test the creation_time property.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertIsNotNone(file_entry.creation_time) + + def testModificationTime(self): + """Test the modification_time property.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertIsNotNone(file_entry.modification_time) + + def testGetFileEntryByPathSpec(self): + """Tests the GetFileEntryByPathSpec function.""" + path_spec = apfs_path_spec.APFSPathSpec( + identifier=19, parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + + def testGetLinkedFileEntry(self): + """Tests the GetLinkedFileEntry function.""" + test_location = '/a_link' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=22, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + linked_file_entry = file_entry.GetLinkedFileEntry() + + self.assertIsNotNone(linked_file_entry) + + self.assertEqual(linked_file_entry.name, 'a_file') + + def testGetParentFileEntry(self): + """Tests the GetParentFileEntry function.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + parent_file_entry = file_entry.GetParentFileEntry() + + self.assertIsNotNone(parent_file_entry) + + self.assertEqual(parent_file_entry.name, 'a_directory') + + def testGetStat(self): + """Tests the GetStat function.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + stat_object = file_entry.GetStat() + + self.assertIsNotNone(stat_object) + self.assertEqual(stat_object.type, stat_object.TYPE_FILE) + self.assertEqual(stat_object.size, 22) + + self.assertEqual(stat_object.mode, 420) + self.assertEqual(stat_object.uid, 99) + self.assertEqual(stat_object.gid, 99) + + self.assertEqual(stat_object.atime, 1539321685) + self.assertEqual(stat_object.atime_nano, 4824067) + + self.assertEqual(stat_object.ctime, 1539321685) + self.assertEqual(stat_object.ctime_nano, 4840758) + + self.assertEqual(stat_object.crtime, 1539321685) + self.assertEqual(stat_object.crtime_nano, 4840758) + + self.assertEqual(stat_object.mtime, 1539321685) + self.assertEqual(stat_object.mtime_nano, 4824067) + + def testIsFunctions(self): + """Tests the Is? functions.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertFalse(file_entry.IsRoot()) + self.assertFalse(file_entry.IsVirtual()) + self.assertTrue(file_entry.IsAllocated()) + + self.assertFalse(file_entry.IsDevice()) + self.assertFalse(file_entry.IsDirectory()) + self.assertTrue(file_entry.IsFile()) + self.assertFalse(file_entry.IsLink()) + self.assertFalse(file_entry.IsPipe()) + self.assertFalse(file_entry.IsSocket()) + + test_location = '/a_directory' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=18, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertFalse(file_entry.IsRoot()) + self.assertFalse(file_entry.IsVirtual()) + self.assertTrue(file_entry.IsAllocated()) + + self.assertFalse(file_entry.IsDevice()) + self.assertTrue(file_entry.IsDirectory()) + self.assertFalse(file_entry.IsFile()) + self.assertFalse(file_entry.IsLink()) + self.assertFalse(file_entry.IsPipe()) + self.assertFalse(file_entry.IsSocket()) + + path_spec = apfs_path_spec.APFSPathSpec( + location='/', parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertTrue(file_entry.IsRoot()) + self.assertFalse(file_entry.IsVirtual()) + self.assertTrue(file_entry.IsAllocated()) + + self.assertFalse(file_entry.IsDevice()) + self.assertTrue(file_entry.IsDirectory()) + self.assertFalse(file_entry.IsFile()) + self.assertFalse(file_entry.IsLink()) + self.assertFalse(file_entry.IsPipe()) + self.assertFalse(file_entry.IsSocket()) + + def testSubFileEntries(self): + """Tests the number_of_sub_file_entries and sub_file_entries properties.""" + path_spec = apfs_path_spec.APFSPathSpec( + location='/', parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertEqual(file_entry.number_of_sub_file_entries, 4) + + expected_sub_file_entry_names = [ + '.fseventsd', + 'a_directory', + 'a_link', + 'passwords.txt'] + + sub_file_entry_names = [] + for sub_file_entry in file_entry.sub_file_entries: + sub_file_entry_names.append(sub_file_entry.name) + + self.assertEqual( + len(sub_file_entry_names), len(expected_sub_file_entry_names)) + self.assertEqual( + sorted(sub_file_entry_names), sorted(expected_sub_file_entry_names)) + + def testDataStreams(self): + """Tests the data streams functionality.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertEqual(file_entry.number_of_data_streams, 1) + + data_stream_names = [] + for data_stream in file_entry.data_streams: + data_stream_names.append(data_stream.name) + + self.assertEqual(data_stream_names, ['']) + + test_location = '/a_directory' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=18, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertEqual(file_entry.number_of_data_streams, 0) + + data_stream_names = [] + for data_stream in file_entry.data_streams: + data_stream_names.append(data_stream.name) + + self.assertEqual(data_stream_names, []) + + def testGetDataStream(self): + """Tests the GetDataStream function.""" + test_location = '/a_directory/another_file' + path_spec = apfs_path_spec.APFSPathSpec( + identifier=21, location=test_location, + parent=self._apfs_container_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + data_stream_name = '' + data_stream = file_entry.GetDataStream(data_stream_name) + self.assertIsNotNone(data_stream) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/apfs_file_system.py b/tests/vfs/apfs_file_system.py new file mode 100644 index 00000000..0a997179 --- /dev/null +++ b/tests/vfs/apfs_file_system.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the file system implementation using pyfsapfs.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.path import apfs_container_path_spec +from dfvfs.path import apfs_path_spec +from dfvfs.path import os_path_spec +from dfvfs.path import raw_path_spec +from dfvfs.path import tsk_partition_path_spec +from dfvfs.resolver import context +from dfvfs.vfs import apfs_file_system + +from tests import test_lib as shared_test_lib + + +@shared_test_lib.skipUnlessHasTestFile(['apfs.dmg']) +class APFSFileSystemTest(shared_test_lib.BaseTestCase): + """Tests the APFS file entry.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_file = self._GetTestFilePath(['apfs.dmg']) + path_spec = os_path_spec.OSPathSpec(location=test_file) + path_spec = raw_path_spec.RawPathSpec(parent=path_spec) + partition_path_spec = tsk_partition_path_spec.TSKPartitionPathSpec( + location='/p1', parent=path_spec) + self._apfs_container_path_spec = ( + apfs_container_path_spec.APFSContainerPathSpec( + location='/apfs1', parent=partition_path_spec)) + self._apfs_path_spec = apfs_path_spec.APFSPathSpec( + location='/', parent=self._apfs_container_path_spec) + + def testOpenAndClose(self): + """Test the open and close functionality.""" + file_system = apfs_file_system.APFSFileSystem(self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._apfs_path_spec) + + file_system.Close() + + def testFileEntryExistsByPathSpec(self): + """Test the file entry exists by path specification functionality.""" + file_system = apfs_file_system.APFSFileSystem(self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._apfs_path_spec) + + path_spec = apfs_path_spec.APFSPathSpec( + location='/passwords.txt', identifier=19, + parent=self._apfs_container_path_spec) + self.assertTrue(file_system.FileEntryExistsByPathSpec(path_spec)) + + path_spec = apfs_path_spec.APFSPathSpec( + location='/bogus.txt', parent=self._apfs_container_path_spec) + self.assertFalse(file_system.FileEntryExistsByPathSpec(path_spec)) + + file_system.Close() + + def testGetFileEntryByPathSpec(self): + """Tests the GetFileEntryByPathSpec function.""" + file_system = apfs_file_system.APFSFileSystem(self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._apfs_path_spec) + + path_spec = apfs_path_spec.APFSPathSpec( + identifier=19, parent=self._apfs_container_path_spec) + + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + # There is no way to determine the file_entry.name without a location string + # in the path_spec or retrieving the file_entry from its parent. + + path_spec = apfs_path_spec.APFSPathSpec( + location='/passwords.txt', identifier=19, + parent=self._apfs_container_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.name, 'passwords.txt') + + path_spec = apfs_path_spec.APFSPathSpec( + location='/bogus.txt', parent=self._apfs_container_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNone(file_entry) + + file_system.Close() + + # TODO: add tests for GetAPFSFileEntryByPathSpec function. + + def testGetRootFileEntry(self): + """Test the get root file entry functionality.""" + file_system = apfs_file_system.APFSFileSystem(self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._apfs_path_spec) + + file_entry = file_system.GetRootFileEntry() + + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.name, '') + + file_system.Close() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/ntfs_file_entry.py b/tests/vfs/ntfs_file_entry.py index 6744d69c..83653aa0 100644 --- a/tests/vfs/ntfs_file_entry.py +++ b/tests/vfs/ntfs_file_entry.py @@ -118,7 +118,7 @@ def testAttributeType(self): @shared_test_lib.skipUnlessHasTestFile(['vsstest.qcow2']) class StandardInformationNTFSAttributeTest(shared_test_lib.BaseTestCase): - """Tests the NTFS attribute.""" + """Tests the NTFS $STANDARD_INFORMATION attribute.""" # pylint: disable=protected-access