# Copyright 2012-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information

"""
Parses replies from the control socket.

**Module Overview:**

::

  convert - translates a ControlMessage into a particular response subclass

  ControlMessage - Message that's read from the control socket.
    |- SingleLineResponse - Simple tor response only including a single line of information.
    |
    |- from_str - provides a ControlMessage for the given string
    |- is_ok - response had a 250 status
    |- content - provides the parsed message content
    +- raw_content - unparsed socket data

  ControlLine - String subclass with methods for parsing controller responses.
    |- remainder - provides the unparsed content
    |- is_empty - checks if the remaining content is empty
    |- is_next_quoted - checks if the next entry is a quoted value
    |- is_next_mapping - checks if the next entry is a KEY=VALUE mapping
    |- peek_key - provides the key of the next entry
    |- pop - removes and returns the next entry
    +- pop_mapping - removes and returns the next entry as a KEY=VALUE mapping
"""

import codecs
import io
import re
import threading

import stem.socket
import stem.util
import stem.util.str_tools

__all__ = [
  'add_onion',
  'events',
  'getinfo',
  'getconf',
  'protocolinfo',
  'authchallenge',
  'convert',
  'ControlMessage',
  'ControlLine',
  'SingleLineResponse',
]

KEY_ARG = re.compile('^(\S+)=')


def convert(response_type, message, **kwargs):
  """
  Converts a :class:`~stem.response.ControlMessage` into a particular kind of
  tor response. This does an in-place conversion of the message from being a
  :class:`~stem.response.ControlMessage` to a subclass for its response type.
  Recognized types include...

  =================== =====
  response_type       Class
  =================== =====
  **ADD_ONION**       :class:`stem.response.add_onion.AddOnionResponse`
  **AUTHCHALLENGE**   :class:`stem.response.authchallenge.AuthChallengeResponse`
  **EVENT**           :class:`stem.response.events.Event` subclass
  **GETCONF**         :class:`stem.response.getconf.GetConfResponse`
  **GETINFO**         :class:`stem.response.getinfo.GetInfoResponse`
  **MAPADDRESS**      :class:`stem.response.mapaddress.MapAddressResponse`
  **PROTOCOLINFO**    :class:`stem.response.protocolinfo.ProtocolInfoResponse`
  **SINGLELINE**      :class:`stem.response.SingleLineResponse`
  =================== =====

  :param str response_type: type of tor response to convert to
  :param stem.response.ControlMessage message: message to be converted
  :param kwargs: optional keyword arguments to be passed to the parser method

  :raises:
    * :class:`stem.ProtocolError` the message isn't a proper response of
      that type
    * :class:`stem.InvalidArguments` the arguments given as input are
      invalid, this is can only be raised if the response_type is: **GETINFO**,
      **GETCONF**
    * :class:`stem.InvalidRequest` the arguments given as input are
      invalid, this is can only be raised if the response_type is:
      **MAPADDRESS**
    * :class:`stem.OperationFailed` if the action the event represents failed,
      this is can only be raised if the response_type is: **MAPADDRESS**
    * **TypeError** if argument isn't a :class:`~stem.response.ControlMessage`
      or response_type isn't supported
  """

  import stem.response.add_onion
  import stem.response.authchallenge
  import stem.response.events
  import stem.response.getinfo
  import stem.response.getconf
  import stem.response.mapaddress
  import stem.response.protocolinfo

  if not isinstance(message, ControlMessage):
    raise TypeError('Only able to convert stem.response.ControlMessage instances')

  response_types = {
    'ADD_ONION': stem.response.add_onion.AddOnionResponse,
    'AUTHCHALLENGE': stem.response.authchallenge.AuthChallengeResponse,
    'EVENT': stem.response.events.Event,
    'GETCONF': stem.response.getconf.GetConfResponse,
    'GETINFO': stem.response.getinfo.GetInfoResponse,
    'MAPADDRESS': stem.response.mapaddress.MapAddressResponse,
    'PROTOCOLINFO': stem.response.protocolinfo.ProtocolInfoResponse,
    'SINGLELINE': SingleLineResponse,
  }

  try:
    response_class = response_types[response_type]
  except TypeError:
    raise TypeError('Unsupported response type: %s' % response_type)

  message.__class__ = response_class
  message._parse_message(**kwargs)


class ControlMessage(object):
  """
  Message from the control socket. This is iterable and can be stringified for
  individual message components stripped of protocol formatting. Messages are
  never empty.

  .. versionchanged:: 1.7.0
     Implemented equality and hashing.
  """

  @staticmethod
  def from_str(content, msg_type = None, normalize = False, **kwargs):
    """
    Provides a ControlMessage for the given content.

    .. versionadded:: 1.1.0

    .. versionchanged:: 1.6.0
       Added the normalize argument.

    :param str content: message to construct the message from
    :param str msg_type: type of tor reply to parse the content as
    :param bool normalize: ensures expected carriage return and ending newline
      are present
    :param kwargs: optional keyword arguments to be passed to the parser method

    :returns: stem.response.ControlMessage instance
    """

    if normalize:
      if not content.endswith('\n'):
        content += '\n'

      content = re.sub('([\r]?)\n', '\r\n', content)

    msg = stem.socket.recv_message(io.BytesIO(stem.util.str_tools._to_bytes(content)))

    if msg_type is not None:
      convert(msg_type, msg, **kwargs)

    return msg

  def __init__(self, parsed_content, raw_content):
    if not parsed_content:
      raise ValueError("ControlMessages can't be empty")

    self._parsed_content = parsed_content
    self._raw_content = raw_content
    self._str = None
    self._hash = stem.util._hash_attr(self, '_raw_content')

  def is_ok(self):
    """
    Checks if any of our lines have a 250 response.

    :returns: **True** if any lines have a 250 response code, **False** otherwise
    """

    for code, _, _ in self._parsed_content:
      if code == '250':
        return True

    return False

  def content(self, get_bytes = False):
    """
    Provides the parsed message content. These are entries of the form...

    ::

      (status_code, divider, content)

    **status_code**
      Three character code for the type of response (defined in section 4 of
      the control-spec).

    **divider**
      Single character to indicate if this is mid-reply, data, or an end to the
      message (defined in section 2.3 of the control-spec).

    **content**
      The following content is the actual payload of the line.

    For data entries the content is the full multi-line payload with newline
    linebreaks and leading periods unescaped.

    The **status_code** and **divider** are both strings (**bytes** in python
    2.x and **unicode** in python 3.x). The **content** however is **bytes** if
    **get_bytes** is **True**.

    .. versionchanged:: 1.1.0
       Added the get_bytes argument.

    :param bool get_bytes: provides **bytes** for the **content** rather than a **str**

    :returns: **list** of (str, str, str) tuples for the components of this message
    """

    if stem.prereq.is_python_3() and not get_bytes:
      return [(code, div, stem.util.str_tools._to_unicode(content)) for (code, div, content) in self._parsed_content]
    else:
      return list(self._parsed_content)

  def raw_content(self, get_bytes = False):
    """
    Provides the unparsed content read from the control socket.

    .. versionchanged:: 1.1.0
       Added the get_bytes argument.

    :param bool get_bytes: if **True** then this provides **bytes** rather than a **str**

    :returns: **str** of the socket data used to generate this message
    """

    if stem.prereq.is_python_3() and not get_bytes:
      return stem.util.str_tools._to_unicode(self._raw_content)
    else:
      return self._raw_content

  def __str__(self):
    """
    Content of the message, stripped of status code and divider protocol
    formatting.
    """

    if self._str is None:
      self._str = '\n'.join(list(self))

    return self._str

  def __iter__(self):
    """
    Provides :class:`~stem.response.ControlLine` instances for the content of
    the message. This is stripped of status codes and dividers, for instance...

    ::

      250+info/names=
      desc/id/* -- Router descriptors by ID.
      desc/name/* -- Router descriptors by nickname.
      .
      250 OK

    Would provide two entries...

    ::

      1st - "info/names=
             desc/id/* -- Router descriptors by ID.
             desc/name/* -- Router descriptors by nickname."
      2nd - "OK"
    """

    for _, _, content in self._parsed_content:
      if stem.prereq.is_python_3():
        content = stem.util.str_tools._to_unicode(content)

      yield ControlLine(content)

  def __len__(self):
    """
    :returns: number of ControlLines
    """

    return len(self._parsed_content)

  def __getitem__(self, index):
    """
    :returns: :class:`~stem.response.ControlLine` at the index
    """

    content = self._parsed_content[index][2]

    if stem.prereq.is_python_3():
      content = stem.util.str_tools._to_unicode(content)

    return ControlLine(content)

  def __hash__(self):
    return self._hash

  def __eq__(self, other):
    return hash(self) == hash(other) if isinstance(other, ControlMessage) else False

  def __ne__(self, other):
    return not self == other


class ControlLine(str):
  """
  String subclass that represents a line of controller output. This behaves as
  a normal string with additional methods for parsing and popping entries from
  a space delimited series of elements like a stack.

  None of these additional methods effect ourselves as a string (which is still
  immutable). All methods are thread safe.
  """

  def __new__(self, value):
    return str.__new__(self, value)

  def __init__(self, value):
    self._remainder = value
    self._remainder_lock = threading.RLock()

  def remainder(self):
    """
    Provides our unparsed content. This is an empty string after we've popped
    all entries.

    :returns: **str** of the unparsed content
    """

    return self._remainder

  def is_empty(self):
    """
    Checks if we have further content to pop or not.

    :returns: **True** if we have additional content, **False** otherwise
    """

    return self._remainder == ''

  def is_next_quoted(self, escaped = False):
    """
    Checks if our next entry is a quoted value or not.

    :param bool escaped: unescapes the string

    :returns: **True** if the next entry can be parsed as a quoted value, **False** otherwise
    """

    start_quote, end_quote = _get_quote_indices(self._remainder, escaped)
    return start_quote == 0 and end_quote != -1

  def is_next_mapping(self, key = None, quoted = False, escaped = False):
    """
    Checks if our next entry is a KEY=VALUE mapping or not.

    :param str key: checks that the key matches this value, skipping the check if **None**
    :param bool quoted: checks that the mapping is to a quoted value
    :param bool escaped: unescapes the string

    :returns: **True** if the next entry can be parsed as a key=value mapping,
      **False** otherwise
    """

    remainder = self._remainder  # temp copy to avoid locking
    key_match = KEY_ARG.match(remainder)

    if key_match:
      if key and key != key_match.groups()[0]:
        return False

      if quoted:
        # checks that we have a quoted value and that it comes after the 'key='
        start_quote, end_quote = _get_quote_indices(remainder, escaped)
        return start_quote == key_match.end() and end_quote != -1
      else:
        return True  # we just needed to check for the key
    else:
      return False  # doesn't start with a key

  def peek_key(self):
    """
    Provides the key of the next entry, providing **None** if it isn't a
    key/value mapping.

    :returns: **str** with the next entry's key
    """

    remainder = self._remainder
    key_match = KEY_ARG.match(remainder)

    if key_match:
      return key_match.groups()[0]
    else:
      return None

  def pop(self, quoted = False, escaped = False):
    """
    Parses the next space separated entry, removing it and the space from our
    remaining content. Examples...

    ::

      >>> line = ControlLine("\\"We're all mad here.\\" says the grinning cat.")
      >>> print line.pop(True)
        "We're all mad here."
      >>> print line.pop()
        "says"
      >>> print line.remainder()
        "the grinning cat."

      >>> line = ControlLine("\\"this has a \\\\\\" and \\\\\\\\ in it\\" foo=bar more_data")
      >>> print line.pop(True, True)
        "this has a \\" and \\\\ in it"

    :param bool quoted: parses the next entry as a quoted value, removing the quotes
    :param bool escaped: unescapes the string

    :returns: **str** of the next space separated entry

    :raises:
      * **ValueError** if quoted is True without the value being quoted
      * **IndexError** if we don't have any remaining content left to parse
    """

    with self._remainder_lock:
      next_entry, remainder = _parse_entry(self._remainder, quoted, escaped, False)
      self._remainder = remainder
      return next_entry

  def pop_mapping(self, quoted = False, escaped = False, get_bytes = False):
    """
    Parses the next space separated entry as a KEY=VALUE mapping, removing it
    and the space from our remaining content.

    .. versionchanged:: 1.6.0
       Added the get_bytes argument.

    :param bool quoted: parses the value as being quoted, removing the quotes
    :param bool escaped: unescapes the string
    :param bool get_bytes: provides **bytes** for the **value** rather than a **str**

    :returns: **tuple** of the form (key, value)

    :raises: **ValueError** if this isn't a KEY=VALUE mapping or if quoted is
      **True** without the value being quoted
    :raises: **IndexError** if there's nothing to parse from the line
    """

    with self._remainder_lock:
      if self.is_empty():
        raise IndexError('no remaining content to parse')

      key_match = KEY_ARG.match(self._remainder)

      if not key_match:
        raise ValueError("the next entry isn't a KEY=VALUE mapping: " + self._remainder)

      # parse off the key
      key = key_match.groups()[0]
      remainder = self._remainder[key_match.end():]

      next_entry, remainder = _parse_entry(remainder, quoted, escaped, get_bytes)
      self._remainder = remainder
      return (key, next_entry)


def _parse_entry(line, quoted, escaped, get_bytes):
  """
  Parses the next entry from the given space separated content.

  :param str line: content to be parsed
  :param bool quoted: parses the next entry as a quoted value, removing the quotes
  :param bool escaped: unescapes the string

  :returns: **tuple** of the form (entry, remainder)

  :raises:
    * **ValueError** if quoted is True without the next value being quoted
    * **IndexError** if there's nothing to parse from the line
  """

  if line == '':
    raise IndexError('no remaining content to parse')

  next_entry, remainder = '', line

  if quoted:
    # validate and parse the quoted value
    start_quote, end_quote = _get_quote_indices(remainder, escaped)

    if start_quote != 0 or end_quote == -1:
      raise ValueError("the next entry isn't a quoted value: " + line)

    next_entry, remainder = remainder[1:end_quote], remainder[end_quote + 1:]
  else:
    # non-quoted value, just need to check if there's more data afterward
    if ' ' in remainder:
      next_entry, remainder = remainder.split(' ', 1)
    else:
      next_entry, remainder = remainder, ''

  if escaped:
    # Tor does escaping in its 'esc_for_log' function of 'common/util.c'. It's
    # hard to tell what controller functions use this in practice, but direct
    # users are...
    #
    #   * 'COOKIEFILE' field of PROTOCOLINFO responses
    #   * logged messages about bugs
    #   * the 'getinfo_helper_listeners' function of control.c
    #
    # Ideally we'd use "next_entry.decode('string_escape')" but it was removed
    # in python 3.x and 'unicode_escape' isn't quite the same...
    #
    #   https://stackoverflow.com/questions/14820429/how-do-i-decodestring-escape-in-python3

    next_entry = codecs.escape_decode(next_entry)[0]

    if stem.prereq.is_python_3() and not get_bytes:
      next_entry = stem.util.str_tools._to_unicode(next_entry)  # normalize back to str

  if get_bytes:
    next_entry = stem.util.str_tools._to_bytes(next_entry)

  return (next_entry, remainder.lstrip())


def _get_quote_indices(line, escaped):
  """
  Provides the indices of the next two quotes in the given content.

  :param str line: content to be parsed
  :param bool escaped: unescapes the string

  :returns: **tuple** of two ints, indices being -1 if a quote doesn't exist
  """

  indices, quote_index = [], -1

  for _ in range(2):
    quote_index = line.find('"', quote_index + 1)

    # if we have escapes then we need to skip any r'\"' entries
    if escaped:
      # skip check if index is -1 (no match) or 0 (first character)
      while quote_index >= 1 and line[quote_index - 1] == '\\':
        quote_index = line.find('"', quote_index + 1)

    indices.append(quote_index)

  return tuple(indices)


class SingleLineResponse(ControlMessage):
  """
  Reply to a request that performs an action rather than querying data. These
  requests only contain a single line, which is 'OK' if successful, and a
  description of the problem if not.

  :var str code: status code for our line
  :var str message: content of the line
  """

  def is_ok(self, strict = False):
    """
    Checks if the response code is "250". If strict is **True** then this
    checks if the response is "250 OK"

    :param bool strict: checks for a "250 OK" message if **True**

    :returns:
      * If strict is **False**: **True** if the response code is "250", **False** otherwise
      * If strict is **True**: **True** if the response is "250 OK", **False** otherwise
    """

    if strict:
      return self.content()[0] == ('250', ' ', 'OK')

    return self.content()[0][0] == '250'

  def _parse_message(self):
    content = self.content()

    if len(content) > 1:
      raise stem.ProtocolError('Received multi-line response')
    elif len(content) == 0:
      raise stem.ProtocolError('Received empty response')
    else:
      self.code, _, self.message = content[0]
