# -*- coding: utf-8 -*-
"""Windows Registry plugin to parse the Application Compatibility Cache key."""

from __future__ import unicode_literals

from dfdatetime import filetime as dfdatetime_filetime
from dfdatetime import semantic_time as dfdatetime_semantic_time

from dtfabric.runtime import data_maps as dtfabric_data_maps

from plaso.containers import events
from plaso.containers import time_events
from plaso.lib import definitions
from plaso.lib import errors
from plaso.parsers import winreg
from plaso.parsers.winreg_plugins import interface
from plaso.parsers.winreg_plugins import dtfabric_plugin


class AppCompatCacheEventData(events.EventData):
  """Application Compatibility Cache event data.

  Attributes:
    entry_index (int): cache entry index number for the record.
    key_path (str): Windows Registry key path.
    path (str): full path to the executable.
  """

  DATA_TYPE = 'windows:registry:appcompatcache'

  def __init__(self):
    """Initializes event data."""
    super(AppCompatCacheEventData, self).__init__(data_type=self.DATA_TYPE)
    self.entry_index = None
    self.key_path = None
    self.path = None


class AppCompatCacheHeader(object):
  """Application Compatibility Cache header."""

  def __init__(self):
    """Initializes the header object."""
    super(AppCompatCacheHeader, self).__init__()
    self.number_of_cached_entries = 0
    self.header_size = 0


class AppCompatCacheCachedEntry(object):
  """Application Compatibility Cache cached entry."""

  def __init__(self):
    """Initializes the cached entry object."""
    super(AppCompatCacheCachedEntry, self).__init__()
    self.cached_entry_size = 0
    self.data = None
    self.file_size = None
    self.insertion_flags = None
    self.last_modification_time = None
    self.last_update_time = None
    self.shim_flags = None
    self.path = None


class AppCompatCacheWindowsRegistryPlugin(
    dtfabric_plugin.DtFabricBaseWindowsRegistryPlugin):
  """Application Compatibility Cache data Windows Registry plugin."""

  NAME = 'appcompatcache'
  DESCRIPTION = 'Parser for Application Compatibility Cache Registry data.'

  FILTERS = frozenset([
      interface.WindowsRegistryKeyPathFilter(
          'HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\'
          'Session Manager\\AppCompatibility'),
      interface.WindowsRegistryKeyPathFilter(
          'HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\'
          'Session Manager\\AppCompatCache')])

  _DEFINITION_FILE = 'appcompatcache.yaml'

  _FORMAT_TYPE_2000 = 1
  _FORMAT_TYPE_XP = 2
  _FORMAT_TYPE_2003 = 3
  _FORMAT_TYPE_VISTA = 4
  _FORMAT_TYPE_7 = 5
  _FORMAT_TYPE_8 = 6
  _FORMAT_TYPE_10 = 7

  _HEADER_SIGNATURES = {
      # AppCompatCache format signature used in Windows XP.
      0xdeadbeef: _FORMAT_TYPE_XP,
      # AppCompatCache format signature used in Windows 2003, Vista and 2008.
      0xbadc0ffe: _FORMAT_TYPE_2003,
      # AppCompatCache format signature used in Windows 7 and 2008 R2.
      0xbadc0fee: _FORMAT_TYPE_7,
      # AppCompatCache format signature used in Windows 8.0 and 8.1.
      0x00000080: _FORMAT_TYPE_8,
      # AppCompatCache format signatures used in Windows 10
      0x00000030: _FORMAT_TYPE_10,
      0x00000034: _FORMAT_TYPE_10}

  _HEADER_DATA_TYPE_MAP_NAMES = {
      _FORMAT_TYPE_XP: 'appcompatcache_header_xp_32bit',
      _FORMAT_TYPE_2003: 'appcompatcache_header_2003',
      _FORMAT_TYPE_VISTA: 'appcompatcache_header_vista',
      _FORMAT_TYPE_7: 'appcompatcache_header_7',
      _FORMAT_TYPE_8: 'appcompatcache_header_8',
      _FORMAT_TYPE_10: 'appcompatcache_header_10'}

  _SUPPORTED_FORMAT_TYPES = frozenset(_HEADER_DATA_TYPE_MAP_NAMES.keys())

  # AppCompatCache format used in Windows 8.0.
  _CACHED_ENTRY_SIGNATURE_8_0 = b'00ts'

  # AppCompatCache format used in Windows 8.1.
  _CACHED_ENTRY_SIGNATURE_8_1 = b'10ts'

  def __init__(self):
    """Initializes a Application Compatibility Cache Registry plugin."""
    super(AppCompatCacheWindowsRegistryPlugin, self).__init__()
    self._cached_entry_data_type_map = None

  def _CheckSignature(self, value_data):
    """Parses and validates the signature.

    Args:
      value_data (bytes): value data.

    Returns:
      int: format type or None if format could not be determined.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    signature_map = self._GetDataTypeMap('uint32le')

    try:
      signature = self._ReadStructureFromByteStream(
          value_data, 0, signature_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse signature value with error: {0!s}'.format(
              exception))

    format_type = self._HEADER_SIGNATURES.get(signature, None)

    if format_type == self._FORMAT_TYPE_2003:
      # TODO: determine which format version is used (2003 or Vista).
      return self._FORMAT_TYPE_2003

    if format_type == self._FORMAT_TYPE_8:
      cached_entry_signature = value_data[signature:signature + 4]
      if cached_entry_signature in (
          self._CACHED_ENTRY_SIGNATURE_8_0, self._CACHED_ENTRY_SIGNATURE_8_1):
        return self._FORMAT_TYPE_8

    elif format_type == self._FORMAT_TYPE_10:
      # Windows 10 uses the same cache entry signature as Windows 8.1
      cached_entry_signature = value_data[signature:signature + 4]
      if cached_entry_signature == self._CACHED_ENTRY_SIGNATURE_8_1:
        return self._FORMAT_TYPE_10

    return format_type

  def _GetCachedEntryDataTypeMap(
      self, format_type, value_data, cached_entry_offset):
    """Determines the cached entry data type map.

    Args:
      format_type (int): format type.
      value_data (bytes): value data.
      cached_entry_offset (int): offset of the first cached entry data
          relative to the start of the value data.

    Returns:
      dtfabric.DataTypeMap: data type map which contains a data type definition,
          such as a structure, that can be mapped onto binary data or None
          if the data type map is not defined.

    Raises:
      ParseError: if the cached entry data type map cannot be determined.
    """
    if format_type not in self._SUPPORTED_FORMAT_TYPES:
      raise errors.ParseError('Unsupported format type: {0:d}'.format(
          format_type))

    data_type_map_name = ''

    if format_type == self._FORMAT_TYPE_XP:
      data_type_map_name = 'appcompatcache_cached_entry_xp_32bit'

    elif format_type in (self._FORMAT_TYPE_8, self._FORMAT_TYPE_10):
      data_type_map_name = 'appcompatcache_cached_entry_header_8'

    else:
      cached_entry = self._ParseCommon2003CachedEntry(
          value_data, cached_entry_offset)

      # Assume the entry is 64-bit if the 32-bit path offset is 0 and
      # the 64-bit path offset is set.
      if (cached_entry.path_offset_32bit == 0 and
          cached_entry.path_offset_64bit != 0):
        number_of_bits = '64'
      else:
        number_of_bits = '32'

      if format_type == self._FORMAT_TYPE_2003:
        data_type_map_name = (
            'appcompatcache_cached_entry_2003_{0:s}bit'.format(number_of_bits))
      elif format_type == self._FORMAT_TYPE_VISTA:
        data_type_map_name = (
            'appcompatcache_cached_entry_vista_{0:s}bit'.format(number_of_bits))
      elif format_type == self._FORMAT_TYPE_7:
        data_type_map_name = (
            'appcompatcache_cached_entry_7_{0:s}bit'.format(number_of_bits))

    return self._GetDataTypeMap(data_type_map_name)

  def _ParseCommon2003CachedEntry(self, value_data, cached_entry_offset):
    """Parses the cached entry structure common for Windows 2003, Vista and 7.

    Args:
      value_data (bytes): value data.
      cached_entry_offset (int): offset of the first cached entry data
          relative to the start of the value data.

    Returns:
      appcompatcache_cached_entry_2003_common: cached entry structure common
          for Windows 2003, Windows Vista and Windows 7.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    data_type_map = self._GetDataTypeMap(
        'appcompatcache_cached_entry_2003_common')

    try:
      cached_entry = self._ReadStructureFromByteStream(
          value_data[cached_entry_offset:], cached_entry_offset, data_type_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry value with error: {0!s}'.format(
              exception))

    if cached_entry.path_size > cached_entry.maximum_path_size:
      raise errors.ParseError('Path size value out of bounds.')

    path_end_of_string_size = (
        cached_entry.maximum_path_size - cached_entry.path_size)
    if cached_entry.path_size == 0 or path_end_of_string_size != 2:
      raise errors.ParseError('Unsupported path size values.')

    return cached_entry

  def _ParseCachedEntryXP(self, value_data, cached_entry_offset):
    """Parses a Windows XP cached entry.

    Args:
      value_data (bytes): value data.
      cached_entry_offset (int): offset of the first cached entry data
          relative to the start of the value data.

    Returns:
      AppCompatCacheCachedEntry: cached entry.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    try:
      cached_entry = self._ReadStructureFromByteStream(
          value_data[cached_entry_offset:], cached_entry_offset,
          self._cached_entry_data_type_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry value with error: {0!s}'.format(
              exception))

    # TODO: have dtFabric handle string conversion.
    string_size = 0
    for string_index in range(0, 528, 2):
      if (cached_entry.path[string_index] == 0 and
          cached_entry.path[string_index + 1] == 0):
        break
      string_size += 2

    try:
      path = bytearray(cached_entry.path[0:string_size]).decode('utf-16-le')
    except UnicodeDecodeError:
      raise errors.ParseError('Unable to decode cached entry path to string')

    cached_entry_object = AppCompatCacheCachedEntry()
    cached_entry_object.cached_entry_size = (
        self._cached_entry_data_type_map.GetByteSize())
    cached_entry_object.file_size = cached_entry.file_size
    cached_entry_object.last_modification_time = (
        cached_entry.last_modification_time)
    cached_entry_object.last_update_time = cached_entry.last_update_time
    cached_entry_object.path = path

    return cached_entry_object

  def _ParseCachedEntry2003(self, value_data, cached_entry_offset):
    """Parses a Windows 2003 cached entry.

    Args:
      value_data (bytes): value data.
      cached_entry_offset (int): offset of the first cached entry data
          relative to the start of the value data.

    Returns:
      AppCompatCacheCachedEntry: cached entry.

    Raises:
      ParseError: if the value data could not be parsed.
    """

    try:
      cached_entry = self._ReadStructureFromByteStream(
          value_data[cached_entry_offset:], cached_entry_offset,
          self._cached_entry_data_type_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry value with error: {0!s}'.format(
              exception))

    path_size = cached_entry.path_size
    maximum_path_size = cached_entry.maximum_path_size
    path_offset = cached_entry.path_offset

    if path_offset > 0 and path_size > 0:
      path_size += path_offset
      maximum_path_size += path_offset

      try:
        path = value_data[path_offset:path_size].decode('utf-16-le')
      except UnicodeDecodeError:
        raise errors.ParseError('Unable to decode cached entry path to string')

    cached_entry_object = AppCompatCacheCachedEntry()
    cached_entry_object.cached_entry_size = (
        self._cached_entry_data_type_map.GetByteSize())
    cached_entry_object.file_size = getattr(cached_entry, 'file_size', None)
    cached_entry_object.last_modification_time = (
        cached_entry.last_modification_time)
    cached_entry_object.path = path

    return cached_entry_object

  def _ParseCachedEntryVista(self, value_data, cached_entry_offset):
    """Parses a Windows Vista cached entry.

    Args:
      value_data (bytes): value data.
      cached_entry_offset (int): offset of the first cached entry data
          relative to the start of the value data.

    Returns:
      AppCompatCacheCachedEntry: cached entry.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    try:
      cached_entry = self._ReadStructureFromByteStream(
          value_data[cached_entry_offset:], cached_entry_offset,
          self._cached_entry_data_type_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry value with error: {0!s}'.format(
              exception))

    path_size = cached_entry.path_size
    maximum_path_size = cached_entry.maximum_path_size
    path_offset = cached_entry.path_offset

    if path_offset > 0 and path_size > 0:
      path_size += path_offset
      maximum_path_size += path_offset

      try:
        path = value_data[path_offset:path_size].decode('utf-16-le')
      except UnicodeDecodeError:
        raise errors.ParseError('Unable to decode cached entry path to string')

    cached_entry_object = AppCompatCacheCachedEntry()
    cached_entry_object.cached_entry_size = (
        self._cached_entry_data_type_map.GetByteSize())
    cached_entry_object.insertion_flags = cached_entry.insertion_flags
    cached_entry_object.last_modification_time = (
        cached_entry.last_modification_time)
    cached_entry_object.path = path
    cached_entry_object.shim_flags = cached_entry.shim_flags

    return cached_entry_object

  def _ParseCachedEntry7(self, value_data, cached_entry_offset):
    """Parses a Windows 7 cached entry.

    Args:
      value_data (bytes): value data.
      cached_entry_offset (int): offset of the first cached entry data
          relative to the start of the value data.

    Returns:
      AppCompatCacheCachedEntry: cached entry.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    try:
      cached_entry = self._ReadStructureFromByteStream(
          value_data[cached_entry_offset:], cached_entry_offset,
          self._cached_entry_data_type_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry value with error: {0!s}'.format(
              exception))

    path_size = cached_entry.path_size
    maximum_path_size = cached_entry.maximum_path_size
    path_offset = cached_entry.path_offset

    if path_offset > 0 and path_size > 0:
      path_size += path_offset
      maximum_path_size += path_offset

      try:
        path = value_data[path_offset:path_size].decode('utf-16-le')
      except UnicodeDecodeError:
        raise errors.ParseError('Unable to decode cached entry path to string')

    data_offset = cached_entry.data_offset
    data_size = cached_entry.data_size

    cached_entry_object = AppCompatCacheCachedEntry()
    cached_entry_object.cached_entry_size = (
        self._cached_entry_data_type_map.GetByteSize())
    cached_entry_object.insertion_flags = cached_entry.insertion_flags
    cached_entry_object.last_modification_time = (
        cached_entry.last_modification_time)
    cached_entry_object.path = path
    cached_entry_object.shim_flags = cached_entry.shim_flags

    if data_size > 0:
      cached_entry_object.data = value_data[data_offset:data_offset + data_size]

    return cached_entry_object

  def _ParseCachedEntry8(self, value_data, cached_entry_offset):
    """Parses a Windows 8.0 or 8.1 cached entry.

    Args:
      value_data (bytes): value data.
      cached_entry_offset (int): offset of the first cached entry data
          relative to the start of the value data.

    Returns:
      AppCompatCacheCachedEntry: cached entry.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    try:
      cached_entry = self._ReadStructureFromByteStream(
          value_data[cached_entry_offset:], cached_entry_offset,
          self._cached_entry_data_type_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry value with error: {0!s}'.format(
              exception))

    if cached_entry.signature not in (
        self._CACHED_ENTRY_SIGNATURE_8_0, self._CACHED_ENTRY_SIGNATURE_8_1):
      raise errors.ParseError('Unsupported cache entry signature')

    cached_entry_data = value_data[cached_entry_offset:]

    if cached_entry.signature == self._CACHED_ENTRY_SIGNATURE_8_0:
      data_type_map_name = 'appcompatcache_cached_entry_body_8_0'
    elif cached_entry.signature == self._CACHED_ENTRY_SIGNATURE_8_1:
      data_type_map_name = 'appcompatcache_cached_entry_body_8_1'

    data_type_map = self._GetDataTypeMap(data_type_map_name)
    context = dtfabric_data_maps.DataTypeMapContext()

    try:
      cached_entry_body = self._ReadStructureFromByteStream(
          cached_entry_data[12:], cached_entry_offset + 12,
          data_type_map, context=context)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry body with error: {0!s}'.format(
              exception))

    data_offset = context.byte_size
    data_size = cached_entry_body.data_size

    cached_entry_object = AppCompatCacheCachedEntry()
    cached_entry_object.cached_entry_size = (
        12 + cached_entry.cached_entry_data_size)
    cached_entry_object.insertion_flags = cached_entry_body.insertion_flags
    cached_entry_object.last_modification_time = (
        cached_entry_body.last_modification_time)
    cached_entry_object.path = cached_entry_body.path
    cached_entry_object.shim_flags = cached_entry_body.shim_flags

    if data_size > 0:
      cached_entry_object.data = cached_entry_data[
          data_offset:data_offset + data_size]

    return cached_entry_object

  def _ParseCachedEntry10(self, value_data, cached_entry_offset):
    """Parses a Windows 10 cached entry.

    Args:
      value_data (bytes): value data.
      cached_entry_offset (int): offset of the first cached entry data
          relative to the start of the value data.

    Returns:
      AppCompatCacheCachedEntry: cached entry.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    try:
      cached_entry = self._ReadStructureFromByteStream(
          value_data[cached_entry_offset:], cached_entry_offset,
          self._cached_entry_data_type_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry value with error: {0!s}'.format(
              exception))

    if cached_entry.signature not in (
        self._CACHED_ENTRY_SIGNATURE_8_0, self._CACHED_ENTRY_SIGNATURE_8_1):
      raise errors.ParseError('Unsupported cache entry signature')

    cached_entry_data = value_data[cached_entry_offset:]

    data_type_map = self._GetDataTypeMap('appcompatcache_cached_entry_body_10')
    context = dtfabric_data_maps.DataTypeMapContext()

    try:
      cached_entry_body = self._ReadStructureFromByteStream(
          cached_entry_data[12:], cached_entry_offset + 12,
          data_type_map, context=context)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse cached entry body with error: {0!s}'.format(
              exception))

    data_offset = cached_entry_offset + context.byte_size
    data_size = cached_entry_body.data_size

    cached_entry_object = AppCompatCacheCachedEntry()
    cached_entry_object.cached_entry_size = (
        12 + cached_entry.cached_entry_data_size)
    cached_entry_object.last_modification_time = (
        cached_entry_body.last_modification_time)
    cached_entry_object.path = cached_entry_body.path

    if data_size > 0:
      cached_entry_object.data = cached_entry_data[
          data_offset:data_offset + data_size]

    return cached_entry_object

  def _ParseHeader(self, format_type, value_data):
    """Parses the header.

    Args:
      format_type (int): format type.
      value_data (bytes): value data.

    Returns:
      AppCompatCacheHeader: header.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    data_type_map_name = self._HEADER_DATA_TYPE_MAP_NAMES.get(format_type, None)
    if not data_type_map_name:
      raise errors.ParseError(
          'Unsupported format type: {0:d}'.format(format_type))

    data_type_map = self._GetDataTypeMap(data_type_map_name)

    try:
      header = self._ReadStructureFromByteStream(
          value_data, 0, data_type_map)
    except (ValueError, errors.ParseError) as exception:
      raise errors.ParseError(
          'Unable to parse header value with error: {0!s}'.format(
              exception))

    header_data_size = data_type_map.GetByteSize()
    if format_type == self._FORMAT_TYPE_10:
      header_data_size = header.signature

    cache_header = AppCompatCacheHeader()
    cache_header.header_size = header_data_size
    cache_header.number_of_cached_entries = getattr(
        header, 'number_of_cached_entries', 0)

    return cache_header

  def ExtractEvents(self, parser_mediator, registry_key, **kwargs):
    """Extracts events from a Windows Registry key.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfvfs.
      registry_key (dfwinreg.WinRegistryKey): Windows Registry key.

    Raises:
      ParseError: if the value data could not be parsed.
    """
    value = registry_key.GetValueByName('AppCompatCache')
    if not value:
      return

    value_data = value.data
    value_data_size = len(value.data)

    format_type = self._CheckSignature(value_data)
    if not format_type:
      parser_mediator.ProduceExtractionWarning(
          'Unsupported signature in AppCompatCache key: {0:s}'.format(
              registry_key.path))
      return

    header_object = self._ParseHeader(format_type, value_data)

    # On Windows Vista and 2008 when the cache is empty it will
    # only consist of the header.
    if value_data_size <= header_object.header_size:
      return

    cached_entry_offset = header_object.header_size

    self._cached_entry_data_type_map = self._GetCachedEntryDataTypeMap(
        format_type, value_data, cached_entry_offset)
    if not self._cached_entry_data_type_map:
      raise errors.ParseError('Unable to determine cached entry data type.')

    parse_cached_entry_function = None
    if format_type == self._FORMAT_TYPE_XP:
      parse_cached_entry_function = self._ParseCachedEntryXP
    elif format_type == self._FORMAT_TYPE_2003:
      parse_cached_entry_function = self._ParseCachedEntry2003
    elif format_type == self._FORMAT_TYPE_VISTA:
      parse_cached_entry_function = self._ParseCachedEntryVista
    elif format_type == self._FORMAT_TYPE_7:
      parse_cached_entry_function = self._ParseCachedEntry7
    elif format_type == self._FORMAT_TYPE_8:
      parse_cached_entry_function = self._ParseCachedEntry8
    elif format_type == self._FORMAT_TYPE_10:
      parse_cached_entry_function = self._ParseCachedEntry10

    cached_entry_index = 0
    while cached_entry_offset < value_data_size:
      cached_entry_object = parse_cached_entry_function(
          value_data, cached_entry_offset)

      event_data = AppCompatCacheEventData()
      event_data.entry_index = cached_entry_index + 1
      event_data.key_path = registry_key.path
      event_data.offset = cached_entry_offset
      event_data.path = cached_entry_object.path

      if cached_entry_object.last_modification_time is not None:
        if not cached_entry_object.last_modification_time:
          date_time = dfdatetime_semantic_time.SemanticTime('Not set')
        else:
          date_time = dfdatetime_filetime.Filetime(
              timestamp=cached_entry_object.last_modification_time)

        # TODO: refactor to file modification event.
        event = time_events.DateTimeValuesEvent(
            date_time, 'File Last Modification Time')
        parser_mediator.ProduceEventWithEventData(event, event_data)

      if cached_entry_object.last_update_time is not None:
        if not cached_entry_object.last_update_time:
          date_time = dfdatetime_semantic_time.SemanticTime('Not set')
        else:
          date_time = dfdatetime_filetime.Filetime(
              timestamp=cached_entry_object.last_update_time)

        # TODO: refactor to process run event.
        event = time_events.DateTimeValuesEvent(
            date_time, definitions.TIME_DESCRIPTION_LAST_RUN)
        parser_mediator.ProduceEventWithEventData(event, event_data)

      cached_entry_offset += cached_entry_object.cached_entry_size
      cached_entry_index += 1

      if (header_object.number_of_cached_entries != 0 and
          cached_entry_index >= header_object.number_of_cached_entries):
        break


winreg.WinRegistryParser.RegisterPlugin(AppCompatCacheWindowsRegistryPlugin)
