Home | History | Annotate | Line # | Download | only in hmaptool
      1 #!/usr/bin/env python
      2 from __future__ import absolute_import, division, print_function
      3 
      4 import json
      5 import optparse
      6 import os
      7 import struct
      8 import sys
      9 
     10 ###
     11 
     12 k_header_magic_LE = 'pamh'
     13 k_header_magic_BE = 'hmap'
     14 
     15 def hmap_hash(str):
     16     """hash(str) -> int
     17 
     18     Apply the "well-known" headermap hash function.
     19     """
     20 
     21     return sum((ord(c.lower()) * 13
     22                 for c in str), 0)
     23 
     24 class HeaderMap(object):
     25     @staticmethod
     26     def frompath(path):
     27         with open(path, 'rb') as f:
     28             magic = f.read(4)
     29             if magic == k_header_magic_LE:
     30                 endian_code = '<'
     31             elif magic == k_header_magic_BE:
     32                 endian_code = '>'
     33             else:
     34                 raise SystemExit("error: %s: not a headermap" % (
     35                         path,))
     36 
     37             # Read the header information.
     38             header_fmt = endian_code + 'HHIIII'
     39             header_size = struct.calcsize(header_fmt)
     40             data = f.read(header_size)
     41             if len(data) != header_size:
     42                 raise SystemExit("error: %s: truncated headermap header" % (
     43                         path,))
     44 
     45             (version, reserved, strtable_offset, num_entries,
     46              num_buckets, max_value_len) = struct.unpack(header_fmt, data)
     47 
     48             if version != 1:
     49                 raise SystemExit("error: %s: unknown headermap version: %r" % (
     50                         path, version))
     51             if reserved != 0:
     52                 raise SystemExit("error: %s: invalid reserved value in header" % (
     53                         path,))
     54 
     55             # The number of buckets must be a power of two.
     56             if num_buckets == 0 or (num_buckets & num_buckets - 1) != 0:
     57                 raise SystemExit("error: %s: invalid number of buckets" % (
     58                         path,))
     59 
     60             # Read all of the buckets.
     61             bucket_fmt = endian_code + 'III'
     62             bucket_size = struct.calcsize(bucket_fmt)
     63             buckets_data = f.read(num_buckets * bucket_size)
     64             if len(buckets_data) != num_buckets * bucket_size:
     65                 raise SystemExit("error: %s: truncated headermap buckets" % (
     66                         path,))
     67             buckets = [struct.unpack(bucket_fmt,
     68                                      buckets_data[i*bucket_size:(i+1)*bucket_size])
     69                        for i in range(num_buckets)]
     70 
     71             # Read the string table; the format doesn't explicitly communicate the
     72             # size of the string table (which is dumb), so assume it is the rest of
     73             # the file.
     74             f.seek(0, 2)
     75             strtable_size = f.tell() - strtable_offset
     76             f.seek(strtable_offset)
     77 
     78             if strtable_size == 0:
     79                 raise SystemExit("error: %s: unable to read zero-sized string table"%(
     80                         path,))
     81             strtable = f.read(strtable_size)
     82 
     83             if len(strtable) != strtable_size:
     84                 raise SystemExit("error: %s: unable to read complete string table"%(
     85                         path,))
     86             if strtable[-1] != '\0':
     87                 raise SystemExit("error: %s: invalid string table in headermap" % (
     88                         path,))
     89 
     90             return HeaderMap(num_entries, buckets, strtable)
     91 
     92     def __init__(self, num_entries, buckets, strtable):
     93         self.num_entries = num_entries
     94         self.buckets = buckets
     95         self.strtable = strtable
     96 
     97     def get_string(self, idx):
     98         if idx >= len(self.strtable):
     99             raise SystemExit("error: %s: invalid string index" % (
    100                     path,))
    101         end_idx = self.strtable.index('\0', idx)
    102         return self.strtable[idx:end_idx]
    103 
    104     @property
    105     def mappings(self):
    106         for key_idx,prefix_idx,suffix_idx in self.buckets:
    107             if key_idx == 0:
    108                 continue
    109             yield (self.get_string(key_idx),
    110                    self.get_string(prefix_idx) + self.get_string(suffix_idx))
    111 
    112 ###
    113 
    114 def action_dump(name, args):
    115     "dump a headermap file"
    116 
    117     parser = optparse.OptionParser("%%prog %s [options] <headermap path>" % (
    118             name,))
    119     parser.add_option("-v", "--verbose", dest="verbose",
    120                       help="show more verbose output [%default]",
    121                       action="store_true", default=False)
    122     (opts, args) = parser.parse_args(args)
    123 
    124     if len(args) != 1:
    125         parser.error("invalid number of arguments")
    126 
    127     path, = args
    128 
    129     hmap = HeaderMap.frompath(path)
    130 
    131     # Dump all of the buckets.
    132     print ('Header Map: %s' % (path,))
    133     if opts.verbose:
    134         print ('headermap: %r' % (path,))
    135         print ('  num entries: %d' % (hmap.num_entries,))
    136         print ('  num buckets: %d' % (len(hmap.buckets),))
    137         print ('  string table size: %d' % (len(hmap.strtable),))
    138         for i,bucket in enumerate(hmap.buckets):
    139             key_idx,prefix_idx,suffix_idx = bucket
    140 
    141             if key_idx == 0:
    142                 continue
    143 
    144             # Get the strings.
    145             key = hmap.get_string(key_idx)
    146             prefix = hmap.get_string(prefix_idx)
    147             suffix = hmap.get_string(suffix_idx)
    148 
    149             print ("  bucket[%d]: %r -> (%r, %r) -- %d" % (
    150                 i, key, prefix, suffix, (hmap_hash(key) & (len(hmap.buckets) - 1))))
    151     else:
    152         mappings = sorted(hmap.mappings)
    153         for key,value in mappings:
    154             print ("%s -> %s" % (key, value))
    155     print ()
    156 
    157 def next_power_of_two(value):
    158     if value < 0:
    159         raise ArgumentError
    160     return 1 if value == 0 else 2**(value - 1).bit_length()
    161 
    162 def action_write(name, args):
    163     "write a headermap file from a JSON definition"
    164 
    165     parser = optparse.OptionParser("%%prog %s [options] <input path> <output path>" % (
    166             name,))
    167     (opts, args) = parser.parse_args(args)
    168 
    169     if len(args) != 2:
    170         parser.error("invalid number of arguments")
    171 
    172     input_path,output_path = args
    173 
    174     with open(input_path, "r") as f:
    175         input_data = json.load(f)
    176 
    177     # Compute the headermap contents, we make a table that is 1/3 full.
    178     mappings = input_data['mappings']
    179     num_buckets = next_power_of_two(len(mappings) * 3)
    180 
    181     table = [(0, 0, 0)
    182              for i in range(num_buckets)]
    183     max_value_len = 0
    184     strtable = "\0"
    185     for key,value in mappings.items():
    186         if not isinstance(key, str):
    187             key = key.decode('utf-8')
    188         if not isinstance(value, str):
    189             value = value.decode('utf-8')
    190         max_value_len = max(max_value_len, len(value))
    191 
    192         key_idx = len(strtable)
    193         strtable += key + '\0'
    194         prefix = os.path.dirname(value) + '/'
    195         suffix = os.path.basename(value)
    196         prefix_idx = len(strtable)
    197         strtable += prefix + '\0'
    198         suffix_idx = len(strtable)
    199         strtable += suffix + '\0'
    200 
    201         hash = hmap_hash(key)
    202         for i in range(num_buckets):
    203             idx = (hash + i) % num_buckets
    204             if table[idx][0] == 0:
    205                 table[idx] = (key_idx, prefix_idx, suffix_idx)
    206                 break
    207         else:
    208             raise RuntimeError
    209 
    210     endian_code = '<'
    211     magic = k_header_magic_LE
    212     magic_size = 4
    213     header_fmt = endian_code + 'HHIIII'
    214     header_size = struct.calcsize(header_fmt)
    215     bucket_fmt = endian_code + 'III'
    216     bucket_size = struct.calcsize(bucket_fmt)
    217     strtable_offset = magic_size + header_size + num_buckets * bucket_size
    218     header = (1, 0, strtable_offset, len(mappings),
    219               num_buckets, max_value_len)
    220 
    221     # Write out the headermap.
    222     with open(output_path, 'wb') as f:
    223         f.write(magic.encode())
    224         f.write(struct.pack(header_fmt, *header))
    225         for bucket in table:
    226             f.write(struct.pack(bucket_fmt, *bucket))
    227         f.write(strtable.encode())
    228 
    229 def action_tovfs(name, args):
    230     "convert a headermap to a VFS layout"
    231 
    232     parser = optparse.OptionParser("%%prog %s [options] <headermap path>" % (
    233             name,))
    234     parser.add_option("", "--build-path", dest="build_path",
    235                       help="build path prefix",
    236                       action="store", type=str)
    237     (opts, args) = parser.parse_args(args)
    238 
    239     if len(args) != 2:
    240         parser.error("invalid number of arguments")
    241     if opts.build_path is None:
    242         parser.error("--build-path is required")
    243 
    244     input_path,output_path = args
    245 
    246     hmap = HeaderMap.frompath(input_path)
    247 
    248     # Create the table for all the objects.
    249     vfs = {}
    250     vfs['version'] = 0
    251     build_dir_contents = []
    252     vfs['roots'] = [{
    253             'name' : opts.build_path,
    254             'type' : 'directory',
    255             'contents' : build_dir_contents }]
    256 
    257     # We assume we are mapping framework paths, so a key of "Foo/Bar.h" maps to
    258     # "<build path>/Foo.framework/Headers/Bar.h".
    259     for key,value in hmap.mappings:
    260         # If this isn't a framework style mapping, ignore it.
    261         components = key.split('/')
    262         if len(components) != 2:
    263             continue
    264         framework_name,header_name = components
    265         build_dir_contents.append({
    266                 'name' : '%s.framework/Headers/%s' % (framework_name,
    267                                                       header_name),
    268                 'type' : 'file',
    269                 'external-contents' : value })
    270 
    271     with open(output_path, 'w') as f:
    272         json.dump(vfs, f, indent=2)
    273 
    274 commands = dict((name[7:].replace("_","-"), f)
    275                 for name,f in locals().items()
    276                 if name.startswith('action_'))
    277 
    278 def usage():
    279     print ("Usage: %s command [options]" % (
    280         os.path.basename(sys.argv[0])), file=sys.stderr)
    281     print (file=sys.stderr)
    282     print ("Available commands:", file=sys.stderr)
    283     cmds_width = max(map(len, commands))
    284     for name,func in sorted(commands.items()):
    285         print ("  %-*s - %s" % (cmds_width, name, func.__doc__), file=sys.stderr)
    286     sys.exit(1)
    287 
    288 def main():
    289     if len(sys.argv) < 2 or sys.argv[1] not in commands:
    290         usage()
    291 
    292     cmd = sys.argv[1]
    293     commands[cmd](cmd, sys.argv[2:])
    294 
    295 if __name__ == '__main__':
    296     main()
    297