#!/usr/bin/env ruby -w
#
# https://developer.apple.com/library/mac/#technotes/tn2004/tn2123.html
require "fileutils"
require "shellwords"

DSYM_ARCHIVE_DIR = "#{ENV['HOME']}/build/TextMate/archive/dsym"
DSYM_CACHE_DIR   = "#{ENV['HOME']}/Library/Caches/com.macromates.TextMate/dsym"

BinaryImage = Struct.new(:first, :last, :name, :file)

def find_dsym(process, version)
  path = File.expand_path("#{process}_#{version}", DSYM_CACHE_DIR)
  return path if File.directory?(path)

  archive = File.expand_path("#{process}_#{version}.tbz", DSYM_ARCHIVE_DIR)
  return nil unless File.file?(archive)

  FileUtils.mkdir_p(path)
  STDERR << "Extracting dsym to ‘#{path}’…\n"
  %x{ tar -jxf #{archive.shellescape} -C #{path.shellescape} }

  path
end

class BinaryImages
  def initialize(files, report)
    uuid_to_file = { }
    files.each do |file|
      res = %x{dwarfdump -u "#{file}"}
      file = file.sub(%r{([^/]+?)(\.(app|framework))?\.dSYM$}, "\\0/Contents/Resources/DWARF/\\1") if File.directory? file
      uuid_to_file[$1.gsub(/-/, '')] = file if res =~ /UUID: ([0-9A-F\-]*)/
    end

    @images = [ ]

    if report =~ /^Binary Images:\n((?:\s*0x[[:xdigit:]]+ - \s*0x[[:xdigit:]]+.*\n)+)/i
      $1.scan(/(0x[[:xdigit:]]+) - \s*(0x[[:xdigit:]]+)\s+\+?(\S+).*?<([0-9a-fA-F\-]+)>/).each do |arr|
        first, last, name, uuid = *arr
        uuid = uuid.gsub(/-/, '').upcase
        @images << BinaryImage.new(first.to_i(16), last.to_i(16), name, uuid_to_file[uuid]) if uuid_to_file.member? uuid
      end
    end
  end

  def lookup(addr)
    @images.each do |image|
      next unless image.first <= addr && addr < image.last
      res = %x{xcrun atos 2>/dev/null -o #{image.file} -l 0x#{image.first.to_s(16)} 0x#{addr.to_s(16)}}.chomp
      res.gsub!(/ \(in #{File.basename image.file}\)/, '')
      return res
    end
    nil
  end
end

report = STDIN.read

process = $1 if report =~ /^Process:\s+(\w+)\s+\[\d+\]$/
version = $1 if report =~ /^Version:\s+(\S+) \(.*\)$/

dsym_dir = find_dsym(process, version)
abort "No dsym information for #{process} #{version}." if dsym_dir.nil?

images = BinaryImages.new(Dir["#{dsym_dir}/*"], report)

symbolicated = report.gsub(/^(Thread \d+(?: Crashed)?:.*)\n(?m:(.*?))\n\n/) do
  threadName, stackDump = $1, $2

  stackDump = stackDump.gsub(/^(.*?\s)(0x[[:xdigit:]]+)\s(.*)$/) do
    left, addr, right = $1, $2, $3
    if info = images.lookup(addr.to_i(16))
      right = info
    end
    "#{left}#{addr} #{right}"
  end

  "#{threadName}\n#{stackDump}\n\n"
end

puts symbolicated
