Skip to content

Commit

Permalink
Added attributes support for fsext back-end log2timeline#504
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimmetz committed Jul 22, 2021
1 parent 1608914 commit 94c7716
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 35 deletions.
2 changes: 1 addition & 1 deletion config/dpkg/control
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dependencies.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
38 changes: 38 additions & 0 deletions dfvfs/vfs/attribute.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
"""The Virtual File System (VFS) attribute interface."""

from dfvfs.lib import definitions


class Attribute(object):
"""Attribute interface."""
Expand All @@ -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.
107 changes: 107 additions & 0 deletions dfvfs/vfs/ext_attribute.py
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions dfvfs/vfs/ext_file_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion dfvfs/vfs/ntfs_attribute.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""The NTFS attribute implementation."""
"""The NTFS attribute implementations."""

import pyfwnt

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
110 changes: 80 additions & 30 deletions tests/vfs/ext_file_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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'
Expand Down

0 comments on commit 94c7716

Please sign in to comment.