Skip to content

Instantly share code, notes, and snippets.

@vincentisambart
Last active May 3, 2019 22:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vincentisambart/842aff8ed96e7b0d9e0ecf04fa82b0e1 to your computer and use it in GitHub Desktop.
Save vincentisambart/842aff8ed96e7b0d9e0ecf04fa82b0e1 to your computer and use it in GitHub Desktop.
# This file is licensed under the MIT license.
#
# Copyright (c) 2017 Cookpad Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'zlib'
raise "Syntax: #{$0} file.xcactivitylog" unless ARGV.length == 1
file_path = ARGV[0]
raw_data = Zlib::GzipReader.open(file_path, encoding: Encoding::BINARY) { |gzip| gzip.read }
require 'strscan'
scanner = StringScanner.new(raw_data)
# StringScanner doesn't have a method to read a specific number of characters so add one.
def scanner.read(length)
string = peek(length)
self.pos += length
string
end
# First parse the SLF0 format as a series of tokens.
# Code used as a reference: https://gist.github.com/yiding/7a22deff33160c84b04e
raise 'Invalid format' unless scanner.scan(/SLF0/)
class_names = []
tokens = []
while !scanner.eos?
if scanner.scan(/([0-9]+)#/) # integer
tokens << { type: :int, value: scanner[1].to_i }
elsif scanner.scan(/([0-9]+)%/) # class name
length = scanner[1].to_i
name = scanner.read(length)
raise "Class name #{name} should not be present multiple times" if class_names.include?(name)
class_names << name.to_sym
elsif scanner.scan(/([0-9]+)@/) # object
class_index = scanner[1].to_i
raise "Unknown class reference #{class_index} - Known classes are #{class_names.join(', ')}" if class_index > class_names.length
tokens << { type: :object, class_name: class_names[class_index-1] }
elsif scanner.scan(/([0-9]+)"/) # string
length = scanner[1].to_i
string = scanner.read(length)
tokens << { type: :string, value: string }
elsif scanner.scan(/([0-9]+)\(/) # list
tokens << { type: :list, count: scanner[1].to_i }
elsif scanner.scan(/([a-f0-9]+)\^/) # double
hexadecimal = scanner[1]
# "cf4c80e55bc2bf41" -> ["cf", "4c", "80", "e5", "5b", "c2", "bf", "41"]
characters_grouped_by_2 = hexadecimal.each_char.each_slice(2).map(&:join)
# ["cf", "4c", "80", "e5", "5b", "c2", "bf", "41"] -> [207, 76, 128, 229, 91, 194, 191, 65]
bytes = characters_grouped_by_2.map { |hex| hex.to_i(16) }
# [207, 76, 128, 229, 91, 194, 191, 65] -> "\xCFL\x80\xE5[\xC2\xBFA" -> [532831205.501172] -> 532831205.501172
double = bytes.pack('C*').unpack('E').first
tokens << { type: :double, value: double }
elsif scanner.scan(/-/) # nil
tokens << { type: :nil }
else
raise "unknown data #{scanner.peek(30).inspect}"
end
end
# Then parse the tokens as objects, lists, ...
class TokenReader
def initialize(tokens)
@tokens = tokens.dup
# Xcode 8.3 -> log format version 7
# Xcode 9.0, 9.1 -> log format version 8
@log_format_version = read(:int)
raise "Unknown log format version #{log_format_version}" unless (7..8).include?(@log_format_version)
end
def tokens_left_count
@tokens.length
end
def read(expected_type, args = {})
token = @tokens.shift
return nil if token[:type] == :nil && args[:nullable]
raise "Expecting token of type #{expected_type.inspect} but got #{token.inspect}" if token[:type] != expected_type
case expected_type
when :list
expected_class_name = args[:class_name]
(1..token[:count]).map { read(:object, class_name: expected_class_name) }
when :object
expected_class_names = [args[:class_name]].flatten
class_name = token[:class_name]
raise "Expected an object of class #{expected_class_names.join('/')} but got an instance of #{class_name}" unless expected_class_names.include?(class_name)
fields = { class_name: class_name }
case class_name
when :IDEActivityLogSection
# Not really sure what the first field is, but it seems to be related to the domain type.
# 0 -> Domain Type Xcode.IDEActivityLogDomainType.BuildLog-Build
# 1 -> Domain Type Xcode.IDEActivityLogDomainType.target.product-type.*
# 2 -> Domain Type com.apple.dt.IDE.BuildLogSection
fields[:field1] = read(:int)
fields[:domain_type] = read(:string)
fields[:title] = read(:string)
fields[:signature] = read(:string)
fields[:time_started_recording] = read(:double)
fields[:time_stopped_recording] = read(:double)
fields[:subsections] = read(:list, nullable: true, class_name: :IDEActivityLogSection)
fields[:field8] = read(:string, nullable: true)
# The following field might be of one of 2 classes (one might be the parent of the other, or they might have a common parent)
fields[:messages] = read(:list, nullable: true, class_name: [:IDEClangDiagnosticActivityLogMessage, :IDEActivityLogMessage])
fields[:field10] = read(:int)
fields[:field11] = read(:int)
fields[:field12] = read(:int)
fields[:field13] = read(:string, nullable: true)
fields[:location] = read(:object, nullable: true, class_name: :DVTDocumentLocation)
fields[:field15] = read(:string, nullable: true)
fields[:field16] = read(:string)
fields[:result] = read(:string, nullable: true)
# The following field is not present in Xcode 8.3 (log format version 7)
fields[:field18] = read(:string, nullable: true) if @log_format_version >= 8
when :DVTDocumentLocation
fields[:url] = read(:string)
fields[:field2] = read(:double)
when :DVTTextDocumentLocation
fields[:url] = read(:string, nullable: true)
fields[:field2] = read(:double)
fields[:field3] = read(:int)
fields[:field4] = read(:int)
fields[:field5] = read(:int)
fields[:field6] = read(:int)
fields[:field7] = read(:int)
fields[:field8] = read(:int)
fields[:field9] = read(:int)
when :IDEActivityLogMessage
fields[:message] = read(:string)
fields[:field2] = read(:string, nullable: true)
fields[:field3] = read(:int)
fields[:field4] = read(:int)
fields[:field5] = read(:int)
fields[:field6] = read(:nil)
fields[:field7] = read(:int)
fields[:field8] = read(:string, nullable: true)
fields[:location] = read(:object, nullable: true, class_name: :DVTTextDocumentLocation)
fields[:field10] = read(:string, nullable: true)
fields[:field11] = read(:nil)
fields[:field12] = read(:nil)
when :IDEClangDiagnosticActivityLogMessage
fields[:message] = read(:string)
fields[:field2] = read(:nil)
fields[:field3] = read(:int)
fields[:field4] = read(:int)
fields[:field5] = read(:int)
fields[:field6] = read(:list, nullable: true, class_name: :IDEClangDiagnosticActivityLogMessage)
fields[:field7] = read(:int)
fields[:field8] = read(:string)
fields[:field9] = read(:object, nullable: true, class_name: :DVTTextDocumentLocation)
fields[:field10] = read(:string)
fields[:field11] = read(:list, nullable: true, class_name: :DVTTextDocumentLocation)
fields[:field12] = read(:nil)
else
raise "Unknown class name #{class_name}"
end
fields
else
token[:value]
end
end
end
reader = TokenReader.new(tokens)
log = reader.read(:object, class_name: :IDEActivityLogSection)
raise "#{reader.tokens_left_count} tokens left after the IDEActivityLogSection" if reader.tokens_left_count != 0
require 'pp'
pp log
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment