Source code for winspsrc.jump_list

"""Windows Jump List files:
* .automaticDestinations-ms
* .customDestinations-ms
"""

import os

import pylnk
import pyolecf

from winspsrc import data_format
from winspsrc import data_range
from winspsrc import errors


[docs] class JumpListEntry: """Jump list entry. Attributes: identifier (str): identifier. lnk_file (pylnk.file): LNK file. """
[docs] def __init__(self, identifier, file_object): """Initializes the jump list entry. Args: identifier (str): identifier. file_object (file): file-like object that contains the LNK file entry data. """ super().__init__() self.identifier = identifier self.lnk_file = pylnk.file() self.lnk_file.open_file_object(file_object)
[docs] def __del__(self): """Destroy the jump list entry.""" self.lnk_file.close() self.lnk_file = None
[docs] class AutomaticDestinationsFile(data_format.BinaryDataFile): """Automatic Destinations Jump List (.automaticDestinations-ms) file.""" # Using a class constant significantly speeds up the time required to load # the dtFabric definition file. _FABRIC = data_format.BinaryDataFile.ReadDefinitionFile("jump_list.yaml")
[docs] def __init__(self): """Initializes an Automatic Destinations Jump List file.""" super().__init__() self._format_version = None self._olecf_file = None
def _FormatIntegerAsPathSize(self, integer): """Formats an integer as a path size. Args: integer (int): integer. Returns: str: integer formatted as path size. """ number_of_bytes = integer * 2 return f"{integer:d} characters ({number_of_bytes:d} bytes)" def _ReadDestList(self, root_item): """Reads the DestList stream. Args: root_item (pyolecf.item): OLECF root item. Raises: ParseError: if the root item or DestList stream is missing. """ if not root_item: raise errors.ParseError("Missing OLECF root item") olecf_item = root_item.get_sub_item_by_name("DestList") if not olecf_item: raise errors.ParseError("Missing DestList stream.") # The DestList stream can be of size 0 if the Jump List is empty. if olecf_item.size > 0: self._ReadDestListHeader(olecf_item) stream_offset = olecf_item.get_offset() stream_size = olecf_item.get_size() while stream_offset < stream_size: entry_size = self._ReadDestListEntry(olecf_item, stream_offset) stream_offset += entry_size def _ReadDestListEntry(self, olecf_item, stream_offset): """Reads a DestList stream entry. Args: olecf_item (pyolecf.item): OLECF item. stream_offset (int): stream offset of the entry. Returns: int: entry data size. Raises: ParseError: if the DestList stream entry cannot be read. """ if self._format_version == 1: data_type_map = self._GetDataTypeMap("dest_list_entry_v1") description = "dest list entry v1" elif self._format_version >= 2: data_type_map = self._GetDataTypeMap("dest_list_entry_v2") description = "dest list entry v2" else: data_type_map = None description = None _, entry_data_size = self._ReadStructureFromFileObject( olecf_item, stream_offset, data_type_map, description ) return entry_data_size def _ReadDestListHeader(self, olecf_item): """Reads the DestList stream header. Args: olecf_item (pyolecf.item): OLECF item. Raises: ParseError: if the DestList stream header cannot be read. """ stream_offset = olecf_item.tell() data_type_map = self._GetDataTypeMap("dest_list_header") dest_list_header, _ = self._ReadStructureFromFileObject( olecf_item, stream_offset, data_type_map, "dest list header" ) if dest_list_header.format_version not in (1, 2, 3, 4): raise errors.ParseError( f"Unsupported format version: {dest_list_header.format_version:d}" ) self._format_version = dest_list_header.format_version
[docs] def Close(self): """Closes an Automatic Destinations Jump List file. Raises: OSError: if the file is not opened. """ if self._olecf_file: self._olecf_file.close() self._olecf_file = None super().Close()
[docs] def GetJumpListEntries(self): """Retrieves jump list entries. Yields: JumpListEntry: a jump list entry. """ # pylint: disable=no-member for olecf_item in iter(self._olecf_file.root_item.sub_items): if olecf_item.name != "DestList": yield JumpListEntry(olecf_item.name, olecf_item)
[docs] def ReadFileObject(self, file_object): """Reads an Automatic Destinations Jump List file-like object. Args: file_object (file): file-like object. Raises: ParseError: if the file cannot be read. """ olecf_file = pyolecf.file() olecf_file.open_file_object(file_object) self._ReadDestList(olecf_file.root_item) self._olecf_file = olecf_file
[docs] class CustomDestinationsFile(data_format.BinaryDataFile): """Custom Destinations Jump List (.customDestinations-ms) file.""" # Using a class constant significantly speeds up the time required to load # the dtFabric definition file. _FABRIC = data_format.BinaryDataFile.ReadDefinitionFile("jump_list.yaml") _CATEGORY_FOOTER_SIGNATURE = b"\xab\xfb\xbf\xba" _LNK_GUID = b"\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46"
[docs] def __init__(self): """Initializes a Custom Destinations Jump List file.""" super().__init__() self._entries = []
def _ReadCategoryFooter(self, file_object): """Reads the category footer. Args: file_object (file): file-like object. Raises: ParseError: if the category footer cannot be read. """ file_offset = file_object.tell() data_type_map = self._GetDataTypeMap("custom_category_footer") category_footer, _ = self._ReadStructureFromFileObject( file_object, file_offset, data_type_map, "category footer" ) if category_footer.signature != self._CATEGORY_FOOTER_SIGNATURE: raise errors.ParseError( f"Invalid footer signature at offset: 0x{file_offset:08x}." ) def _ReadCategoryHeader(self, file_object, file_offset): """Reads a category header. Args: file_object (file): file-like object. file_offset (int): offset of the category header relative to the start of the file. Returns: tuple[custom_category_header, int]: category header and the number of bytes read. Raises: ParseError: if the category header cannot be read. """ data_type_map = self._GetDataTypeMap("custom_category_header") category_header, bytes_read = self._ReadStructureFromFileObject( file_object, file_offset, data_type_map, "category header" ) if category_header.category_type > 2: raise errors.ParseError( f"Unsupported category type: {category_header.category_type:d}." ) file_offset += bytes_read total_bytes_read = bytes_read data_type_map = self._GetDataTypeMap( f"custom_category_header_type_{category_header.category_type:d}" ) category_header_value, bytes_read = self._ReadStructureFromFileObject( file_object, file_offset, data_type_map, "category header values" ) if category_header.category_type in (0, 2): setattr( category_header, "number_of_entries", category_header_value.number_of_entries, ) total_bytes_read += bytes_read return category_header, total_bytes_read def _ReadFileHeader(self, file_object): """Reads the file header. Args: file_object (file): file-like object. Returns: tuple[custom_file_header, int]: file header and the number of bytes read. Raises: ParseError: if the file header cannot be read. """ data_type_map = self._GetDataTypeMap("custom_file_header") file_header, bytes_read = self._ReadStructureFromFileObject( file_object, 0, data_type_map, "file header" ) return file_header, bytes_read
[docs] def GetJumpListEntries(self): """Retrieves jump list entries. Yields: JumpListEntry: a jump list entry. Raises: ParseError: if the jump list entries cannot be read. """ yield from self._entries
[docs] def ReadFileObject(self, file_object): """Reads a Custom Destinations Jump List file-like object. Args: file_object (file): file-like object. Raises: ParseError: if the file cannot be read. """ file_header, file_offset = self._ReadFileHeader(file_object) data_type_map = self._GetDataTypeMap("custom_entry_header") for _ in range(file_header.number_of_categories): category_header, bytes_read = self._ReadCategoryHeader( file_object, file_offset ) file_offset += bytes_read number_of_entries = getattr(category_header, "number_of_entries", 0) for entry_index in range(number_of_entries): if self._file_size - file_offset < 16: break try: entry_header, _ = self._ReadStructureFromFileObject( file_object, file_offset, data_type_map, "entry header" ) except errors.ParseError as exception: raise errors.ParseError( ( f"Unable to parse entry: {entry_index:d} at offset: " f"0x{file_offset:08x} with error: {exception!s}" ) ) if entry_header.guid == self._LNK_GUID: file_offset += 16 remaining_file_size = self._file_size - file_offset data_range_file_object = data_range.DataRange( file_object, data_offset=file_offset, data_size=remaining_file_size, ) jump_list_entry = JumpListEntry( f"0x{file_offset:08x}", data_range_file_object ) self._entries.append(jump_list_entry) # We cannot trust the file size in the LNK data so we get the last # offset that was read instead. Because of DataRange the offset will # be relative to the start of the LNK data. entry_data_size = data_range_file_object.get_offset() file_offset += entry_data_size elif entry_header.guid[:4] != self._CATEGORY_FOOTER_SIGNATURE: raise errors.ParseError( f"Unsupported entry: {entry_index:d} at offset: " f"0x{file_offset:08x}" ) file_object.seek(file_offset, os.SEEK_SET) self._ReadCategoryFooter(file_object) file_offset += 4