123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- #!/usr/bin/env python3
- # Copyright 2017 The Chromium Authors. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """A tool for interacting with .pak files.
- For details on the pak file format, see:
- https://dev.chromium.org/developers/design-documents/linuxresourcesandlocalizedstrings
- """
- from __future__ import print_function
- import argparse
- import gzip
- import hashlib
- import os
- import shutil
- import subprocess
- import sys
- import tempfile
- _HERE_PATH = os.path.dirname(__file__)
- _SRC_PATH = os.path.normpath(os.path.join(_HERE_PATH, '..', '..'))
- sys.path.insert(0, os.path.join(_SRC_PATH, 'third_party', 'six', 'src'))
- import six
- from grit import constants
- from grit.format import data_pack
- _GZIP_HEADER = b'\x1f\x8b'
- def _RepackMain(args):
- output_info_filepath = args.output_pak_file + '.info'
- if args.compress:
- # If the file needs to be compressed, call RePack with a tempfile path,
- # then compress the tempfile to args.output_pak_file.
- temp_outfile = tempfile.NamedTemporaryFile()
- out_path = temp_outfile.name
- # Strip any non .pak extension from the .info output file path.
- splitext = os.path.splitext(args.output_pak_file)
- if splitext[1] != '.pak':
- output_info_filepath = splitext[0] + '.info'
- else:
- out_path = args.output_pak_file
- data_pack.RePack(out_path,
- args.input_pak_files,
- args.allowlist,
- args.suppress_removed_key_output,
- output_info_filepath=output_info_filepath)
- if args.compress:
- with open(args.output_pak_file, 'wb') as out:
- with gzip.GzipFile(filename='', mode='wb', fileobj=out, mtime=0) as outgz:
- shutil.copyfileobj(temp_outfile, outgz)
- def _MaybeDecompress(payload, brotli_path=None):
- if payload.startswith(_GZIP_HEADER):
- return gzip.decompress(payload)
- if payload.startswith(constants.BROTLI_CONST):
- shell = brotli_path is None
- brotli_path = brotli_path or 'brotli'
- # Header is 2 bytes, size is 6 bytes.
- payload = payload[8:]
- try:
- result = subprocess.run([brotli_path, '--decompress', '--stdout'],
- shell=shell,
- input=payload,
- stdout=subprocess.PIPE,
- check=True)
- # I don't know why with "sudo apt-get install brotli", files come out 4
- # bytes larger and the command doesn't fail.
- if len(result.stdout) == len(payload) + 4:
- sys.stderr.write('Brotli decompression failed. You likely need to use '
- 'the version of brotli built by Chrome '
- '(out/Release/clang_x64/brotli).\n')
- sys.exit(1)
- return result.stdout
- except subprocess.CalledProcessError as e:
- sys.stderr.write(str(e) + '\n')
- sys.exit(1)
- return payload
- def _ExtractMain(args):
- pak = data_pack.ReadDataPack(args.pak_file)
- if args.textual_id:
- info_dict = data_pack.ReadGrdInfo(args.pak_file)
- for resource_id, payload in pak.resources.items():
- filename = (
- info_dict[resource_id].textual_id
- if args.textual_id else str(resource_id))
- path = os.path.join(args.output_dir, filename)
- with open(path, 'wb') as f:
- if not args.raw:
- payload = _MaybeDecompress(payload, args.brotli)
- f.write(payload)
- def _CreateMain(args):
- pak = {}
- for name in os.listdir(args.input_dir):
- try:
- resource_id = int(name)
- except:
- continue
- filename = os.path.join(args.input_dir, name)
- if os.path.isfile(filename):
- with open(filename, 'rb') as f:
- pak[resource_id] = f.read()
- data_pack.WriteDataPack(pak, args.output_pak_file, data_pack.UTF8)
- def _PrintMain(args):
- pak = data_pack.ReadDataPack(args.pak_file)
- if args.textual_id:
- info_dict = data_pack.ReadGrdInfo(args.pak_file)
- output = args.output
- encoding = 'binary'
- if pak.encoding == 1:
- encoding = 'utf-8'
- elif pak.encoding == 2:
- encoding = 'utf-16'
- else:
- encoding = '?' + str(pak.encoding)
- output.write('version: {}\n'.format(pak.version))
- output.write('encoding: {}\n'.format(encoding))
- output.write('num_resources: {}\n'.format(len(pak.resources)))
- output.write('num_aliases: {}\n'.format(len(pak.aliases)))
- breakdown = ', '.join('{}: {}'.format(*x) for x in pak.sizes)
- output.write('total_size: {} ({})\n'.format(pak.sizes.total, breakdown))
- try_decode = args.decode and encoding.startswith('utf')
- # Print IDs in ascending order, since that's the order in which they appear in
- # the file (order is lost by Python dict).
- for resource_id in sorted(pak.resources):
- data = pak.resources[resource_id]
- canonical_id = pak.aliases.get(resource_id, resource_id)
- desc = '<data>'
- if try_decode:
- try:
- desc = six.text_type(data, encoding)
- if len(desc) > 60:
- desc = desc[:60] + '...'
- desc = desc.replace('\n', '\\n')
- except UnicodeDecodeError:
- pass
- sha1 = hashlib.sha1(data).hexdigest()[:10]
- if args.textual_id:
- textual_id = info_dict[resource_id].textual_id
- canonical_textual_id = info_dict[canonical_id].textual_id
- output.write(
- 'Entry(id={}, canonical_id={}, size={}, sha1={}): {}\n'.format(
- textual_id, canonical_textual_id, len(data), sha1,
- desc))
- else:
- output.write(
- 'Entry(id={}, canonical_id={}, size={}, sha1={}): {}\n'.format(
- resource_id, canonical_id, len(data), sha1, desc))
- def _ListMain(args):
- pak = data_pack.ReadDataPack(args.pak_file)
- if args.textual_id or args.path:
- info_dict = data_pack.ReadGrdInfo(args.pak_file)
- fmt = ''.join([
- '{id}', ' = {textual_id}' if args.textual_id else '',
- ' @ {path}' if args.path else '', '\n'
- ])
- for resource_id in sorted(pak.resources):
- item = info_dict[resource_id]
- args.output.write(
- fmt.format(textual_id=item.textual_id, id=item.id, path=item.path))
- else:
- for resource_id in sorted(pak.resources):
- args.output.write('%d\n' % resource_id)
- def main():
- parser = argparse.ArgumentParser(
- description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
- # Subparsers are required by default under Python 2. Python 3 changed to
- # not required, but didn't include a required option until 3.7. Setting
- # the required member works in all versions (and setting dest name).
- sub_parsers = parser.add_subparsers(dest='action')
- sub_parsers.required = True
- sub_parser = sub_parsers.add_parser('repack',
- help='Combines several .pak files into one.')
- sub_parser.add_argument('output_pak_file', help='File to create.')
- sub_parser.add_argument('input_pak_files', nargs='+',
- help='Input .pak files.')
- sub_parser.add_argument(
- '--allowlist',
- help='Path to a allowlist used to filter output pak file resource IDs.')
- sub_parser.add_argument(
- '--suppress-removed-key-output',
- action='store_true',
- help='Do not log which keys were removed by the allowlist.')
- sub_parser.add_argument('--compress', dest='compress', action='store_true',
- default=False, help='Compress output_pak_file using gzip.')
- sub_parser.set_defaults(func=_RepackMain)
- sub_parser = sub_parsers.add_parser('extract', help='Extracts pak file')
- sub_parser.add_argument('pak_file')
- sub_parser.add_argument('-o', '--output-dir', default='.',
- help='Directory to extract to.')
- sub_parser.add_argument('--raw',
- action='store_true',
- help='Do not decompress when extracting.')
- sub_parser.add_argument('--brotli',
- help='Path to brotli executable. Needed only to '
- 'decompress brotli-compressed entries. For a '
- 'chromium checkout, find in your output directory')
- sub_parser.add_argument(
- '-t',
- '--textual-id',
- action='store_true',
- help='Use textual resource ID (name) (from .info file) as filenames.')
- sub_parser.set_defaults(func=_ExtractMain)
- sub_parser = sub_parsers.add_parser('create',
- help='Creates pak file from extracted directory.')
- sub_parser.add_argument('output_pak_file', help='File to create.')
- sub_parser.add_argument('-i', '--input-dir', default='.',
- help='Directory to create from.')
- sub_parser.set_defaults(func=_CreateMain)
- sub_parser = sub_parsers.add_parser('print',
- help='Prints all pak IDs and contents. Useful for diffing.')
- sub_parser.add_argument('pak_file')
- sub_parser.add_argument('--output', type=argparse.FileType('w'),
- default=sys.stdout,
- help='The resource list path to write (default stdout)')
- sub_parser.add_argument('--no-decode', dest='decode', action='store_false',
- default=True, help='Do not print entry data.')
- sub_parser.add_argument(
- '-t',
- '--textual-id',
- action='store_true',
- help='Print textual ID (name) (from .info file) instead of the ID.')
- sub_parser.set_defaults(func=_PrintMain)
- sub_parser = sub_parsers.add_parser('list-id',
- help='Outputs all resource IDs to a file.')
- sub_parser.add_argument('pak_file')
- sub_parser.add_argument('--output', type=argparse.FileType('w'),
- default=sys.stdout,
- help='The resource list path to write (default stdout)')
- sub_parser.add_argument(
- '-t',
- '--textual-id',
- action='store_true',
- help='Print the textual resource ID (from .info file).')
- sub_parser.add_argument(
- '-p',
- '--path',
- action='store_true',
- help='Print the resource path (from .info file).')
- sub_parser.set_defaults(func=_ListMain)
- args = parser.parse_args()
- args.func(args)
- if __name__ == '__main__':
- main()
|