macho.py 4.56 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# -*- coding: utf-8 -*-
#
# diffoscope: in-depth comparison of files, archives, and directories
#
# Copyright © 2014-2015 Jérémy Bobbio <lunar@debian.org>
# Copyright © 2015 Clemens Lang <cal@macports.org>
#
# diffoscope is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# diffoscope is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
19
# along with diffoscope.  If not, see <https://www.gnu.org/licenses/>.
20 21 22

import re
import subprocess
Chris Lamb's avatar
Chris Lamb committed
23

24
from diffoscope.tools import tool_required
25
from diffoscope.difference import Difference
26

27
from .utils.file import File
28
from .utils.command import Command
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45


class Otool(Command):
    def __init__(self, path, arch, *args, **kwargs):
        self._path = path
        self._arch = arch
        super().__init__(path, *args, **kwargs)

    @tool_required('otool')
    def cmdline(self):
        return ['otool'] + self.otool_options() + [self.path]

    def otool_options(self):
        return ['-arch', self._arch]

    def filter(self, line):
        try:
46 47
            # Strip the filename itself, it's in the first line on its own,
            # terminated by a colon
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
            if line and line.decode('utf-8').strip() == self._path + ':':
                return b""
            return line
        except UnicodeDecodeError:
            return line


class OtoolHeaders(Otool):
    def otool_options(self):
        return super().otool_options() + ['-h']


class OtoolLibraries(Otool):
    def otool_options(self):
        return super().otool_options() + ['-L']


class OtoolDisassemble(Otool):
    def otool_options(self):
        return super().otool_options() + ['-tdvV']


70 71 72 73 74
class OtoolDisassembleInternal(Otool):
    def otool_options(self):
        return super().otool_options() + ['-tdvVQ']


75
class MachoFile(File):
76
    FILE_TYPE_RE = re.compile(r'^Mach-O ')
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    RE_EXTRACT_ARCHS = re.compile(r'^(?:Architectures in the fat file: .* are|Non-fat file: .* is architecture): (.*)$')

    @staticmethod
    @tool_required('lipo')
    def get_arch_from_macho(path):
        lipo_output = subprocess.check_output(['lipo', '-info', path]).decode('utf-8')
        lipo_match = MachoFile.RE_EXTRACT_ARCHS.match(lipo_output)
        if lipo_match is None:
            raise ValueError('lipo -info on Mach-O file %s did not produce expected output. Output was: %s' % path, lipo_output)
        return lipo_match.group(1).split()

    def compare_details(self, other, source=None):
        differences = []
        # Check for fat binaries, trigger a difference if the architectures differ
        my_archs = MachoFile.get_arch_from_macho(self.path)
        other_archs = MachoFile.get_arch_from_macho(other.path)

        differences.append(Difference.from_text('\n'.join(my_archs),
                                                '\n'.join(other_archs),
                                                self.name, other.name, source='architectures'))

        # Compare common architectures for differences
        for common_arch in set(my_archs) & set(other_archs):
            differences.append(Difference.from_command(OtoolHeaders, self.path, other.path, command_args=[common_arch],
Rainer Müller's avatar
Rainer Müller committed
101
                                                       comment="Mach-O headers for architecture %s" % common_arch))
102 103
            differences.append(Difference.from_command(OtoolLibraries, self.path, other.path, command_args=[common_arch],
                                                       comment="Mach-O load commands for architecture %s" % common_arch))
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122

            x = Difference.from_command(
                OtoolDisassemble,
                self.path,
                other.path,
                command_args=[common_arch],
                comment="Code for architecture %s" % common_arch,
            )
            differences.append(x)

            # If the LLVM disassembler does not work, try the internal one.
            if x is None:
                differences.append(Difference.from_command(
                    OtoolDisassembleInternal,
                    self.path,
                    other.path,
                    command_args=[common_arch],
                    comment="Code for architecture %s (internal disassembler)" % common_arch,
                ))
123 124

        return differences