hmaptool 9.8 KB

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