ART: Add python+debugfs based ART APEX checker

Using debugfs (from the tree) allows to run the tests in the build
and on unprivileged devices.

Bug: 123427238
Test: m
Test: m check-release-apex-gen-fakelib
Test: m check-debug-apex-gen-fakelib
Change-Id: I49cbee476bc14cbe230ff82bae8600744f100688
diff --git a/Android.mk b/Android.mk
index 1a5daff..b0a918c 100644
--- a/Android.mk
+++ b/Android.mk
@@ -352,13 +352,15 @@
   # Module with both release and debug variants, as well as
   # additional tools.
   TARGET_RUNTIME_APEX := com.android.runtime.debug
+  APEX_TEST_MODULE := art-check-debug-apex-gen-fakelib
 else
   # Release module (without debug variants nor tools).
   TARGET_RUNTIME_APEX := com.android.runtime.release
+  APEX_TEST_MODULE := art-check-release-apex-gen-fakelib
 endif
 
 LOCAL_MODULE := com.android.runtime
-LOCAL_REQUIRED_MODULES := $(TARGET_RUNTIME_APEX)
+LOCAL_REQUIRED_MODULES := $(TARGET_RUNTIME_APEX) $(APEX_TEST_MODULE)
 
 # Clear locally used variable.
 art_target_include_debug_build :=
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index d53a7f2..620abe3 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -211,3 +211,61 @@
         },
     },
 }
+
+python_binary_host {
+    name: "art-apex-tester",
+    srcs: ["art_apex_test.py"],
+    main: "art_apex_test.py",
+    version: {
+        py3: {
+            enabled: true,
+        },
+    },
+}
+
+// Genrules so we can run the checker, and empty Java library so that it gets executed.
+
+java_genrule_host {
+    name: "art-check-release-apex-gen",
+    srcs: [":com.android.runtime.release"],
+    tools: [
+        "art-apex-tester",
+        "debugfs",
+        "soong_zip",
+    ],
+    cmd: "$(location art-apex-tester)"
+              + " --debugfs $(location debugfs)"
+              + " --tmpdir $(genDir)"
+              + " --target"
+              + " $(in)"
+         + " && $(location soong_zip) -o $(out)",
+    out: ["art-check-release-apex-gen.srcjar"],
+}
+java_library_host {
+    name: "art-check-release-apex-gen-fakelib",
+    srcs: [":art-check-release-apex-gen"],
+    installable: false,
+}
+
+java_genrule_host {
+    name: "art-check-debug-apex-gen",
+    srcs: [":com.android.runtime.debug"],
+    tools: [
+        "art-apex-tester",
+        "debugfs",
+        "soong_zip",
+    ],
+    cmd: "$(location art-apex-tester)"
+              + " --debugfs $(location debugfs)"
+              + " --tmpdir $(genDir)"
+              + " --target"
+              + " --debug"
+              + " $(in)"
+         + " && $(location soong_zip) -o $(out)",
+    out: ["art-check-debug-apex-gen.srcjar"],
+}
+java_library_host {
+    name: "art-check-debug-apex-gen-fakelib",
+    srcs: [":art-check-debug-apex-gen"],
+    installable: false,
+}
diff --git a/build/apex/art_apex_test.py b/build/apex/art_apex_test.py
new file mode 100755
index 0000000..5ac2f72
--- /dev/null
+++ b/build/apex/art_apex_test.py
@@ -0,0 +1,372 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import argparse
+import logging
+import os
+import subprocess
+import sys
+import zipfile
+
+logging.basicConfig(format='%(message)s')
+
+class FSObject:
+  def __init__(self, name, is_dir, is_exec, is_symlink):
+    self.name = name
+    self.is_dir = is_dir
+    self.is_exec = is_exec
+    self.is_symlink = is_symlink
+  def __str__(self):
+    return '%s(dir=%r,exec=%r,symlink=%r)' % (self.name, self.is_dir, self.is_exec, self.is_symlink)
+
+class TargetApexProvider:
+  def __init__(self, apex, tmpdir, debugfs):
+    self._tmpdir = tmpdir
+    self._debugfs = debugfs
+    self._folder_cache = {}
+    self._payload = os.path.join(self._tmpdir, 'apex_payload.img')
+    # Extract payload to tmpdir.
+    zip = zipfile.ZipFile(apex)
+    zip.extract('apex_payload.img', tmpdir)
+
+  def __del__(self):
+    # Delete temps.
+    if os.path.exists(self._payload):
+      os.remove(self._payload)
+
+  def get(self, path):
+    dir, name = os.path.split(path)
+    if len(dir) == 0:
+      dir = '/'
+    map = self.read_dir(dir)
+    return map[name] if name in map else None
+
+  def read_dir(self, dir):
+    if dir in self._folder_cache:
+      return self._folder_cache[dir]
+    # Cannot use check_output as it will annoy with stderr.
+    process = subprocess.Popen([self._debugfs, '-R', 'ls -l -p %s' % (dir), self._payload],
+                               stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                               universal_newlines=True)
+    stdout, stderr = process.communicate()
+    res = str(stdout)
+    map = {}
+    # Debugfs output looks like this:
+    #   debugfs 1.44.4 (18-Aug-2018)
+    #   /12/040755/0/2000/.//
+    #   /2/040755/1000/1000/..//
+    #   /13/100755/0/2000/dalvikvm32/28456/
+    #   /14/100755/0/2000/dexoptanalyzer/20396/
+    #   /15/100755/0/2000/linker/1152724/
+    #   /16/100755/0/2000/dex2oat/563508/
+    #   /17/100755/0/2000/linker64/1605424/
+    #   /18/100755/0/2000/profman/85304/
+    #   /19/100755/0/2000/dalvikvm64/28576/
+    #    |     |   |   |       |        |
+    #    |     |   |   #- gid  #- name  #- size
+    #    |     |   #- uid
+    #    |     #- type and permission bits
+    #    #- inode nr (?)
+    #
+    # Note: could break just on '/' to avoid names with newlines.
+    for line in res.split("\n"):
+      if not line:
+        continue
+      comps = line.split('/')
+      if len(comps) != 8:
+        logging.warn('Could not break and parse line \'%s\'', line)
+        continue
+      bits = comps[2]
+      name = comps[5]
+      if len(bits) != 6:
+        logging.warn('Dont understand bits \'%s\'', bits)
+        continue
+      is_dir = True if bits[1] == '4' else False
+      def is_exec_bit(ch):
+        return True if int(ch) & 1 == 1 else False
+      is_exec = is_exec_bit(bits[3]) and is_exec_bit(bits[4]) and is_exec_bit(bits[5])
+      # TODO: Figure out how this is represented
+      is_symlink = False
+      map[name] = FSObject(name, is_dir, is_exec, is_symlink)
+    self._folder_cache[dir] = map
+    return map
+
+class Checker:
+  def __init__(self, provider):
+    self._provider = provider
+    self._errors = 0
+    self._is_multilib = provider.get('lib64') is not None;
+
+  def fail(self, msg, *args):
+    self._errors += 1
+    logging.error(msg, args)
+
+  def error_count(self):
+    return self._errors
+
+  def check_file(self, file):
+    fs_object = self._provider.get(file)
+    if fs_object is None:
+      self.fail('Could not find %s', file)
+      return False
+    if fs_object.is_dir:
+      self.fail('%s is a directory', file)
+      return False
+    return True
+
+  def check_binary(self, file):
+    path = 'bin/%s' % (file)
+    if not self.check_file(path):
+      return False
+    if not self._provider.get(path).is_exec:
+      self.fail('%s is not executable', path)
+      return False
+    return True
+
+  def check_multilib_binary(self, file):
+    res = self.check_binary('%s32' % (file))
+    if self._is_multilib:
+      res = self.check_binary('%s64' % (file)) and res
+    return res
+
+  def check_binary_symlink(self, file):
+    path = 'bin/%s' % (file)
+    fs_object = self._provider.get(path)
+    if fs_object is None:
+      self.fail('Could not find %s', path)
+      return False
+    if fs_object.is_dir:
+      self.fail('%s is a directory', path)
+      return False
+    if not fs_object.is_symlink:
+      self.fail('%s is not a symlink', path)
+      return False
+    return True
+
+  def check_library(self, file):
+    # TODO: Use $TARGET_ARCH (e.g. check whether it is "arm" or "arm64") to improve
+    # the precision of this test?
+    res = self.check_file('lib/%s' % (file))
+    if self._is_multilib:
+      res = self.check_file('lib64/%s' % (file)) and res
+    return res
+
+  def check_java_library(self, file):
+    return self.check_file('javalib/%s' % (file))
+
+class ReleaseChecker(Checker):
+  def __init__(self, provider):
+    super().__init__(provider)
+  def __str__(self):
+    return 'Release Checker'
+
+  def run(self):
+    # Check that the mounted image contains an APEX manifest.
+    self.check_file('apex_manifest.json')
+
+    # Check that the mounted image contains ART base binaries.
+    self.check_multilib_binary('dalvikvm')
+    # TODO: Does not work yet (b/119942078).
+    # self.check_binary_symlink('dalvikvm')
+    self.check_binary('dex2oat')
+    self.check_binary('dexoptanalyzer')
+    self.check_binary('profman')
+
+    # oatdump is only in device apex's due to build rules
+    # TODO: Check for it when it is also built for host.
+    # self.check_binary('oatdump')
+
+    # Check that the mounted image contains Android Runtime libraries.
+    self.check_library('libart-compiler.so')
+    self.check_library('libart-dexlayout.so')
+    self.check_library('libart.so')
+    self.check_library('libartbase.so')
+    self.check_library('libdexfile.so')
+    self.check_library('libopenjdkjvm.so')
+    self.check_library('libopenjdkjvmti.so')
+    self.check_library('libprofile.so')
+    # Check that the mounted image contains Android Core libraries.
+    self.check_library('libexpat.so')
+    self.check_library('libjavacore.so')
+    self.check_library('libopenjdk.so')
+    self.check_library('libz.so')
+    self.check_library('libziparchive.so')
+    # Check that the mounted image contains additional required libraries.
+    self.check_library('libadbconnection.so')
+
+    # TODO: Should we check for other libraries, such as:
+    #
+    #   libbacktrace.so
+    #   libbase.so
+    #   liblog.so
+    #   libsigchain.so
+    #   libtombstoned_client.so
+    #   libunwindstack.so
+    #   libvixl.so
+    #   libvixld.so
+    #   ...
+    #
+    # ?
+
+    self.check_java_library('core-oj.jar')
+    self.check_java_library('core-libart.jar')
+    self.check_java_library('okhttp.jar')
+    self.check_java_library('bouncycastle.jar')
+    self.check_java_library('apache-xml.jar')
+
+class DebugChecker(Checker):
+  def __init__(self, provider):
+    super().__init__(provider)
+  def __str__(self):
+    return 'Debug Checker'
+
+  def run(self):
+    # Check that the mounted image contains ART tools binaries.
+    self.check_binary('dexdiag')
+    self.check_binary('dexdump')
+    self.check_binary('dexlist')
+
+    # Check that the mounted image contains ART debug binaries.
+    # TODO(b/123427238): This should probably be dex2oatd, fix!
+    self.check_binary('dex2oatd32')
+    self.check_binary('dexoptanalyzerd')
+    self.check_binary('profmand')
+
+    # Check that the mounted image contains Android Runtime debug libraries.
+    self.check_library('libartbased.so')
+    self.check_library('libartd-compiler.so')
+    self.check_library('libartd-dexlayout.so')
+    self.check_library('libartd.so')
+    self.check_library('libdexfiled.so')
+    self.check_library('libopenjdkjvmd.so')
+    self.check_library('libopenjdkjvmtid.so')
+    self.check_library('libprofiled.so')
+    # Check that the mounted image contains Android Core debug libraries.
+    self.check_library('libopenjdkd.so')
+    # Check that the mounted image contains additional required debug libraries.
+    self.check_library('libadbconnectiond.so')
+
+# Note: do not sys.exit early, for __del__ cleanup.
+def artApexTestMain(args):
+  if not args.host and not args.target and not args.debug:
+    logging.error("None of --host, --target nor --debug set")
+    return 1
+  if args.host and (args.target or args.debug):
+    logging.error("Both of --host and --target|--debug set")
+    return 1
+  if args.debug and not args.target:
+    args.target = True
+  if args.target and not args.tmpdir:
+    logging.error("Need a tmpdir.")
+    return 1
+  if args.target and not args.debugfs:
+    logging.error("Need debugfs.")
+    return 1
+
+  try:
+    apex_provider = TargetApexProvider(args.apex, args.tmpdir, args.debugfs)
+  except:
+    logging.error('Failed to create provider')
+    return 1
+
+  checkers = []
+  if args.host:
+    logging.error('host checking not yet supported')
+    return 1
+
+  checkers.append(ReleaseChecker(apex_provider))
+  if args.debug:
+    checkers.append(DebugChecker(apex_provider))
+
+  failed = False
+  for checker in checkers:
+    logging.info('%s...', checker)
+    checker.run()
+    if checker.error_count() > 0:
+      logging.error('%s FAILED', checker)
+      failed = True
+    else:
+      logging.info('%s SUCCEEDED', checker)
+
+  return 1 if failed else 0
+
+def artApexTestDefault(parser):
+  if not 'ANDROID_PRODUCT_OUT' in os.environ:
+    logging.error('No-argument use requires ANDROID_PRODUCT_OUT')
+    sys.exit(1)
+  product_out = os.environ['ANDROID_PRODUCT_OUT']
+  if not 'ANDROID_HOST_OUT' in os.environ:
+    logging.error('No-argument use requires ANDROID_HOST_OUT')
+    sys.exit(1)
+  host_out = os.environ['ANDROID_HOST_OUT']
+
+  args = parser.parse_args(['dummy'])  # For consistency.
+  args.debugfs = '%s/bin/debugfs' % (host_out)
+  args.tmpdir = '.'
+  failed = False
+
+  if not os.path.exists(args.debugfs):
+    logging.error("Cannot find debugfs (default path %s). Please build it, e.g., m debugfs",
+                  args.debugfs)
+    sys.exit(1)
+
+  # TODO: Add host support
+  configs= [
+    {'name': 'com.android.runtime.release', 'target': True, 'debug': False, 'host': False},
+    {'name': 'com.android.runtime.debug', 'target': True, 'debug': True, 'host': False},
+  ]
+
+  for config in configs:
+    logging.info(config['name'])
+    # TODO: Host will need different path.
+    args.apex = '%s/system/apex/%s.apex' % (product_out, config['name'])
+    if not os.path.exists(args.apex):
+      failed = True
+      logging.error("Cannot find APEX %s. Please build it first.", args.apex)
+      continue
+    args.target = config['target']
+    args.debug = config['debug']
+    args.host = config['host']
+    exit_code = artApexTestMain(args)
+    if exit_code != 0:
+      failed = True
+
+  if failed:
+    sys.exit(1)
+
+if __name__ == "__main__":
+  parser = argparse.ArgumentParser(description='Check integrity of a Runtime APEX.')
+
+  parser.add_argument('apex', help='apex file input')
+
+  parser.add_argument('--host', help='Check as host apex', action='store_true')
+  parser.add_argument('--target', help='Check as target apex', action='store_true')
+  parser.add_argument('--debug', help='Check as debug apex', action='store_true')
+
+  parser.add_argument('--tmpdir', help='Directory for temp files')
+  parser.add_argument('--debugfs', help='Path to debugfs')
+
+  if len(sys.argv) == 1:
+    artApexTestDefault(parser)
+  else:
+    args = parser.parse_args()
+
+    if args is None:
+      sys.exit(1)
+
+    exit_code = artApexTestMain(args)
+    sys.exit(exit_code)