| #!/usr/bin/env python2.7 |
| # |
| # Copyright 2018 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. |
| |
| import argparse |
| import re |
| import os |
| import posixpath |
| import StringIO |
| import sys |
| import subprocess |
| |
| from contextlib import closing |
| |
| |
| def BuildFileMatchRegex(*file_matchers): |
| return re.compile('^' + '|'.join(file_matchers) + '$') |
| |
| |
| # Chrome specific files which are not in Monochrome.apk |
| CHROME_SPECIFIC = BuildFileMatchRegex( |
| r'lib/.*/libchrome\.so', |
| r'lib/.*/libchrome\.\d{4}\.\d{2,3}\.so', # libchrome placeholders |
| r'lib/.*/libchromium_android_linker\.so', |
| r'lib/.*/libchromeview\.so', # placeholder library |
| r'lib/.*/libcrashpad_handler\.so', |
| r'lib/.*/crazy\.libchrome\.so', |
| r'lib/.*/crazy\.libchrome\.align', |
| r'lib/.*/gdbserver', |
| # Monochrome doesn't have any res directories whose api number is less |
| # than v24. |
| r'res/.*-v1\d/.*\.xml', |
| r'res/.*-v2[0-3]/.*\.xml', |
| r'META-INF/.*', |
| r'assets/metaresources.arsc', |
| r'assets/AndroidManifest.xml') |
| |
| # WebView specific files which are not in Monochrome.apk |
| WEBVIEW_SPECIFIC = BuildFileMatchRegex( |
| r'lib/.*/libwebviewchromium\.so', |
| r'assets/webview_licenses.notice', |
| r'res/.*/icon_webview.webp', |
| r'META-INF/.*', |
| # Monochrome doesn't have any res directories |
| # whose api level is less than v24. |
| r'res/.*-v1\d/.*\.xml', |
| r'res/.*-v2[0-3]/.*\.xml', |
| r'lib/.*/gdbserver') |
| |
| # The files in Chrome are not same as those in Monochrome |
| CHROME_CHANGES = BuildFileMatchRegex( |
| r'AndroidManifest\.xml', |
| r'resources\.arsc', |
| r'classes\.dex', |
| r'classes2\.dex', |
| r'res/.*\.xml', # Resource id isn't same |
| r'assets/unwind_cfi_32', # Generated from apk's shared library |
| # All pak files except chrome_100_percent.pak are different |
| r'assets/resources\.pak', |
| r'assets/locales/am\.pak', |
| r'assets/locales/ar\.pak', |
| r'assets/locales/bg\.pak', |
| r'assets/locales/ca\.pak', |
| r'assets/locales/cs\.pak', |
| r'assets/locales/da\.pak', |
| r'assets/locales/de\.pak', |
| r'assets/locales/el\.pak', |
| r'assets/locales/en-GB\.pak', |
| r'assets/locales/en-US\.pak', |
| r'assets/locales/es-419\.pak', |
| r'assets/locales/es\.pak', |
| r'assets/locales/fa\.pak', |
| r'assets/locales/fi\.pak', |
| r'assets/locales/fil\.pak', |
| r'assets/locales/fr\.pak', |
| r'assets/locales/he\.pak', |
| r'assets/locales/hi\.pak', |
| r'assets/locales/hr\.pak', |
| r'assets/locales/hu\.pak', |
| r'assets/locales/id\.pak', |
| r'assets/locales/it\.pak', |
| r'assets/locales/ja\.pak', |
| r'assets/locales/ko\.pak', |
| r'assets/locales/lt\.pak', |
| r'assets/locales/lv\.pak', |
| r'assets/locales/nb\.pak', |
| r'assets/locales/nl\.pak', |
| r'assets/locales/pl\.pak', |
| r'assets/locales/pt-BR\.pak', |
| r'assets/locales/pt-PT\.pak', |
| r'assets/locales/ro\.pak', |
| r'assets/locales/ru\.pak', |
| r'assets/locales/sk\.pak', |
| r'assets/locales/sl\.pak', |
| r'assets/locales/sr\.pak', |
| r'assets/locales/sv\.pak', |
| r'assets/locales/sw\.pak', |
| r'assets/locales/th\.pak', |
| r'assets/locales/tr\.pak', |
| r'assets/locales/uk\.pak', |
| r'assets/locales/vi\.pak', |
| r'assets/locales/zh-CN\.pak', |
| r'assets/locales/zh-TW\.pak') |
| |
| # The files in WebView are not same as those in Monochrome |
| WEBVIEW_CHANGES = BuildFileMatchRegex( |
| r'AndroidManifest\.xml', |
| r'resources\.arsc', |
| r'classes\.dex', |
| r'res/.*\.xml', # Resource id isn't same |
| r'assets/.*\.pak') # All pak files are not same as Monochrome |
| |
| # Parse the output of unzip -lv, like |
| # 2384 Defl:N 807 66% 2001-01-01 00:00 2f2d9fce res/xml/privacy.xml |
| ZIP_ENTRY = re.compile( |
| "^ *[0-9]+ +\S+ +[0-9]+ +(?P<cmpr>[0-9]{1,2})% +\S+ +\S+ +" |
| "(?P<crc>[0-9a-fA-F]+) +(?P<name>\S+)" |
| ) |
| |
| class APKEntry: |
| def __init__(self, filename, crc, uncompressed): |
| self.filename = filename |
| self.CRC = crc |
| self.uncompressed = uncompressed |
| |
| def DumpAPK(apk): |
| args = ['unzip', '-lv'] |
| args.append(apk) |
| content = subprocess.check_output(args) |
| apk_entries = [] |
| with closing(StringIO.StringIO(content)) as f: |
| for line in f: |
| match = ZIP_ENTRY.match(line) |
| if match: |
| apk_entries.append(APKEntry(match.group('name'), |
| match.group('crc'), |
| match.group('cmpr') == 0)) |
| return apk_entries |
| |
| def VerifySameFile(monochrome_dict, apk, changes): |
| """ Verify files from apk are same as those in monochrome except files |
| in changes |
| """ |
| diff = [] |
| for a in apk: |
| # File may not exists due to exists_in_some_form(). |
| m = monochrome_dict.get(a.filename) |
| if m and m.CRC != a.CRC and not changes.match(m.filename): |
| diff.append(a.filename) |
| if len(diff): |
| raise Exception("The following files are not same as Monochrome:\n %s" % |
| '\n'.join(diff)) |
| |
| |
| def VerifyUncompressed(monochrome, apk): |
| """ Verify files not being compressed in apk are also uncompressed in |
| Monochrome APK |
| """ |
| uncompressed = [i.filename for i in apk if i.uncompressed ] |
| monochrome_uncompressed = [i.filename for i in monochrome if i.uncompressed] |
| compressed = [u for u in uncompressed if u not in monochrome_uncompressed] |
| if len(compressed): |
| raise Exception("The following files are compressed in Monochrome:\n %s" % |
| '\n'.join(compressed)) |
| |
| def SuperSetOf(monochrome, apk): |
| """ Verify Monochrome is super set of apk. |
| """ |
| def exists_in_some_form(f): |
| if f in monochrome: |
| return True |
| if not f.startswith('res/'): |
| return False |
| name = '/' + posixpath.basename(f) |
| # Some resources will exists in apk but not in monochrome due to the |
| # difference in minSdkVersion. https://crbug.com/794438 |
| # E.g.: |
| # apk could have: res/drawable/foo.png, res/drawable-v23/foo.png |
| # monochrome (minSdkVersion=24) would need only: res/drawable-v23/foo.png |
| return any(x.endswith(name) for x in monochrome) |
| |
| missing_files = [f for f in apk if not exists_in_some_form(f)] |
| if len(missing_files): |
| raise Exception("The following files are missing in Monochrome:\n %s" % |
| '\n'.join(missing_files)) |
| |
| |
| def RemoveSpecific(apk_entries, specific): |
| return [i for i in apk_entries |
| if not specific.search(i.filename) ] |
| |
| |
| def ParseArgs(args): |
| """Parses command line options. |
| |
| Returns: |
| An Namespace from argparse.parse_args() |
| """ |
| parser = argparse.ArgumentParser(prog='monochrome_apk_checker') |
| |
| parser.add_argument('--monochrome-apk', |
| required=True, |
| help='The monochrome APK path') |
| parser.add_argument('--chrome-apk', |
| required=True, |
| help='The chrome APK path.') |
| parser.add_argument('--system-webview-apk', |
| required=True, |
| help='The system webview APK path.') |
| return parser.parse_args(args) |
| |
| |
| def main(): |
| options = ParseArgs(sys.argv[1:]) |
| monochrome = DumpAPK(options.monochrome_apk) |
| monochrome_files = [f.filename for f in monochrome] |
| monochrome_dict = dict([(i.filename, i) for i in monochrome]) |
| |
| chrome = RemoveSpecific(DumpAPK(options.chrome_apk), |
| CHROME_SPECIFIC) |
| if len(chrome) == 0: |
| raise Exception("Chrome should have common files with Monochrome") |
| |
| webview = RemoveSpecific(DumpAPK(options.system_webview_apk), |
| WEBVIEW_SPECIFIC) |
| if len(webview) == 0: |
| raise Exception("WebView should have common files with Monochrome") |
| |
| def check_apk(apk): |
| apk_files = [f.filename for f in apk] |
| SuperSetOf(monochrome_files, apk_files) |
| VerifyUncompressed(monochrome, apk) |
| VerifySameFile(monochrome_dict, chrome, CHROME_CHANGES) |
| VerifySameFile(monochrome_dict, webview, WEBVIEW_CHANGES) |
| |
| check_apk(chrome) |
| check_apk(webview) |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |