Pathname: add Mach-O module

The MachO module contains methods for learning about Mach-O binaries,
and can be used where one might normally shell out to file(1).

Signed-off-by: Jack Nagel <jacknagel@gmail.com>
This commit is contained in:
Jack Nagel 2012-05-25 23:44:11 -05:00
parent b6e0dfed23
commit a786178382
7 changed files with 240 additions and 0 deletions

View File

@ -1,8 +1,11 @@
require 'pathname'
require 'bottles'
require 'mach'
# we enhance pathname to make our code more readable
class Pathname
include MachO
def install *sources
results = []
sources.each do |src|
@ -268,6 +271,10 @@ class Pathname
end
end
def text_executable?
%r[#!\s*(/.+)+] === open('r') { |f| f.readline }
end
def incremental_hash(hasher)
incr_hash = hasher.new
self.open('r') do |f|

93
Library/Homebrew/mach.rb Normal file
View File

@ -0,0 +1,93 @@
module MachO
# Mach-O binary methods, see:
# /usr/include/mach-o/loader.h
# /usr/include/mach-o/fat.h
def mach_data
@mach_data ||= begin
offsets = []
mach_data = []
header = read(8).unpack("N2")
case header[0]
when 0xcafebabe # universal
header[1].times do |i|
# header[1] is the number of struct fat_arch in the file.
# Each struct fat_arch is 20 bytes, and the 'offset' member
# begins 8 bytes into the struct, with an additional 8 byte
# offset due to the struct fat_header at the beginning of
# the file.
offsets << read(4, 20*i + 16).unpack("N")[0]
end
when 0xcefaedfe, 0xcffaedfe, 0xfeedface, 0xfeedfacf # Single arch
offsets << 0
else
raise "Not a Mach-O binary."
end
offsets.each do |offset|
arch = case read(8, offset).unpack("N2")
when [0xcefaedfe, 0x07000000] then :i386
when [0xcffaedfe, 0x07000001] then :x86_64
when [0xfeedface, 0x00000012] then :ppc7400
when [0xfeedfacf, 0x01000012] then :ppc64
else :dunno
end
type = case read(4, offset + 12).unpack("N")[0]
when 0x00000002, 0x02000000 then :executable
when 0x00000006, 0x06000000 then :dylib
else :dunno
end
mach_data << { :arch => arch, :type => type }
end
mach_data
rescue
# read() will raise if it sees EOF, which should only happen if the
# file is < 8 bytes. Otherwise, we raise if the file is not a Mach-O
# binary. In both cases, we want to return an empty array.
[]
end
end
def archs
mach_data.map{ |m| m.fetch :arch }
end
def arch
case archs.length
when 0 then :dunno
when 1 then archs.first
else :universal
end
end
def universal?
arch == :universal
end
def i386?
arch == :i386
end
def x86_64?
arch == :x86_64
end
def ppc7400?
arch == :ppc7400
end
def ppc64?
arch == :ppc64
end
def dylib?
mach_data.map{ |m| m.fetch :type }.include? :dylib
end
def mach_o_executable?
mach_data.map{ |m| m.fetch :type }.include? :executable
end
end

BIN
Library/Homebrew/test/mach/a.out Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,140 @@
require 'testing_env'
require 'extend/ARGV' # needs to be after test/unit to avoid conflict with OptionsParser
ARGV.extend(HomebrewArgvExtension)
class MachOPathnameTests < Test::Unit::TestCase
def test_fat_dylib
pn = Pathname.new("#{TEST_FOLDER}/mach/fat.dylib")
assert pn.universal?
assert !pn.i386?
assert !pn.x86_64?
assert !pn.ppc7400?
assert !pn.ppc64?
assert pn.dylib?
assert !pn.mach_o_executable?
assert !pn.text_executable?
assert pn.arch == :universal
assert_match /Mach-O (64-bit )?dynamically linked shared library/,
`/usr/bin/file -h '#{pn}'`.chomp
end
def test_i386_dylib
pn = Pathname.new("#{TEST_FOLDER}/mach/i386.dylib")
assert !pn.universal?
assert pn.i386?
assert !pn.x86_64?
assert !pn.ppc7400?
assert !pn.ppc64?
assert pn.dylib?
assert !pn.mach_o_executable?
assert !pn.text_executable?
assert_match /Mach-O (64-bit )?dynamically linked shared library/,
`/usr/bin/file -h '#{pn}'`.chomp
end
def test_x86_64_dylib
pn = Pathname.new("#{TEST_FOLDER}/mach/x86_64.dylib")
assert !pn.universal?
assert !pn.i386?
assert pn.x86_64?
assert !pn.ppc7400?
assert !pn.ppc64?
assert pn.dylib?
assert !pn.mach_o_executable?
assert !pn.text_executable?
assert_match /Mach-O (64-bit )?dynamically linked shared library/,
`/usr/bin/file -h '#{pn}'`.chomp
end
def test_mach_o_executable
pn = Pathname.new("#{TEST_FOLDER}/mach/a.out")
assert pn.universal?
assert !pn.i386?
assert !pn.x86_64?
assert !pn.ppc7400?
assert !pn.ppc64?
assert !pn.dylib?
assert pn.mach_o_executable?
assert !pn.text_executable?
assert_match /Mach-O (64-bit )?executable/,
`/usr/bin/file -h '#{pn}'`.chomp
end
def test_non_mach_o
pn = Pathname.new("#{TEST_FOLDER}/tarballs/testball-0.1.tbz")
assert !pn.universal?
assert !pn.i386?
assert !pn.x86_64?
assert !pn.ppc7400?
assert !pn.ppc64?
assert !pn.dylib?
assert !pn.mach_o_executable?
assert !pn.text_executable?
assert pn.arch == :dunno
assert_no_match /Mach-O (64-bit )?dynamically linked shared library/,
`/usr/bin/file -h '#{pn}'`.chomp
assert_no_match /Mach-O [^ ]* ?executable/,
`/usr/bin/file -h '#{pn}'`.chomp
end
end
class TextExecutableTests < Test::Unit::TestCase
TMPDIR = HOMEBREW_PREFIX/'tmp'
def setup
FileUtils.mkdir_p TMPDIR
end
def test_simple_shebang
pn = Pathname.new('foo')
pn.write '#!/bin/sh'
assert !pn.universal?
assert !pn.i386?
assert !pn.x86_64?
assert !pn.ppc7400?
assert !pn.ppc64?
assert !pn.dylib?
assert !pn.mach_o_executable?
assert pn.text_executable?
assert_equal [], pn.archs
assert pn.arch == :dunno
assert_match /text executable/, `/usr/bin/file -h '#{pn}'`.chomp
end
def test_shebang_with_options
pn = Pathname.new('bar')
pn.write '#! /usr/bin/perl -w'
assert !pn.universal?
assert !pn.i386?
assert !pn.x86_64?
assert !pn.ppc7400?
assert !pn.ppc64?
assert !pn.dylib?
assert !pn.mach_o_executable?
assert pn.text_executable?
assert_equal [], pn.archs
assert pn.arch == :dunno
assert_match /text executable/, `/usr/bin/file -h '#{pn}'`.chomp
end
def test_malformed_shebang
pn = Pathname.new('baz')
pn.write '#! '
assert !pn.universal?
assert !pn.i386?
assert !pn.x86_64?
assert !pn.ppc7400?
assert !pn.ppc64?
assert !pn.dylib?
assert !pn.mach_o_executable?
assert !pn.text_executable?
assert_equal [], pn.archs
assert pn.arch == :dunno
assert_match /text executable/, `/usr/bin/file -h '#{pn}'`.chomp
end
def teardown
TMPDIR.rmtree
end
end