diff --git a/appveyor.yml b/appveyor.yml index 9dba327a..a09b67b7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,13 +17,16 @@ install: - cmd: "%PYTHON%\\python.exe -m pip install pywin32 WMI" - cmd: "%PYTHON%\\python.exe %PYTHON%\\Scripts\\pywin32_postinstall.py -install" - cmd: git clone https://github.com/log2timeline/l2tdevtools.git ..\l2tdevtools -- cmd: if [%PYTHON_VERSION%]==[3.8] ( +- cmd: IF [%PYTHON_VERSION%]==[3.8] ( mkdir dependencies && set PYTHONPATH=..\l2tdevtools && - "%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type %MACHINE_TYPE% --msi-targetdir "%PYTHON%" --track "%L2TBINARIES_TRACK%" PyYAML cffi cryptography dfdatetime dtfabric idna libbde libewf libfsapfs libfsntfs libfvde libfwnt libluksde libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvshadow libvslvm mock pbr pytsk3 six ) + "%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type %MACHINE_TYPE% --msi-targetdir "%PYTHON%" --track "%L2TBINARIES_TRACK%" PyYAML cffi cryptography dfdatetime dtfabric idna libbde libewf libfsapfs libfsext libfsntfs libfvde libfwnt libluksde libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvshadow libvslvm mock pbr pytsk3 six ) build: off test_script: -- cmd: if [%TARGET%]==[unittests] ( - "%PYTHON%\\python.exe" run_tests.py ) +- cmd: IF [%TARGET%]==[unittests] ( + "%PYTHON%\\python.exe" run_tests.py && + IF EXIST "tests\\end-to-end.py" ( + set PYTHONPATH=. && + "%PYTHON%\\python.exe" "tests\\end-to-end.py" --debug -c "config\\end-to-end.ini" ) ) diff --git a/config/dpkg/control b/config/dpkg/control index 000e5c04..bd4ddccb 100644 --- a/config/dpkg/control +++ b/config/dpkg/control @@ -2,14 +2,14 @@ Source: dfvfs Section: python Priority: extra Maintainer: Log2Timeline maintainers -Build-Depends: debhelper (>= 9), dh-python, python3-all (>= 3.5~), python3-setuptools +Build-Depends: debhelper (>= 9), dh-python, python3-all (>= 3.6~), python3-setuptools Standards-Version: 4.1.4 -X-Python3-Version: >= 3.5 +X-Python3-Version: >= 3.6 Homepage: https://github.com/log2timeline/dfvfs Package: python3-dfvfs Architecture: all -Depends: libbde-python3 (>= 20140531), libewf-python3 (>= 20131210), libfsapfs-python3 (>= 20181205), libfsntfs-python3 (>= 20200424), libfvde-python3 (>= 20160719), libfwnt-python3 (>= 20160418), libluksde-python3 (>= 20200101), libqcow-python3 (>= 20131204), libsigscan-python3 (>= 20191221), libsmdev-python3 (>= 20140529), libsmraw-python3 (>= 20140612), libvhdi-python3 (>= 20131210), libvmdk-python3 (>= 20140421), libvshadow-python3 (>= 20160109), libvslvm-python3 (>= 20160109), python3-cffi-backend (>= 1.9.1), python3-cryptography (>= 2.0.2), python3-dfdatetime (>= 20200121), python3-dtfabric (>= 20170524), python3-idna (>= 2.5), python3-pytsk3 (>= 20160721), python3-yaml (>= 3.10), ${python3:Depends}, ${misc:Depends} +Depends: libbde-python3 (>= 20140531), libewf-python3 (>= 20131210), libfsapfs-python3 (>= 20181205), libfsext-python3 (>= 20200810), libfsntfs-python3 (>= 20200424), libfvde-python3 (>= 20160719), libfwnt-python3 (>= 20160418), libluksde-python3 (>= 20200101), libqcow-python3 (>= 20131204), libsigscan-python3 (>= 20191221), libsmdev-python3 (>= 20140529), libsmraw-python3 (>= 20140612), libvhdi-python3 (>= 20131210), libvmdk-python3 (>= 20140421), libvshadow-python3 (>= 20160109), libvslvm-python3 (>= 20160109), python3-cffi-backend (>= 1.9.1), python3-cryptography (>= 2.0.2), python3-dfdatetime (>= 20200121), python3-dtfabric (>= 20170524), python3-idna (>= 2.5), 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/jenkins/linux/run_end_to_end_tests.sh b/config/jenkins/linux/run_end_to_end_tests.sh index 72974f72..a9e130e3 100755 --- a/config/jenkins/linux/run_end_to_end_tests.sh +++ b/config/jenkins/linux/run_end_to_end_tests.sh @@ -34,6 +34,10 @@ if ! test -f ${CONFIGURATION_FILE}; then CONFIGURATION_FILE="config/jenkins/sans/${CONFIGURATION_NAME}.ini"; fi +if ! test -f ${CONFIGURATION_FILE}; +then + CONFIGURATION_FILE="config/jenkins/other/${CONFIGURATION_NAME}.ini"; +fi PYTHONPATH=. python3 ./utils/check_dependencies.py diff --git a/config/linux/gift_copr_install.sh b/config/linux/gift_copr_install.sh index 5343d7b5..9d59e255 100644 --- a/config/linux/gift_copr_install.sh +++ b/config/linux/gift_copr_install.sh @@ -11,6 +11,7 @@ set -e PYTHON3_DEPENDENCIES="libbde-python3 libewf-python3 libfsapfs-python3 + libfsext-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 @@ -47,6 +48,8 @@ DEBUG_DEPENDENCIES="libbde-debuginfo libewf-python3-debuginfo libfsapfs-debuginfo libfsapfs-python3-debuginfo + libfsext-debuginfo + libfsext-python3-debuginfo libfsntfs-debuginfo libfsntfs-python3-debuginfo libfvde-debuginfo diff --git a/config/linux/gift_ppa_install_py3.sh b/config/linux/gift_ppa_install_py3.sh index 12fb0087..01c320d8 100755 --- a/config/linux/gift_ppa_install_py3.sh +++ b/config/linux/gift_ppa_install_py3.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# # This script is deprecated and kept for backwards compatibility. +# This script is deprecated and kept for backwards compatibility. # # This file is generated by l2tdevtools update-dependencies.py any dependency # related changes should be made in dependencies.ini. diff --git a/config/linux/ubuntu_install_dfvfs.sh b/config/linux/ubuntu_install_dfvfs.sh index 95911c54..8e80756d 100755 --- a/config/linux/ubuntu_install_dfvfs.sh +++ b/config/linux/ubuntu_install_dfvfs.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # -# Script to install dfvfs on Ubuntu from the GIFT PPA. +# Script to install dfvfs on Ubuntu from the GIFT PPA. Set the environment +# variable GIFT_PPA_TRACK if want to use a specific track. The default is dev. # # This file is generated by l2tdevtools update-dependencies.py any dependency # related changes should be made in dependencies.ini. @@ -8,6 +9,8 @@ # Exit on error. set -e +GIFT_PPA_TRACK=${GIFT_PPA_TRACK:-dev} + export DEBIAN_FRONTEND=noninteractive # Dependencies for running dfvfs, alphabetized, one per line. @@ -15,6 +18,7 @@ export DEBIAN_FRONTEND=noninteractive PYTHON_DEPENDENCIES="libbde-python3 libewf-python3 libfsapfs-python3 + libfsext-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 @@ -53,6 +57,8 @@ DEBUG_DEPENDENCIES="libbde-dbg libewf-python3-dbg libfsapfs-dbg libfsapfs-python3-dbg + libfsext-dbg + libfsext-python3-dbg libfsntfs-dbg libfsntfs-python3-dbg libfvde-dbg @@ -78,7 +84,7 @@ DEBUG_DEPENDENCIES="libbde-dbg libvslvm-dbg libvslvm-python3-dbg"; -sudo add-apt-repository ppa:gift/dev -y +sudo add-apt-repository ppa:gift/${GIFT_PPA_TRACK} -y sudo apt-get update -q sudo apt-get install -y ${PYTHON_DEPENDENCIES} diff --git a/config/travis/install.sh b/config/travis/install.sh index cb1ffa4b..71f9a2bf 100755 --- a/config/travis/install.sh +++ b/config/travis/install.sh @@ -5,11 +5,11 @@ # This file is generated by l2tdevtools update-dependencies.py any dependency # related changes should be made in dependencies.ini. -DPKG_PYTHON3_DEPENDENCIES="libbde-python3 libewf-python3 libfsapfs-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvshadow-python3 libvslvm-python3 python3-cffi-backend python3-cryptography python3-dfdatetime python3-dtfabric python3-idna python3-pytsk3 python3-yaml"; +DPKG_PYTHON3_DEPENDENCIES="libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvshadow-python3 libvslvm-python3 python3-cffi-backend python3-cryptography python3-dfdatetime python3-dtfabric python3-idna python3-pytsk3 python3-yaml"; DPKG_PYTHON3_TEST_DEPENDENCIES="python3-coverage python3-distutils python3-mock python3-pbr python3-setuptools python3-six"; -RPM_PYTHON3_DEPENDENCIES="libbde-python3 libewf-python3 libfsapfs-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvshadow-python3 libvslvm-python3 python3-cffi python3-cryptography python3-dfdatetime python3-dtfabric python3-idna python3-pytsk3 python3-pyyaml"; +RPM_PYTHON3_DEPENDENCIES="libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfsntfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvshadow-python3 libvslvm-python3 python3-cffi python3-cryptography python3-dfdatetime python3-dtfabric python3-idna python3-pytsk3 python3-pyyaml"; RPM_PYTHON3_TEST_DEPENDENCIES="python3-mock python3-pbr python3-setuptools python3-six"; diff --git a/config/travis/runtests.sh b/config/travis/runtests.sh index b4ef8202..8e4f4986 100755 --- a/config/travis/runtests.sh +++ b/config/travis/runtests.sh @@ -50,7 +50,14 @@ then elif test "${TARGET}" = "dockerfile"; then - cd config/docker && docker build --build-arg PPA_TRACK="dev" -f Dockerfile . + SOURCE_PATH=${PWD}; + CONTAINER_NAME="test"; + + cd config/docker + + docker build --build-arg PPA_TRACK="dev" -f Dockerfile -t ${CONTAINER_NAME} . + + # TODO: add tests elif test "${TRAVIS_OS_NAME}" = "osx"; then diff --git a/dependencies.ini b/dependencies.ini index fa52e603..25a00506 100644 --- a/dependencies.ini +++ b/dependencies.ini @@ -53,6 +53,14 @@ pypi_name: libfsapfs-python rpm_name: libfsapfs-python3 version_property: get_version() +[pyfsext] +dpkg_name: libfsext-python3 +l2tbinaries_name: libfsext +minimum_version: 20200810 +pypi_name: libfsext-python +rpm_name: libfsext-python3 +version_property: get_version() + [pyfsntfs] dpkg_name: libfsntfs-python3 l2tbinaries_name: libfsntfs diff --git a/dfvfs/analyzer/__init__.py b/dfvfs/analyzer/__init__.py index 8960310c..ac81b8f8 100644 --- a/dfvfs/analyzer/__init__.py +++ b/dfvfs/analyzer/__init__.py @@ -7,6 +7,7 @@ from dfvfs.analyzer import bzip2_analyzer_helper from dfvfs.analyzer import cpio_analyzer_helper from dfvfs.analyzer import ewf_analyzer_helper +from dfvfs.analyzer import ext_analyzer_helper from dfvfs.analyzer import fvde_analyzer_helper from dfvfs.analyzer import gzip_analyzer_helper from dfvfs.analyzer import lvm_analyzer_helper diff --git a/dfvfs/analyzer/ext_analyzer_helper.py b/dfvfs/analyzer/ext_analyzer_helper.py new file mode 100644 index 00000000..2ffddda0 --- /dev/null +++ b/dfvfs/analyzer/ext_analyzer_helper.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""The EXT format analyzer helper implementation.""" + +from __future__ import unicode_literals + +from dfvfs.analyzer import analyzer +from dfvfs.analyzer import analyzer_helper +from dfvfs.analyzer import specification +from dfvfs.lib import definitions + + +class EXTAnalyzerHelper(analyzer_helper.AnalyzerHelper): + """EXT analyzer helper.""" + + FORMAT_CATEGORIES = frozenset([ + definitions.FORMAT_CATEGORY_FILE_SYSTEM]) + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_EXT + + def GetFormatSpecification(self): + """Retrieves the format specification. + + Returns: + FormatSpecification: format specification or None if the format cannot + be defined by a specification object. + """ + format_specification = specification.FormatSpecification( + self.type_indicator) + + # EXT file system signature. + format_specification.AddNewSignature(b'\x53\xef', offset=1080) + + return format_specification + + def IsEnabled(self): + """Determines if the analyzer helper is enabled. + + Returns: + bool: True if the analyzer helper is enabled. + """ + return definitions.PREFERRED_EXT_BACK_END == self.TYPE_INDICATOR + + +analyzer.Analyzer.RegisterHelper(EXTAnalyzerHelper()) diff --git a/dfvfs/analyzer/tsk_analyzer_helper.py b/dfvfs/analyzer/tsk_analyzer_helper.py index 9dbdf1fb..b920c853 100644 --- a/dfvfs/analyzer/tsk_analyzer_helper.py +++ b/dfvfs/analyzer/tsk_analyzer_helper.py @@ -46,8 +46,9 @@ def GetFormatSpecification(self): # HFSX file system signature. format_specification.AddNewSignature(b'HX', offset=1024) - # Ext file system signature. - format_specification.AddNewSignature(b'\x53\xef', offset=1080) + if definitions.PREFERRED_EXT_BACK_END == self.TYPE_INDICATOR: + # Ext file system signature. + format_specification.AddNewSignature(b'\x53\xef', offset=1080) # ISO9660 file system signature. format_specification.AddNewSignature(b'CD001', offset=32769) diff --git a/dfvfs/file_io/ext_file_io.py b/dfvfs/file_io/ext_file_io.py new file mode 100644 index 00000000..d7f42362 --- /dev/null +++ b/dfvfs/file_io/ext_file_io.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +"""The Extended File System (EXT) file-like object implementation.""" + +from __future__ import unicode_literals + +import os + +from dfvfs.file_io import file_io +from dfvfs.lib import errors +from dfvfs.resolver import resolver + + +class EXTFile(file_io.FileIO): + """File-like object using pyfsext.file_entry""" + + def __init__(self, resolver_context): + """Initializes a file-like object. + + Args: + resolver_context (Context): resolver context. + """ + super(EXTFile, self).__init__(resolver_context) + self._file_system = None + self._fsext_file_entry = None + + def _Close(self): + """Closes the file-like object.""" + self._fsext_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. + NotSupported: if a data stream, like the resource or named fork, is + requested to 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) + if data_stream: + raise errors.NotSupported( + 'Open data stream: {0:s} not supported.'.format(data_stream)) + + 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.') + + fsext_file_entry = file_entry.GetEXTFileEntry() + if not fsext_file_entry: + raise IOError('Unable to open EXT file entry.') + + self._fsext_file_entry = fsext_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.') + + return self._fsext_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.') + + self._fsext_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.') + + return self._fsext_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.') + + return self._fsext_file_entry.get_size() diff --git a/dfvfs/lib/definitions.py b/dfvfs/lib/definitions.py index ea3e6580..5b3fbf02 100644 --- a/dfvfs/lib/definitions.py +++ b/dfvfs/lib/definitions.py @@ -39,6 +39,7 @@ TYPE_INDICATOR_ENCODED_STREAM = 'ENCODED_STREAM' TYPE_INDICATOR_ENCRYPTED_STREAM = 'ENCRYPTED_STREAM' TYPE_INDICATOR_EWF = 'EWF' +TYPE_INDICATOR_EXT = 'EXT' TYPE_INDICATOR_FAKE = 'FAKE' TYPE_INDICATOR_FVDE = 'FVDE' TYPE_INDICATOR_GZIP = 'GZIP' @@ -74,6 +75,7 @@ FILE_SYSTEM_TYPE_INDICATORS = frozenset([ TYPE_INDICATOR_APFS, + TYPE_INDICATOR_EXT, TYPE_INDICATOR_FAKE, TYPE_INDICATOR_NTFS, TYPE_INDICATOR_TSK]) @@ -92,6 +94,7 @@ TYPE_INDICATOR_VSHADOW]) # The preferred back-ends. +PREFERRED_EXT_BACK_END = TYPE_INDICATOR_TSK PREFERRED_NTFS_BACK_END = TYPE_INDICATOR_NTFS # The NTFS attribute types. diff --git a/dfvfs/path/__init__.py b/dfvfs/path/__init__.py index 30f79977..614b8486 100644 --- a/dfvfs/path/__init__.py +++ b/dfvfs/path/__init__.py @@ -10,6 +10,7 @@ from dfvfs.path import encoded_stream_path_spec from dfvfs.path import encrypted_stream_path_spec from dfvfs.path import ewf_path_spec +from dfvfs.path import ext_path_spec from dfvfs.path import fake_path_spec from dfvfs.path import fvde_path_spec from dfvfs.path import gzip_path_spec diff --git a/dfvfs/path/ext_path_spec.py b/dfvfs/path/ext_path_spec.py new file mode 100644 index 00000000..fcf399ad --- /dev/null +++ b/dfvfs/path/ext_path_spec.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +"""The EXT 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 EXTPathSpec(path_spec.PathSpec): + """EXT path specification implementation. + + Attributes: + inode (int): inode. + location (str): location. + """ + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_EXT + + def __init__( + self, inode=None, location=None, parent=None, **kwargs): + """Initializes a path specification. + + Note that an EXT path specification must have a parent. + + Args: + inode (Optional[int]): inode. + location (Optional[str]): location. + parent (Optional[PathSpec]): parent path specification. + + Raises: + ValueError: when parent or both inode and location are not set. + """ + if (not inode and not location) or not parent: + raise ValueError('Missing inode and location, or parent value.') + + super(EXTPathSpec, self).__init__(parent=parent, **kwargs) + self.inode = inode + self.location = location + + @property + def comparable(self): + """str: comparable representation of the path specification.""" + string_parts = [] + + if self.inode is not None: + string_parts.append('inode: {0:d}'.format(self.inode)) + 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(EXTPathSpec) diff --git a/dfvfs/resolver_helpers/__init__.py b/dfvfs/resolver_helpers/__init__.py index 3918c411..d6e98154 100644 --- a/dfvfs/resolver_helpers/__init__.py +++ b/dfvfs/resolver_helpers/__init__.py @@ -23,6 +23,11 @@ except ImportError: pass +try: + from dfvfs.resolver_helpers import ext_resolver_helper +except ImportError: + pass + from dfvfs.resolver_helpers import fake_resolver_helper try: diff --git a/dfvfs/resolver_helpers/ext_resolver_helper.py b/dfvfs/resolver_helpers/ext_resolver_helper.py new file mode 100644 index 00000000..37515dc0 --- /dev/null +++ b/dfvfs/resolver_helpers/ext_resolver_helper.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""The EXT path specification resolver helper implementation.""" + +from __future__ import unicode_literals + +from dfvfs.file_io import ext_file_io +from dfvfs.lib import definitions +from dfvfs.resolver_helpers import manager +from dfvfs.resolver_helpers import resolver_helper +from dfvfs.vfs import ext_file_system + + +class EXTResolverHelper(resolver_helper.ResolverHelper): + """EXT resolver helper.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_EXT + + def NewFileObject(self, resolver_context): + """Creates a new file-like object. + + Args: + resolver_context (Context): resolver context. + + Returns: + FileIO: file-like object. + """ + return ext_file_io.EXTFile(resolver_context) + + def NewFileSystem(self, resolver_context): + """Creates a new file system object. + + Args: + resolver_context (Context): resolver context. + + Returns: + FileSystem: file system. + """ + return ext_file_system.EXTFileSystem(resolver_context) + + +manager.ResolverHelperManager.RegisterHelper(EXTResolverHelper()) diff --git a/dfvfs/vfs/ext_file_entry.py b/dfvfs/vfs/ext_file_entry.py new file mode 100644 index 00000000..5a27173e --- /dev/null +++ b/dfvfs/vfs/ext_file_entry.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +"""The EXT file entry implementation.""" + +from __future__ import unicode_literals + +from dfdatetime import posix_time as dfdatetime_posix_time + +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import ext_path_spec +from dfvfs.vfs import file_entry + + +class EXTDirectory(file_entry.Directory): + """File system directory that uses pyfsext.""" + + def _EntriesGenerator(self): + """Retrieves directory entries. + + Since a directory can contain a vast number of entries using + a generator is more memory efficient. + + Yields: + EXTPathSpec: EXT path specification. + """ + try: + fsext_file_entry = self._file_system.GetEXTFileEntryByPathSpec( + self.path_spec) + except errors.PathSpecError: + return + + location = getattr(self.path_spec, 'location', None) + + for fsext_sub_file_entry in fsext_file_entry.sub_file_entries: + directory_entry = fsext_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 ext_path_spec.EXTPathSpec( + inode=fsext_sub_file_entry.inode_number, location=directory_entry, + parent=self.path_spec.parent) + + +class EXTFileEntry(file_entry.FileEntry): + """File system file entry that uses pyfsext.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_EXT + + # Mappings of EXT 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} + + def __init__( + self, resolver_context, file_system, path_spec, fsext_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. + fsext_file_entry (Optional[pyfsext.file_entry]): EXT 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 pyfsext file entry is missing. + """ + if not fsext_file_entry: + fsext_file_entry = file_system.GetEXTFileEntryByPathSpec(path_spec) + if not fsext_file_entry: + raise errors.BackEndError('Missing pyfsext file entry.') + + if is_root: + file_entry_name = '' + else: + file_entry_name = fsext_file_entry.name + + # Use the path specification location to determine the file entry name + # if the file entry was retrieved by inode. + if file_entry_name is None: + location = getattr(path_spec, 'location', None) + if location: + location_segments = file_system.SplitPath(location) + if location_segments: + file_entry_name = location_segments[-1] + + super(EXTFileEntry, self).__init__( + resolver_context, file_system, path_spec, is_root=is_root, + is_virtual=is_virtual) + self._fsext_file_entry = fsext_file_entry + self._name = file_entry_name + + self.entry_type = self._ENTRY_TYPES.get( + fsext_file_entry.file_mode & 0xf000, None) + + def _GetDirectory(self): + """Retrieves a directory. + + Returns: + EXTDirectory: directory or None if not available. + """ + if self.entry_type != definitions.FILE_ENTRY_TYPE_DIRECTORY: + return None + + return EXTDirectory(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._fsext_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(EXTFileEntry, self)._GetStat() + + # File data stat information. + stat_object.size = self._fsext_file_entry.size + + # Ownership and permissions stat information. + stat_object.mode = self._fsext_file_entry.file_mode & 0x0fff + stat_object.uid = self._fsext_file_entry.owner_identifier + stat_object.gid = self._fsext_file_entry.group_identifier + + # File entry type stat information. + stat_object.type = self.entry_type + + # Other stat information. + stat_object.ino = self._fsext_file_entry.inode_number + stat_object.fs_type = 'EXT' + + stat_object.is_allocated = True + + return stat_object + + def _GetSubFileEntries(self): + """Retrieves a sub file entries generator. + + Yields: + EXTFileEntry: a sub file entry. + """ + if self._directory is None: + self._directory = self._GetDirectory() + + if self._directory: + for path_spec in self._directory.entries: + yield EXTFileEntry( + self._resolver_context, self._file_system, path_spec) + + @property + def access_time(self): + """dfdatetime.DateTimeValues: access time or None if not available.""" + timestamp = self._fsext_file_entry.get_access_time_as_integer() + return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp) + + @property + def change_time(self): + """dfdatetime.DateTimeValues: change time or None if not available.""" + timestamp = self._fsext_file_entry.get_inode_change_time_as_integer() + return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp) + + @property + def creation_time(self): + """dfdatetime.DateTimeValues: creation time or None if not available.""" + timestamp = self._fsext_file_entry.get_creation_time_as_integer() + return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp) + + @property + def deletion_time(self): + """dfdatetime.DateTimeValues: deletion time or None if not available.""" + timestamp = self._fsext_file_entry.get_deletion_time_as_integer() + return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp) + + @property + def name(self): + """str: name of the file entry, which does not include the full path.""" + return self._name + + @property + def modification_time(self): + """dfdatetime.DateTimeValues: modification time or None if not available.""" + timestamp = self._fsext_file_entry.get_modification_time_as_integer() + return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp) + + def GetEXTFileEntry(self): + """Retrieves the EXT file entry. + + Returns: + pyfsext.file_entry: EXT file entry. + """ + return self._fsext_file_entry + + def GetLinkedFileEntry(self): + """Retrieves the linked file entry, e.g. for a symbolic link. + + Returns: + EXTFileEntry: linked file entry or None if not available. + """ + link = self._GetLink() + if not link: + return None + + parent_path_spec = getattr(self.path_spec, 'parent', None) + path_spec = ext_path_spec.EXTPathSpec( + location=link, parent=parent_path_spec) + + is_root = bool(link == self._file_system.LOCATION_ROOT) + + return EXTFileEntry( + self._resolver_context, self._file_system, path_spec, is_root=is_root) + + def GetParentFileEntry(self): + """Retrieves the parent file entry. + + Returns: + EXTFileEntry: 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_path_spec = getattr(self.path_spec, 'parent', None) + path_spec = ext_path_spec.EXTPathSpec( + location=parent_location, parent=parent_path_spec) + + is_root = bool(parent_location == self._file_system.LOCATION_ROOT) + + return EXTFileEntry( + self._resolver_context, self._file_system, path_spec, is_root=is_root) diff --git a/dfvfs/vfs/ext_file_system.py b/dfvfs/vfs/ext_file_system.py new file mode 100644 index 00000000..eba0db2c --- /dev/null +++ b/dfvfs/vfs/ext_file_system.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +"""The EXT file system implementation.""" + +from __future__ import unicode_literals + +import pyfsext + +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import ext_path_spec +from dfvfs.resolver import resolver +from dfvfs.vfs import file_system +from dfvfs.vfs import ext_file_entry + + +class EXTFileSystem(file_system.FileSystem): + """File system that uses pyfsext.""" + + ROOT_DIRECTORY_INODE_NUMBER = 2 + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_EXT + + def __init__(self, resolver_context): + """Initializes an EXT file system. + + Args: + resolver_context (Context): resolver context. + """ + super(EXTFileSystem, self).__init__(resolver_context) + self._file_object = None + self._fsext_volume = None + + def _Close(self): + """Closes the file system. + + Raises: + IOError: if the close failed. + """ + self._fsext_volume = 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): 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. + 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: + fsext_volume = pyfsext.volume() + fsext_volume.open_file_object(file_object) + except: + file_object.close() + raise + + self._file_object = file_object + self._fsext_volume = fsext_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 inode is faster than opening a file by location. + fsext_file_entry = None + location = getattr(path_spec, 'location', None) + inode = getattr(path_spec, 'inode', None) + + try: + if inode is not None: + fsext_file_entry = self._fsext_volume.get_file_entry_by_inode(inode) + elif location is not None: + fsext_file_entry = self._fsext_volume.get_file_entry_by_path(location) + + except IOError as exception: + raise errors.BackEndError(exception) + + return fsext_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: + EXTFileEntry: file entry or None if not available. + + Raises: + BackEndError: if the file entry cannot be opened. + """ + # Opening a file by inode is faster than opening a file by location. + fsext_file_entry = None + location = getattr(path_spec, 'location', None) + inode = getattr(path_spec, 'inode', None) + + if (location == self.LOCATION_ROOT or + inode == self.ROOT_DIRECTORY_INODE_NUMBER): + fsext_file_entry = self._fsext_volume.get_root_directory() + return ext_file_entry.EXTFileEntry( + self._resolver_context, self, path_spec, + fsext_file_entry=fsext_file_entry, is_root=True) + + try: + if inode is not None: + fsext_file_entry = self._fsext_volume.get_file_entry_by_inode( + inode) + elif location is not None: + fsext_file_entry = self._fsext_volume.get_file_entry_by_path(location) + + except IOError as exception: + raise errors.BackEndError(exception) + + if fsext_file_entry is None: + return None + + return ext_file_entry.EXTFileEntry( + self._resolver_context, self, path_spec, + fsext_file_entry=fsext_file_entry) + + def GetEXTFileEntryByPathSpec(self, path_spec): + """Retrieves the EXT file entry for a path specification. + + Args: + path_spec (PathSpec): a path specification. + + Returns: + pyfsext.file_entry: file entry. + + Raises: + PathSpecError: if the path specification is missing location and + inode. + """ + # Opening a file by inode is faster than opening a file by location. + location = getattr(path_spec, 'location', None) + inode = getattr(path_spec, 'inode', None) + + if inode is not None: + fsext_file_entry = self._fsext_volume.get_file_entry_by_inode(inode) + elif location is not None: + fsext_file_entry = self._fsext_volume.get_file_entry_by_path(location) + else: + raise errors.PathSpecError( + 'Path specification missing location and inode.') + + return fsext_file_entry + + def GetRootFileEntry(self): + """Retrieves the root file entry. + + Returns: + EXTFileEntry: file entry. + """ + path_spec = ext_path_spec.EXTPathSpec( + location=self.LOCATION_ROOT, inode=self.ROOT_DIRECTORY_INODE_NUMBER, + parent=self._path_spec.parent) + return self.GetFileEntryByPathSpec(path_spec) diff --git a/requirements.txt b/requirements.txt index 798faafc..4c38807e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ idna >= 2.5 libbde-python >= 20140531 libewf-python >= 20131210 libfsapfs-python >= 20181205 +libfsext-python >= 20200810 libfsntfs-python >= 20200424 libfvde-python >= 20160719 libfwnt-python >= 20160418 diff --git a/setup.cfg b/setup.cfg index 15b1d0f5..92215ba9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,7 @@ build_requires = python3-setuptools requires = libbde-python3 >= 20140531 libewf-python3 >= 20131210 libfsapfs-python3 >= 20181205 + libfsext-python3 >= 20200810 libfsntfs-python3 >= 20200424 libfvde-python3 >= 20160719 libfwnt-python3 >= 20160418 diff --git a/setup.py b/setup.py index b337fc7c..f38458da 100755 --- a/setup.py +++ b/setup.py @@ -27,9 +27,9 @@ from distutils.command.sdist import sdist version_tuple = (sys.version_info[0], sys.version_info[1]) -if version_tuple < (3, 5): +if version_tuple < (3, 6): print(( - 'Unsupported Python version: {0:s}, version 3.5 or higher ' + 'Unsupported Python version: {0:s}, version 3.6 or higher ' 'required.').format(sys.version)) sys.exit(1) @@ -189,7 +189,7 @@ def _make_spec_file(self): 'Programming Language :: Python', ], packages=find_packages('.', exclude=[ - 'examples', 'tests', 'tests.*', 'utils']), + 'docs', 'examples', 'tests', 'tests.*', 'utils']), package_dir={ 'dfvfs': 'dfvfs' }, diff --git a/tests/file_io/ext_file_io.py b/tests/file_io/ext_file_io.py new file mode 100644 index 00000000..b58d3798 --- /dev/null +++ b/tests/file_io/ext_file_io.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the Extended File System (EXT) file-like object.""" + +from __future__ import unicode_literals + +import os +import unittest + +from dfvfs.file_io import ext_file_io +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import context + +from tests.file_io import test_lib + + +class EXTFileTest(test_lib.ImageFileTestCase): + """Tests the file-like object implementation using pyfsext.file_entry.""" + + _IDENTIFIER_ANOTHER_FILE = 21 + _IDENTIFIER_PASSWORDS_TXT = 20 + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + super(EXTFileTest, self).setUp() + self._resolver_context = context.Context() + test_path = self._GetTestFilePath(['ext.raw']) + self._SkipIfPathNotExists(test_path) + + test_os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_path) + self._raw_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_RAW, parent=test_os_path_spec) + + def testOpenCloseIdentifier(self): + """Test the open and close functionality using an inode.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, + inode=self._IDENTIFIER_PASSWORDS_TXT, + parent=self._raw_path_spec) + file_object = ext_file_io.EXTFile(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 = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, location='/passwords.txt', + parent=self._raw_path_spec) + file_object = ext_file_io.EXTFile(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 = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, location='/a_directory/another_file', + inode=self._IDENTIFIER_ANOTHER_FILE, + parent=self._raw_path_spec) + file_object = ext_file_io.EXTFile(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 = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, location='/passwords.txt', + inode=self._IDENTIFIER_PASSWORDS_TXT, + parent=self._raw_path_spec) + file_object = ext_file_io.EXTFile(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/path/ext_path_spec.py b/tests/path/ext_path_spec.py new file mode 100644 index 00000000..3e0b30fa --- /dev/null +++ b/tests/path/ext_path_spec.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the EXT path specification implementation.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.path import ext_path_spec + +from tests.path import test_lib + + +class EXTPathSpecTest(test_lib.PathSpecTestCase): + """Tests for the EXT path specification implementation.""" + + def testInitialize(self): + """Tests the path specification initialization.""" + path_spec = ext_path_spec.EXTPathSpec( + location='/test', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = ext_path_spec.EXTPathSpec( + inode=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = ext_path_spec.EXTPathSpec( + location='/test', inode=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + with self.assertRaises(ValueError): + ext_path_spec.EXTPathSpec(location='/test', parent=None) + + with self.assertRaises(ValueError): + ext_path_spec.EXTPathSpec(location=None, parent=self._path_spec) + + with self.assertRaises(ValueError): + ext_path_spec.EXTPathSpec(inode=None, parent=self._path_spec) + + with self.assertRaises(ValueError): + ext_path_spec.EXTPathSpec( + location='/test', parent=self._path_spec, bogus='BOGUS') + + def testComparable(self): + """Tests the path specification comparable property.""" + path_spec = ext_path_spec.EXTPathSpec( + location='/test', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: EXT, location: /test', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = ext_path_spec.EXTPathSpec( + inode=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: EXT, inode: 1', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = ext_path_spec.EXTPathSpec( + location='/test', inode=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: EXT, inode: 1, location: /test', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/resolver_helpers/ext_resolver_helper.py b/tests/resolver_helpers/ext_resolver_helper.py new file mode 100644 index 00000000..23799f2f --- /dev/null +++ b/tests/resolver_helpers/ext_resolver_helper.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the EXT resolver helper implementation.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.resolver_helpers import ext_resolver_helper + +from tests.resolver_helpers import test_lib + + +class EXTResolverHelperTest(test_lib.ResolverHelperTestCase): + """Tests for the EXT resolver helper implementation.""" + + def testNewFileObject(self): + """Tests the NewFileObject function.""" + resolver_helper_object = ext_resolver_helper.EXTResolverHelper() + self._TestNewFileObject(resolver_helper_object) + + def testNewFileSystem(self): + """Tests the NewFileSystem function.""" + resolver_helper_object = ext_resolver_helper.EXTResolverHelper() + self._TestNewFileSystem(resolver_helper_object) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/ext_file_entry.py b/tests/vfs/ext_file_entry.py new file mode 100644 index 00000000..f66232d2 --- /dev/null +++ b/tests/vfs/ext_file_entry.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the file entry implementation using pyfsext.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.path import ext_path_spec +from dfvfs.path import os_path_spec +from dfvfs.path import raw_path_spec +from dfvfs.resolver import context +from dfvfs.vfs import ext_file_entry +from dfvfs.vfs import ext_file_system + +from tests import test_lib as shared_test_lib + + +# TODO: add tests for EXTDirectory. + + +class EXTFileEntryTest(shared_test_lib.BaseTestCase): + """Tests the EXT file entry.""" + + _INODE_A_DIRECTORY = 12 + _INODE_A_LINK = 16 + _INODE_ANOTHER_FILE = 15 + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_file = self._GetTestFilePath(['ext2.raw']) + self._SkipIfPathNotExists(test_file) + + test_os_path_spec = os_path_spec.OSPathSpec(location=test_file) + self._raw_path_spec = raw_path_spec.RawPathSpec(parent=test_os_path_spec) + self._ext_path_spec = ext_path_spec.EXTPathSpec( + location='/', parent=self._raw_path_spec) + + self._file_system = ext_file_system.EXTFileSystem(self._resolver_context) + self._file_system.Open(self._ext_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 = ext_file_entry.EXTFileEntry( + self._resolver_context, self._file_system, self._ext_path_spec) + + self.assertIsNotNone(file_entry) + + def testAccessTime(self): + """Test the access_time property.""" + test_location = '/a_directory/another_file' + path_spec = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec(inode=19, parent=self._raw_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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_A_LINK, location=test_location, + parent=self._raw_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, 'another_file') + + def testGetParentFileEntry(self): + """Tests the GetParentFileEntry function.""" + test_location = '/a_directory/another_file' + path_spec = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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, 436) + self.assertEqual(stat_object.uid, 1000) + self.assertEqual(stat_object.gid, 1000) + + self.assertEqual(stat_object.atime, 1567246979) + self.assertEqual(stat_object.atime_nano, 0) + + self.assertEqual(stat_object.ctime, 1567246979) + self.assertEqual(stat_object.ctime_nano, 0) + + self.assertEqual(stat_object.crtime, 0) + self.assertEqual(stat_object.crtime_nano, 0) + + self.assertEqual(stat_object.mtime, 1567246979) + self.assertEqual(stat_object.mtime_nano, 0) + + def testIsFunctions(self): + """Tests the Is? functions.""" + test_location = '/a_directory/another_file' + path_spec = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_A_DIRECTORY, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec( + location='/', parent=self._raw_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 = ext_path_spec.EXTPathSpec( + location='/', parent=self._raw_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 = [ + 'a_directory', + 'a_link', + 'lost+found', + '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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_A_DIRECTORY, location=test_location, + parent=self._raw_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 = ext_path_spec.EXTPathSpec( + inode=self._INODE_ANOTHER_FILE, location=test_location, + parent=self._raw_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/ext_file_system.py b/tests/vfs/ext_file_system.py new file mode 100644 index 00000000..69794f1b --- /dev/null +++ b/tests/vfs/ext_file_system.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the file system implementation using pyfsext.""" + +from __future__ import unicode_literals + +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import context +from dfvfs.vfs import ext_file_system + +from tests import test_lib as shared_test_lib + + +class EXTFileSystemTest(shared_test_lib.BaseTestCase): + """Tests the EXT file entry.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_path = self._GetTestFilePath(['ext2.raw']) + self._SkipIfPathNotExists(test_path) + + test_os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_path) + self._raw_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_RAW, parent=test_os_path_spec) + self._ext_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, location='/', + parent=self._raw_path_spec) + + def testOpenAndClose(self): + """Test the open and close functionality.""" + file_system = ext_file_system.EXTFileSystem(self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._ext_path_spec) + + file_system.Close() + + def testFileEntryExistsByPathSpec(self): + """Test the file entry exists by path specification functionality.""" + file_system = ext_file_system.EXTFileSystem(self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._ext_path_spec) + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, location='/passwords.txt', + inode=20, parent=self._raw_path_spec) + self.assertTrue(file_system.FileEntryExistsByPathSpec(path_spec)) + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, location='/bogus.txt', + parent=self._raw_path_spec) + self.assertFalse(file_system.FileEntryExistsByPathSpec(path_spec)) + + file_system.Close() + + def testGetFileEntryByPathSpec(self): + """Tests the GetFileEntryByPathSpec function.""" + file_system = ext_file_system.EXTFileSystem(self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._ext_path_spec) + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, inode=20, + parent=self._raw_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 = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, location='/passwords.txt', + inode=20, parent=self._raw_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.name, 'passwords.txt') + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, location='/bogus.txt', + parent=self._raw_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNone(file_entry) + + file_system.Close() + + # TODO: add tests for GetEXTFileEntryByPathSpec function. + + def testGetRootFileEntry(self): + """Test the get root file entry functionality.""" + file_system = ext_file_system.EXTFileSystem(self._resolver_context) + self.assertIsNotNone(file_system) + + file_system.Open(self._ext_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/tox.ini b/tox.ini index a8f2517e..5dd5522c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py3{5,6,7,8},coverage,docs,pylint +envlist = py3{6,7,8},coverage,docs,pylint [testenv] pip_pre = True @@ -10,7 +10,7 @@ deps = -rtest_requirements.txt coverage: coverage commands = - py3{5,6,7,8}: ./run_tests.py + py3{6,7,8}: ./run_tests.py coverage: coverage erase coverage: coverage run --source=dfvfs --omit="*_test*,*__init__*,*test_lib*" run_tests.py