text: # This Source Code Form is subject to the terms of the Mozilla Public
  # License, v. 2.0. If a copy of the MPL was not distributed with this
  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  
  """This module provides a function that will transate Minidump Stackwalk pipe
  dump into a json format.
  
  {
    # "status": string, // OK | ERROR_* | SYMBOL_SUPPLIER_INTERRUPTED
    "system_info": {
      "os": string,
      "os_ver": string,
      "cpu_arch": string, // x86 | amd64 | arm | ppc | sparc
      "cpu_info": string,
      "cpu_count": int
    },
    "crash_info": {
      "type": string,
      "crash_address": string, // 0x[[:xdigit:]]+
      "crashing_thread": int // | null
    }
    "main_module": int, // index into modules
    "modules": [
           // zero or more
      {
        "base_addr": string, // 0x[[:xdigit:]]+
        "debug_file": string,
        "debug_id": string, // [[:xdigit:]]{33}
        "end_addr": string, // 0x[[:xdigit:]]+
        "filename": string,
        "version": string
      }
    ],
    "thread_count": int,
    "threads": [
      // for i in range(thread_count)
      {
        "frame_count": int,
        "frames": [
          // for i in range(frame_count)
          {
            "frame": int,
            "module": string,   // optional
            "function": string, // optional
            "file": string,     // optional
            "line": int,        // optional
            "offset": string,         // 0x[[:xdigit:]]+
            "module_offset": string,  // 0x[[:xdigit:]]+ , optional
            "function_offset": string // 0x[[:xdigit:]]+ , optional
          }
        ]
      }
    ],
    // repeated here for ease of searching
    // (max 10 frames)
    "crashing_thread": {
      "threads_index": int,
      "total_frames": int,
      "frames": [
        // for i in range(length)
        {
          // as per "frames" entries from "threads" above
        }
      ]
    }
  }
  """
  
  
  class DotDict(dict):
      __getattr__ = dict.__getitem__
      __setattr__ = dict.__setitem__
      __delattr__ = dict.__delitem__
  
  
  class DotDictWithPut(DotDict):
      #--------------------------------------------------------------------------
      def put_if_not_none(self, key, value):
          if value is not None and value != '':
              self[key] = value
  
  
  def pipe_dump_to_json_dump(pipe_dump_iterable):
      """given a list (or any iterable) of strings representing a MDSW pipe dump,
      this function will convert it into a json format."""
      json_dump = DotDict()
      crashing_thread = None
      module_counter = 0
      for a_line in pipe_dump_iterable:
          parts = a_line.split('|')
          if parts[0] == 'OS':
              _extract_OS_info(parts, json_dump)
          elif parts[0] == 'CPU':
              _extract_CPU_info(parts, json_dump)
          elif parts[0] == 'Crash':
              crashing_thread = _extract_crash_info(parts, json_dump)
          elif parts[0] == 'Module':
              _extract_module_info(parts, json_dump, module_counter)
              module_counter += 1
          else:
              try:
                  thread_number = int(parts[0])
              except (ValueError, IndexError):
                  continue  # unknow line type, ignore it
              _extract_frame_info(parts, json_dump)
      try:
          json_dump.thread_count = len(json_dump.threads)
      except KeyError:  # no threads were over found, 'threads' key was not made
          json_dump.thread_count = 0
      if crashing_thread is not None:
          crashing_thread_frames = DotDict()
          crashing_thread_frames.threads_index = crashing_thread
          crashing_thread_frames.total_frames = \
              len(json_dump.threads[crashing_thread].frames)
          crashing_thread_frames.frames = \
              json_dump.threads[crashing_thread].frames[:10]
          json_dump.crashing_thread = crashing_thread_frames
      return json_dump
  
  
  #------------------------------------------------------------------------------
  def _get(indexable_container, index, default):
      """like 'get' on a dict, but it works on lists, too"""
      try:
          return indexable_container[index]
      except (IndexError, KeyError):
          return default
  
  
  #------------------------------------------------------------------------------
  def _get_int(indexable_container, index, default):
      """try to get an int from an indexable container.  If that fails
      return the default"""
      try:
          return int(indexable_container[index])
      # exceptions separated to make case coverage clearer
      except (IndexError, KeyError):
          # item not found in the container
          return default
      except ValueError:
          # conversion to integer has failed
          return default
  
  
  #------------------------------------------------------------------------------
  def _extract_OS_info(os_line, json_dump):
      """given a pipe dump OS line, extract the parts and put them in their
      proper location within the json_dump"""
      system_info = DotDictWithPut()
      system_info.put_if_not_none('os', _get(os_line, 1, None))
      system_info.put_if_not_none('os_ver', _get(os_line, 2, None))
      if 'system_info' in json_dump:
          json_dump.system_info.update(system_info)
      else:
          json_dump.system_info = system_info
  
  
  #------------------------------------------------------------------------------
  def _extract_CPU_info(cpu_line, json_dump):
      """given a pipe dump CPU line, extract the parts and put them in their
      proper location within the json_dump"""
      system_info = DotDictWithPut()
      system_info.put_if_not_none('cpu_arch', _get(cpu_line, 1, None))
      system_info.put_if_not_none('cpu_info', _get(cpu_line, 2, None))
      system_info.put_if_not_none('cpu_count', _get_int(cpu_line, 3, None))
      if 'system_info' in json_dump:
          json_dump.system_info.update(system_info)
      else:
          json_dump.system_info = system_info
  
  
  #------------------------------------------------------------------------------
  def _extract_crash_info(crash_line, json_dump):
      """given a pipe dump CRASH line, extract the parts and put them in their
      proper location within the json_dump"""
      crash_info = DotDictWithPut()
      crash_info.put_if_not_none('type', _get(crash_line, 1, None))
      crash_info.put_if_not_none('crash_address', _get(crash_line, 2, None))
      crash_info.put_if_not_none('crashing_thread', _get_int(crash_line, 3, None))
      json_dump.crash_info = crash_info
      return crash_info.get('crashing_thread', None)
  
  
  #------------------------------------------------------------------------------
  def _extract_module_info(module_line, json_dump, module_counter):
      """given a pipe dump Module line, extract the parts and put them in their
      proper location within the json_dump"""
      module = DotDictWithPut()
      module.put_if_not_none('filename', _get(module_line, 1, None))
      module.put_if_not_none('version', _get(module_line, 2, None))
      module.put_if_not_none('debug_file', _get(module_line, 3, None))
      module.put_if_not_none('debug_id', _get(module_line, 4, None))
      module.put_if_not_none('base_addr', _get(module_line, 5, None))
      module.put_if_not_none('end_addr', _get(module_line, 6, None))
      is_main_module = _get_int(module_line, 7, 0)
      if is_main_module:
          json_dump.main_module = module_counter
      if 'modules' not in json_dump:
          json_dump.modules = []
      json_dump.modules.append(module)
  
  
  #------------------------------------------------------------------------------
  def _extract_frame_info(frame_line, json_dump):
      """given a pipe dump Frame line, extract the parts and put them in their
      proper location within the json_dump"""
      if 'threads' not in json_dump:
          json_dump.threads = []
      thread_number = _get_int(frame_line, 0, None)
      if thread_number is None:
          return
      if thread_number >= len(json_dump.threads):
          # threads are supposed to arrive in order.  We've not seen this thread
          # before, fill in a new entry in the 'threads' section of the json_dump
          # making sure that intervening missing threads have empty thread data
          for i in range(thread_number - len(json_dump.threads) + 1):
              thread = DotDict()
              thread.frame_count = 0
              thread.frames = []
              json_dump.threads.append(thread)
      # collect frame info from the pipe dump line
      tmp_frame = _get_int(frame_line, 1, None)
      tmp_module = _get(frame_line, 2, None)
      tmp_function = _get(frame_line, 3, None)
      tmp_file = _get(frame_line, 4, None)
      tmp_line = _get_int(frame_line, 5, None)
      tmp_offset = _get(frame_line, 6, None)
      frame = DotDictWithPut()
      frame.put_if_not_none('frame', tmp_frame)
      frame.put_if_not_none('filename', tmp_module)
      frame.put_if_not_none('function', tmp_function)
      frame.put_if_not_none('abs_path', tmp_file)
      frame.put_if_not_none('lineno', tmp_line)
      if tmp_file and tmp_line is not None:
          # skip offset entirely
          pass
      elif not tmp_file and tmp_function:
          frame.function_offset = tmp_offset
      elif not tmp_function and tmp_module:
          frame.module_offset = tmp_offset
      else:
          frame.offset = tmp_offset
      # save the frame info into the json
      json_dump.threads[thread_number].frames.append(frame)
      json_dump.threads[thread_number].frame_count += 1
  