From 94c771630884d6f303900aa48122ea2a48e067fc Mon Sep 17 00:00:00 2001 From: Joachim Metz Date: Wed, 21 Jul 2021 07:26:12 +0200 Subject: [PATCH] Added attributes support for fsext back-end #504 --- config/dpkg/control | 2 +- dependencies.ini | 2 +- dfvfs/vfs/attribute.py | 38 +++++++++++++ dfvfs/vfs/ext_attribute.py | 107 +++++++++++++++++++++++++++++++++++ dfvfs/vfs/ext_file_entry.py | 37 ++++++++++++ dfvfs/vfs/ntfs_attribute.py | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- tests/vfs/ext_file_entry.py | 110 ++++++++++++++++++++++++++---------- 9 files changed, 267 insertions(+), 35 deletions(-) create mode 100644 dfvfs/vfs/ext_attribute.py diff --git a/config/dpkg/control b/config/dpkg/control index be603c5c..5090c5a3 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 (>= 20140531), libewf-python3 (>= 20131210), libfsapfs-python3 (>= 20201107), libfsext-python3 (>= 20210424), libfshfs-python3 (>= 20210530), libfsntfs-python3 (>= 20200921), libfsxfs-python3 (>= 20201114), libfvde-python3 (>= 20160719), libfwnt-python3 (>= 20210717), libluksde-python3 (>= 20200101), libmodi-python3 (>= 20210405), libqcow-python3 (>= 20201213), libsigscan-python3 (>= 20191221), libsmdev-python3 (>= 20140529), libsmraw-python3 (>= 20140612), libvhdi-python3 (>= 20201014), libvmdk-python3 (>= 20140421), libvsgpt-python3 (>= 20210207), libvshadow-python3 (>= 20160109), libvslvm-python3 (>= 20160109), python3-cffi-backend (>= 1.9.1), python3-cryptography (>= 2.0.2), python3-dfdatetime (>= 20210509), python3-dtfabric (>= 20170524), python3-idna (>= 2.5), python3-pytsk3 (>= 20210419), python3-yaml (>= 3.10), ${python3:Depends}, ${misc:Depends} +Depends: libbde-python3 (>= 20140531), libewf-python3 (>= 20131210), libfsapfs-python3 (>= 20201107), libfsext-python3 (>= 20210721), libfshfs-python3 (>= 20210530), libfsntfs-python3 (>= 20200921), libfsxfs-python3 (>= 20201114), libfvde-python3 (>= 20160719), libfwnt-python3 (>= 20210717), libluksde-python3 (>= 20200101), libmodi-python3 (>= 20210405), libqcow-python3 (>= 20201213), libsigscan-python3 (>= 20191221), libsmdev-python3 (>= 20140529), libsmraw-python3 (>= 20140612), libvhdi-python3 (>= 20201014), libvmdk-python3 (>= 20140421), libvsgpt-python3 (>= 20210207), libvshadow-python3 (>= 20160109), libvslvm-python3 (>= 20160109), python3-cffi-backend (>= 1.9.1), python3-cryptography (>= 2.0.2), python3-dfdatetime (>= 20210509), python3-dtfabric (>= 20170524), python3-idna (>= 2.5), python3-pytsk3 (>= 20210419), python3-yaml (>= 3.10), ${python3:Depends}, ${misc:Depends} Description: Python 3 module of dfVFS dfVFS, or Digital Forensics Virtual File System, provides read-only access to file-system objects from various storage media types and file formats. The goal diff --git a/dependencies.ini b/dependencies.ini index 4d8e9f30..a02d1269 100644 --- a/dependencies.ini +++ b/dependencies.ini @@ -56,7 +56,7 @@ version_property: get_version() [pyfsext] dpkg_name: libfsext-python3 l2tbinaries_name: libfsext -minimum_version: 20210424 +minimum_version: 20210721 pypi_name: libfsext-python rpm_name: libfsext-python3 version_property: get_version() diff --git a/dfvfs/vfs/attribute.py b/dfvfs/vfs/attribute.py index 58c37b19..356962bb 100644 --- a/dfvfs/vfs/attribute.py +++ b/dfvfs/vfs/attribute.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """The Virtual File System (VFS) attribute interface.""" +from dfvfs.lib import definitions + class Attribute(object): """Attribute interface.""" @@ -9,3 +11,39 @@ class Attribute(object): def type_indicator(self): """str: type indicator or None if not known.""" return getattr(self, 'TYPE_INDICATOR', None) + + +class StatAttribute(object): + """Attribute that represents a POSIX stat. + + Attributes: + group_identifier (int): group identifier (GID), equivalent to st_gid. + inode_number (int): number of the corresponding inode, equivalent to st_ino. + mode (int): access mode, equivalent to st_mode & 0x0fff. + number_of_links (int): number of hard links, equivalent to st_nlink. + owner_user_identifier (int): user identifier (UID) of the owner, equivalent + to st_uid. + size (int): size, in number of bytes, equivalent to st_size. + type (str): file type, value derived from st_mode >> 12. + """ + + TYPE_DEVICE = definitions.FILE_ENTRY_TYPE_DEVICE + TYPE_DIRECTORY = definitions.FILE_ENTRY_TYPE_DIRECTORY + TYPE_FILE = definitions.FILE_ENTRY_TYPE_FILE + TYPE_LINK = definitions.FILE_ENTRY_TYPE_LINK + TYPE_SOCKET = definitions.FILE_ENTRY_TYPE_SOCKET + TYPE_PIPE = definitions.FILE_ENTRY_TYPE_PIPE + TYPE_WHITEOUT = definitions.FILE_ENTRY_TYPE_WHITEOUT + + def __init__(self): + """Initializes an attribute.""" + super(StatAttribute, self).__init__() + self.group_identifier = None + self.inode_number = None + self.mode = None + self.number_of_links = None + self.owner_identifier = None + self.size = None + self.type = None + + # TODO: consider adding st_dev, st_rdev, st_blksize or st_blocks. diff --git a/dfvfs/vfs/ext_attribute.py b/dfvfs/vfs/ext_attribute.py new file mode 100644 index 00000000..8f781ee7 --- /dev/null +++ b/dfvfs/vfs/ext_attribute.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""The EXT attribute implementations.""" + +import os + +from dfvfs.lib import errors +from dfvfs.vfs import attribute + + +class EXTExtendedAttribute(attribute.Attribute): + """EXT extended attribute that uses pyfsext.""" + + def __init__(self, fsext_extended_attribute): + """Initializes an attribute. + + Args: + fsext_extended_attribute (pyfsext.extended_attribute): EXT extended + attribute. + + Raises: + BackEndError: if the pyfsext extended attribute is missing. + """ + if not fsext_extended_attribute: + raise errors.BackEndError('Missing pyfsext extended attribute.') + + super(EXTExtendedAttribute, self).__init__() + self._fsext_extended_attribute = fsext_extended_attribute + + @property + def name(self): + """str: name.""" + return self._fsext_extended_attribute.name + + # 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 input/output (IO) object. + + 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. + """ + return self._fsext_extended_attribute.read_buffer(size) + + def seek(self, offset, whence=os.SEEK_SET): + """Seeks to an offset within the file input/output (IO) object. + + Args: + offset (int): offset to seek. + 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. + """ + self._fsext_extended_attribute.seek_offset(offset, whence) + + # get_offset() is preferred above tell() by the libbfio layer used in libyal. + def get_offset(self): + """Retrieves the current offset into the file input/output (IO) object. + + Returns: + int: current offset into the file input/output (IO) object. + + Raises: + IOError: if the file input/output (IO)-like object has not been opened. + OSError: if the file input/output (IO)-like object has not been opened. + """ + return self._fsext_extended_attribute.get_offset() + + # Pythonesque alias for get_offset(). + def tell(self): + """Retrieves the current offset into the file input/output (IO) object.""" + return self.get_offset() + + def get_size(self): + """Retrieves the size of the file input/output (IO) object. + + Returns: + int: size of the file input/output (IO) object. + + Raises: + IOError: if the file input/output (IO) object has not been opened. + OSError: if the file input/output (IO) object has not been opened. + """ + return self._fsext_extended_attribute.get_size() + + def seekable(self): + """Determines if a file input/output (IO) object is seekable. + + Returns: + bool: True since a file IO object provides a seek method. + """ + return True diff --git a/dfvfs/vfs/ext_file_entry.py b/dfvfs/vfs/ext_file_entry.py index 7b032cff..df3937cc 100644 --- a/dfvfs/vfs/ext_file_entry.py +++ b/dfvfs/vfs/ext_file_entry.py @@ -6,6 +6,8 @@ from dfvfs.lib import definitions from dfvfs.lib import errors from dfvfs.path import ext_path_spec +from dfvfs.vfs import attribute +from dfvfs.vfs import ext_attribute from dfvfs.vfs import file_entry @@ -107,6 +109,24 @@ def __init__( self.entry_type = self._ENTRY_TYPES.get( fsext_file_entry.file_mode & 0xf000, None) + def _GetAttributes(self): + """Retrieves the attributes. + + Returns: + list[Attribute]: attributes. + """ + if self._attributes is None: + stat_attribute = self._GetStatAttribute() + self._attributes = [stat_attribute] + + for fsext_extended_attribute in ( + self._fsext_file_entry.extended_attributes): + extended_attribute = ext_attribute.EXTExtendedAttribute( + fsext_extended_attribute) + self._attributes.append(extended_attribute) + + return self._attributes + def _GetDirectory(self): """Retrieves a directory. @@ -159,6 +179,23 @@ def _GetStat(self): return stat_object + def _GetStatAttribute(self): + """Retrieves a stat attribute. + + Returns: + StatAttribute: a stat attribute. + """ + stat_attribute = attribute.StatAttribute() + stat_attribute.group_identifier = self._fsext_file_entry.group_identifier + stat_attribute.inode_number = self._fsext_file_entry.inode_number + stat_attribute.mode = self._fsext_file_entry.file_mode & 0x0fff + stat_attribute.number_of_links = self._fsext_file_entry.number_of_links + stat_attribute.owner_identifier = self._fsext_file_entry.owner_identifier + stat_attribute.size = self._fsext_file_entry.size + stat_attribute.type = self.entry_type + + return stat_attribute + def _GetSubFileEntries(self): """Retrieves a sub file entries generator. diff --git a/dfvfs/vfs/ntfs_attribute.py b/dfvfs/vfs/ntfs_attribute.py index 2917b416..dd6e9755 100644 --- a/dfvfs/vfs/ntfs_attribute.py +++ b/dfvfs/vfs/ntfs_attribute.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""The NTFS attribute implementation.""" +"""The NTFS attribute implementations.""" import pyfwnt diff --git a/requirements.txt b/requirements.txt index 9edb1af6..b00be0eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ idna >= 2.5 libbde-python >= 20140531 libewf-python >= 20131210 libfsapfs-python >= 20201107 -libfsext-python >= 20210424 +libfsext-python >= 20210721 libfshfs-python >= 20210530 libfsntfs-python >= 20200921 libfsxfs-python >= 20201114 diff --git a/setup.cfg b/setup.cfg index 3deb3910..e938e183 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ build_requires = python3-setuptools requires = libbde-python3 >= 20140531 libewf-python3 >= 20131210 libfsapfs-python3 >= 20201107 - libfsext-python3 >= 20210424 + libfsext-python3 >= 20210721 libfshfs-python3 >= 20210530 libfsntfs-python3 >= 20200921 libfsxfs-python3 >= 20201114 diff --git a/tests/vfs/ext_file_entry.py b/tests/vfs/ext_file_entry.py index fd5b9005..daff5031 100644 --- a/tests/vfs/ext_file_entry.py +++ b/tests/vfs/ext_file_entry.py @@ -7,6 +7,8 @@ from dfvfs.lib import definitions from dfvfs.path import factory as path_spec_factory from dfvfs.resolver import context +from dfvfs.vfs import attribute +from dfvfs.vfs import ext_attribute from dfvfs.vfs import ext_file_entry from dfvfs.vfs import ext_file_system @@ -19,6 +21,8 @@ class EXTFileEntryTestWithEXT2(shared_test_lib.BaseTestCase): """Tests the EXT file entry on an ext2 image.""" + # pylint: disable=protected-access + _INODE_A_DIRECTORY = 12 _INODE_A_FILE = 13 _INODE_A_LINK = 16 @@ -97,6 +101,82 @@ def testModificationTime(self): self.assertIsNotNone(file_entry) self.assertIsNotNone(file_entry.modification_time) + def testGetAttributes(self): + """Tests the _GetAttributes function.""" + test_location = '/a_directory/another_file' + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, inode=self._INODE_ANOTHER_FILE, + location=test_location, parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + self.assertIsNone(file_entry._attributes) + + file_entry._GetAttributes() + self.assertIsNotNone(file_entry._attributes) + self.assertEqual(len(file_entry._attributes), 2) + + test_attribute = file_entry._attributes[0] + self.assertIsInstance(test_attribute, attribute.StatAttribute) + + test_attribute = file_entry._attributes[1] + self.assertIsInstance(test_attribute, ext_attribute.EXTExtendedAttribute) + self.assertEqual(test_attribute.name, 'security.selinux') + + test_attribute_value_data = test_attribute.read() + self.assertEqual( + test_attribute_value_data, b'unconfined_u:object_r:unlabeled_t:s0\x00') + + def testGetStat(self): + """Tests the _GetStat function.""" + test_location = '/a_directory/another_file' + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, inode=self._INODE_ANOTHER_FILE, + location=test_location, parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + stat_object = file_entry._GetStat() + + self.assertIsNotNone(stat_object) + self.assertEqual(stat_object.type, stat_object.TYPE_FILE) + self.assertEqual(stat_object.size, 22) + + self.assertEqual(stat_object.mode, 436) + self.assertEqual(stat_object.uid, 1000) + self.assertEqual(stat_object.gid, 1000) + + self.assertEqual(stat_object.atime, 1567246979) + self.assertFalse(hasattr(stat_object, 'atime_nano')) + + self.assertEqual(stat_object.ctime, 1567246979) + self.assertFalse(hasattr(stat_object, 'ctime_nano')) + + self.assertFalse(hasattr(stat_object, 'crtime')) + + self.assertEqual(stat_object.mtime, 1567246979) + self.assertFalse(hasattr(stat_object, 'mtime_nano')) + + def testGetStatAttribute(self): + """Tests the _GetStatAttribute function.""" + test_location = '/a_directory/another_file' + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_EXT, inode=self._INODE_ANOTHER_FILE, + location=test_location, parent=self._raw_path_spec) + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + self.assertIsNotNone(file_entry) + + stat_attribute = file_entry._GetStatAttribute() + + self.assertIsNotNone(stat_attribute) + self.assertEqual(stat_attribute.group_identifier, 1000) + self.assertEqual(stat_attribute.inode_number, 15) + self.assertEqual(stat_attribute.mode, 0o664) + self.assertEqual(stat_attribute.number_of_links, 1) + self.assertEqual(stat_attribute.owner_identifier, 1000) + self.assertEqual(stat_attribute.size, 22) + self.assertEqual(stat_attribute.type, stat_attribute.TYPE_FILE) + def testGetFileEntryByPathSpec(self): """Tests the GetFileEntryByPathSpec function.""" path_spec = path_spec_factory.Factory.NewPathSpec( @@ -136,36 +216,6 @@ def testGetParentFileEntry(self): self.assertEqual(parent_file_entry.name, 'a_directory') - def testGetStat(self): - """Tests the GetStat function.""" - test_location = '/a_directory/another_file' - path_spec = path_spec_factory.Factory.NewPathSpec( - definitions.TYPE_INDICATOR_EXT, inode=self._INODE_ANOTHER_FILE, - location=test_location, parent=self._raw_path_spec) - file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) - self.assertIsNotNone(file_entry) - - stat_object = file_entry.GetStat() - - self.assertIsNotNone(stat_object) - self.assertEqual(stat_object.type, stat_object.TYPE_FILE) - self.assertEqual(stat_object.size, 22) - - self.assertEqual(stat_object.mode, 436) - self.assertEqual(stat_object.uid, 1000) - self.assertEqual(stat_object.gid, 1000) - - self.assertEqual(stat_object.atime, 1567246979) - self.assertFalse(hasattr(stat_object, 'atime_nano')) - - self.assertEqual(stat_object.ctime, 1567246979) - self.assertFalse(hasattr(stat_object, 'ctime_nano')) - - self.assertFalse(hasattr(stat_object, 'crtime')) - - self.assertEqual(stat_object.mtime, 1567246979) - self.assertFalse(hasattr(stat_object, 'mtime_nano')) - def testIsFunctions(self): """Tests the Is? functions.""" test_location = '/a_directory/another_file'