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