From f4c73f54673d0790e253bbcec3349c8547084eec 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 --- 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/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 | 182 ++++++++++ dfvfs/vfs/fat_file_system.py | 178 ++++++++++ dfvfs/vfs/ntfs_file_entry.py | 5 +- 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 ++++++ 24 files changed, 1481 insertions(+), 17 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/dependencies.ini b/dependencies.ini index aad0a3da..29a61f44 100644 --- a/dependencies.ini +++ b/dependencies.ini @@ -13,7 +13,7 @@ version_property: __version__ [dfdatetime] dpkg_name: python3-dfdatetime -minimum_version: 20211113 +minimum_version: 20220728 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: 20220729 +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/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..fba77283 --- /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): catalog node identifier (CNID). + 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]): catalog node identifier (CNID). + 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..3e735c8a --- /dev/null +++ b/dfvfs/vfs/fat_file_entry.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +"""The FAT file entry implementation.""" + +import copy + +import pyfsfat + +from dfdatetime import fat_date_time as dfdatetime_fat_date_time +from dfdatetime import posix_time as dfdatetime_posix_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 + + 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.') + + 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._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() + if timestamp is None: + return None + + return dfdatetime_fat_date_time.FATTimestamp(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() + return dfdatetime_fat_date_time.FATTimestamp(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() + return dfdatetime_fat_date_time.FATTimestamp(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..20f46227 --- /dev/null +++ b/dfvfs/vfs/fat_file_system.py @@ -0,0 +1,178 @@ +# -*- 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 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/tests/analyzer/analyzer.py b/tests/analyzer/analyzer.py index 34603b98..4506345d 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 FAT-12file 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()