# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022 Eduardo Aguiar
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
import traceback
import mobius

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# thread_download.pas:
#
# * tthread_download.ICH_delete_unused_phashes removes PHash_<hash>.dat if
#   hash is not in lista_download.
#
# * tthread_download.add_downloads_recursive adds ___ARESTRA___*.* files from
#   folder 'My Shared Folder' and its subfolders to lista_download.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief get data as string (sanitize non UTF-8 strings)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def get_utf8_string (data):
  try:
    s = data.decode ('utf-8')
  except UnicodeDecodeError as e:
    s = data.decode ('iso-8859-1', errors='ignore')

  return s

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief get file type according to amime field
# @see const_ares.pas and helper_mimetypes.pas
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def get_file_type (mime):
  return {
      1 : 'Audio',
      2 : 'Audio',
      3 : 'Software',
      4 : 'Audio',
      5 : 'Video',
      6 : 'Document',
      7 : 'Image'}.get (mime, 'Other')

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Entry
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Entry (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief initialize object
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self):
    self.path = None
    self.size = -1
    self.title = None
    self.artist = None
    self.album = None
    self.category = None
    self.year = None
    self.language = None
    self.url = None
    self.comment = None
    self.subfolder = None
    self.begin_time = None
    self.phash_verified = None
    self.flag_paused = None
    self.sources = []

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Source
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Source (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief initialize object
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self):
    self.ip = None
    self.port = None
    self.ip_server = None
    self.port_server = None
    self.alt_ip = None

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Decode file
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def decode (f):
  try:
     return decode_file (f)
  except Exception as e:
     mobius.core.logf ('WRN %s %s' % (str (e), traceback.format_exc ()))

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Decode ___ARESTRA___ file
# @see read_details_DB_Download - helper_download_disk.pas (line 722)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def decode_file (f):
  if f.size < 4096:
    return

  reader = f.new_reader ()
  if not reader:
    return

  # check signature
  decoder = mobius.decoder.data_decoder (reader)
  decoder.seek (f.size - 4096)
  signature = decoder.get_string_by_size (13)

  if signature == '___ARESTRA___':
    version = 1

  elif signature == '___ARESTRA__2':
    version = 2

  elif signature == '___ARESTRA__3':
    version = 3

  else:
    mobius.core.logf ('WRN Invalid signature (%s)' % signature)
    return

  # create entry object
  entry = Entry ()
  entry.signature = signature
  entry.version = version
  entry.path = f.path.replace ('/', '\\')
  entry.name = entry.path.rsplit ('\\')[-1][13:]
  entry.creation_time = f.creation_time
  entry.last_modification_time = f.modification_time

  # get data
  if version == 1:
    entry.size = decoder.get_uint32_le ()
    entry.progress = decoder.get_uint32_le ()
  else:
    entry.size = decoder.get_uint64_le ()
    entry.progress = decoder.get_uint64_le ()

  # get gaps
  entry.gaps = []
  end_pchunk = -1

  while end_pchunk != 0:

    if version == 1:
      end_pchunk = decoder.get_uint32_le ()
      start_pchunk = decoder.get_uint32_le ()
    else:
      start_pchunk = decoder.get_uint64_le ()
      end_pchunk = decoder.get_uint64_le ()

    if end_pchunk != 0:
      entry.gaps.append ((start_pchunk, end_pchunk))

  # get params
  entry.mime = decoder.get_uint8 ()
  entry.filetype = get_file_type (entry.mime)
  entry.flag_paused = decoder.get_uint8 () == 1
  entry.param1 = decoder.get_uint32_le ()
  entry.param2 = decoder.get_uint32_le ()
  entry.param3 = decoder.get_uint32_le ()

  # decode metadata
  size = decoder.get_uint16_le ()

  if size == 0 or size > 3500:
    return entry

  while size > 0:
    tagtype = decoder.get_uint8 ()
    lun = decoder.get_uint16_le ()
    size = size - lun - 3

    if tagtype == 1:
      value = decoder.get_bytearray_by_size (lun)
      entry.kwgenre = get_utf8_string (value)

    elif tagtype == 2:
      value = decoder.get_bytearray_by_size (lun)
      entry.title = get_utf8_string (value)

    elif tagtype == 3:
      value = decoder.get_bytearray_by_size (lun)
      entry.artist = get_utf8_string (value)

    elif tagtype == 4:
      value = decoder.get_bytearray_by_size (lun)
      entry.album = get_utf8_string (value)

    elif tagtype == 5:
      value = decoder.get_bytearray_by_size (lun)
      entry.category = get_utf8_string (value)

    elif tagtype == 6:
      value = decoder.get_bytearray_by_size (lun)
      entry.date = get_utf8_string (value)

    elif tagtype == 7:
      entry.sources = decode_altsources (decoder, lun, False)

    elif tagtype == 8:
      value = decoder.get_bytearray_by_size (lun)
      entry.language = get_utf8_string (value)

    elif tagtype == 9:
      value = decoder.get_bytearray_by_size (lun)
      entry.url = get_utf8_string (value)

    elif tagtype == 10:
      value = decoder.get_bytearray_by_size (lun)
      entry.comment = get_utf8_string (value)

    elif tagtype == 13:
      entry.sources = decode_altsources (decoder, lun, True)

    elif tagtype == 15:
      value = decoder.get_bytearray_by_size (lun)
      entry.hash_sha1 = mobius.encoder.hexstring (value)

    elif tagtype == 19:
      value = decoder.get_bytearray_by_size (lun)
      entry.subfolder = value

    elif tagtype == 20:
      entry.phash_verified = decoder.get_uint64_le ()

    elif tagtype == 25:
      entry.begin_time = decoder.get_unix_datetime ()

    else:
      mobius.core.logf ('DEV Unknown tag type (%d)' % tagtype)

  # flags
  entry.flag_completed = (entry.size == entry.progress)
  entry.flag_corrupted = entry.phash_verified < entry.progress

  return entry

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Decode altsources
# @see add_sources - helper_altsources (line 106)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def decode_altsources (decoder, size, is_new):
  sources = []
  pos = 0

  while pos < size:
    source = Source ()
    source.ip = decoder.get_ipv4_be ()
    source.port = decoder.get_uint16_le ()
    source.ip_server = decoder.get_ipv4_be ()
    source.port_server = decoder.get_uint16_le ()

    pos += 12

    if is_new:
      source.ip_alt = decoder.get_ipv4_be ()
      dummy = decoder.get_uint8 ()
      pos += 5

    if source.ip != '0.0.0.0' and source.port != 0:
      sources.append (source)

  return sources
