#!/usr/bin/env python3 from __future__ import absolute_import, division, print_function from ctypes import ArgumentError import json import optparse import os import struct import sys ### k_header_magic_LE = b'pamh' k_header_magic_BE = b'hmap' def hmap_hash(str): """hash(str) -> int Apply the "well-known" headermap hash function. """ return sum((ord(c.lower()) * 13 for c in str), 0) class HeaderMap(object): @staticmethod def frompath(path): with open(path, 'rb') as f: magic = f.read(4) if magic == k_header_magic_LE: endian_code = '<' elif magic == k_header_magic_BE: endian_code = '>' else: raise SystemExit("error: %s: not a headermap" % ( path,)) # Read the header information. header_fmt = endian_code + 'HHIIII' header_size = struct.calcsize(header_fmt) data = f.read(header_size) if len(data) != header_size: raise SystemExit("error: %s: truncated headermap header" % ( path,)) (version, reserved, strtable_offset, num_entries, num_buckets, _) = struct.unpack(header_fmt, data) if version != 1: raise SystemExit("error: %s: unknown headermap version: %r" % ( path, version)) if reserved != 0: raise SystemExit("error: %s: invalid reserved value in header" % ( path,)) # The number of buckets must be a power of two. if num_buckets == 0 or (num_buckets & num_buckets - 1) != 0: raise SystemExit("error: %s: invalid number of buckets" % ( path,)) # Read all of the buckets. bucket_fmt = endian_code + 'III' bucket_size = struct.calcsize(bucket_fmt) buckets_data = f.read(num_buckets * bucket_size) if len(buckets_data) != num_buckets * bucket_size: raise SystemExit("error: %s: truncated headermap buckets" % ( path,)) buckets = [struct.unpack(bucket_fmt, buckets_data[i*bucket_size:(i+1)*bucket_size]) for i in range(num_buckets)] # Read the string table; the format doesn't explicitly communicate the # size of the string table (which is dumb), so assume it is the rest of # the file. f.seek(0, 2) strtable_size = f.tell() - strtable_offset f.seek(strtable_offset) if strtable_size == 0: raise SystemExit("error: %s: unable to read zero-sized string table"%( path,)) strtable = f.read(strtable_size) if len(strtable) != strtable_size: raise SystemExit("error: %s: unable to read complete string table"%( path,)) if strtable[-1] != 0: raise SystemExit("error: %s: invalid string table in headermap" % ( path,)) return HeaderMap(num_entries, buckets, strtable) def __init__(self, num_entries, buckets, strtable): self.num_entries = num_entries self.buckets = buckets self.strtable = strtable def get_string(self, idx): if idx >= len(self.strtable): raise SystemExit("error: %s: invalid string index" % ( idx,)) end_idx = self.strtable.index(0, idx) return self.strtable[idx:end_idx].decode() @property def mappings(self): for key_idx,prefix_idx,suffix_idx in self.buckets: if key_idx == 0: continue yield (self.get_string(key_idx), self.get_string(prefix_idx) + self.get_string(suffix_idx)) ### def action_dump(name, args): "dump a headermap file" parser = optparse.OptionParser("%%prog %s [options] <headermap path>" % ( name,)) parser.add_option("-v", "--verbose", dest="verbose", help="show more verbose output [%default]", action="store_true", default=False) (opts, args) = parser.parse_args(args) if len(args) != 1: parser.error("invalid number of arguments") path, = args hmap = HeaderMap.frompath(path) # Dump all of the buckets. print ('Header Map: %s' % (path,)) if opts.verbose: print ('headermap: %r' % (path,)) print (' num entries: %d' % (hmap.num_entries,)) print (' num buckets: %d' % (len(hmap.buckets),)) print (' string table size: %d' % (len(hmap.strtable),)) for i,bucket in enumerate(hmap.buckets): key_idx,prefix_idx,suffix_idx = bucket if key_idx == 0: continue # Get the strings. key = hmap.get_string(key_idx) prefix = hmap.get_string(prefix_idx) suffix = hmap.get_string(suffix_idx) print (" bucket[%d]: %r -> (%r, %r) -- %d" % ( i, key, prefix, suffix, (hmap_hash(key) & (len(hmap.buckets) - 1)))) else: mappings = sorted(hmap.mappings) for key,value in mappings: print ("%s -> %s" % (key, value)) print () def next_power_of_two(value): if value < 0: raise ArgumentError return 1 if value == 0 else 2**(value - 1).bit_length() def action_write(name, args): "write a headermap file from a JSON definition" parser = optparse.OptionParser("%%prog %s [options] <input path> <output path>" % ( name,)) (opts, args) = parser.parse_args(args) if len(args) != 2: parser.error("invalid number of arguments") input_path,output_path = args with open(input_path, "r") as f: input_data = json.load(f) # Compute the headermap contents, we make a table that is 1/3 full. mappings = input_data['mappings'] num_buckets = next_power_of_two(len(mappings) * 3) table = [(0, 0, 0) for i in range(num_buckets)] max_value_len = 0 strtable = "\0" for key,value in mappings.items(): if not isinstance(key, str): key = key.decode('utf-8') if not isinstance(value, str): value = value.decode('utf-8') max_value_len = max(max_value_len, len(value)) key_idx = len(strtable) strtable += key + '\0' prefix = os.path.dirname(value) + '/' suffix = os.path.basename(value) prefix_idx = len(strtable) strtable += prefix + '\0' suffix_idx = len(strtable) strtable += suffix + '\0' hash = hmap_hash(key) for i in range(num_buckets): idx = (hash + i) % num_buckets if table[idx][0] == 0: table[idx] = (key_idx, prefix_idx, suffix_idx) break else: raise RuntimeError endian_code = '<' magic = k_header_magic_LE magic_size = 4 header_fmt = endian_code + 'HHIIII' header_size = struct.calcsize(header_fmt) bucket_fmt = endian_code + 'III' bucket_size = struct.calcsize(bucket_fmt) strtable_offset = magic_size + header_size + num_buckets * bucket_size header = (1, 0, strtable_offset, len(mappings), num_buckets, max_value_len) # Write out the headermap. with open(output_path, 'wb') as f: f.write(magic) f.write(struct.pack(header_fmt, *header)) for bucket in table: f.write(struct.pack(bucket_fmt, *bucket)) f.write(strtable.encode()) def action_tovfs(name, args): "convert a headermap to a VFS layout" parser = optparse.OptionParser("%%prog %s [options] <headermap path>" % ( name,)) parser.add_option("", "--build-path", dest="build_path", help="build path prefix", action="store", type=str) (opts, args) = parser.parse_args(args) if len(args) != 2: parser.error("invalid number of arguments") if opts.build_path is None: parser.error("--build-path is required") input_path,output_path = args hmap = HeaderMap.frompath(input_path) # Create the table for all the objects. vfs = {} vfs['version'] = 0 build_dir_contents = [] vfs['roots'] = [{ 'name' : opts.build_path, 'type' : 'directory', 'contents' : build_dir_contents }] # We assume we are mapping framework paths, so a key of "Foo/Bar.h" maps to # "<build path>/Foo.framework/Headers/Bar.h". for key,value in hmap.mappings: # If this isn't a framework style mapping, ignore it. components = key.split('/') if len(components) != 2: continue framework_name,header_name = components build_dir_contents.append({ 'name' : '%s.framework/Headers/%s' % (framework_name, header_name), 'type' : 'file', 'external-contents' : value }) with open(output_path, 'w') as f: json.dump(vfs, f, indent=2) commands = dict((name[7:].replace("_","-"), f) for name,f in locals().items() if name.startswith('action_')) def usage(): print ("Usage: %s command [options]" % ( os.path.basename(sys.argv[0])), file=sys.stderr) print (file=sys.stderr) print ("Available commands:", file=sys.stderr) cmds_width = max(map(len, commands)) for name,func in sorted(commands.items()): print (" %-*s - %s" % (cmds_width, name, func.__doc__), file=sys.stderr) sys.exit(1) def main(): if len(sys.argv) < 2 or sys.argv[1] not in commands: usage() cmd = sys.argv[1] commands[cmd](cmd, sys.argv[2:]) if __name__ == '__main__': main()