Skip to content
executable file 276 lines (215 sloc) 6.33 KB
#!/usr/bin/env ruby
require "rubygems"
root_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
if File.directory?(File.join(root_dir, '.git'))
Dir.chdir(root_dir) do |path|
require 'bundler'
begin
Bundler.setup(:default)
rescue Bundler::BundlerError => e
warn e.message
warn "Run `bundle install` to install missing gems"
exit e.status_code
end
end
end
lib_dir = File.join(root_dir,'lib')
$LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
require "trollop"
require "furnace-avm2"
require "parallel"
require "thread"
include Furnace
DEBUG_INFO = %w(names funids)
opts = Trollop::options do
version "furnace-avm2 #{AVM2::VERSION} decompiler"
banner <<-EOS
furnace-avm2-decompiler is a decompiler for ActionScript3 bytecode.
Debugging information classes: #{DEBUG_INFO.join(", ")}.
Usage: #{$0} [options]
EOS
opt :input, "Input file", :type => :string, :required => true
opt :verbose, "Be verbose", :default => false
opt :quiet, "Be quiet", :default => false
opt :debug, "Show debugging information for <s+>.", :type => :strings, :short => '-D'
opt :only, "Only operate on classes <s+>", :type => :strings, :short => '-O'
opt :except, "Operate on all classes except <s+>", :type => :strings, :short => '-E'
opt :grep, "Search <s> (regexp) in class names", :type => :string, :short => '-G'
opt :no_output, "Don't write out any code", :default => false
opt :fix_names, "Remove invalid characters from names", :default => true, :short => '-q'
opt :decompile, "Write ActionScript 3 code", :type => :boolean, :short => '-d'
opt :destructurize, "Write internal token structure", :type => :boolean, :short => '-s'
opt :profile, "Write KCachegrind profiling data", :type => :string, :short => '-p'
opt :wait, "Wait for a tool like profiler to be attached", :type => :boolean, :short => '-w'
opt :loop, "Perform the decompiling in loop for profiling", :default => false
opt :threads, "Override thread count", :type => :integer
end
Trollop::die "Stray arguments: #{ARGV}" unless ARGV.empty?
if opts[:profile]
require 'ruby-prof'
RubyProf.start
RubyProf.pause
end
decompile_options = {}
(opts[:debug] || []).each do |opt|
Trollop::die "Unknown debug option #{opt}." unless DEBUG_INFO.include? opt
decompile_options[:"debug_#{opt}"] = true
end
shound_skip = ->(obj) {
(opts[:except] && opts[:except].include?(obj.to_s)) ||
(opts[:only] && !opts[:only].include?(obj.to_s))
}
$stderr.puts "Reading input data..."
abc = nil
File.open(opts[:input]) do |file|
abc = AVM2::ABC::File.new
abc.read(file)
end
if opts[:grep]
regexp = Regexp.new(opts[:grep], Regexp::IGNORECASE)
abc.instances.each do |inst|
if inst.name.to_s =~ regexp
if inst.interface?
print "Iface "
else
print "Class "
end
print inst.name.to_s.ljust(30)
if inst.super_name
print " extends #{inst.super_name.to_s}"
end
puts
end
end
exit
end
if opts[:fix_names]
abc.fix_names!
end
global_slots = {}
abc.scripts.each do |script|
(script.slot_traits + script.const_traits).each do |trait|
next if trait.idx == 0
global_slots[trait.idx] = trait
end
end
decompile_options[:global_slots] = global_slots
if opts[:wait]
puts "Press Enter to continue."
gets
end
trap("QUIT") {
puts "Backtrace:"
puts caller
}
print_one_stat = lambda do |color, count|
str = ":" * (count / 2)
str << "." * (count % 2)
$stderr.print "\e[0;#{color}m#{str}\e[0m"
end
print_stat = lambda do |stat|
print_one_stat.("1;31", stat[:failed])
print_one_stat.("32", stat[:success])
print_one_stat.("1;33", stat[:partial])
end
workqueue = abc.instances + abc.scripts
total_size = workqueue.size
$stderr.puts "Found #{total_size} classes and packages."
process = lambda do |what|
if what.is_a? AVM2::ABC::InstanceInfo
name = what.name
ns = name.ns
else # ScriptInfo
if what.has_name?
name = what.package_name.ns
ns = name.to_s.sub(/(^|\.)[^.]+$/, '')
else
index = abc.scripts.index(what)
name = "__global_name_#{index}"
ns = "__global_ns_#{index}"
end
end
next if shound_skip.(name)
options = decompile_options.merge(
stat: {
total: 0,
success: 0,
partial: 0,
failed: 0,
}
)
if opts[:profile]
RubyProf.resume
source = what.decompile(options)
RubyProf.pause
else
source = what.decompile(options)
end
stat = options[:stat]
print_stat.(stat) unless opts[:quiet]
if source && source.children.any?
{
stat: stat,
ns: ns.to_s,
source: source.to_text,
}
end
end
threads = opts[:threads] || Parallel.processor_count
if defined?(JRUBY_VERSION)
parallel_options = { in_threads: threads }
else
parallel_options = { in_processes: threads }
end
Random.srand 0
workqueue.shuffle!
stat = roots = nil
loop {
start_time = Time.now
roots = {}
stat = {
total: 0,
success: 0,
partial: 0,
failed: 0,
}
Parallel.map(workqueue, parallel_options, &process).each do |result|
if result
stat.merge!(result[:stat]) do |key, global, local|
global + local
end
ns = result[:ns]
roots[ns] ||= []
roots[ns] << result[:source]
end
end
end_time = Time.now
$stderr.puts "\nTime taken: #{"%.2f" % (end_time - start_time)}s"
break unless opts[:loop]
}
if stat[:total] > 0
$stderr.puts
{ "Decompiled" => :success,
"Partially decompiled" => :partial,
"Failed" => :failed,
}.each do |facet_name, facet|
$stderr.puts "#{facet_name.rjust(21)}: " \
"#{stat[facet]}/#{stat[:total]} " \
"(#{(stat[facet].to_f * 100 / stat[:total]).to_i}%)"
end
else
$stderr.puts "No methods were processed."
end
if opts[:profile]
prof_result = RubyProf.stop
File.open(opts[:profile], 'w') do |f|
printer = RubyProf::CallTreePrinter.new(prof_result)
printer.print(f)
end
end
unless opts[:no_output]
roots.values.flatten.each do |code|
puts code.gsub(/ +$/, '')
puts
end
end
Jump to Line
Something went wrong with that request. Please try again.