| # 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 logging |
| import unittest |
| |
| from pylib.symbols import apk_native_libs |
| |
| # Mock ELF-like data |
| MOCK_ELF_DATA = '\x7fELFFFFFFFFFFFFFFFF' |
| |
| class MockApkZipInfo(object): |
| """A mock ApkZipInfo class, returned by MockApkReaderFactory instances.""" |
| def __init__(self, filename, file_size, compress_size, file_offset, |
| file_data): |
| self.filename = filename |
| self.file_size = file_size |
| self.compress_size = compress_size |
| self.file_offset = file_offset |
| self._data = file_data |
| |
| def __repr__(self): |
| """Convert to string for debugging.""" |
| return 'MockApkZipInfo["%s",size=%d,compressed=%d,offset=%d]' % ( |
| self.filename, self.file_size, self.compress_size, self.file_offset) |
| |
| def IsCompressed(self): |
| """Returns True iff the entry is compressed.""" |
| return self.file_size != self.compress_size |
| |
| def IsElfFile(self): |
| """Returns True iff the entry is an ELF file.""" |
| if not self._data or len(self._data) < 4: |
| return False |
| |
| return self._data[0:4] == '\x7fELF' |
| |
| |
| class MockApkReader(object): |
| """A mock ApkReader instance used during unit-testing. |
| |
| Do not use directly, but use a MockApkReaderFactory context, as in: |
| |
| with MockApkReaderFactory() as mock: |
| mock.AddTestEntry(file_path, file_size, compress_size, file_data) |
| ... |
| |
| # Actually returns the mock instance. |
| apk_reader = apk_native_libs.ApkReader('/some/path.apk') |
| """ |
| def __init__(self, apk_path='test.apk'): |
| """Initialize instance.""" |
| self._entries = [] |
| self._fake_offset = 0 |
| self._path = apk_path |
| |
| def __enter__(self): |
| return self |
| |
| def __exit__(self, *kwarg): |
| self.Close() |
| return |
| |
| @property |
| def path(self): |
| return self._path |
| |
| def AddTestEntry(self, filepath, file_size, compress_size, file_data): |
| """Add a new entry to the instance for unit-tests. |
| |
| Do not call this directly, use the AddTestEntry() method on the parent |
| MockApkReaderFactory instance. |
| |
| Args: |
| filepath: archive file path. |
| file_size: uncompressed file size in bytes. |
| compress_size: compressed size in bytes. |
| file_data: file data to be checked by IsElfFile() |
| |
| Note that file_data can be None, or that its size can be actually |
| smaller than |compress_size| when used during unit-testing. |
| """ |
| self._entries.append(MockApkZipInfo(filepath, file_size, compress_size, |
| self._fake_offset, file_data)) |
| self._fake_offset += compress_size |
| |
| def Close(self): # pylint: disable=no-self-use |
| """Close this reader instance.""" |
| return |
| |
| def ListEntries(self): |
| """Return a list of MockApkZipInfo instances for this input APK.""" |
| return self._entries |
| |
| def FindEntry(self, file_path): |
| """Find the MockApkZipInfo instance corresponds to a given file path.""" |
| for entry in self._entries: |
| if entry.filename == file_path: |
| return entry |
| raise KeyError('Could not find mock zip archive member for: ' + file_path) |
| |
| |
| class MockApkReaderTest(unittest.TestCase): |
| |
| def testEmpty(self): |
| with MockApkReader() as reader: |
| entries = reader.ListEntries() |
| self.assertTrue(len(entries) == 0) |
| with self.assertRaises(KeyError): |
| reader.FindEntry('non-existent-entry.txt') |
| |
| def testSingleEntry(self): |
| with MockApkReader() as reader: |
| reader.AddTestEntry('some-path/some-file', 20000, 12345, file_data=None) |
| entries = reader.ListEntries() |
| self.assertTrue(len(entries) == 1) |
| entry = entries[0] |
| self.assertEqual(entry.filename, 'some-path/some-file') |
| self.assertEqual(entry.file_size, 20000) |
| self.assertEqual(entry.compress_size, 12345) |
| self.assertTrue(entry.IsCompressed()) |
| |
| entry2 = reader.FindEntry('some-path/some-file') |
| self.assertEqual(entry, entry2) |
| |
| def testMultipleEntries(self): |
| with MockApkReader() as reader: |
| _ENTRIES = { |
| 'foo.txt': (1024, 1024, 'FooFooFoo'), |
| 'lib/bar/libcode.so': (16000, 3240, 1024, '\x7fELFFFFFFFFFFFF'), |
| } |
| for path, props in _ENTRIES.iteritems(): |
| reader.AddTestEntry(path, props[0], props[1], props[2]) |
| |
| entries = reader.ListEntries() |
| self.assertEqual(len(entries), len(_ENTRIES)) |
| for path, props in _ENTRIES.iteritems(): |
| entry = reader.FindEntry(path) |
| self.assertEqual(entry.filename, path) |
| self.assertEqual(entry.file_size, props[0]) |
| self.assertEqual(entry.compress_size, props[1]) |
| |
| |
| class ApkNativeLibrariesTest(unittest.TestCase): |
| |
| def setUp(self): |
| logging.getLogger().setLevel(logging.ERROR) |
| |
| def testEmptyApk(self): |
| with MockApkReader() as reader: |
| libs_map = apk_native_libs.ApkNativeLibraries(reader) |
| self.assertTrue(libs_map.IsEmpty()) |
| self.assertEqual(len(libs_map.GetLibraries()), 0) |
| lib_path, lib_offset = libs_map.FindLibraryByOffset(0) |
| self.assertIsNone(lib_path) |
| self.assertEqual(lib_offset, 0) |
| |
| def testSimpleApk(self): |
| with MockApkReader() as reader: |
| _MOCK_ENTRIES = [ |
| # Top-level library should be ignored. |
| ('libfoo.so', 1000, 1000, MOCK_ELF_DATA, False), |
| # Library not under lib/ should be ignored. |
| ('badlib/test-abi/libfoo2.so', 1001, 1001, MOCK_ELF_DATA, False), |
| # Library under lib/<abi>/ but without .so extension should be ignored. |
| ('lib/test-abi/libfoo4.so.1', 1003, 1003, MOCK_ELF_DATA, False), |
| # Library under lib/<abi>/ with .so suffix, but compressed -> ignored. |
| ('lib/test-abi/libfoo5.so', 1004, 1003, MOCK_ELF_DATA, False), |
| # First correct library |
| ('lib/test-abi/libgood1.so', 1005, 1005, MOCK_ELF_DATA, True), |
| # Second correct library: support sub-directories |
| ('lib/test-abi/subdir/libgood2.so', 1006, 1006, MOCK_ELF_DATA, True), |
| # Third correct library, no lib prefix required |
| ('lib/test-abi/crazy.libgood3.so', 1007, 1007, MOCK_ELF_DATA, True), |
| ] |
| file_offsets = [] |
| prev_offset = 0 |
| for ent in _MOCK_ENTRIES: |
| reader.AddTestEntry(ent[0], ent[1], ent[2], ent[3]) |
| file_offsets.append(prev_offset) |
| prev_offset += ent[2] |
| |
| libs_map = apk_native_libs.ApkNativeLibraries(reader) |
| self.assertFalse(libs_map.IsEmpty()) |
| self.assertEqual(libs_map.GetLibraries(), [ |
| 'lib/test-abi/crazy.libgood3.so', |
| 'lib/test-abi/libgood1.so', |
| 'lib/test-abi/subdir/libgood2.so', |
| ]) |
| |
| BIAS = 10 |
| for mock_ent, file_offset in zip(_MOCK_ENTRIES, file_offsets): |
| if mock_ent[4]: |
| lib_path, lib_offset = libs_map.FindLibraryByOffset( |
| file_offset + BIAS) |
| self.assertEqual(lib_path, mock_ent[0]) |
| self.assertEqual(lib_offset, BIAS) |
| |
| |
| def testMultiAbiApk(self): |
| with MockApkReader() as reader: |
| _MOCK_ENTRIES = [ |
| ('lib/abi1/libfoo.so', 1000, 1000, MOCK_ELF_DATA), |
| ('lib/abi2/libfoo.so', 1000, 1000, MOCK_ELF_DATA), |
| ] |
| for ent in _MOCK_ENTRIES: |
| reader.AddTestEntry(ent[0], ent[1], ent[2], ent[3]) |
| |
| libs_map = apk_native_libs.ApkNativeLibraries(reader) |
| self.assertFalse(libs_map.IsEmpty()) |
| self.assertEqual(libs_map.GetLibraries(), [ |
| 'lib/abi1/libfoo.so', 'lib/abi2/libfoo.so']) |
| |
| lib1_name, lib1_offset = libs_map.FindLibraryByOffset(10) |
| self.assertEqual(lib1_name, 'lib/abi1/libfoo.so') |
| self.assertEqual(lib1_offset, 10) |
| |
| lib2_name, lib2_offset = libs_map.FindLibraryByOffset(1000) |
| self.assertEqual(lib2_name, 'lib/abi2/libfoo.so') |
| self.assertEqual(lib2_offset, 0) |
| |
| |
| class MockApkNativeLibraries(apk_native_libs.ApkNativeLibraries): |
| """A mock ApkNativeLibraries instance that can be used as input to |
| ApkLibraryPathTranslator without creating an ApkReader instance. |
| |
| Create a new instance, then call AddTestEntry or AddTestEntries |
| as many times as necessary, before using it as a regular |
| ApkNativeLibraries instance. |
| """ |
| # pylint: disable=super-init-not-called |
| def __init__(self): |
| self._native_libs = [] |
| |
| # pylint: enable=super-init-not-called |
| |
| def AddTestEntry(self, lib_path, file_offset, file_size): |
| """Add a new test entry. |
| |
| Args: |
| entry: A tuple of (library-path, file-offset, file-size) values, |
| (e.g. ('lib/armeabi-v8a/libfoo.so', 0x10000, 0x2000)). |
| """ |
| self._native_libs.append((lib_path, file_offset, file_offset + file_size)) |
| |
| def AddTestEntries(self, entries): |
| """Add a list of new test entries. |
| |
| Args: |
| entries: A list of (library-path, file-offset, file-size) values. |
| """ |
| for entry in entries: |
| self.AddTestEntry(entry[0], entry[1], entry[2]) |
| |
| |
| class MockApkNativeLibrariesTest(unittest.TestCase): |
| |
| def testEmptyInstance(self): |
| mock = MockApkNativeLibraries() |
| self.assertTrue(mock.IsEmpty()) |
| self.assertEqual(mock.GetLibraries(), []) |
| self.assertEqual(mock.GetDumpList(), []) |
| |
| def testAddTestEntry(self): |
| mock = MockApkNativeLibraries() |
| mock.AddTestEntry('lib/armeabi-v7a/libfoo.so', 0x20000, 0x4000) |
| mock.AddTestEntry('lib/x86/libzoo.so', 0x10000, 0x10000) |
| mock.AddTestEntry('lib/armeabi-v7a/libbar.so', 0x24000, 0x8000) |
| self.assertFalse(mock.IsEmpty()) |
| self.assertEqual(mock.GetLibraries(), ['lib/armeabi-v7a/libbar.so', |
| 'lib/armeabi-v7a/libfoo.so', |
| 'lib/x86/libzoo.so']) |
| self.assertEqual(mock.GetDumpList(), [ |
| ('lib/x86/libzoo.so', 0x10000, 0x10000), |
| ('lib/armeabi-v7a/libfoo.so', 0x20000, 0x4000), |
| ('lib/armeabi-v7a/libbar.so', 0x24000, 0x8000), |
| ]) |
| |
| def testAddTestEntries(self): |
| mock = MockApkNativeLibraries() |
| mock.AddTestEntries([ |
| ('lib/armeabi-v7a/libfoo.so', 0x20000, 0x4000), |
| ('lib/x86/libzoo.so', 0x10000, 0x10000), |
| ('lib/armeabi-v7a/libbar.so', 0x24000, 0x8000), |
| ]) |
| self.assertFalse(mock.IsEmpty()) |
| self.assertEqual(mock.GetLibraries(), ['lib/armeabi-v7a/libbar.so', |
| 'lib/armeabi-v7a/libfoo.so', |
| 'lib/x86/libzoo.so']) |
| self.assertEqual(mock.GetDumpList(), [ |
| ('lib/x86/libzoo.so', 0x10000, 0x10000), |
| ('lib/armeabi-v7a/libfoo.so', 0x20000, 0x4000), |
| ('lib/armeabi-v7a/libbar.so', 0x24000, 0x8000), |
| ]) |
| |
| |
| class ApkLibraryPathTranslatorTest(unittest.TestCase): |
| |
| def _CheckUntranslated(self, translator, path, offset): |
| """Check that a given (path, offset) is not modified by translation.""" |
| self.assertEqual(translator.TranslatePath(path, offset), (path, offset)) |
| |
| |
| def _CheckTranslated(self, translator, path, offset, new_path, new_offset): |
| """Check that (path, offset) is translated into (new_path, new_offset).""" |
| self.assertEqual(translator.TranslatePath(path, offset), |
| (new_path, new_offset)) |
| |
| def testEmptyInstance(self): |
| translator = apk_native_libs.ApkLibraryPathTranslator() |
| self._CheckUntranslated( |
| translator, '/data/data/com.example.app-1/base.apk', 0x123456) |
| |
| def testSimpleApk(self): |
| mock_libs = MockApkNativeLibraries() |
| mock_libs.AddTestEntries([ |
| ('lib/test-abi/libfoo.so', 200, 2000), |
| ('lib/test-abi/libbar.so', 3200, 3000), |
| ('lib/test-abi/crazy.libzoo.so', 6200, 2000), |
| ]) |
| translator = apk_native_libs.ApkLibraryPathTranslator() |
| translator.AddHostApk('com.example.app', mock_libs) |
| |
| # Offset is within the first uncompressed library |
| self._CheckTranslated( |
| translator, |
| '/data/data/com.example.app-9.apk', 757, |
| '/data/data/com.example.app-9.apk!lib/libfoo.so', 557) |
| |
| # Offset is within the second compressed library. |
| self._CheckUntranslated( |
| translator, |
| '/data/data/com.example.app-9/base.apk', 2800) |
| |
| # Offset is within the third uncompressed library. |
| self._CheckTranslated( |
| translator, |
| '/data/data/com.example.app-1/base.apk', 3628, |
| '/data/data/com.example.app-1/base.apk!lib/libbar.so', 428) |
| |
| # Offset is within the fourth uncompressed library with crazy. prefix |
| self._CheckTranslated( |
| translator, |
| '/data/data/com.example.app-XX/base.apk', 6500, |
| '/data/data/com.example.app-XX/base.apk!lib/libzoo.so', 300) |
| |
| # Out-of-bounds apk offset. |
| self._CheckUntranslated( |
| translator, |
| '/data/data/com.example.app-1/base.apk', 10000) |
| |
| # Invalid package name. |
| self._CheckUntranslated( |
| translator, '/data/data/com.example2.app-1/base.apk', 757) |
| |
| # Invalid apk name. |
| self._CheckUntranslated( |
| translator, '/data/data/com.example.app-2/not-base.apk', 100) |
| |
| # Invalid file extensions. |
| self._CheckUntranslated( |
| translator, '/data/data/com.example.app-2/base', 100) |
| |
| self._CheckUntranslated( |
| translator, '/data/data/com.example.app-2/base.apk.dex', 100) |
| |
| def testBundleApks(self): |
| mock_libs1 = MockApkNativeLibraries() |
| mock_libs1.AddTestEntries([ |
| ('lib/test-abi/libfoo.so', 200, 2000), |
| ('lib/test-abi/libbbar.so', 3200, 3000), |
| ]) |
| mock_libs2 = MockApkNativeLibraries() |
| mock_libs2.AddTestEntries([ |
| ('lib/test-abi/libzoo.so', 200, 2000), |
| ('lib/test-abi/libtool.so', 3000, 4000), |
| ]) |
| translator = apk_native_libs.ApkLibraryPathTranslator() |
| translator.AddHostApk('com.example.app', mock_libs1, 'base-master.apk') |
| translator.AddHostApk('com.example.app', mock_libs2, 'feature-master.apk') |
| |
| self._CheckTranslated( |
| translator, |
| '/data/app/com.example.app-XUIYIUW/base-master.apk', 757, |
| '/data/app/com.example.app-XUIYIUW/base-master.apk!lib/libfoo.so', 557) |
| |
| self._CheckTranslated( |
| translator, |
| '/data/app/com.example.app-XUIYIUW/feature-master.apk', 3200, |
| '/data/app/com.example.app-XUIYIUW/feature-master.apk!lib/libtool.so', |
| 200) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |