From 5c46dd584f6f31e709ef548d47b6e23725371b1b Mon Sep 17 00:00:00 2001 From: Joachim Metz Date: Fri, 29 Jul 2022 08:21:09 +0200 Subject: [PATCH] Added fsfat back-end #580 --- .github/workflows/test_docker.yml | 4 +- .github/workflows/test_docs.yml | 2 +- .github/workflows/test_tox.yml | 2 +- config/appveyor/install.ps1 | 2 +- config/dpkg/control | 2 +- dependencies.ini | 10 +- dfvfs/analyzer/__init__.py | 1 + dfvfs/analyzer/fat_analyzer_helper.py | 52 +++ dfvfs/analyzer/tsk_analyzer_helper.py | 21 +- dfvfs/file_io/fat_file_io.py | 149 ++++++++ dfvfs/helpers/source_scanner.py | 3 +- dfvfs/lib/definitions.py | 3 + dfvfs/path/__init__.py | 1 + dfvfs/path/fat_path_spec.py | 53 +++ dfvfs/resolver_helpers/__init__.py | 5 + dfvfs/resolver_helpers/fat_resolver_helper.py | 41 +++ dfvfs/vfs/fat_directory.py | 44 +++ dfvfs/vfs/fat_file_entry.py | 208 +++++++++++ dfvfs/vfs/fat_file_system.py | 186 ++++++++++ dfvfs/vfs/ntfs_file_entry.py | 5 +- dfvfs/vfs/tsk_file_entry.py | 3 +- requirements.txt | 3 +- setup.cfg | 3 +- tests/analyzer/analyzer.py | 12 + tests/file_io/fat_file_io.py | 86 +++++ tests/helpers/source_scanner.py | 3 +- tests/helpers/volume_scanner.py | 16 +- tests/path/fat_path_spec.py | 85 +++++ tests/path/hfs_path_spec.py | 1 - tests/resolver_helpers/fat_resolver_helper.py | 49 +++ tests/vfs/fat_directory.py | 66 ++++ tests/vfs/fat_file_entry.py | 322 ++++++++++++++++++ tests/vfs/fat_file_system.py | 113 ++++++ 33 files changed, 1528 insertions(+), 28 deletions(-) create mode 100644 dfvfs/analyzer/fat_analyzer_helper.py create mode 100644 dfvfs/file_io/fat_file_io.py create mode 100644 dfvfs/path/fat_path_spec.py create mode 100644 dfvfs/resolver_helpers/fat_resolver_helper.py create mode 100644 dfvfs/vfs/fat_directory.py create mode 100644 dfvfs/vfs/fat_file_entry.py create mode 100644 dfvfs/vfs/fat_file_system.py create mode 100644 tests/file_io/fat_file_io.py create mode 100644 tests/path/fat_path_spec.py create mode 100644 tests/resolver_helpers/fat_resolver_helper.py create mode 100644 tests/vfs/fat_directory.py create mode 100644 tests/vfs/fat_file_entry.py create mode 100644 tests/vfs/fat_file_system.py diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index 71972706..d7e746f5 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -17,7 +17,7 @@ jobs: - name: Install dependencies run: | dnf copr -y enable @gift/dev - dnf install -y @development-tools python3 python3-devel libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 python3-cffi python3-cryptography python3-dfdatetime python3-dtfabric python3-idna python3-mock python3-pbr python3-pytsk3 python3-pyxattr python3-pyyaml python3-setuptools python3-six + dnf install -y @development-tools python3 python3-devel libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfsfat-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 python3-cffi python3-cryptography python3-dfdatetime python3-dtfabric python3-idna python3-mock python3-pbr python3-pytsk3 python3-pyxattr python3-pyyaml python3-setuptools python3-six - name: Run tests env: LANG: C.utf8 @@ -57,7 +57,7 @@ jobs: run: | add-apt-repository -y ppa:gift/dev apt-get update -q - apt-get install -y build-essential python3 python3-dev libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 python3-cffi-backend python3-cryptography python3-dfdatetime python3-distutils python3-dtfabric python3-idna python3-mock python3-pbr python3-pytsk3 python3-pyxattr python3-setuptools python3-six python3-yaml + apt-get install -y build-essential python3 python3-dev libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfsfat-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 python3-cffi-backend python3-cryptography python3-dfdatetime python3-distutils python3-dtfabric python3-idna python3-mock python3-pbr python3-pytsk3 python3-pyxattr python3-setuptools python3-six python3-yaml - name: Run tests env: LANG: en_US.UTF-8 diff --git a/.github/workflows/test_docs.yml b/.github/workflows/test_docs.yml index da3d8aff..d579963c 100644 --- a/.github/workflows/test_docs.yml +++ b/.github/workflows/test_docs.yml @@ -35,7 +35,7 @@ jobs: add-apt-repository -y ppa:deadsnakes/ppa add-apt-repository -y ppa:gift/dev apt-get update -q - apt-get install -y build-essential git libffi-dev python${{ matrix.python-version }} python${{ matrix.python-version }}-dev python${{ matrix.python-version }}-venv libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 python3-cffi-backend python3-cryptography python3-dfdatetime python3-distutils python3-dtfabric python3-idna python3-mock python3-pbr python3-pip python3-pytsk3 python3-pyxattr python3-setuptools python3-six python3-yaml + apt-get install -y build-essential git libffi-dev python${{ matrix.python-version }} python${{ matrix.python-version }}-dev python${{ matrix.python-version }}-venv libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfsfat-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 python3-cffi-backend python3-cryptography python3-dfdatetime python3-distutils python3-dtfabric python3-idna python3-mock python3-pbr python3-pip python3-pytsk3 python3-pyxattr python3-setuptools python3-six python3-yaml - name: Install tox run: | python3 -m pip install tox diff --git a/.github/workflows/test_tox.yml b/.github/workflows/test_tox.yml index 919eab7a..fb602e40 100644 --- a/.github/workflows/test_tox.yml +++ b/.github/workflows/test_tox.yml @@ -45,7 +45,7 @@ jobs: add-apt-repository -y ppa:deadsnakes/ppa add-apt-repository -y ppa:gift/dev apt-get update -q - apt-get install -y build-essential git libffi-dev python${{ matrix.python-version }} python${{ matrix.python-version }}-dev python${{ matrix.python-version }}-venv libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 python3-cffi-backend python3-cryptography python3-dfdatetime python3-distutils python3-dtfabric python3-idna python3-mock python3-pbr python3-pip python3-pytsk3 python3-pyxattr python3-setuptools python3-six python3-yaml + apt-get install -y build-essential git libffi-dev python${{ matrix.python-version }} python${{ matrix.python-version }}-dev python${{ matrix.python-version }}-venv libbde-python3 libewf-python3 libfsapfs-python3 libfsext-python3 libfsfat-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 python3-cffi-backend python3-cryptography python3-dfdatetime python3-distutils python3-dtfabric python3-idna python3-mock python3-pbr python3-pip python3-pytsk3 python3-pyxattr python3-setuptools python3-six python3-yaml - name: Install tox run: | python3 -m pip install tox diff --git a/config/appveyor/install.ps1 b/config/appveyor/install.ps1 index c5fe08f5..d1ad8a6e 100644 --- a/config/appveyor/install.ps1 +++ b/config/appveyor/install.ps1 @@ -1,6 +1,6 @@ # Script to set up tests on AppVeyor Windows. -$Dependencies = "PyYAML cffi cryptography dfdatetime dtfabric idna libbde libewf libfsapfs libfsext libfshfs libfsntfs libfsxfs libfvde libfwnt libluksde libmodi libphdi libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvsgpt libvshadow libvslvm mock pbr pytsk3 six xattr" +$Dependencies = "PyYAML cffi cryptography dfdatetime dtfabric idna libbde libewf libfsapfs libfsext libfsfat libfshfs libfsntfs libfsxfs libfvde libfwnt libluksde libmodi libphdi libqcow libsigscan libsmdev libsmraw libvhdi libvmdk libvsgpt libvshadow libvslvm mock pbr pytsk3 six xattr" $Dependencies = ${Dependencies} -split " " $Output = Invoke-Expression -Command "git clone https://github.com/log2timeline/l2tdevtools.git ..\l2tdevtools 2>&1" diff --git a/config/dpkg/control b/config/dpkg/control index 4d8c5587..1da9f3cc 100644 --- a/config/dpkg/control +++ b/config/dpkg/control @@ -9,7 +9,7 @@ Homepage: https://github.com/log2timeline/dfvfs Package: python3-dfvfs Architecture: all -Depends: libbde-python3 (>= 20220121), libewf-python3 (>= 20131210), libfsapfs-python3 (>= 20220709), libfsext-python3 (>= 20220529), libfshfs-python3 (>= 20220709), libfsntfs-python3 (>= 20211229), libfsxfs-python3 (>= 20220706), libfvde-python3 (>= 20220121), libfwnt-python3 (>= 20210717), libluksde-python3 (>= 20220121), libmodi-python3 (>= 20210405), libphdi-python3 (>= 20220228), libqcow-python3 (>= 20201213), libsigscan-python3 (>= 20191221), libsmdev-python3 (>= 20140529), libsmraw-python3 (>= 20140612), libvhdi-python3 (>= 20201014), libvmdk-python3 (>= 20140421), libvsgpt-python3 (>= 20211115), libvshadow-python3 (>= 20160109), libvslvm-python3 (>= 20160109), python3-cffi-backend (>= 1.9.1), python3-cryptography (>= 2.0.2), python3-dfdatetime (>= 20211113), python3-dtfabric (>= 20220219), python3-idna (>= 2.5), python3-pytsk3 (>= 20210419), python3-pyxattr (>= 0.7.2), python3-yaml (>= 3.10), ${misc:Depends} +Depends: libbde-python3 (>= 20220121), libewf-python3 (>= 20131210), libfsapfs-python3 (>= 20220709), libfsext-python3 (>= 20220529), libfsfat-python3 (>= 20220816), libfshfs-python3 (>= 20220709), libfsntfs-python3 (>= 20211229), libfsxfs-python3 (>= 20220706), libfvde-python3 (>= 20220121), libfwnt-python3 (>= 20210717), libluksde-python3 (>= 20220121), libmodi-python3 (>= 20210405), libphdi-python3 (>= 20220228), libqcow-python3 (>= 20201213), libsigscan-python3 (>= 20191221), libsmdev-python3 (>= 20140529), libsmraw-python3 (>= 20140612), libvhdi-python3 (>= 20201014), libvmdk-python3 (>= 20140421), libvsgpt-python3 (>= 20211115), libvshadow-python3 (>= 20160109), libvslvm-python3 (>= 20160109), python3-cffi-backend (>= 1.9.1), python3-cryptography (>= 2.0.2), python3-dfdatetime (>= 20220816), python3-dtfabric (>= 20220219), python3-idna (>= 2.5), python3-pytsk3 (>= 20210419), python3-pyxattr (>= 0.7.2), python3-yaml (>= 3.10), ${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/dependencies.ini b/dependencies.ini index b4ad425f..9afa71ba 100644 --- a/dependencies.ini +++ b/dependencies.ini @@ -13,7 +13,7 @@ version_property: __version__ [dfdatetime] dpkg_name: python3-dfdatetime -minimum_version: 20211113 +minimum_version: 20220816 rpm_name: python3-dfdatetime version_property: __version__ @@ -63,6 +63,14 @@ pypi_name: libfsext-python rpm_name: libfsext-python3 version_property: get_version() +[pyfsfat] +dpkg_name: libfsfat-python3 +l2tbinaries_name: libfsfat +minimum_version: 20220816 +pypi_name: libfsfat-python +rpm_name: libfsfat-python3 +version_property: get_version() + [pyfshfs] dpkg_name: libfshfs-python3 l2tbinaries_name: libfshfs diff --git a/dfvfs/analyzer/__init__.py b/dfvfs/analyzer/__init__.py index af798cb5..948f35d5 100644 --- a/dfvfs/analyzer/__init__.py +++ b/dfvfs/analyzer/__init__.py @@ -9,6 +9,7 @@ from dfvfs.analyzer import cs_analyzer_helper from dfvfs.analyzer import ewf_analyzer_helper from dfvfs.analyzer import ext_analyzer_helper +from dfvfs.analyzer import fat_analyzer_helper from dfvfs.analyzer import gpt_analyzer_helper from dfvfs.analyzer import gzip_analyzer_helper from dfvfs.analyzer import hfs_analyzer_helper diff --git a/dfvfs/analyzer/fat_analyzer_helper.py b/dfvfs/analyzer/fat_analyzer_helper.py new file mode 100644 index 00000000..0c43ed48 --- /dev/null +++ b/dfvfs/analyzer/fat_analyzer_helper.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +"""The FAT format analyzer helper implementation.""" + +from dfvfs.analyzer import analyzer +from dfvfs.analyzer import analyzer_helper +from dfvfs.analyzer import specification +from dfvfs.lib import definitions + + +class FATAnalyzerHelper(analyzer_helper.AnalyzerHelper): + """FAT analyzer helper.""" + + FORMAT_CATEGORIES = frozenset([ + definitions.FORMAT_CATEGORY_FILE_SYSTEM]) + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_FAT + + 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) + + # Boot sector signature. + format_specification.AddNewSignature(b'\x55\xaa', offset=510) + + # FAT-12 and FAT-16 file system hint. + format_specification.AddNewSignature(b'FAT12 ', offset=54) + format_specification.AddNewSignature(b'FAT16 ', offset=54) + + # FAT-32 file system hint. + format_specification.AddNewSignature(b'FAT32 ', offset=82) + + # exFAT file system signature. + format_specification.AddNewSignature(b'EXFAT ', offset=3) + + 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_FAT_BACK_END == self.TYPE_INDICATOR + + +analyzer.Analyzer.RegisterHelper(FATAnalyzerHelper()) diff --git a/dfvfs/analyzer/tsk_analyzer_helper.py b/dfvfs/analyzer/tsk_analyzer_helper.py index 69be1e09..597d8e22 100644 --- a/dfvfs/analyzer/tsk_analyzer_helper.py +++ b/dfvfs/analyzer/tsk_analyzer_helper.py @@ -25,17 +25,28 @@ def GetFormatSpecification(self): format_specification = specification.FormatSpecification( self.type_indicator) - # FAT volume header signature. - format_specification.AddNewSignature(b'\x55\xaa', offset=510) + if definitions.PREFERRED_FAT_BACK_END == self.TYPE_INDICATOR: + # Boot sector signature. + format_specification.AddNewSignature(b'\x55\xaa', offset=510) + + # FAT-12 and FAT-16 file system hint. + format_specification.AddNewSignature(b'FAT12 ', offset=54) + format_specification.AddNewSignature(b'FAT16 ', offset=54) + + # FAT-32 file system hint. + format_specification.AddNewSignature(b'FAT32 ', offset=82) + + # exFAT file system signature. + format_specification.AddNewSignature(b'EXFAT ', offset=3) if definitions.PREFERRED_NTFS_BACK_END == self.TYPE_INDICATOR: # NTFS file system signature. format_specification.AddNewSignature(b'NTFS ', offset=3) - # HFS boot block signature. - format_specification.AddNewSignature(b'LK', offset=0) - if definitions.PREFERRED_HFS_BACK_END == self.TYPE_INDICATOR: + # HFS boot block signature. + # format_specification.AddNewSignature(b'LK', offset=0) + # HFS+ file system signature. format_specification.AddNewSignature(b'H+', offset=1024) diff --git a/dfvfs/file_io/fat_file_io.py b/dfvfs/file_io/fat_file_io.py new file mode 100644 index 00000000..57cbde2b --- /dev/null +++ b/dfvfs/file_io/fat_file_io.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +"""The Hierarchical File System (FAT) file-like object implementation.""" + +import os + +from dfvfs.file_io import file_io +from dfvfs.resolver import resolver + + +class FATFile(file_io.FileIO): + """File input/output (IO) object using pyfsfat.file_entry""" + + def __init__(self, resolver_context, path_spec): + """Initializes a file input/output (IO) object. + + Args: + resolver_context (Context): resolver context. + path_spec (PathSpec): a path specification. + """ + super(FATFile, self).__init__(resolver_context, path_spec) + self._file_system = None + self._fsfat_data_stream = None + self._fsfat_file_entry = None + + def _Close(self): + """Closes the file-like object.""" + self._fsfat_data_stream = None + self._fsfat_file_entry = None + + self._file_system = None + + def _Open(self, mode='rb'): + """Opens the file-like object defined by path specification. + + Args: + 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. + """ + data_stream_name = getattr(self._path_spec, 'data_stream', None) + + self._file_system = resolver.Resolver.OpenFileSystem( + self._path_spec, resolver_context=self._resolver_context) + + file_entry = self._file_system.GetFileEntryByPathSpec(self._path_spec) + if not file_entry: + raise IOError('Unable to open file entry.') + + fsfat_data_stream = None + fsfat_file_entry = file_entry.GetFATFileEntry() + if not fsfat_file_entry: + raise IOError('Unable to open FAT file entry.') + + if data_stream_name == 'rsrc': + fsfat_data_stream = fsfat_file_entry.get_resource_fork() + elif data_stream_name: + raise IOError('Unable to open data stream: {0:s}.'.format( + data_stream_name)) + + self._fsfat_data_stream = fsfat_data_stream + self._fsfat_file_entry = fsfat_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._fsfat_data_stream: + return self._fsfat_data_stream.read(size=size) + return self._fsfat_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._fsfat_data_stream: + self._fsfat_data_stream.seek(offset, whence) + else: + self._fsfat_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._fsfat_data_stream: + return self._fsfat_data_stream.get_offset() + return self._fsfat_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._fsfat_data_stream: + return self._fsfat_data_stream.get_size() + return self._fsfat_file_entry.get_size() diff --git a/dfvfs/helpers/source_scanner.py b/dfvfs/helpers/source_scanner.py index 24cb9166..ddd9ab90 100644 --- a/dfvfs/helpers/source_scanner.py +++ b/dfvfs/helpers/source_scanner.py @@ -718,7 +718,8 @@ def ScanForFileSystem(self, source_path_spec): 'Unsupported source found more than one file system types.') # TODO: determine root location from file system or path specification. - if type_indicator == definitions.TYPE_INDICATOR_NTFS: + if type_indicator in ( + definitions.TYPE_INDICATOR_FAT, definitions.TYPE_INDICATOR_NTFS): root_location = '\\' else: root_location = '/' diff --git a/dfvfs/lib/definitions.py b/dfvfs/lib/definitions.py index 59bd94b1..50d82a19 100644 --- a/dfvfs/lib/definitions.py +++ b/dfvfs/lib/definitions.py @@ -45,6 +45,7 @@ TYPE_INDICATOR_EWF = 'EWF' TYPE_INDICATOR_EXT = 'EXT' TYPE_INDICATOR_FAKE = 'FAKE' +TYPE_INDICATOR_FAT = 'FAT' TYPE_INDICATOR_GPT = 'GPT' TYPE_INDICATOR_GZIP = 'GZIP' TYPE_INDICATOR_HFS = 'HFS' @@ -90,6 +91,7 @@ TYPE_INDICATOR_APFS, TYPE_INDICATOR_EXT, TYPE_INDICATOR_FAKE, + TYPE_INDICATOR_FAT, TYPE_INDICATOR_HFS, TYPE_INDICATOR_NTFS, TYPE_INDICATOR_TSK, @@ -116,6 +118,7 @@ # The preferred back-ends. PREFERRED_EXT_BACK_END = TYPE_INDICATOR_EXT +PREFERRED_FAT_BACK_END = TYPE_INDICATOR_TSK PREFERRED_GPT_BACK_END = TYPE_INDICATOR_GPT PREFERRED_HFS_BACK_END = TYPE_INDICATOR_HFS PREFERRED_NTFS_BACK_END = TYPE_INDICATOR_NTFS diff --git a/dfvfs/path/__init__.py b/dfvfs/path/__init__.py index 3c05ebe2..39adca1a 100644 --- a/dfvfs/path/__init__.py +++ b/dfvfs/path/__init__.py @@ -13,6 +13,7 @@ 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 fat_path_spec from dfvfs.path import fvde_path_spec from dfvfs.path import gpt_path_spec from dfvfs.path import gzip_path_spec diff --git a/dfvfs/path/fat_path_spec.py b/dfvfs/path/fat_path_spec.py new file mode 100644 index 00000000..ce037101 --- /dev/null +++ b/dfvfs/path/fat_path_spec.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +"""The FAT path specification implementation.""" + +from dfvfs.lib import definitions +from dfvfs.path import factory +from dfvfs.path import path_spec + + +class FATPathSpec(path_spec.PathSpec): + """FAT path specification implementation. + + Attributes: + identifier (int): (virtual) identifier. + location (str): location. + """ + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_FAT + + def __init__( + self, identifier=None, location=None, parent=None, **kwargs): + """Initializes a path specification. + + Note that an FAT path specification must have a parent. + + Args: + identifier (Optional[int]): (virtual) 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(FATPathSpec, 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(FATPathSpec) diff --git a/dfvfs/resolver_helpers/__init__.py b/dfvfs/resolver_helpers/__init__.py index ca7d9d5a..9a5b4c0f 100644 --- a/dfvfs/resolver_helpers/__init__.py +++ b/dfvfs/resolver_helpers/__init__.py @@ -36,6 +36,11 @@ from dfvfs.resolver_helpers import fake_resolver_helper +try: + from dfvfs.resolver_helpers import fat_resolver_helper +except ImportError: + pass + try: from dfvfs.resolver_helpers import fvde_resolver_helper except ImportError: diff --git a/dfvfs/resolver_helpers/fat_resolver_helper.py b/dfvfs/resolver_helpers/fat_resolver_helper.py new file mode 100644 index 00000000..8188deec --- /dev/null +++ b/dfvfs/resolver_helpers/fat_resolver_helper.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""The FAT path specification resolver helper implementation.""" + +from dfvfs.file_io import fat_file_io +from dfvfs.lib import definitions +from dfvfs.resolver_helpers import manager +from dfvfs.resolver_helpers import resolver_helper +from dfvfs.vfs import fat_file_system + + +class FATResolverHelper(resolver_helper.ResolverHelper): + """FAT resolver helper.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_FAT + + def NewFileObject(self, resolver_context, path_spec): + """Creates a new file input/output (IO) object. + + Args: + resolver_context (Context): resolver context. + path_spec (PathSpec): a path specification. + + Returns: + FileIO: file input/output (IO) object. + """ + return fat_file_io.FATFile(resolver_context, path_spec) + + def NewFileSystem(self, resolver_context, path_spec): + """Creates a new file system object. + + Args: + resolver_context (Context): resolver context. + path_spec (PathSpec): a path specification. + + Returns: + FileSystem: file system. + """ + return fat_file_system.FATFileSystem(resolver_context, path_spec) + + +manager.ResolverHelperManager.RegisterHelper(FATResolverHelper()) diff --git a/dfvfs/vfs/fat_directory.py b/dfvfs/vfs/fat_directory.py new file mode 100644 index 00000000..87c40484 --- /dev/null +++ b/dfvfs/vfs/fat_directory.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""The FAT directory implementation.""" + +from dfvfs.path import fat_path_spec +from dfvfs.vfs import directory + + +class FATDirectory(directory.Directory): + """File system directory that uses pyfsfat.""" + + def __init__(self, file_system, path_spec, fsfat_file_entry): + """Initializes a directory. + + Args: + file_system (FileSystem): file system. + path_spec (PathSpec): path specification. + fsfat_file_entry (pyfsfat.file_entry): FAT file entry. + """ + super(FATDirectory, self).__init__(file_system, path_spec) + self._fsfat_file_entry = fsfat_file_entry + + def _EntriesGenerator(self): + """Retrieves directory entries. + + Since a directory can contain a vast number of entries using + a generator is more memory efficient. + + Yields: + FATPathSpec: FAT path specification. + """ + location = getattr(self.path_spec, 'location', None) + + for fsfat_sub_file_entry in self._fsfat_file_entry.sub_file_entries: + directory_entry = fsfat_sub_file_entry.name + + if not location or 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 fat_path_spec.FATPathSpec( + identifier=fsfat_sub_file_entry.identifier, location=directory_entry, + parent=self.path_spec.parent) diff --git a/dfvfs/vfs/fat_file_entry.py b/dfvfs/vfs/fat_file_entry.py new file mode 100644 index 00000000..5510690c --- /dev/null +++ b/dfvfs/vfs/fat_file_entry.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +"""The FAT file entry implementation.""" + +import pyfsfat + +from dfdatetime import definitions as dfdatetime_definitions +from dfdatetime import fat_date_time as dfdatetime_fat_date_time + +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import fat_path_spec +from dfvfs.resolver import resolver +from dfvfs.vfs import attribute +from dfvfs.vfs import fat_directory +from dfvfs.vfs import file_entry + + +class FATFileEntry(file_entry.FileEntry): + """File system file entry that uses pyfsfat.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_FAT + + _FILE_SYSTEM_FORMAT_EXFAT = pyfsfat.file_system_formats.EXFAT + + def __init__( + self, resolver_context, file_system, path_spec, fsfat_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. + fsfat_file_entry (Optional[pyfsfat.file_entry]): FAT 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 pyfsfat file entry is missing. + """ + if not fsfat_file_entry: + fsfat_file_entry = file_system.GetFATFileEntryByPathSpec(path_spec) + if not fsfat_file_entry: + raise errors.BackEndError('Missing pyfsfat file entry.') + + fsfat_volume = file_system.GetFATVolume() + + if is_root: + file_entry_name = '' + else: + file_entry_name = fsfat_file_entry.name + + super(FATFileEntry, self).__init__( + resolver_context, file_system, path_spec, is_root=is_root, + is_virtual=is_virtual) + self._file_system_format = fsfat_volume.get_file_system_format() + self._fsfat_file_entry = fsfat_file_entry + self._name = file_entry_name + + file_attribute_flags = fsfat_file_entry.file_attribute_flags + if (file_attribute_flags is None or + file_attribute_flags & pyfsfat.file_attribute_flags.DIRECTORY): + self.entry_type = definitions.FILE_ENTRY_TYPE_DIRECTORY + elif file_attribute_flags & pyfsfat.file_attribute_flags.DEVICE: + self.entry_type = definitions.FILE_ENTRY_TYPE_DEVICE + else: + self.entry_type = definitions.FILE_ENTRY_TYPE_FILE + + def _GetDirectory(self): + """Retrieves a directory. + + Returns: + FATDirectory: directory or None if not available. + """ + if self.entry_type != definitions.FILE_ENTRY_TYPE_DIRECTORY: + return None + + return fat_directory.FATDirectory( + self._file_system, self.path_spec, self._fsfat_file_entry) + + def _GetStatAttribute(self): + """Retrieves a stat attribute. + + Returns: + StatAttribute: a stat attribute or None if not available. + """ + stat_attribute = attribute.StatAttribute() + stat_attribute.inode_number = self._fsfat_file_entry.identifier + stat_attribute.size = self._fsfat_file_entry.size + stat_attribute.type = self.entry_type + + return stat_attribute + + def _GetSubFileEntries(self): + """Retrieves a sub file entries generator. + + Yields: + FATFileEntry: a sub file entry. + """ + if self._directory is None: + self._directory = self._GetDirectory() + + if self._directory: + for path_spec in self._directory.entries: + yield FATFileEntry(self._resolver_context, self._file_system, path_spec) + + @property + def access_time(self): + """dfdatetime.DateTimeValues: access time or None if not available.""" + timestamp = self._fsfat_file_entry.get_access_time_as_integer() + # Access time can be None if not present and 0 if not set. + if not timestamp: + return None + + if self._file_system_format == self._FILE_SYSTEM_FORMAT_EXFAT: + precision = dfdatetime_definitions.PRECISION_10_MILLISECONDS + else: + precision = dfdatetime_definitions.PRECISION_1_DAY + + return dfdatetime_fat_date_time.FATTimestamp( + precision=precision, timestamp=timestamp) + + @property + def creation_time(self): + """dfdatetime.DateTimeValues: creation time or None if not available.""" + timestamp = self._fsfat_file_entry.get_creation_time_as_integer() + # Creation time can be None if not present and 0 if not set. + if not timestamp: + return None + + return dfdatetime_fat_date_time.FATTimestamp( + precision=dfdatetime_definitions.PRECISION_10_MILLISECONDS, + 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._fsfat_file_entry.get_modification_time_as_integer() + # Modification time can be None if not present and 0 if not set. + if not timestamp: + return None + + if self._file_system_format == self._FILE_SYSTEM_FORMAT_EXFAT: + precision = dfdatetime_definitions.PRECISION_10_MILLISECONDS + else: + precision = dfdatetime_definitions.PRECISION_2_SECONDS + + return dfdatetime_fat_date_time.FATTimestamp( + precision=precision, timestamp=timestamp) + + @property + def size(self): + """int: size of the file entry in bytes or None if not available.""" + return self._fsfat_file_entry.size + + def GetFileObject(self, data_stream_name=''): + """Retrieves a file-like object of a specific data stream. + + Args: + data_stream_name (Optional[str]): name of the data stream, where an empty + string represents the default data stream. + + Returns: + FileIO: a file-like object or None if not available. + """ + if self.entry_type != definitions.FILE_ENTRY_TYPE_FILE or data_stream_name: + return None + + return resolver.Resolver.OpenFileObject( + self.path_spec, resolver_context=self._resolver_context) + + def GetFATFileEntry(self): + """Retrieves the FAT file entry. + + Returns: + pyfsfat.file_entry: FAT file entry. + """ + return self._fsfat_file_entry + + def GetParentFileEntry(self): + """Retrieves the parent file entry. + + Returns: + FATFileEntry: 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 = fat_path_spec.FATPathSpec( + location=parent_location, parent=parent_path_spec) + + is_root = bool(parent_location == self._file_system.LOCATION_ROOT) + + return FATFileEntry( + self._resolver_context, self._file_system, path_spec, is_root=is_root) diff --git a/dfvfs/vfs/fat_file_system.py b/dfvfs/vfs/fat_file_system.py new file mode 100644 index 00000000..6fbabc3f --- /dev/null +++ b/dfvfs/vfs/fat_file_system.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +"""The FAT file system implementation.""" + +import pyfsfat + +from dfvfs.lib import definitions +from dfvfs.lib import errors +from dfvfs.path import fat_path_spec +from dfvfs.resolver import resolver +from dfvfs.vfs import fat_file_entry +from dfvfs.vfs import file_system + + +class FATFileSystem(file_system.FileSystem): + """File system that uses pyfsfat.""" + + TYPE_INDICATOR = definitions.TYPE_INDICATOR_FAT + + LOCATION_ROOT = '\\' + PATH_SEPARATOR = '\\' + + def __init__(self, resolver_context, path_spec): + """Initializes an FAT file system. + + Args: + resolver_context (Context): resolver context. + path_spec (PathSpec): a path specification. + """ + super(FATFileSystem, self).__init__(resolver_context, path_spec) + self._file_object = None + self._fsfat_volume = None + self._root_directory_identifier = None + + def _Close(self): + """Closes the file system. + + Raises: + IOError: if the close failed. + """ + self._fsfat_volume = None + self._file_object = None + + def _Open(self, mode='rb'): + """Opens the file system defined by path specification. + + Args: + 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 self._path_spec.HasParent(): + raise errors.PathSpecError( + 'Unsupported path specification without parent.') + + file_object = resolver.Resolver.OpenFileObject( + self._path_spec.parent, resolver_context=self._resolver_context) + + fsfat_volume = pyfsfat.volume() + fsfat_volume.open_file_object(file_object) + fsfat_root_directory = fsfat_volume.get_root_directory() + + self._file_object = file_object + self._fsfat_volume = fsfat_volume + self._root_directory_identifier = fsfat_root_directory.identifier + + 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. + fsfat_file_entry = None + location = getattr(path_spec, 'location', None) + identifier = getattr(path_spec, 'identifier', None) + + try: + if identifier is not None: + fsfat_file_entry = self._fsfat_volume.get_file_entry_by_identifier( + identifier) + elif location is not None: + fsfat_file_entry = self._fsfat_volume.get_file_entry_by_path(location) + + except IOError as exception: + raise errors.BackEndError(exception) + + return fsfat_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: + FATFileEntry: 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. + fsfat_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): + fsfat_file_entry = self._fsfat_volume.get_root_directory() + return fat_file_entry.FATFileEntry( + self._resolver_context, self, path_spec, + fsfat_file_entry=fsfat_file_entry, is_root=True) + + try: + if identifier is not None: + fsfat_file_entry = self._fsfat_volume.get_file_entry_by_identifier( + identifier) + elif location is not None: + fsfat_file_entry = self._fsfat_volume.get_file_entry_by_path(location) + + except IOError as exception: + raise errors.BackEndError(exception) + + if fsfat_file_entry is None: + return None + + return fat_file_entry.FATFileEntry( + self._resolver_context, self, path_spec, + fsfat_file_entry=fsfat_file_entry) + + def GetFATFileEntryByPathSpec(self, path_spec): + """Retrieves the FAT file entry for a path specification. + + Args: + path_spec (PathSpec): a path specification. + + Returns: + pyfsfat.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: + fsfat_file_entry = self._fsfat_volume.get_file_entry_by_identifier( + identifier) + elif location is not None: + fsfat_file_entry = self._fsfat_volume.get_file_entry_by_path(location) + else: + raise errors.PathSpecError( + 'Path specification missing location and identifier.') + + return fsfat_file_entry + + def GetFATVolume(self): + """Retrieves the FAT volume. + + Returns: + pyfsfat.volume: a FAT volume. + """ + return self._fsfat_volume + + def GetRootFileEntry(self): + """Retrieves the root file entry. + + Returns: + FATFileEntry: file entry. + """ + path_spec = fat_path_spec.FATPathSpec( + 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_entry.py b/dfvfs/vfs/ntfs_file_entry.py index 41909e7c..b3fa64bd 100644 --- a/dfvfs/vfs/ntfs_file_entry.py +++ b/dfvfs/vfs/ntfs_file_entry.py @@ -62,10 +62,13 @@ def __init__( is_virtual=is_virtual) self._fsntfs_file_entry = fsntfs_file_entry - if self._IsLink(fsntfs_file_entry.file_attribute_flags): + file_attribute_flags = fsntfs_file_entry.file_attribute_flags + if self._IsLink(file_attribute_flags): self.entry_type = definitions.FILE_ENTRY_TYPE_LINK elif fsntfs_file_entry.has_directory_entries_index(): self.entry_type = definitions.FILE_ENTRY_TYPE_DIRECTORY + elif file_attribute_flags & pyfsntfs.file_attribute_flags.DEVICE: + self.entry_type = definitions.FILE_ENTRY_TYPE_DEVICE else: self.entry_type = definitions.FILE_ENTRY_TYPE_FILE diff --git a/dfvfs/vfs/tsk_file_entry.py b/dfvfs/vfs/tsk_file_entry.py index 0538d242..542b1e80 100644 --- a/dfvfs/vfs/tsk_file_entry.py +++ b/dfvfs/vfs/tsk_file_entry.py @@ -53,9 +53,8 @@ def __init__(self, fraction_of_second=None, precision=None, timestamp=None): else: granularity = dfdatetime_definitions.PRECISION_100_NANOSECONDS - super(TSKTime, self).__init__() + super(TSKTime, self).__init__(precision=precision or granularity) self._granularity = granularity - self._precision = precision or granularity self._timestamp = timestamp self.fraction_of_second = fraction_of_second diff --git a/requirements.txt b/requirements.txt index 0eafdbd8..0dfd223d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,12 +2,13 @@ pip >= 7.0.0 PyYAML >= 3.10 cffi >= 1.9.1 cryptography >= 2.0.2 -dfdatetime >= 20211113 +dfdatetime >= 20220816 dtfabric >= 20220219 libbde-python >= 20220121 libewf-python >= 20131210 libfsapfs-python >= 20220709 libfsext-python >= 20220529 +libfsfat-python >= 20220816 libfshfs-python >= 20220709 libfsntfs-python >= 20211229 libfsxfs-python >= 20220706 diff --git a/setup.cfg b/setup.cfg index 11f8d956..0dadb9fb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ requires = libbde-python3 >= 20220121 libewf-python3 >= 20131210 libfsapfs-python3 >= 20220709 libfsext-python3 >= 20220529 + libfsfat-python3 >= 20220816 libfshfs-python3 >= 20220709 libfsntfs-python3 >= 20211229 libfsxfs-python3 >= 20220706 @@ -40,7 +41,7 @@ requires = libbde-python3 >= 20220121 libvslvm-python3 >= 20160109 python3-cffi >= 1.9.1 python3-cryptography >= 2.0.2 - python3-dfdatetime >= 20211113 + python3-dfdatetime >= 20220816 python3-dtfabric >= 20220219 python3-idna >= 2.5 python3-pytsk3 >= 20210419 diff --git a/tests/analyzer/analyzer.py b/tests/analyzer/analyzer.py index 34603b98..30f69827 100644 --- a/tests/analyzer/analyzer.py +++ b/tests/analyzer/analyzer.py @@ -190,6 +190,18 @@ def testGetFileSystemTypeIndicatorsEXT2(self): type_indicators = analyzer.Analyzer.GetFileSystemTypeIndicators(path_spec) self.assertEqual(type_indicators, expected_type_indicators) + def testGetFileSystemTypeIndicatorsFAT12(self): + """Tests the GetFileSystemTypeIndicators function on a FAT12 file system.""" + test_file = self._GetTestFilePath(['fat12.raw']) + self._SkipIfPathNotExists(test_file) + + path_spec = os_path_spec.OSPathSpec(location=test_file) + path_spec = raw_path_spec.RawPathSpec(parent=path_spec) + + expected_type_indicators = [definitions.PREFERRED_FAT_BACK_END] + type_indicators = analyzer.Analyzer.GetFileSystemTypeIndicators(path_spec) + self.assertEqual(type_indicators, expected_type_indicators) + def testGetFileSystemTypeIndicatorsHFSPlus(self): """Tests the GetFileSystemTypeIndicators function on a HFS+ file system.""" test_file = self._GetTestFilePath(['hfsplus.raw']) diff --git a/tests/file_io/fat_file_io.py b/tests/file_io/fat_file_io.py new file mode 100644 index 00000000..80faa46e --- /dev/null +++ b/tests/file_io/fat_file_io.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the Hierarchical File System (FAT) file-like object.""" + +import unittest + +from dfvfs.file_io import fat_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 FATFileTest(test_lib.FAT12ImageFileTestCase): + """Tests the file-like object implementation using pyfsfat.file_entry.""" + + _IDENTIFIER_ANOTHER_FILE = 0x62a0 + _IDENTIFIER_PASSWORDS_TXT = 0x1a80 + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + super(FATFileTest, self).setUp() + self._resolver_context = context.Context() + test_path = self._GetTestFilePath(['fat12.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 tearDown(self): + """Cleans up the needed objects used throughout the test.""" + self._resolver_context.Empty() + + def testOpenCloseIdentifier(self): + """Test the open and close functionality using an identifier.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_PASSWORDS_TXT, parent=self._raw_path_spec) + file_object = fat_file_io.FATFile(self._resolver_context, path_spec) + + self._TestOpenCloseIdentifier(file_object) + + def testOpenCloseLocation(self): + """Test the open and close functionality using a location.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_PASSWORDS_TXT, location='\\passwords.txt', + parent=self._raw_path_spec) + file_object = fat_file_io.FATFile(self._resolver_context, path_spec) + + self._TestOpenCloseLocation(file_object) + + # Try open with a path specification that has no parent. + path_spec.parent = None + file_object = fat_file_io.FATFile(self._resolver_context, path_spec) + + with self.assertRaises(errors.PathSpecError): + self._TestOpenCloseLocation(file_object) + + def testSeek(self): + """Test the seek functionality.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='\\a_directory\\another_file', + identifier=self._IDENTIFIER_ANOTHER_FILE, + parent=self._raw_path_spec) + file_object = fat_file_io.FATFile(self._resolver_context, path_spec) + + self._TestSeek(file_object) + + def testRead(self): + """Test the read functionality.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='\\passwords.txt', + identifier=self._IDENTIFIER_PASSWORDS_TXT, + parent=self._raw_path_spec) + file_object = fat_file_io.FATFile(self._resolver_context, path_spec) + + self._TestRead(file_object) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/helpers/source_scanner.py b/tests/helpers/source_scanner.py index db380001..39461b62 100644 --- a/tests/helpers/source_scanner.py +++ b/tests/helpers/source_scanner.py @@ -600,7 +600,8 @@ def testScanOnBDE(self): scan_node = scan_node.GetSubNodeByLocation('/') self.assertIsNotNone(scan_node) self.assertIsNotNone(scan_node.path_spec) - self.assertEqual(scan_node.type_indicator, definitions.TYPE_INDICATOR_TSK) + self.assertEqual( + scan_node.type_indicator, definitions.PREFERRED_FAT_BACK_END) def testScanOnFVDE(self): """Test the Scan function on FVDE.""" diff --git a/tests/helpers/volume_scanner.py b/tests/helpers/volume_scanner.py index 12ed9e6b..ecb0740b 100644 --- a/tests/helpers/volume_scanner.py +++ b/tests/helpers/volume_scanner.py @@ -428,16 +428,16 @@ def testScanEncryptedVolumeOnBDE(self): self.assertEqual( bde_scan_node.type_indicator, definitions.TYPE_INDICATOR_BDE) - ntfs_scan_node = scan_node.sub_nodes[1] + fat_scan_node = scan_node.sub_nodes[1] self.assertEqual( - ntfs_scan_node.type_indicator, definitions.TYPE_INDICATOR_TSK) + fat_scan_node.type_indicator, definitions.PREFERRED_FAT_BACK_END) test_scanner._ScanEncryptedVolume(scan_context, bde_scan_node, test_options) self.assertEqual(len(bde_scan_node.sub_nodes), 1) - ntfs_scan_node = bde_scan_node.sub_nodes[0] + fat_scan_node = bde_scan_node.sub_nodes[0] self.assertEqual( - ntfs_scan_node.type_indicator, definitions.TYPE_INDICATOR_TSK) + fat_scan_node.type_indicator, definitions.PREFERRED_FAT_BACK_END) # Test without mediator. resolver.Resolver.key_chain.Empty() @@ -458,9 +458,9 @@ def testScanEncryptedVolumeOnBDE(self): self.assertEqual( bde_scan_node.type_indicator, definitions.TYPE_INDICATOR_BDE) - ntfs_scan_node = scan_node.sub_nodes[1] + fat_scan_node = scan_node.sub_nodes[1] self.assertEqual( - ntfs_scan_node.type_indicator, definitions.TYPE_INDICATOR_TSK) + fat_scan_node.type_indicator, definitions.PREFERRED_FAT_BACK_END) with self.assertRaises(errors.ScannerError): test_scanner._ScanEncryptedVolume( @@ -471,9 +471,9 @@ def testScanEncryptedVolumeOnBDE(self): test_scanner._ScanEncryptedVolume(scan_context, bde_scan_node, test_options) self.assertEqual(len(bde_scan_node.sub_nodes), 1) - ntfs_scan_node = bde_scan_node.sub_nodes[0] + fat_scan_node = bde_scan_node.sub_nodes[0] self.assertEqual( - ntfs_scan_node.type_indicator, definitions.TYPE_INDICATOR_TSK) + fat_scan_node.type_indicator, definitions.PREFERRED_FAT_BACK_END) def testScanEncryptedVolumeOnEncryptedAPFS(self): """Tests the _ScanEncryptedVolume function on an encrypted APFS image.""" diff --git a/tests/path/fat_path_spec.py b/tests/path/fat_path_spec.py new file mode 100644 index 00000000..10ad5f35 --- /dev/null +++ b/tests/path/fat_path_spec.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the FAT path specification implementation.""" + +import unittest + +from dfvfs.path import fat_path_spec + +from tests.path import test_lib + + +class FATPathSpecTest(test_lib.PathSpecTestCase): + """Tests for the FAT path specification implementation.""" + + def testInitialize(self): + """Tests the path specification initialization.""" + path_spec = fat_path_spec.FATPathSpec( + location='/test', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = fat_path_spec.FATPathSpec( + identifier=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + path_spec = fat_path_spec.FATPathSpec( + location='/test', identifier=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + with self.assertRaises(ValueError): + fat_path_spec.FATPathSpec(location='/test', parent=None) + + with self.assertRaises(ValueError): + fat_path_spec.FATPathSpec(location=None, parent=self._path_spec) + + with self.assertRaises(ValueError): + fat_path_spec.FATPathSpec(identifier=None, parent=self._path_spec) + + with self.assertRaises(ValueError): + fat_path_spec.FATPathSpec( + location='/test', parent=self._path_spec, bogus='BOGUS') + + def testComparable(self): + """Tests the path specification comparable property.""" + path_spec = fat_path_spec.FATPathSpec( + location='/test', parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: FAT, location: /test', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = fat_path_spec.FATPathSpec( + identifier=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: FAT, identifier: 1', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + path_spec = fat_path_spec.FATPathSpec( + location='/test', identifier=1, parent=self._path_spec) + + self.assertIsNotNone(path_spec) + + expected_comparable = '\n'.join([ + 'type: TEST', + 'type: FAT, identifier: 1, location: /test', + '']) + + self.assertEqual(path_spec.comparable, expected_comparable) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/path/hfs_path_spec.py b/tests/path/hfs_path_spec.py index c7483551..1c193193 100644 --- a/tests/path/hfs_path_spec.py +++ b/tests/path/hfs_path_spec.py @@ -73,7 +73,6 @@ def testComparable(self): self.assertEqual(path_spec.comparable, expected_comparable) - path_spec = hfs_path_spec.HFSPathSpec( identifier=1, parent=self._path_spec) diff --git a/tests/resolver_helpers/fat_resolver_helper.py b/tests/resolver_helpers/fat_resolver_helper.py new file mode 100644 index 00000000..045a952d --- /dev/null +++ b/tests/resolver_helpers/fat_resolver_helper.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the FAT resolver helper implementation.""" + +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver_helpers import fat_resolver_helper + +from tests.resolver_helpers import test_lib + + +class FATResolverHelperTest(test_lib.ResolverHelperTestCase): + """Tests for the FAT resolver helper implementation.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + super(FATResolverHelperTest, self).setUp() + + test_path = self._GetTestFilePath(['fat12.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 testNewFileObject(self): + """Tests the NewFileObject function.""" + test_fat_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='/', + parent=self._raw_path_spec) + + resolver_helper_object = fat_resolver_helper.FATResolverHelper() + self._TestNewFileObject(resolver_helper_object, test_fat_path_spec) + + def testNewFileSystem(self): + """Tests the NewFileSystem function.""" + test_fat_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='/', + parent=self._raw_path_spec) + + resolver_helper_object = fat_resolver_helper.FATResolverHelper() + self._TestNewFileSystem(resolver_helper_object, test_fat_path_spec) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/fat_directory.py b/tests/vfs/fat_directory.py new file mode 100644 index 00000000..9c7c8030 --- /dev/null +++ b/tests/vfs/fat_directory.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the directory implementation using pyfsfat.""" + +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 fat_directory +from dfvfs.vfs import fat_file_system + +from tests import test_lib as shared_test_lib + + +class FATDirectoryTest(shared_test_lib.BaseTestCase): + """Tests the FAT directory.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_path = self._GetTestFilePath(['fat12.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._fat_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='\\', + parent=self._raw_path_spec) + + self._file_system = fat_file_system.FATFileSystem( + self._resolver_context, self._fat_path_spec) + self._file_system.Open() + + def tearDown(self): + """Cleans up the needed objects used throughout the test.""" + self._resolver_context.Empty() + + def testInitialize(self): + """Tests the __init__ function.""" + fsfat_file_entry = self._file_system.GetFATFileEntryByPathSpec( + self._fat_path_spec) + + directory = fat_directory.FATDirectory( + self._file_system, self._fat_path_spec, fsfat_file_entry) + + self.assertIsNotNone(directory) + + def testEntriesGenerator(self): + """Tests the _EntriesGenerator function.""" + fsfat_file_entry = self._file_system.GetFATFileEntryByPathSpec( + self._fat_path_spec) + + directory = fat_directory.FATDirectory( + self._file_system, self._fat_path_spec, fsfat_file_entry) + + self.assertIsNotNone(directory) + + entries = list(directory.entries) + self.assertEqual(len(entries), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/fat_file_entry.py b/tests/vfs/fat_file_entry.py new file mode 100644 index 00000000..c27240b2 --- /dev/null +++ b/tests/vfs/fat_file_entry.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the file entry implementation using pyfsfat.""" + +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 fat_file_entry +from dfvfs.vfs import fat_file_system + +from tests import test_lib as shared_test_lib + + +class FATFileEntryTest(shared_test_lib.BaseTestCase): + """Tests the FAT file entry.""" + + # pylint: disable=protected-access + + _IDENTIFIER_A_DIRECTORY = 0x1a40 + _IDENTIFIER_A_FILE = 0x6260 + _IDENTIFIER_ANOTHER_FILE = 0x62a0 + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_path = self._GetTestFilePath(['fat12.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._fat_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='\\', + parent=self._raw_path_spec) + + self._file_system = fat_file_system.FATFileSystem( + self._resolver_context, self._fat_path_spec) + self._file_system.Open() + + def tearDown(self): + """Cleans up the needed objects used throughout the test.""" + self._resolver_context.Empty() + + def testInitialize(self): + """Tests the __init__ function.""" + file_entry = fat_file_entry.FATFileEntry( + self._resolver_context, self._file_system, self._fat_path_spec) + + self.assertIsNotNone(file_entry) + + def testGetDataStreams(self): + """Tests the _GetDataStreams function.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + data_streams = file_entry._GetDataStreams() + self.assertEqual(len(data_streams), 1) + + def testGetStatAttribute(self): + """Tests the _GetStatAttribute function.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + stat_attribute = file_entry._GetStatAttribute() + + self.assertIsNotNone(stat_attribute) + self.assertIsNone(stat_attribute.group_identifier) + self.assertEqual(stat_attribute.inode_number, 0x62a0) + self.assertIsNone(stat_attribute.mode) + self.assertIsNone(stat_attribute.number_of_links) + self.assertIsNone(stat_attribute.owner_identifier) + self.assertEqual(stat_attribute.size, 22) + self.assertEqual(stat_attribute.type, stat_attribute.TYPE_FILE) + + def testAccessTime(self): + """Test the access_time property.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertIsNotNone(file_entry.access_time) + + def testCreationTime(self): + """Test the creation_time property.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertIsNotNone(file_entry.creation_time) + + def testDataStreams(self): + """Tests the data_streams property.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + 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, ['']) + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, identifier=self._IDENTIFIER_A_DIRECTORY, + location='\\a_directory', 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 testModificationTime(self): + """Test the modification_time property.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertIsNotNone(file_entry.modification_time) + + def testSize(self): + """Test the size property.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.size, 22) + + def testSubFileEntries(self): + """Tests the number_of_sub_file_entries and sub_file_entries properties.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, 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, 2) + + expected_sub_file_entry_names = [ + 'a_directory', + '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)) + + # Test a path specification without a location. + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, identifier=self._IDENTIFIER_A_DIRECTORY, + 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, 2) + + def testGetDataStream(self): + """Tests the GetDataStream function.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + data_stream = file_entry.GetDataStream('') + self.assertIsNotNone(data_stream) + + def testGetFileEntryByPathSpec(self): + """Tests the GetFileEntryByPathSpec function.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, identifier=self._IDENTIFIER_A_FILE, + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + + self.assertIsNotNone(file_entry) + + def testGetFileObject(self): + """Tests the GetFileObject function.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + file_object = file_entry.GetFileObject() + self.assertIsNotNone(file_object) + + self.assertEqual(file_object.get_size(), 22) + + file_object = file_entry.GetFileObject(data_stream_name='bogus') + self.assertIsNone(file_object) + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, identifier=self._IDENTIFIER_A_DIRECTORY, + location='\\a_directory', parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + file_object = file_entry.GetFileObject() + self.assertIsNone(file_object) + + def testGetParentFileEntry(self): + """Tests the GetParentFileEntry function.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + 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 testIsFunctions(self): + """Tests the Is? functions.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_ANOTHER_FILE, + location='\\a_directory\\another_file', + 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()) + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, identifier=self._IDENTIFIER_A_DIRECTORY, + location='\\a_directory', 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 = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, 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()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vfs/fat_file_system.py b/tests/vfs/fat_file_system.py new file mode 100644 index 00000000..3137791c --- /dev/null +++ b/tests/vfs/fat_file_system.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tests for the file system implementation using pyfsfat.""" + +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 fat_file_system + +from tests import test_lib as shared_test_lib + + +class FATFileSystemTest(shared_test_lib.BaseTestCase): + """Tests the FAT file entry.""" + + _IDENTIFIER_PASSWORDS_TXT = 0x1a80 + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._resolver_context = context.Context() + test_path = self._GetTestFilePath(['fat12.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._fat_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='\\', + parent=self._raw_path_spec) + + def tearDown(self): + """Cleans up the needed objects used throughout the test.""" + self._resolver_context.Empty() + + def testOpenAndClose(self): + """Test the open and close functionality.""" + file_system = fat_file_system.FATFileSystem( + self._resolver_context, self._fat_path_spec) + self.assertIsNotNone(file_system) + + file_system.Open() + + def testFileEntryExistsByPathSpec(self): + """Test the file entry exists by path specification functionality.""" + file_system = fat_file_system.FATFileSystem( + self._resolver_context, self._fat_path_spec) + self.assertIsNotNone(file_system) + + file_system.Open() + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='\\passwords.txt', + identifier=self._IDENTIFIER_PASSWORDS_TXT, parent=self._raw_path_spec) + result = file_system.FileEntryExistsByPathSpec(path_spec) + self.assertTrue(result) + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, location='\\bogus.txt', + parent=self._raw_path_spec) + result = file_system.FileEntryExistsByPathSpec(path_spec) + self.assertFalse(result) + + def testGetFileEntryByPathSpec(self): + """Tests the GetFileEntryByPathSpec function.""" + file_system = fat_file_system.FATFileSystem( + self._resolver_context, self._fat_path_spec) + self.assertIsNotNone(file_system) + + file_system.Open() + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_FAT, + identifier=self._IDENTIFIER_PASSWORDS_TXT, 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_FAT, location='\\passwords.txt', + identifier=self._IDENTIFIER_PASSWORDS_TXT, 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_FAT, location='\\bogus.txt', + parent=self._raw_path_spec) + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNone(file_entry) + + # TODO: add tests for GetFATFileEntryByPathSpec function. + + def testGetRootFileEntry(self): + """Test the get root file entry functionality.""" + file_system = fat_file_system.FATFileSystem( + self._resolver_context, self._fat_path_spec) + self.assertIsNotNone(file_system) + + file_system.Open() + + file_entry = file_system.GetRootFileEntry() + self.assertIsNotNone(file_entry) + self.assertEqual(file_entry.name, '') + + +if __name__ == '__main__': + unittest.main()