Skip to content

Commit

Permalink
Added support for resource fork as data stream #254 (#636)
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimmetz committed Jan 14, 2022
1 parent a28741f commit 26aa235
Show file tree
Hide file tree
Showing 28 changed files with 742 additions and 315 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 (>= 20220112), libfshfs-python3 (>= 20220113), libfsntfs-python3 (>= 20211229), libfsxfs-python3 (>= 20220113), libfvde-python3 (>= 20160719), libfwnt-python3 (>= 20210717), libluksde-python3 (>= 20200101), libmodi-python3 (>= 20210405), libphdi-python3 (>= 20220110), 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 (>= 20170524), python3-idna (>= 2.5), python3-pytsk3 (>= 20210419), python3-pyxattr (>= 0.7.2), python3-yaml (>= 3.10), ${misc:Depends}
Depends: libbde-python3 (>= 20140531), libewf-python3 (>= 20131210), libfsapfs-python3 (>= 20201107), libfsext-python3 (>= 20220112), libfshfs-python3 (>= 20220114), libfsntfs-python3 (>= 20211229), libfsxfs-python3 (>= 20220113), libfvde-python3 (>= 20160719), libfwnt-python3 (>= 20210717), libluksde-python3 (>= 20200101), libmodi-python3 (>= 20210405), libphdi-python3 (>= 20220110), 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 (>= 20170524), 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
Expand Down
2 changes: 1 addition & 1 deletion dependencies.ini
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ version_property: get_version()
[pyfshfs]
dpkg_name: libfshfs-python3
l2tbinaries_name: libfshfs
minimum_version: 20220113
minimum_version: 20220114
pypi_name: libfshfs-python
rpm_name: libfshfs-python3
version_property: get_version()
Expand Down
27 changes: 21 additions & 6 deletions dfvfs/file_io/hfs_file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os

from dfvfs.file_io import file_io
from dfvfs.lib import errors
from dfvfs.resolver import resolver


Expand All @@ -20,10 +19,12 @@ def __init__(self, resolver_context, path_spec):
"""
super(HFSFile, self).__init__(resolver_context, path_spec)
self._file_system = None
self._fshfs_data_stream = None
self._fshfs_file_entry = None

def _Close(self):
"""Closes the file-like object."""
self._fshfs_data_stream = None
self._fshfs_file_entry = None

self._file_system = None
Expand All @@ -42,10 +43,7 @@ def _Open(self, mode='rb'):
OSError: if the file-like object could not be opened.
PathSpecError: if the path specification is incorrect.
"""
data_stream = getattr(self._path_spec, 'data_stream', None)
if data_stream:
raise errors.NotSupported(
'Open data stream: {0:s} not supported.'.format(data_stream))
data_stream_name = getattr(self._path_spec, 'data_stream', None)

self._file_system = resolver.Resolver.OpenFileSystem(
self._path_spec, resolver_context=self._resolver_context)
Expand All @@ -54,10 +52,18 @@ def _Open(self, mode='rb'):
if not file_entry:
raise IOError('Unable to open file entry.')

fshfs_data_stream = None
fshfs_file_entry = file_entry.GetHFSFileEntry()
if not fshfs_file_entry:
raise IOError('Unable to open HFS file entry.')

if data_stream_name == 'rsrc':
fshfs_data_stream = fshfs_file_entry.get_resource_fork()
elif data_stream_name:
raise IOError('Unable to open data stream: {0:s}.'.format(
data_stream_name))

self._fshfs_data_stream = fshfs_data_stream
self._fshfs_file_entry = fshfs_file_entry

# Note: that the following functions do not follow the style guide
Expand All @@ -84,6 +90,8 @@ def read(self, size=None):
if not self._is_open:
raise IOError('Not opened.')

if self._fshfs_data_stream:
return self._fshfs_data_stream.read(size=size)
return self._fshfs_file_entry.read(size=size)

def seek(self, offset, whence=os.SEEK_SET):
Expand All @@ -101,7 +109,10 @@ def seek(self, offset, whence=os.SEEK_SET):
if not self._is_open:
raise IOError('Not opened.')

self._fshfs_file_entry.seek(offset, whence)
if self._fshfs_data_stream:
self._fshfs_data_stream.seek(offset, whence)
else:
self._fshfs_file_entry.seek(offset, whence)

def get_offset(self):
"""Retrieves the current offset into the file-like object.
Expand All @@ -116,6 +127,8 @@ def get_offset(self):
if not self._is_open:
raise IOError('Not opened.')

if self._fshfs_data_stream:
return self._fshfs_data_stream.get_offset()
return self._fshfs_file_entry.get_offset()

def get_size(self):
Expand All @@ -131,4 +144,6 @@ def get_size(self):
if not self._is_open:
raise IOError('Not opened.')

if self._fshfs_data_stream:
return self._fshfs_data_stream.get_size()
return self._fshfs_file_entry.get_size()
8 changes: 4 additions & 4 deletions dfvfs/file_io/ntfs_file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def _Open(self, mode='rb'):
OSError: if the file-like object could not be opened.
PathSpecError: if the path specification is incorrect.
"""
data_stream = getattr(self._path_spec, 'data_stream', None)
data_stream_name = getattr(self._path_spec, 'data_stream', None)

self._file_system = resolver.Resolver.OpenFileSystem(
self._path_spec, resolver_context=self._resolver_context)
Expand All @@ -55,12 +55,12 @@ def _Open(self, mode='rb'):
if not fsntfs_file_entry:
raise IOError('Unable to open NTFS file entry.')

if data_stream:
if data_stream_name:
fsntfs_data_stream = fsntfs_file_entry.get_alternate_data_stream_by_name(
data_stream)
data_stream_name)
if not fsntfs_data_stream:
raise IOError('Unable to open data stream: {0:s}.'.format(
data_stream))
data_stream_name))

elif not fsntfs_file_entry.has_default_data_stream():
raise IOError('Missing default data stream.')
Expand Down
56 changes: 35 additions & 21 deletions dfvfs/file_io/tsk_file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import pytsk3

from dfvfs.lib import errors
from dfvfs.file_io import file_io
from dfvfs.resolver import resolver

Expand Down Expand Up @@ -41,11 +42,12 @@ def _Open(self, mode='rb'):
Raises:
AccessError: if the access to open the file was denied.
BackEndError: if pytsk3 returns a non UTF-8 formatted name.
IOError: if the file-like object could not be opened.
OSError: if the file-like object could not be opened.
PathSpecError: if the path specification is incorrect.
"""
data_stream = getattr(self._path_spec, 'data_stream', None)
data_stream_name = getattr(self._path_spec, 'data_stream', None)

file_system = resolver.Resolver.OpenFileSystem(
self._path_spec, resolver_context=self._resolver_context)
Expand Down Expand Up @@ -82,35 +84,47 @@ def _Open(self, mode='rb'):
raise IOError(
'Missing attribute type in file.info.meta (pytsk3.TSK_FS_META).')

if data_stream:
for attribute in tsk_file:
if getattr(attribute, 'info', None) is None:
if data_stream_name:
for pytsk_attribute in tsk_file:
if getattr(pytsk_attribute, 'info', None) is None:
continue

# The value of the attribute name will be None for the default
# data stream.
attribute_name = getattr(attribute.info, 'name', None)
if attribute_name is None:
attribute_name = ''

else:
attribute_name = getattr(pytsk_attribute.info, 'name', None)
if attribute_name:
try:
# pytsk3 returns an UTF-8 encoded byte string.
attribute_name = attribute_name.decode('utf8')
except UnicodeError:
# Continue here since we cannot represent the attribute name.
continue

attribute_type = getattr(attribute.info, 'type', None)
if attribute_name == data_stream and attribute_type in (
pytsk3.TSK_FS_ATTR_TYPE_HFS_DEFAULT,
pytsk3.TSK_FS_ATTR_TYPE_HFS_DATA,
pytsk3.TSK_FS_ATTR_TYPE_NTFS_DATA):
tsk_attribute = attribute
raise errors.BackEndError(
'pytsk3 returned a non UTF-8 formatted name.')

attribute_type = getattr(pytsk_attribute.info, 'type', None)
if attribute_type == pytsk3.TSK_FS_ATTR_TYPE_HFS_DATA and (
not data_stream_name and not attribute_name):
tsk_attribute = pytsk_attribute
break

if attribute_type == pytsk3.TSK_FS_ATTR_TYPE_HFS_RSRC and (
data_stream_name == 'rsrc'):
tsk_attribute = pytsk_attribute
break

if attribute_type == pytsk3.TSK_FS_ATTR_TYPE_NTFS_DATA and (
(not data_stream_name and not attribute_name) or
(data_stream_name == attribute_name)):
tsk_attribute = pytsk_attribute
break

# The data stream is returned as a name-less attribute of type
# pytsk3.TSK_FS_ATTR_TYPE_DEFAULT.
if (attribute_type == pytsk3.TSK_FS_ATTR_TYPE_DEFAULT and
not data_stream_name and not attribute_name):
tsk_attribute = pytsk_attribute
break

if tsk_attribute is None:
raise IOError('Unable to open data stream: {0:s}.'.format(data_stream))
raise IOError('Unable to open data stream: {0:s}.'.format(
data_stream_name))

if (not tsk_attribute and
tsk_file.info.meta.type != pytsk3.TSK_FS_META_TYPE_REG):
Expand Down
10 changes: 9 additions & 1 deletion dfvfs/path/hfs_path_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,24 @@ class HFSPathSpec(path_spec.PathSpec):
"""HFS path specification implementation.
Attributes:
data_stream (str): data stream name, where None indicates the default
data stream.
identifier (int): catalog node identifier (CNID).
location (str): location.
"""

TYPE_INDICATOR = definitions.TYPE_INDICATOR_HFS

def __init__(
self, identifier=None, location=None, parent=None, **kwargs):
self, data_stream=None, identifier=None, location=None, parent=None,
**kwargs):
"""Initializes a path specification.
Note that an HFS path specification must have a parent.
Args:
data_stream (Optional[str]): data stream name, where None indicates
the default data stream.
identifier (Optional[int]): catalog node identifier (CNID).
location (Optional[str]): location.
parent (Optional[PathSpec]): parent path specification.
Expand All @@ -34,6 +39,7 @@ def __init__(
raise ValueError('Missing identifier and location, or parent value.')

super(HFSPathSpec, self).__init__(parent=parent, **kwargs)
self.data_stream = data_stream
self.identifier = identifier
self.location = location

Expand All @@ -42,6 +48,8 @@ def comparable(self):
"""str: comparable representation of the path specification."""
string_parts = []

if self.data_stream:
string_parts.append('data stream: {0:s}'.format(self.data_stream))
if self.identifier is not None:
string_parts.append('identifier: {0:d}'.format(self.identifier))
if self.location is not None:
Expand Down
4 changes: 4 additions & 0 deletions dfvfs/vfs/apfs_file_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ def GetAPFSFileEntry(self):
def GetExtents(self, data_stream_name=''):
"""Retrieves extents of a specific data stream.
Args:
data_stream_name (Optional[str]): data stream name, where an empty
string represents the default data stream.
Returns:
list[Extent]: extents of the data stream.
"""
Expand Down
4 changes: 4 additions & 0 deletions dfvfs/vfs/ext_file_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ def size(self):
def GetExtents(self, data_stream_name=''):
"""Retrieves extents of a specific data stream.
Args:
data_stream_name (Optional[str]): data stream name, where an empty
string represents the default data stream.
Returns:
list[Extent]: extents of the data stream.
"""
Expand Down
4 changes: 4 additions & 0 deletions dfvfs/vfs/file_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ def GetDataStream(self, name, case_sensitive=True):
def GetExtents(self, data_stream_name=''): # pylint: disable=unused-argument
"""Retrieves extents of a specific data stream.
Args:
data_stream_name (Optional[str]): data stream name, where an empty
string represents the default data stream.
Returns:
list[Extent]: extents of the data stream.
"""
Expand Down
34 changes: 34 additions & 0 deletions dfvfs/vfs/hfs_data_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
"""The HFS data stream implementation."""

from dfvfs.vfs import data_stream


class HFSDataStream(data_stream.DataStream):
"""File system data stream that uses pyfshfs."""

def __init__(self, fshfs_data_stream):
"""Initializes the data stream.
Args:
fshfs_data_stream (pyfshfs.data_stream): HFS data stream.
"""
super(HFSDataStream, self).__init__()
self._fshfs_data_stream = fshfs_data_stream
self._name = ''

if fshfs_data_stream:
self._name = 'rsrc'

@property
def name(self):
"""str: name."""
return self._name

def IsDefault(self):
"""Determines if the data stream is the default (data fork) data stream.
Returns:
bool: True if the data stream is the default (data fork) data stream.
"""
return not self._fshfs_data_stream
Loading

0 comments on commit 26aa235

Please sign in to comment.