1#!/usr/bin/env python 2# 3# Copyright (C) 2019 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""deapexer is a tool that prints out content of an APEX. 17 18To print content of an APEX to stdout: 19 deapexer list foo.apex 20 21To extract content of an APEX to the given directory: 22 deapexer extract foo.apex dest 23""" 24from __future__ import print_function 25 26import argparse 27import os 28import shutil 29import sys 30import subprocess 31import tempfile 32import zipfile 33import apex_manifest 34 35class ApexImageEntry(object): 36 37 def __init__(self, name, base_dir, permissions, size, is_directory=False, is_symlink=False): 38 self._name = name 39 self._base_dir = base_dir 40 self._permissions = permissions 41 self._size = size 42 self._is_directory = is_directory 43 self._is_symlink = is_symlink 44 45 @property 46 def name(self): 47 return self._name 48 49 @property 50 def full_path(self): 51 return os.path.join(self._base_dir, self._name) 52 53 @property 54 def is_directory(self): 55 return self._is_directory 56 57 @property 58 def is_symlink(self): 59 return self._is_symlink 60 61 @property 62 def is_regular_file(self): 63 return not self.is_directory and not self.is_symlink 64 65 @property 66 def permissions(self): 67 return self._permissions 68 69 @property 70 def size(self): 71 return self._size 72 73 def __str__(self): 74 ret = '' 75 if self._is_directory: 76 ret += 'd' 77 elif self._is_symlink: 78 ret += 'l' 79 else: 80 ret += '-' 81 82 def mask_as_string(m): 83 ret = 'r' if m & 4 == 4 else '-' 84 ret += 'w' if m & 2 == 2 else '-' 85 ret += 'x' if m & 1 == 1 else '-' 86 return ret 87 88 ret += mask_as_string(self._permissions >> 6) 89 ret += mask_as_string((self._permissions >> 3) & 7) 90 ret += mask_as_string(self._permissions & 7) 91 92 return ret + ' ' + self._size + ' ' + self._name 93 94 95class ApexImageDirectory(object): 96 97 def __init__(self, path, entries, apex): 98 self._path = path 99 self._entries = sorted(entries, key=lambda e: e.name) 100 self._apex = apex 101 102 def list(self, is_recursive=False): 103 for e in self._entries: 104 yield e 105 if e.is_directory and e.name != '.' and e.name != '..': 106 for ce in self.enter_subdir(e).list(is_recursive): 107 yield ce 108 109 def enter_subdir(self, entry): 110 return self._apex._list(self._path + entry.name + '/') 111 112 def extract(self, dest): 113 path = self._path 114 self._apex._extract(self._path, dest) 115 116 117class Apex(object): 118 119 def __init__(self, args): 120 self._debugfs = args.debugfs_path 121 self._apex = args.apex 122 self._tempdir = tempfile.mkdtemp() 123 # TODO(b/139125405): support flattened APEXes. 124 with zipfile.ZipFile(self._apex, 'r') as zip_ref: 125 self._payload = zip_ref.extract('apex_payload.img', path=self._tempdir) 126 self._cache = {} 127 128 def __del__(self): 129 shutil.rmtree(self._tempdir) 130 131 def __enter__(self): 132 return self._list('./') 133 134 def __exit__(self, type, value, traceback): 135 pass 136 137 def _list(self, path): 138 if path in self._cache: 139 return self._cache[path] 140 process = subprocess.Popen([self._debugfs, '-R', 'ls -l -p %s' % path, self._payload], 141 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 142 universal_newlines=True) 143 stdout, _ = process.communicate() 144 res = str(stdout) 145 entries = [] 146 for line in res.split('\n'): 147 if not line: 148 continue 149 parts = line.split('/') 150 if len(parts) != 8: 151 continue 152 name = parts[5] 153 if not name: 154 continue 155 bits = parts[2] 156 size = parts[6] 157 entries.append(ApexImageEntry(name, base_dir=path, permissions=int(bits[3:], 8), size=size, 158 is_directory=bits[1]=='4', is_symlink=bits[1]=='2')) 159 return ApexImageDirectory(path, entries, self) 160 161 def _extract(self, path, dest): 162 process = subprocess.Popen([self._debugfs, '-R', 'rdump %s %s' % (path, dest), self._payload], 163 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 164 universal_newlines=True) 165 _, stderr = process.communicate() 166 if process.returncode != 0: 167 print(stderr, file=sys.stderr) 168 169 170def RunList(args): 171 with Apex(args) as apex: 172 for e in apex.list(is_recursive=True): 173 if e.is_directory: 174 continue 175 if args.size: 176 print(e.size, e.full_path) 177 else: 178 print(e.full_path) 179 180 181def RunExtract(args): 182 with Apex(args) as apex: 183 if not os.path.exists(args.dest): 184 os.makedirs(args.dest, mode=0o755) 185 apex.extract(args.dest) 186 shutil.rmtree(os.path.join(args.dest, "lost+found")) 187 188 189def RunInfo(args): 190 manifest = apex_manifest.fromApex(args.apex) 191 print(apex_manifest.toJsonString(manifest)) 192 193 194def main(argv): 195 parser = argparse.ArgumentParser() 196 197 debugfs_default = 'debugfs' # assume in PATH by default 198 if 'ANDROID_HOST_OUT' in os.environ: 199 debugfs_default = '%s/bin/debugfs_static' % os.environ['ANDROID_HOST_OUT'] 200 parser.add_argument('--debugfs_path', help='The path to debugfs binary', default=debugfs_default) 201 202 subparsers = parser.add_subparsers(required=True, dest='cmd') 203 204 parser_list = subparsers.add_parser('list', help='prints content of an APEX to stdout') 205 parser_list.add_argument('apex', type=str, help='APEX file') 206 parser_list.add_argument('--size', help='also show the size of the files', action="store_true") 207 parser_list.set_defaults(func=RunList) 208 209 parser_extract = subparsers.add_parser('extract', help='extracts content of an APEX to the given ' 210 'directory') 211 parser_extract.add_argument('apex', type=str, help='APEX file') 212 parser_extract.add_argument('dest', type=str, help='Directory to extract content of APEX to') 213 parser_extract.set_defaults(func=RunExtract) 214 215 parser_info = subparsers.add_parser('info', help='prints APEX manifest') 216 parser_info.add_argument('apex', type=str, help='APEX file') 217 parser_info.set_defaults(func=RunInfo) 218 219 args = parser.parse_args(argv) 220 221 args.func(args) 222 223 224if __name__ == '__main__': 225 main(sys.argv[1:]) 226