diff --git a/.clang-format b/.clang-format index 1a3fe26..7fa2ae1 100644 --- a/.clang-format +++ b/.clang-format @@ -15,12 +15,12 @@ AlignArrayOfStructures: Left AlignConsecutiveMacros: AcrossEmptyLinesAndComments AlignOperands: false AlignTrailingComments: true -AlignEscapedNewlines: Right +AlignEscapedNewlines: Right AllowAllArgumentsOnNextLine: true AllowShortBlocksOnASingleLine: Empty AllowShortLambdasOnASingleLine: Empty AllowShortLoopsOnASingleLine: true -BraceWrapping: +BraceWrapping: AfterCaseLabel: false AfterClass: false AfterControlStatement: false @@ -41,7 +41,7 @@ BreakBeforeBraces: Custom BreakConstructorInitializers: AfterColon ColumnLimit: 120 ContinuationIndentWidth: 2 -IncludeCategories: +IncludeCategories: - Regex: '^<.*' Priority: 1 - Regex: '^".*' diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f1b1b98..a1e6f60 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,66 +2,69 @@ name: Android CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: set up JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'temurin' - cache: gradle + - uses: actions/checkout@v3 + - name: set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: "11" + distribution: "temurin" + cache: gradle + + - name: Install store file + run: echo ${{ secrets.STORE_FILE }} | base64 -d > fake.jks + - name: Install build configuration + run: | + echo storeFile=`pwd`/fake.jks > local.properties + echo storePassword=${{ secrets.STORE_PASSWORD }} >> local.properties + echo keyAlias=${{ secrets.KEY_ALIAS }} >> local.properties + echo keyPassword=${{ secrets.KEY_PASSWORD }} >> local.properties + echo logLevel=3 >> local.properties + echo targetSdk=33 >> local.properties + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build -PmergeBuild=true -PemulatorBuild=false + - name: Copy output + run: | + mkdir -p out/debug/test_binary out/release/test_binary out/debug/test_apk out/release/test_apk + mv fakelinker-test/src/main/cpp/build/Debug out/debug/test_binary + mv fakelinker-test/src/main/cpp/build/Release out/release/test_binary + mv fakelinker-test/build/outputs/apk/debug/fakelinker-test-debug.apk out/debug/test_apk/fakelinker-test-debug.apk + mv library/build/outputs/aar/library-debug.aar out/faklinker-lib-debug.aar + mv library/build/outputs/aar/library-release.aar out/fakelinker-lib-release.aar - - name: Install store file - run: echo ${{ secrets.STORE_FILE }} | base64 -d > fake.jks - - name: Install build configuration - run: | - echo storeFile=`pwd`/fake.jks > local.properties - echo storePassword=${{ secrets.STORE_PASSWORD }} >> local.properties - echo keyAlias=${{ secrets.KEY_ALIAS }} >> local.properties - echo keyPassword=${{ secrets.KEY_PASSWORD }} >> local.properties - echo logLevel=3 >> local.properties - echo targetSdk=33 >> local.properties - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build -PmergeBuild=true -PemulatorBuild=false - - name: Copy output - run: | - mkdir -p out/debug/test_binary out/release/test_binary - mv emualtor-testapp/build/outputs/apk/debug/emualtor-testapp-debug.apk out/debug/testapp-hook-debug.apk - mv emualtor-testapp/build/outputs/apk/release/emualtor-testapp-release.apk out/release/testapp-hook-release.apk - mv library/src/main/cpp/build/Debug out/debug/test_binary - mv library/src/main/cpp/build/Release out/release/test_binary - mv library/build/outputs/aar/library-debug.aar out/faklinker-debug.aar - mv library/build/outputs/aar/library-release.aar out/fakelinker-release.aar + - name: Build android test version with Gradle + run: ./gradlew fakelinker-test:assembleDebugAndroidTest + - name: Copy android test output + run: | + mv fakelinker-test/build/outputs/apk/androidTest/debug/fakelinker-test-debug-androidTest.apk out/debug/test_apk/fakelinker-androidtest-debug.apk - - name: Build emulator version with Gradle - run: | - ./gradlew clean - ./gradlew build -PmergeBuild=true -PemulatorBuild=true + - name: Build emulator version with Gradle + run: | + ./gradlew clean + ./gradlew build -PmergeBuild=true -PemulatorBuild=true - - name: Copy emulator version output - run: | - mv emualtor-testapp/build/outputs/apk/debug/emualtor-testapp-debug.apk out/debug/emulator-testapp-hook-debug.apk - mv emualtor-testapp/build/outputs/apk/release/emualtor-testapp-release.apk out/release/emulator-testapp-hook-release.apk - mv library/build/outputs/aar/library-debug.aar out/faklinker-emualtor-debug.aar - mv library/build/outputs/aar/library-release.aar out/fakelinker-emulator-release.aar + - name: Copy emulator version output + run: | + mv emulator-testapp/build/outputs/apk/debug/emulator-testapp-debug.apk out/debug/test_apk/fakelinker-emulator-test-debug.apk + mv emulator-testapp/build/outputs/apk/release/emulator-testapp-release.apk out/release/fakelinker-emulator-test-release.apk + mv library/build/outputs/aar/library-debug.aar out/faklinker-emulator-lib-debug.aar + mv library/build/outputs/aar/library-release.aar out/fakelinker-emulator-lib-release.aar - - name: Uninstall store file - run: rm -f fake.jks - - name: Upload Build Artifact - uses: actions/upload-artifact@v3.1.1 - with: - name: ${{ github.sha }}-build - # A file, directory or wildcard pattern that describes what to upload - path: out - + - name: Uninstall store file + run: rm -f fake.jks + - name: Upload Build Artifact + uses: actions/upload-artifact@v3.1.1 + with: + name: ${{ github.sha }}-build + # A file, directory or wildcard pattern that describes what to upload + path: out diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d92d1ce..9fbeba2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,24 @@ repos: - id: chinese-transform-encoding - id: crlf-to-lf - repo: https://github.com/pre-commit/mirrors-clang-format - rev: 'v14.0.6' + rev: "v14.0.6" hooks: - - id: clang-format + - id: clang-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: double-quote-string-fixer + - id: name-tests-test + - id: requirements-txt-fixer + - id: check-case-conflict + - id: fix-encoding-pragma + - id: mixed-line-ending + - repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.0 + hooks: + - id: autopep8 + - repo: https://github.com/asottile/reorder_python_imports + rev: v3.9.0 + hooks: + - id: reorder-python-imports diff --git a/README.md b/README.md index 7fb42f9..300afbf 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The module has integrated the installation executable file, and the Java layer c } return ret; } - + static void InitHook() { int error_code; // Add this hook module to the global module, which will affect all modules loaded later @@ -84,4 +84,4 @@ The module has integrated the installation executable file, and the Java layer c } ``` 3. For other more uses -[FakeXposed](https://github.com/sanfengAndroid/FakeXposed) `Xposed`,`root` Shield detection,File redirection, etc. \ No newline at end of file +[FakeXposed](https://github.com/sanfengAndroid/FakeXposed) `Xposed`,`root` Shield detection,File redirection, etc. \ No newline at end of file diff --git a/README_CN.md b/README_CN.md index 76d1a33..148f5ca 100644 --- a/README_CN.md +++ b/README_CN.md @@ -57,7 +57,7 @@ } return ret; } - + static void InitHook() { int error_code; // 将本Hook模块添加到全局模块中,这会影响之后加载的所有模块 diff --git a/browser_stack_test.py b/browser_stack_test.py new file mode 100644 index 0000000..9931dad --- /dev/null +++ b/browser_stack_test.py @@ -0,0 +1,1021 @@ +# -*- coding: utf-8 -*- +import argparse +import logging +import random +import sys +import time +from enum import Enum +from enum import unique +from pathlib import Path + +import requests + + +@unique +class DeviceBrand(Enum): + Samsung = 'Samsung' + Google = 'Google' + OnePlus = 'OnePlus' + Xiaomi = 'Xiaomi' + Vivo = 'Vivo' + Oppo = 'Oppo' + Motorola = 'Motorola' + Huawei = 'Huawei' + SansungTablet = 'Samsung Galaxy Tab' + + @staticmethod + def to_brand(brand_str: str): + for brand in DeviceBrand: + if brand_str == brand.value: + return brand + raise Exception(f'Unknown brand {brand_str}') + + +@unique +class TestState(Enum): + Running = 'running' + Failed = 'failed' + Error = 'error' + TimedOut = 'timed out' + Passed = 'passed' + Queued = 'queued' + Skipped = 'skipped' + + def to_state(state_str: str): + if not state_str: + return TestState.Running + state_str = state_str.lower() + for state in TestState: + if state.value == state_str: + return state + raise Exception(f'Unknown test state {state_str}') + + def is_complete(self): + if self == TestState.Failed: + return True + if self == TestState.Error: + return True + if self == TestState.TimedOut: + return True + if self == TestState.Passed: + return True + if self == TestState.Skipped: + return True + return False + + def is_success(self): + if self == TestState.Passed: + return True + if self == TestState.Skipped: + return True + return False + + +API_MAP = { + '13.0': 33, + '12.0': 31, + '11.0': 30, + '10.0': 29, + '9.0': 28, + '8.1': 27, + '8.0': 26, + '7.1': 25, + '7.1.1': 25, + '7.0': 24, + '6.0': 23, + '5.1': 22, + '5.0': 21 +} + +MAX_PARALLEL = 1 +BUILD_QUERY_INTERVAL = 15 +TEST_PROJECT = 'fakelinker' +DEVICE_LOG_ENABLE = True + + +def api_string_to_level(api_str) -> int: + return API_MAP.get(api_str, 0) + + +class DeviceInfo: + def __init__(self, device: str) -> None: + strs = device.split(' ') + self.brand = DeviceBrand.to_brand(strs[0]) + self.name = device + self.api = api_string_to_level(strs[-1].split('-')[-1]) + + def is_device(self, name, version): + return self.name == f'{name}-{version}' + + def __eq__(self, other): + if isinstance(other, DeviceInfo): + return self.name == other.name + return False + + def __hash__(self): + return self.name.__hash__() + + +class BrowserStackDevice: + default_device = DeviceInfo('Google Pixel 7 Pro-13.0') + + def __init__(self) -> None: + self.android_13 = [] + self.android_13.append(DeviceInfo('Google Pixel 7 Pro-13.0')) + self.android_13.append(DeviceInfo('Google Pixel 7-13.0')) + self.android_13.append(DeviceInfo('Google Pixel 6 Pro-13.0')) + + self.android_12 = [] + self.android_12.append(DeviceInfo('Samsung Galaxy S22 Ultra-12.0')) + self.android_12.append(DeviceInfo('Samsung Galaxy S22 Plus-12.0')) + self.android_12.append(DeviceInfo('Samsung Galaxy S22-12.0')) + self.android_12.append(DeviceInfo('Samsung Galaxy S21-12.0')) + self.android_12.append(DeviceInfo('Google Pixel 6 Pro-12.0')) + self.android_12.append(DeviceInfo('Google Pixel 6-12.0')) + self.android_12.append(DeviceInfo('Google Pixel 5-12.0')) + self.android_12.append(DeviceInfo('Samsung Galaxy Tab S8-12.0')) + + self.android_11 = [] + self.android_11.append(DeviceInfo('Samsung Galaxy S21 Ultra-11.0')) + self.android_11.append(DeviceInfo('Samsung Galaxy S21-11.0')) + self.android_11.append(DeviceInfo('Samsung Galaxy S21 Plus-11.0')) + self.android_11.append(DeviceInfo('Samsung Galaxy M52-11.0')) + self.android_11.append(DeviceInfo('Samsung Galaxy M32-11.0')) + self.android_11.append(DeviceInfo('Samsung Galaxy A52-11.0')) + self.android_11.append(DeviceInfo('Google Pixel 5-11.0')) + self.android_11.append(DeviceInfo('Google Pixel 4-11.0')) + self.android_11.append(DeviceInfo('Xiaomi Redmi Note 11-11.0')) + self.android_11.append(DeviceInfo('Vivo Y21-11.0')) + self.android_11.append(DeviceInfo('Vivo V21-11.0')) + self.android_11.append(DeviceInfo('Oppo Reno 6-11.0')) + self.android_11.append(DeviceInfo('Oppo A96-11.0')) + self.android_11.append(DeviceInfo('Motorola Moto G71 5G-11.0')) + self.android_11.append(DeviceInfo('Samsung Galaxy Tab S7-11.0')) + + self.android_10 = [] + self.android_10.append(DeviceInfo('Samsung Galaxy S20-10.0')) + self.android_10.append(DeviceInfo('Samsung Galaxy S20 Plus-10.0')) + self.android_10.append(DeviceInfo('Samsung Galaxy S20 Ultra-10.0')) + self.android_10.append(DeviceInfo('Samsung Galaxy Note 20 Ultra-10.0')) + self.android_10.append(DeviceInfo('Samsung Galaxy Note 20-10.0')) + self.android_10.append(DeviceInfo('Samsung Galaxy A51-10.0')) + self.android_10.append(DeviceInfo('Samsung Galaxy A11-10.0')) + self.android_10.append(DeviceInfo('Google Pixel 4 XL-10.0')) + self.android_10.append(DeviceInfo('Google Pixel 4-10.0')) + self.android_10.append(DeviceInfo('Google Pixel 3-10.0')) + self.android_10.append(DeviceInfo('OnePlus 8-10.0')) + self.android_10.append(DeviceInfo('OnePlus 7T-10.0')) + self.android_10.append(DeviceInfo('Xiaomi Redmi Note 9-10.0')) + self.android_10.append(DeviceInfo('Oppo Reno 3 Pro-10.0')) + self.android_10.append(DeviceInfo('Motorola Moto G9 Play-10.0')) + self.android_10.append(DeviceInfo('Samsung Galaxy Tab S7-10.0')) + + self.android_9 = [] + self.android_9.append(DeviceInfo('Samsung Galaxy S9 Plus-9.0')) + self.android_9.append(DeviceInfo('Samsung Galaxy S10e-9.0')) + self.android_9.append(DeviceInfo('Samsung Galaxy S10 Plus-9.0')) + self.android_9.append(DeviceInfo('Samsung Galaxy S10-9.0')) + self.android_9.append(DeviceInfo('Samsung Galaxy Note 10 Plus-9.0')) + self.android_9.append(DeviceInfo('Samsung Galaxy Note 10-9.0')) + self.android_9.append(DeviceInfo('Samsung Galaxy A10-9.0')) + self.android_9.append(DeviceInfo('Google Pixel 3a XL-9.0')) + self.android_9.append(DeviceInfo('Google Pixel 3a-9.0')) + self.android_9.append(DeviceInfo('Google Pixel 3 XL-9.0')) + self.android_9.append(DeviceInfo('Google Pixel 3-9.0')) + self.android_9.append(DeviceInfo('Google Pixel 2-9.0')) + self.android_9.append(DeviceInfo('OnePlus 7-9.0')) + self.android_9.append(DeviceInfo('OnePlus 6T-9.0')) + self.android_9.append(DeviceInfo('Xiaomi Redmi Note 8-9.0')) + self.android_9.append(DeviceInfo('Xiaomi Redmi Note 7-9.0')) + self.android_9.append(DeviceInfo('Motorola Moto G7 Play-9.0')) + self.android_9.append(DeviceInfo('Huawei P30-9.0')) + self.android_9.append(DeviceInfo('Samsung Galaxy Tab S6-9.0')) + self.android_9.append(DeviceInfo('Samsung Galaxy Tab S5e-9.0')) + + self.android_8_1 = [] + self.android_8_1.append(DeviceInfo('Samsung Galaxy Note 9-8.1')) + self.android_8_1.append(DeviceInfo('Samsung Galaxy J7 Prime-8.1')) + self.android_8_1.append(DeviceInfo('Samsung Galaxy Tab S4-8.1')) + + self.android_8_0 = [] + self.android_8_0.append(DeviceInfo('Samsung Galaxy S9 Plus-8.0')) + self.android_8_0.append(DeviceInfo('Samsung Galaxy S9-8.0')) + self.android_8_0.append(DeviceInfo('Google Pixel 2-8.0')) + + self.android_7_1 = [] + self.android_7_1.append(DeviceInfo('Samsung Galaxy Note 8-7.1')) + self.android_7_1.append(DeviceInfo('Samsung Galaxy A8-7.1')) + self.android_7_1.append(DeviceInfo('Google Pixel-7.1')) + + self.android_7_0 = [] + self.android_7_0.append(DeviceInfo('Samsung Galaxy S8 Plus-7.0')) + self.android_7_0.append(DeviceInfo('Samsung Galaxy S8-7.0')) + + self.android_6 = [] + self.android_6.append(DeviceInfo('Samsung Galaxy S7-6.0')) + self.android_6.append(DeviceInfo('Google Nexus 6-6.0')) + + self.android_5_1 = [] + + self.android_5_0 = [] + self.android_5_0.append(DeviceInfo('Samsung Galaxy S6-5.0')) + + self.devices = { + 33: self.android_13, + 32: self.android_12, + 31: self.android_12, + 30: self.android_11, + 29: self.android_10, + 28: self.android_9, + 27: self.android_8_1, + 26: self.android_8_0, + 25: self.android_7_1, + 24: self.android_7_0, + 23: self.android_6, + 22: self.android_5_1, + 21: self.android_5_0 + } + + def random_device(self, api_level=33): + devices = self.devices.get(api_level, self.android_13) + return random.choice(devices) + + def random_devices(self, apis: list[int] = [33]): + result = set() + for level in apis: + devices = self.devices.get(level) + if devices: + result.add(random.choice(devices)) + + return list(result) + + def find_device(self, info: DeviceInfo) -> DeviceInfo: + devices: list[DeviceInfo] = self.devices.get(info.api, []) + if devices.count(info) != 0: + return info + return None + + +class BrowserStackResponse: + def __init__(self, res: requests.Response) -> None: + self.res = res + if not res.encoding: + res.encoding = 'utf-8' + try: + self.value: dict = res.json() + except requests.exceptions.JSONDecodeError as e: + logging.error('The request returns a non-json format %s', e) + self.value = res.content + if not res.ok: + logging.error('request error: %s', self.content) + raise requests.exceptions.RequestException( + f'The request returned an error code: {res.status_code}') + + logging.debug('request url:%s, response:\n%s', res.url, self.value) + + def parse_value(self, name: str, default_value=None): + return self.value.get(name, default_value) + + +class BrowserStackBean: + def __init__(self, res: BrowserStackResponse | dict) -> None: + if isinstance(res, BrowserStackResponse): + self.data = res.value + else: + self.data = res + + def parse_value(self, name: str, default_value=None): + return self.data.get(name, default_value) + + def parse_time(self, name: str): + value = self.parse_value(name) + if value: + try: + s_time = time.strptime(value, '%Y-%m-%d %H:%M:%S %Z') + except ValueError: + s_time = time.strptime(value, '%Y-%m-%d %H:%M:%S %z') + return int(time.mktime(s_time)) + return int(time.time()) + + def parse_state(self, name: str): + val = self.parse_value(name) + if not val: + return TestState.Running + return TestState.to_state(val) + + def parse_int(self, name: str, default_value=-1): + val = self.parse_value(name, default_value) + if not val: + return default_value + return int(val) + + +class AppBean(BrowserStackBean): + def __init__(self, res) -> None: + super().__init__(res) + self.app_name = self.parse_value('app_name') + self.app_url = self.parse_value('app_url') + self.app_version = self.parse_value('app_version') + self.app_id = self.parse_value('app_id') + self.uploaded_at = self.parse_time('uploaded_at') + self.custom_id = self.parse_value('custom_id') + self.shareable_id = self.parse_value('shareable_id') + self.expiry = self.parse_time('expiry') + + def get_app_url(self): + if self.custom_id: + return self.custom_id + if self.shareable_id: + return self.shareable_id + return self.app_url + + +class TestSuiteBean(BrowserStackBean): + def __init__(self, res) -> None: + super().__init__(res) + self.test_suite_name = self.parse_value('test_suite_name') + self.test_suite_url = self.parse_value('test_suite_url') + self.test_suite_id = self.parse_value('test_suite_id') + self.uploaded_at = self.parse_time('uploaded_at') + self.custom_id = self.parse_value('custom_id') + self.shareable_id = self.parse_value('shareable_id') + self.framework = self.parse_value('framework') + self.expiry = self.parse_time('expiry') + + def get_test_suite_url(self): + if self.custom_id: + return self.custom_id + if self.shareable_id: + return self.shareable_id + return self.test_suite_url + + +class SessionBean(BrowserStackBean): + def __init__(self, res) -> None: + super().__init__(res) + self.id = self.parse_value('id') + self.status = self.parse_state('status') + self.start_time = self.parse_time('start_time') + self.duration = self.parse_int('duration') + self.testcases = self.parse_value('testcases') + + def is_success(self): + return self.status.is_success() + + +class BuildBean(BrowserStackBean): + def __init__(self, res) -> None: + super().__init__(res) + self.build_id = self.parse_value('build_id') + self.message = self.parse_value('message') + if not self.build_id: + self.id = self.parse_value('id') + self.start_time = self.parse_value('start_time') + + self.framework = self.parse_value('framework') + if self.framework: + self.duration = self.parse_int('duration') + self.status = self.parse_state('status') + self.input_capabilities = self.parse_value( + 'input_capabilities') + self.start_time = self.parse_time('start_time') + self.app_details = self.parse_value('app_details') + self.test_suite_details = self.parse_value( + 'test_suite_details') + self.devices: list = self.parse_value('devices') + + def parse_session(self, device: DeviceInfo = None) -> list[SessionBean]: + if not self.devices or len(self.devices) == 0: + return [] + device_info: dict = None + for info in self.devices: + if not device or device.is_device(info.get('device'), info.get('os_version')): + device_info = info + break + if not device_info: + return [] + return [SessionBean(session) for session in device_info.get('sessions')] + + def parse_failed_device(self) -> list[DeviceInfo]: + result = [] + for info in self.devices: + name = info.get('device') + version = info.get('os_version') + for session_info in info.get('sessions'): + if not SessionBean(session_info).is_success(): + result.append(DeviceInfo(f'{name}-{version}')) + break + return result + + +class BrowserStack: + _cloud_api_url = 'https://api-cloud.browserstack.com/' + + def __init__(self, auth: str) -> None: + ss = auth.split(':') + self.user = ss[0] + self.password = ss[1] + + def post(self, url: str, files: dict = None, json_data: dict = None, headers: dict = None): + res = requests.post(BrowserStack._cloud_api_url + url, auth=(self.user, self.password), + json=json_data, headers=headers, files=files) + logging.debug( + f'post request url: {url}\n\tfiles: {files}\n\tjson content: {json_data}\n\theaders:{headers}') + return BrowserStackResponse(res) + + def get(self, url: str, app_id: str = '', test_suite_id: str = '', build_id: str = '', session_id: str = ''): + url = BrowserStack._cloud_api_url + url + url = url.format(app_id=app_id, test_suite_id=test_suite_id, + build_id=build_id, session_id=session_id) + logging.debug(f'get request url:{url}') + return BrowserStackResponse(requests.get(url, auth=(self.user, self.password))) + + def delete(self, url: str, app_id: str = '', test_suite_id: str = '', build_id: str = '', session_id: str = ''): + url = BrowserStack._cloud_api_url + url + url = url.format(app_id=app_id, test_suite_id=test_suite_id, + build_id=build_id, session_id=session_id) + logging.debug(f'delete request url:{url}') + return BrowserStackResponse(requests.delete(url, auth=(self.user, self.password))) + + +class BrowserStackApp(BrowserStack): + # POST + _upload_app_url = 'app-automate/espresso/v2/app' + # GET + _list_upload_app_url = 'app-automate/espresso/v2/apps' + # GET + _app_detail_get_url = 'app-automate/espresso/v2/apps/{app_id}' + # DELETE + _delete_app_url = 'app-automate/espresso/v2/apps/{app_id}' + + def __init__(self, auth: str, app_id=None, custom_id=None) -> None: + super().__init__(auth) + self.app_id = app_id + self.custom_id = custom_id + + def _get_app_id(self, aid): + app_id = aid if aid else self.app_id + if isinstance(app_id, AppBean): + return app_id.app_id + return app_id + + def _get_custom_id(self, custom_id): + id = custom_id if custom_id else self.custom_id + if isinstance(id, AppBean): + return id.custom_id + return id + + def update_app(self, file: Path, custom_id: AppBean | str = None) -> AppBean: + if not file.is_file(): + raise FileExistsError(f'update file not exist {file}') + files = { + 'file': file.open('rb') + } + custom_id = self._get_custom_id(custom_id) + if custom_id: + files['custom_id'] = (None, custom_id) + return AppBean(self.post(self._upload_app_url, files=files)) + + def list_upload_app(self) -> list[AppBean]: + res = self.get(self._list_upload_app_url) + return [AppBean(data) for data in res.parse_value('apps')] + + def get_last_uplad_app(self) -> AppBean: + apps = self.list_upload_app() + if apps: + return apps[0] + return None + + def find_custom_id_app(self, custom_id: str) -> AppBean: + result = None + for app in self.list_upload_app(): + if app.custom_id == custom_id: + if not result or app.uploaded_at > result.uploaded_at: + result = app + return app + + def get_app_details(self, app_id: AppBean | str = None) -> AppBean: + res = self.get(self._app_detail_get_url, + app_id=self._get_app_id(app_id)) + return AppBean(res.parse_value('app')) + + def delete_app(self, app_id: AppBean | str = None) -> bool: + try: + res = self.delete(self._delete_app_url, + app_id=self._get_app_id(app_id)) + return res.parse_value('success') != None + except requests.exceptions.RequestException as e: + logging.error('Delete app failed app_id: %s, error: %s', app_id, e) + return False + + def delete_recent_app(self): + for app in self.list_upload_app(): + logging.info(f'delete recent app %s result: %s', + app.app_id, self.delete_app(app)) + + +class BrowserStackTestSuite(BrowserStack): + # POST + _upload_test_suite_url = 'app-automate/espresso/v2/test-suite' + # GET + _list_test_suites_url = 'app-automate/espresso/v2/test-suites' + # GET + _test_suite_get_url = 'app-automate/espresso/v2/test-suites/{test_suite_id}' + # DELETE + _delete_test_suite_url = 'app-automate/espresso/v2/test-suites/{test_suite_id}' + + def __init__(self, auth: str, test_suite_id=None, custom_id=None) -> None: + super().__init__(auth) + self.test_suite_id = test_suite_id + self.custom_id = custom_id + + def _get_test_suite_id(self, tid): + suite = tid if tid else self.test_suite_id + if isinstance(suite, TestSuiteBean): + return suite.test_suite_id + return suite + + def _get_custom_id(self, custom_id): + id = custom_id if custom_id else self.custom_id + if isinstance(id, TestSuiteBean): + return id.custom_id + return id + + def upload_test_suite(self, file: Path, custom_id: TestSuiteBean | str = None): + if not file.is_file(): + raise FileExistsError(f'update file not exist {file}') + files = { + 'file': file.open('rb') + } + custom_id = self._get_custom_id(custom_id) + if custom_id: + files['custom_id'] = (None, custom_id) + return TestSuiteBean(self.post(self._upload_test_suite_url, files=files)) + + def list_test_suites(self) -> list[TestSuiteBean]: + res = self.get(self._list_test_suites_url) + return [TestSuiteBean(suite) for suite in res.parse_value('test_suites')] + + def get_last_test_suite(self) -> TestSuiteBean: + suites = self.list_test_suites() + if len(suites) > 0: + return suites[0] + return None + + def find_custom_id_test_suite(self, custom_id) -> TestSuiteBean: + result = None + for suite in self.list_test_suites(): + if suite.custom_id == custom_id: + if not result or suite.uploaded_at > result.uploaded_at: + result = suite + return result + + def get_test_suite_details(self, test_suite_id: TestSuiteBean | str = None) -> TestSuiteBean: + res = self.get(self._test_suite_get_url, + test_suite_id=self._get_test_suite_id(test_suite_id)) + return TestSuiteBean(res.parse_value('test_suite')) + + def delete_test_suite(self, test_suite_id: TestSuiteBean | str = None) -> bool: + try: + res = self.delete(self._delete_test_suite_url, + test_suite_id=self._get_test_suite_id(test_suite_id)) + return res.parse_value('success') != None + except requests.exceptions.RequestException as e: + logging.error( + 'Delete test suite failed id: %s, error: %s', test_suite_id, e) + return False + + def delete_recent_test_suite(self): + for suite in self.list_test_suites(): + logging.info('delete test suite %s result %s', + suite.test_suite_id, self.delete_test_suite(suite)) + + +class BrowserStackBuild(BrowserStack): + # POST + _espresso_build_url = 'app-automate/espresso/v2/build' + # GET + _build_state_get_url = 'app-automate/espresso/v2/builds/{build_id}' + # GET + _list_recent_builds_url = 'app-automate/espresso/v2/builds' + + def __init__(self, auth: str, build_id=None, app_url=None, test_suite_url=None, devices: list[DeviceInfo] = []) -> None: + super().__init__(auth) + self.build_id = build_id + self.test_suite_url = test_suite_url + self.app_url = app_url + self.devices = [] + + def _get_build_id(self, bid): + build = bid if bid else self.build_id + if isinstance(build, BuildBean): + return build.build_id if build.build_id else build.id + return build + + def _get_app_url(self, url): + app = url if url else self.app_url + if isinstance(app, AppBean): + return app.get_app_url() + return app + + def _get_test_suite_url(self, url): + suite = url if url else self.test_suite_url + if isinstance(suite, TestSuiteBean): + return suite.get_test_suite_url() + return suite + + def _get_devices(self, ds: list[DeviceInfo]): + device_list = ds + if not ds or len(ds) == 0: + device_list = self.devices + + return [device.name for device in device_list] + + def espresso_build(self, app_url: AppBean | str = None, test_suite_url: TestSuiteBean | str = None, devices: list[DeviceInfo] = [BrowserStackDevice.default_device]) -> BuildBean: + data = { + 'app': self._get_app_url(app_url), + 'testSuite': self._get_test_suite_url(test_suite_url), + 'devices': self._get_devices(devices) + } + if TEST_PROJECT: + data['project'] = TEST_PROJECT + if DEVICE_LOG_ENABLE: + data['deviceLogs'] = True + + return BuildBean(self.post(self._espresso_build_url, json_data=data)) + + def get_build_state(self, build_id: BuildBean | str = None) -> BuildBean: + return BuildBean(self.get(self._build_state_get_url, build_id=self._get_build_id(build_id))) + + def list_recent_builds(self) -> list[BuildBean]: + res = self.get(self._list_recent_builds_url) + return [BuildBean(build) for build in res.value] + + def get_last_build_task(self) -> BuildBean: + tasks = self.list_recent_builds() + if len(tasks) > 0: + return tasks[0] + return None + + @staticmethod + def split_device_chunks(devices, n): + """Yield successive n-sized chunks from lst.""" + for i in range(0, len(devices), n): + yield devices[i:i + n] + + def build_device_test(self, app_url: AppBean | str = None, test_suite_url: TestSuiteBean | str = None, + devices: list[DeviceInfo] = BrowserStackDevice.default_device) -> list[BuildBean]: + build_result = [] + + for split_devices in BrowserStackBuild.split_device_chunks(devices, MAX_PARALLEL * 2): + build_bean = self.espresso_build( + app_url, test_suite_url, split_devices) + build_state = None + while True: + build_state = self.get_build_state(build_bean) + if build_state.status.is_complete(): + break + logging.info('The test task is being executed: %s', + build_state.status) + time.sleep(BUILD_QUERY_INTERVAL) + + if build_state.status.is_success(): + logging.info('test app all pass') + else: + logging.info('test app exist error') + build_result.append(build_state) + + return build_result + + def get_build_test(self, build_bean: BuildBean): + build_state = None + while True: + build_state = self.get_build_state(build_bean) + if build_state.status.is_complete(): + break + logging.info('The test task is being executed: %s', + build_state.status) + time.sleep(BUILD_QUERY_INTERVAL) + if build_state.status.is_success(): + logging.info('test app all pass') + else: + logging.info('test app exist error') + return build_state + + +class BrowserStackSession(BrowserStack): + # GET + _session_details_get_url = 'app-automate/espresso/v2/builds/{build_id}/sessions/{session_id}' + # GET + _juint_report_get_url = 'app-automate/espresso/v2/builds/{build_id}/sessions/{session_id}/report' + # GET + _code_coverage_get_url = 'app-automate/espresso/v2/builds/{build_id}/sessions/{session_id}/coverage' + + def __init__(self, auth: str, build_id=None, session_id=None) -> None: + super().__init__(auth) + self.build_id: str = build_id + self.session_id: str = session_id + + def _get_session_id(self, sid): + session = sid if sid else self.session_id + if isinstance(session, SessionBean): + return session.id + return session + + def _get_build_id(self, bid): + build = bid if bid else self.build_id + if isinstance(build, BuildBean): + return build.build_id if build.build_id else build.id + return build + + def _get(self, url, build_id=None, session_id=None): + return self.get(url, build_id=self._get_build_id(build_id), session_id=self._get_session_id(session_id)) + + def get_session_details(self, build_id: BuildBean | str = None, session_id: SessionBean | str = None): + res = self._get(self._session_details_get_url, build_id, session_id) + return SessionBean(res) + + def get_juint_report(self, build_id: BuildBean | str = None, session_id: SessionBean | str = None) -> str: + return self._get(self._juint_report_get_url, build_id, session_id) + + def get_code_coverage(self, build_id: BuildBean | str = None, session_id: SessionBean | str = None): + return self._get(self._code_coverage_get_url, build_id, session_id) + + +def get_last_build_test_task(user: str) -> BuildBean: + try: + build = BrowserStackBuild(user) + task = build.get_last_build_task() + if not task: + logging.error( + 'There is no build test task, please create it and try again') + return 12 + return build.get_build_test(task) + except requests.exceptions.RequestException as e: + logging.error('get last recent build info error: %s', e) + raise e + + +def execute_remove_command(args): + if args.apk: + app = BrowserStackApp(args.user) + app.delete_recent_app() + + if args.suite: + suite = BrowserStackTestSuite(args.user) + suite.delete_recent_test_suite() + + return 0 + + +def parse_test_devices(api: list[int], all_api: bool, names: list[str]) -> list[DeviceInfo]: + result = [] + device = BrowserStackDevice() + if names: + devices = set() + for name in names: + info = device.find_device(DeviceInfo(name)) + if info: + devices.add(info) + else: + logging.error( + 'The specified device name was not found: %s', name) + result = list(devices) + elif all_api: + api = [33, 32, 31, 30, 29, 28, 27, 26, 25, 25, 24, 23, 22, 21] + result = device.random_devices(api) + else: + result = device.random_devices(api) + return result + + +def parse_test_app(user: str, custom_id: str, apk: Path) -> AppBean: + app = BrowserStackApp(user) + if apk and apk.is_file(): + bean = app.update_app(apk, custom_id) + elif custom_id: + bean = app.find_custom_id_app(custom_id) + else: + bean = app.get_last_uplad_app() + return bean + + +def parse_test_suite(user: str, custom_id: str, suite_path: Path) -> TestSuiteBean: + suite = BrowserStackTestSuite(user) + if suite_path and suite_path.is_file(): + bean = suite.upload_test_suite(suite_path, custom_id) + elif custom_id: + bean = suite.find_custom_id_test_suite(custom_id) + else: + bean = suite.get_last_test_suite() + return bean + + +def execute_test_command(args): + global MAX_PARALLEL + MAX_PARALLEL = args.max_parallel + global BUILD_QUERY_INTERVAL + BUILD_QUERY_INTERVAL = args.query_interval + + if args.is_32bit: + if args.apk_custom_id: + args.apk_custom_id += '32' + if args.project: + args.project += '32' + + if args.project: + global TEST_PROJECT + TEST_PROJECT = args.project + + global DEVICE_LOG_ENABLE + DEVICE_LOG_ENABLE = args.device_log + + if args.get_last_build: + get_last_build_test_task(args.user) + return 0 + + if args.build_last_faild: + task = get_last_build_test_task(args.user) + test_devices = task.parse_failed_device() + else: + test_devices = parse_test_devices(args.api, args.all_api, args.devices) + + if not test_devices: + logging.error('There is no device to test') + return 10 + + app_bean = parse_test_app(args.user, args.apk_custom_id, args.apk) + if not app_bean: + logging.error( + 'The test apk does not exist or the upload path `%s` does not exist, please upload and try again', args.apk) + return 11 + + suite_bean = parse_test_suite( + args.user, args.test_suite_custom_id, args.test_suite) + if not suite_bean: + logging.error( + 'The test suite does not exist or the upload path `%s` does not exist, please upload and try again', args.test_suite) + return 12 + + try: + build = BrowserStackBuild(args.user) + success = True + for state in build.build_device_test(app_bean, suite_bean, test_devices): + if state.status.is_success(): + logging.info('test build %s all passed', state.id) + else: + logging.error('test build %s has errors', state.id) + success = False + return 0 if success else 13 + except requests.exceptions.RequestException as e: + logging.error('test app error: %s', e) + raise e + + +class PathAction(argparse._StoreAction): + def __init__(self, option_strings, dest, must_exist=True, nargs=None, **kwargs) -> None: + self._must_exist = must_exist + super().__init__(option_strings, dest, nargs, **kwargs) + + def check_value(self, value, option_string): + path = Path(value).resolve() + if self._must_exist and not path.exists(): + name = option_string if option_string else self.dest.upper() + raise argparse.ArgumentError( + self, f'input path does not exist: {name} {path}') + return path + + def __call__(self, parser, namespace, values, option_string=None) -> None: + if values == None or len(values) < 1: + raise argparse.ArgumentError( + self, f'The required path parameter does not exist: {option_string}') + if isinstance(values, list): + paths = [self.check_value(x, option_string) for x in values] + else: + paths = self.check_value(values, option_string) + setattr(namespace, self.dest, paths) + + +class FileAction(PathAction): + def check_value(self, value, option_string): + p = Path(value).resolve() + if (p.exists() and not p.is_file()) or (self._must_exist and not p.is_file()): + name = option_string if option_string else self.dest.upper() + raise argparse.ArgumentError( + self, f'Invalid file path input: {name} {p}') + return p + + +class DirectoryAction(argparse._StoreAction): + + def check_value(self, value, option_string): + p = Path(value).resolve() + if (p.exists() and not p.is_dir()) or (self._must_exist and not p.is_dir()): + name = option_string if option_string else self.dest.upper() + raise argparse.ArgumentError( + self, f'Invalid directory path input: {name} {p}') + return p + + +class RequiredAction(argparse._StoreAction): + + def __init__(self, option_strings, dest, required_actions: list[argparse.Action] = [], nargs='?', **kwargs) -> None: + self._actions = required_actions + super().__init__(option_strings, dest, nargs='?', const=True, **kwargs) + + def get_requires(self): + return self._actions + + def __call__(self, parser, namespace, values, option_string=None) -> None: + return super().__call__(parser, namespace, values, option_string) + + +class TrueRequiredAction(RequiredAction): + def __call__(self, parser, namespace, values, option_string=None) -> None: + for action in self.get_requires(): + action.required = True + super().__call__(parser, namespace, values, option_string) + + +class FalseRequiredAction(RequiredAction): + def __call__(self, parser, namespace, values, option_string=None): + for action in self.get_requires(): + action.required = False + super().__call__(parser, namespace, values, option_string) + + +def parse_argument(): + parser = argparse.ArgumentParser('fakelinker_browserstack_test', description='Automated testing of the fakelinker project', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('-i', '--log', help='Show more log', + action='store_true') + subparser = parser.add_subparsers(description='test subcommand') + + remove_cmd = subparser.add_parser( + 'remove', help='Remove app/test suite/build etc.') + remove_cmd.add_argument( + '-u', '--user', help='Specify BrowserStack access key', required=True) + remove_cmd.add_argument( + '-a', '--apk', help='Remove test apk', action='store_true') + remove_cmd.add_argument( + '-s', '--suite', help='Remove test suite', action='store_true') + remove_cmd.add_argument( + '-b', '--build', help='Remove test build', action='store_true') + remove_cmd.set_defaults(func=execute_remove_command) + + test_cmd = subparser.add_parser('test', help='Run test tasks') + test_cmd.add_argument( + '-u', '--user', help='Specify BrowserStack access key', required=True) + test_cmd.add_argument('--apk', help='apk to be tested', action=FileAction) + test_cmd.add_argument( + '--is-32bit', help='Specifies that the test apk is a 32-bit program', action='store_true') + test_cmd.add_argument('--apk-custom-id', help='Specify apk custom id', + type=str, default='FakelinkerTestApp') + test_cmd.add_argument( + '--test-suite', help='specify test suite', action=FileAction) + test_cmd.add_argument('--test-suite-custom-id', help='Specify test suite custom id', + type=str, default='FakelinkerTestSuite') + test_cmd.add_argument( + '--api', help='Specify the api level of the test', type=int, nargs='+', default=33) + test_cmd.add_argument( + '--all-api', help='Execute one test per api level', action='store_true') + test_cmd.add_argument( + '--devices', help='Specify the name of the device to test', type=str, nargs='+') + test_cmd.add_argument( + '--max-parallel', help='The maximum number of parallel tests', type=int, default=5) + test_cmd.add_argument('--query-interval', + help='Specifies the time interval for query build test tasks', type=int, default=15) + + test_cmd.add_argument( + '--repeat-last', help='Test again with recent test apk and test suite', action='store_true') + test_cmd.add_argument( + '--get-last-build', help='Get recent build test information', action='store_true') + test_cmd.add_argument('--build-last-faild', + help='Select the device that failed the upload test to test again', action='store_true') + test_cmd.add_argument( + '--project', help='Set test project name', type=str, default='fakelinker') + test_cmd.add_argument( + '--device-log', help='Open test task log', action='store_true') + test_cmd.set_defaults(func=execute_test_command) + args = parser.parse_args() + args.print_help = parser.print_help + if args.log: + logging.basicConfig(level=logging.DEBUG) + return args + + +def main(): + + args = parse_argument() + if hasattr(args, 'func'): + sys.exit(args.func(args)) + else: + args.print_help() + sys.exit(1) + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + pass diff --git a/build.gradle b/build.gradle index 8df3409..11ede3e 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/emualtor-testapp/src/main/cpp/CMakeLists.txt b/emualtor-testapp/src/main/cpp/CMakeLists.txt deleted file mode 100644 index 688f467..0000000 --- a/emualtor-testapp/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# For more information about using CMake with Android Studio, read the -# documentation: https://d.android.com/studio/projects/add-native-code.html - -# Sets the minimum version of CMake required to build the native library. - -project(fakelinker-test) -cmake_minimum_required(VERSION 3.4.1) -# Creates and names a library, sets it as either STATIC -# or SHARED, and provides the relative paths to its source code. -# You can define multiple libraries, and CMake builds them for you. -# Gradle automatically packages shared libraries with your APK. -if (${CMAKE_ANDROID_ARCH_ABI} STREQUAL "arm64-v8a" OR ${CMAKE_ANDROID_ARCH_ABI} STREQUAL "x86_64") - set(MODULE_SUFFIX "64") -else () - set(MODULE_SUFFIX "32") -endif () -add_library(Dobby STATIC IMPORTED) -set_target_properties(Dobby PROPERTIES IMPORTED_LOCATION - ${CMAKE_CURRENT_SOURCE_DIR}/Dobby/${CMAKE_ANDROID_ARCH_ABI}/libdobby.a) - -set(FAKELINKER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../library/src/main/cpp) - -include_directories(${FAKELINKER_DIR}/export) -include_directories(${FAKELINKER_DIR}/common) - -add_library( # Sets the name of the library. - fakelinker-test - - # Sets the library as a shared library. - SHARED - - # Provides a relative path to your source file(s). - fakelinker_module.cpp - ${FAKELINKER_DIR}/common/maps_util.cpp - ) - -add_library(test_module - SHARED - test_module.cpp - ) - -add_library(test_module_hook - SHARED - test_module.cpp - ) - -target_compile_definitions(test_module_hook PUBLIC OPEN_HOOK) - -# Searches for a specified prebuilt library and stores the path as a -# variable. Because CMake includes system libraries in the search path by -# default, you only need to specify the name of the public NDK library -# you want to add. CMake verifies that the library exists before -# completing its build. - -find_library( # Sets the name of the path variable. - log-lib - - # Specifies the name of the NDK library that - # you want CMake to locate. - log) - - -# Specifies libraries CMake should link to your target library. You -# can link multiple libraries, such as libraries you define in this -# build script, prebuilt third-party libraries, or system libraries. - -target_link_libraries( # Specifies the target library. - fakelinker-test - - # Links the target library to the log library - # included in the NDK. - ${log-lib}) - -set_target_properties(fakelinker-test PROPERTIES - LINK_FLAGS - "${LINK_FLAGS} -Wl,--gc-sections,--version-script=\"${CMAKE_CURRENT_SOURCE_DIR}/symbol.map.txt\"" - SUFFIX "${MODULE_SUFFIX}.so" - ) - -target_link_libraries(test_module - ${log-lib} - ) - -target_link_libraries(test_module_hook - ${log-lib} - Dobby - ) - diff --git a/emualtor-testapp/.gitignore b/emulator-testapp/.gitignore similarity index 100% rename from emualtor-testapp/.gitignore rename to emulator-testapp/.gitignore diff --git a/emualtor-testapp/build.gradle b/emulator-testapp/build.gradle similarity index 74% rename from emualtor-testapp/build.gradle rename to emulator-testapp/build.gradle index d9ba575..622bd01 100644 --- a/emualtor-testapp/build.gradle +++ b/emulator-testapp/build.gradle @@ -27,7 +27,7 @@ android { targetSdkVersion rootProject.targetSdk versionCode 100 versionName "1.0" - applicationId "com.sanfengandroid.fakelinker_test" + applicationId "com.sanfengandroid.fakelinker.emulator_test" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { @@ -35,11 +35,14 @@ android { cppFlags "-Werror" cFlags "-Werror" abiFilters.addAll(rootProject.abis) + arguments "-DHOOK_LOG_LEVEL=${rootProject.logLevel}", + "-DMODULE_VERSION=${rootProject.versionCode}", + "-DMODULE_VERSION_NAME=${rootProject.versionName}" } } } - if (rootProject.hasSign){ + if (rootProject.hasSign) { signingConfigs { sign { storeFile file(rootProject.storeFile) @@ -69,16 +72,19 @@ android { } } ndkVersion '25.1.8937393' - namespace 'com.sanfengandroid.testapp' + namespace 'com.sanfengandroid.fakelinker_emulator_test' buildToolsVersion '33.0.0' + buildFeatures { + viewBinding true + } } tasks.whenTaskAdded { task -> if (task.name.startsWith('merge') && task.name.endsWith('NativeLibs')) { task.doLast { - merge32BitTo64BitLibrary(it.outputs.files, "fakelinker-test") - emulatorBuildMoveLibrary(it.outputs.files, "fakelinker-test") + merge32BitTo64BitLibrary(it.outputs.files, "fakelinker-module") + emulatorBuildMoveLibrary(it.outputs.files, "fakelinker-module") reducedLibrary(it.outputs.files) } } @@ -86,10 +92,12 @@ tasks.whenTaskAdded { task -> dependencies { implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'com.google.android.material:material:1.6.0' + implementation 'com.google.android.material:material:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'androidx.navigation:navigation-fragment:2.5.3' + implementation 'androidx.navigation:navigation-ui:2.5.3' implementation(project(':library')) + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.4' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' } \ No newline at end of file diff --git a/emualtor-testapp/consumer-rules.pro b/emulator-testapp/consumer-rules.pro similarity index 100% rename from emualtor-testapp/consumer-rules.pro rename to emulator-testapp/consumer-rules.pro diff --git a/emualtor-testapp/proguard-rules.pro b/emulator-testapp/proguard-rules.pro similarity index 100% rename from emualtor-testapp/proguard-rules.pro rename to emulator-testapp/proguard-rules.pro diff --git a/emualtor-testapp/src/androidTest/java/com/sanfengandroid/testapp/ExampleInstrumentedTest.java b/emulator-testapp/src/androidTest/java/com/sanfengandroid/fakelinker/emulator_test/ExampleInstrumentedTest.java similarity index 92% rename from emualtor-testapp/src/androidTest/java/com/sanfengandroid/testapp/ExampleInstrumentedTest.java rename to emulator-testapp/src/androidTest/java/com/sanfengandroid/fakelinker/emulator_test/ExampleInstrumentedTest.java index 8f02b7b..b56a237 100644 --- a/emualtor-testapp/src/androidTest/java/com/sanfengandroid/testapp/ExampleInstrumentedTest.java +++ b/emulator-testapp/src/androidTest/java/com/sanfengandroid/fakelinker/emulator_test/ExampleInstrumentedTest.java @@ -15,7 +15,7 @@ * */ -package com.sanfengandroid.testapp; +package com.sanfengandroid.fakelinker.emulator_test; import static org.junit.Assert.*; @@ -37,7 +37,7 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("com.sanfengandroid.testapp.test", + assertEquals("com.sanfengandroid.fakelinker_test", appContext.getPackageName()); } } \ No newline at end of file diff --git a/emualtor-testapp/src/main/AndroidManifest.xml b/emulator-testapp/src/main/AndroidManifest.xml similarity index 83% rename from emualtor-testapp/src/main/AndroidManifest.xml rename to emulator-testapp/src/main/AndroidManifest.xml index 1025057..82aa898 100644 --- a/emualtor-testapp/src/main/AndroidManifest.xml +++ b/emulator-testapp/src/main/AndroidManifest.xml @@ -3,11 +3,11 @@ diff --git a/emulator-testapp/src/main/cpp/CMakeLists.txt b/emulator-testapp/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..e5f705d --- /dev/null +++ b/emulator-testapp/src/main/cpp/CMakeLists.txt @@ -0,0 +1,62 @@ +project(fakelinker_emulator_test) +cmake_minimum_required(VERSION 3.4.1) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 17) + +if(${CMAKE_ANDROID_ARCH_ABI} STREQUAL "arm64-v8a" OR ${CMAKE_ANDROID_ARCH_ABI} STREQUAL "x86_64") + set(MODULE_SUFFIX "64") +else() + set(MODULE_SUFFIX "32") +endif() + +add_library(Dobby STATIC IMPORTED) +set_target_properties(Dobby PROPERTIES IMPORTED_LOCATION + ${CMAKE_CURRENT_SOURCE_DIR}/Dobby/${CMAKE_ANDROID_ARCH_ABI}/libdobby.a) + +set(FAKELINKER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../library/src/main/cpp) + +add_library( # Sets the name of the library. + fakelinker-module + + SHARED + + fakelinker_module.cpp + ${FAKELINKER_DIR}/common/maps_util.cpp +) + +target_include_directories(fakelinker-module PRIVATE ${FAKELINKER_DIR}/export ${FAKELINKER_DIR}/common) + +add_library(test_module + SHARED + test_module.cpp +) + +add_library(test_module_hook + SHARED + test_module.cpp +) + +target_compile_definitions(test_module_hook PUBLIC OPEN_HOOK) + +find_library(log-lib log) + +target_link_libraries( + fakelinker-module + + ${log-lib}) + +set_target_properties(fakelinker-module PROPERTIES + LINK_FLAGS + "${LINK_FLAGS} -Wl,--gc-sections,--version-script=\"${CMAKE_CURRENT_SOURCE_DIR}/symbol.map.txt\"" + SUFFIX "${MODULE_SUFFIX}.so" +) + +target_link_libraries(test_module + ${log-lib} +) + +target_link_libraries(test_module_hook + ${log-lib} + Dobby +) \ No newline at end of file diff --git a/emualtor-testapp/src/main/cpp/Dobby/arm64-v8a/libdobby.a b/emulator-testapp/src/main/cpp/Dobby/arm64-v8a/libdobby.a similarity index 100% rename from emualtor-testapp/src/main/cpp/Dobby/arm64-v8a/libdobby.a rename to emulator-testapp/src/main/cpp/Dobby/arm64-v8a/libdobby.a diff --git a/emualtor-testapp/src/main/cpp/Dobby/armeabi-v7a/libdobby.a b/emulator-testapp/src/main/cpp/Dobby/armeabi-v7a/libdobby.a similarity index 100% rename from emualtor-testapp/src/main/cpp/Dobby/armeabi-v7a/libdobby.a rename to emulator-testapp/src/main/cpp/Dobby/armeabi-v7a/libdobby.a diff --git a/emualtor-testapp/src/main/cpp/Dobby/dobby.h b/emulator-testapp/src/main/cpp/Dobby/dobby.h similarity index 100% rename from emualtor-testapp/src/main/cpp/Dobby/dobby.h rename to emulator-testapp/src/main/cpp/Dobby/dobby.h diff --git a/emualtor-testapp/src/main/cpp/Dobby/x86/libdobby.a b/emulator-testapp/src/main/cpp/Dobby/x86/libdobby.a similarity index 100% rename from emualtor-testapp/src/main/cpp/Dobby/x86/libdobby.a rename to emulator-testapp/src/main/cpp/Dobby/x86/libdobby.a diff --git a/emualtor-testapp/src/main/cpp/Dobby/x86_64/libdobby.a b/emulator-testapp/src/main/cpp/Dobby/x86_64/libdobby.a similarity index 100% rename from emualtor-testapp/src/main/cpp/Dobby/x86_64/libdobby.a rename to emulator-testapp/src/main/cpp/Dobby/x86_64/libdobby.a diff --git a/emualtor-testapp/src/main/cpp/fakelinker_module.cpp b/emulator-testapp/src/main/cpp/fakelinker_module.cpp similarity index 100% rename from emualtor-testapp/src/main/cpp/fakelinker_module.cpp rename to emulator-testapp/src/main/cpp/fakelinker_module.cpp diff --git a/emualtor-testapp/src/main/cpp/symbol.map.txt b/emulator-testapp/src/main/cpp/symbol.map.txt similarity index 100% rename from emualtor-testapp/src/main/cpp/symbol.map.txt rename to emulator-testapp/src/main/cpp/symbol.map.txt diff --git a/emualtor-testapp/src/main/cpp/test_module.cpp b/emulator-testapp/src/main/cpp/test_module.cpp similarity index 100% rename from emualtor-testapp/src/main/cpp/test_module.cpp rename to emulator-testapp/src/main/cpp/test_module.cpp diff --git a/emualtor-testapp/src/main/ic_launcher-playstore.png b/emulator-testapp/src/main/ic_launcher-playstore.png similarity index 100% rename from emualtor-testapp/src/main/ic_launcher-playstore.png rename to emulator-testapp/src/main/ic_launcher-playstore.png diff --git a/emualtor-testapp/src/main/java/com/sanfengandroid/testapp/MainActivity.java b/emulator-testapp/src/main/java/com/sanfengandroid/fakelinker/emulator_test/MainActivity.java similarity index 96% rename from emualtor-testapp/src/main/java/com/sanfengandroid/testapp/MainActivity.java rename to emulator-testapp/src/main/java/com/sanfengandroid/fakelinker/emulator_test/MainActivity.java index e2770bc..8dc928c 100644 --- a/emualtor-testapp/src/main/java/com/sanfengandroid/testapp/MainActivity.java +++ b/emulator-testapp/src/main/java/com/sanfengandroid/fakelinker/emulator_test/MainActivity.java @@ -15,13 +15,14 @@ * */ -package com.sanfengandroid.testapp; +package com.sanfengandroid.fakelinker.emulator_test; import android.os.Bundle; import android.util.Log; import android.widget.EditText; import androidx.appcompat.app.AppCompatActivity; import com.sanfengandroid.fakelinker.FakeLinker; +import com.sanfengandroid.fakelinker_emulator_test.R; public class MainActivity extends AppCompatActivity { private EditText relocateEdit; diff --git a/emualtor-testapp/src/main/res/drawable/ic_launcher_background.xml b/emulator-testapp/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from emualtor-testapp/src/main/res/drawable/ic_launcher_background.xml rename to emulator-testapp/src/main/res/drawable/ic_launcher_background.xml diff --git a/emualtor-testapp/src/main/res/layout/activity_main.xml b/emulator-testapp/src/main/res/layout/activity_main.xml similarity index 100% rename from emualtor-testapp/src/main/res/layout/activity_main.xml rename to emulator-testapp/src/main/res/layout/activity_main.xml diff --git a/emualtor-testapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/emulator-testapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to emulator-testapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/emualtor-testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/emulator-testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to emulator-testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/emualtor-testapp/src/main/res/mipmap-hdpi/ic_launcher.png b/emulator-testapp/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-hdpi/ic_launcher.png rename to emulator-testapp/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/emualtor-testapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/emulator-testapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png rename to emulator-testapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png diff --git a/emualtor-testapp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/emulator-testapp/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to emulator-testapp/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/emualtor-testapp/src/main/res/mipmap-mdpi/ic_launcher.png b/emulator-testapp/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-mdpi/ic_launcher.png rename to emulator-testapp/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/emualtor-testapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/emulator-testapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png rename to emulator-testapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/emualtor-testapp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/emulator-testapp/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to emulator-testapp/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/emualtor-testapp/src/main/res/mipmap-xhdpi/ic_launcher.png b/emulator-testapp/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xhdpi/ic_launcher.png rename to emulator-testapp/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/emualtor-testapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/emulator-testapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png rename to emulator-testapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/emualtor-testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/emulator-testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to emulator-testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/emualtor-testapp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/emulator-testapp/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to emulator-testapp/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/emualtor-testapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/emulator-testapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png rename to emulator-testapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/emualtor-testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/emulator-testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to emulator-testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/emualtor-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/emulator-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to emulator-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/emualtor-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/emulator-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png rename to emulator-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/emualtor-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/emulator-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from emualtor-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to emulator-testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/emualtor-testapp/src/main/res/values/strings.xml b/emulator-testapp/src/main/res/values/strings.xml similarity index 100% rename from emualtor-testapp/src/main/res/values/strings.xml rename to emulator-testapp/src/main/res/values/strings.xml diff --git a/emualtor-testapp/src/test/java/com/sanfengandroid/testapp/ExampleUnitTest.java b/emulator-testapp/src/test/java/com/sanfengandroid/fakelinker/emulator_test/ExampleUnitTest.java similarity index 94% rename from emualtor-testapp/src/test/java/com/sanfengandroid/testapp/ExampleUnitTest.java rename to emulator-testapp/src/test/java/com/sanfengandroid/fakelinker/emulator_test/ExampleUnitTest.java index e784892..d2162e4 100644 --- a/emualtor-testapp/src/test/java/com/sanfengandroid/testapp/ExampleUnitTest.java +++ b/emulator-testapp/src/test/java/com/sanfengandroid/fakelinker/emulator_test/ExampleUnitTest.java @@ -15,7 +15,7 @@ * */ -package com.sanfengandroid.testapp; +package com.sanfengandroid.fakelinker.emulator_test; import static org.junit.Assert.*; diff --git a/fakelinker-test/.gitignore b/fakelinker-test/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/fakelinker-test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/fakelinker-test/build.gradle b/fakelinker-test/build.gradle new file mode 100644 index 0000000..f512e16 --- /dev/null +++ b/fakelinker-test/build.gradle @@ -0,0 +1,96 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.sanfengandroid.fakelinker.test' + compileSdk rootProject.targetSdk + + defaultConfig { + applicationId "com.sanfengandroid.fakelinker.test" + minSdk 21 + targetSdk rootProject.targetSdk + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + externalNativeBuild { + cmake { + abiFilters.addAll(rootProject.abis) + arguments '-DANDROID_PLATFORM=21', + "-DLINKER_MODULE_NAME=${rootProject.fakeLinkerModuleName}", + "-DINSTALLER_MODULE_NAME=${rootProject.installerModuleName}", + "-DHOOK_LOG_LEVEL=${rootProject.logLevel}", + "-DMODULE_VERSION=${rootProject.versionCode}", + "-DMODULE_VERSION_NAME=${rootProject.versionName}" + } + } + } + + if (rootProject.hasSign) { + signingConfigs { + sign { + storeFile file(rootProject.storeFile) + storePassword rootProject.storePassword + keyAlias rootProject.keyAlias + keyPassword rootProject.keyPassword + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + if (rootProject.hasSign) { + signingConfig signingConfigs.sign + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + } + } + ndkVersion '25.1.8937393' + buildFeatures { + prefab true + } + + packagingOptions{ + exclude 'lib/armeabi-v7a/libadder.so' + exclude 'lib/armeabi-v7a/libapptest.so' + exclude 'lib/armeabi-v7a/libc++_shared.so' + exclude 'lib/arm64-v8a/libadder.so' + exclude 'lib/arm64-v8a/libapptest.so' + exclude 'lib/arm64-v8a/libc++_shared.so' + exclude 'lib/x86/libadder.so' + exclude 'lib/x86/libapptest.so' + exclude 'lib/x86/libc++_shared.so' + exclude 'lib/x86_64/libadder.so' + exclude 'lib/x86_64/libapptest.so' + exclude 'lib/x86_64/libc++_shared.so' + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.8.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.google.android.material:material:1.7.0' + implementation 'androidx.test.ext:junit-gtest:1.0.0-alpha01' + implementation 'com.android.ndk.thirdparty:googletest:1.11.0-beta-1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.4' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' +} \ No newline at end of file diff --git a/fakelinker-test/proguard-rules.pro b/fakelinker-test/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/fakelinker-test/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/fakelinker-test/src/androidTest/java/com/sanfengandroid/fakelinker/test/FakelinkerGTest.kt b/fakelinker-test/src/androidTest/java/com/sanfengandroid/fakelinker/test/FakelinkerGTest.kt new file mode 100644 index 0000000..8d8f8a5 --- /dev/null +++ b/fakelinker-test/src/androidTest/java/com/sanfengandroid/fakelinker/test/FakelinkerGTest.kt @@ -0,0 +1,9 @@ +package com.sanfengandroid.fakelinker.test + +import androidx.test.ext.junitgtest.GtestRunner +import androidx.test.ext.junitgtest.TargetLibrary +import org.junit.runner.RunWith + +@RunWith(GtestRunner::class) +@TargetLibrary(libraryName = "fakelinker_test") +class FakelinkerGTest \ No newline at end of file diff --git a/fakelinker-test/src/main/AndroidManifest.xml b/fakelinker-test/src/main/AndroidManifest.xml new file mode 100644 index 0000000..40726d5 --- /dev/null +++ b/fakelinker-test/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fakelinker-test/src/main/cpp/CMakeLists.txt b/fakelinker-test/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..95ea387 --- /dev/null +++ b/fakelinker-test/src/main/cpp/CMakeLists.txt @@ -0,0 +1,46 @@ +project(fakelinker_test) +cmake_minimum_required(VERSION 3.4.1) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 17) + +find_package(googletest REQUIRED CONFIG) +find_package(junit-gtest REQUIRED CONFIG) + +add_library(fakelinker_test + SHARED + test/test_elf_reader.cpp + test/test_fakelinker.cpp +) + +add_executable(fakelinker_static_test + test/test_elf_reader.cpp + test/test_fakelinker.cpp +) + +set(FAKELINKER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../library/src/main/cpp) + +option(FAKELINKER_BUILD_STATIC_LIB "build fakelinker static library" ON) +add_subdirectory(${FAKELINKER_DIR} lib) + +target_link_libraries(fakelinker_test + PRIVATE + fakelinker_static + googletest::gtest + junit-gtest::junit-gtest +) + +target_compile_definitions(fakelinker_static_test PUBLIC FAKELINKER_STATIC_TEST STD_LOG) + +target_link_libraries(fakelinker_static_test PRIVATE googletest::gtest fakelinker_static) + +if(${CMAKE_BUILD_TYPE} STREQUAL "Debug") + set(OUTPUT_TYPE "Debug") +else() + set(OUTPUT_TYPE "Release") +endif() + +set_target_properties(fakelinker_static_test PROPERTIES + RUNTIME_OUTPUT_DIRECTORY + ${CMAKE_CURRENT_SOURCE_DIR}/build/${OUTPUT_TYPE}/${CMAKE_ANDROID_ARCH_ABI} +) \ No newline at end of file diff --git a/library/src/main/cpp/test/test_elf_reader.cpp b/fakelinker-test/src/main/cpp/test/test_elf_reader.cpp similarity index 100% rename from library/src/main/cpp/test/test_elf_reader.cpp rename to fakelinker-test/src/main/cpp/test/test_elf_reader.cpp diff --git a/library/src/main/cpp/test/test_fakelinker.cpp b/fakelinker-test/src/main/cpp/test/test_fakelinker.cpp similarity index 84% rename from library/src/main/cpp/test/test_fakelinker.cpp rename to fakelinker-test/src/main/cpp/test/test_fakelinker.cpp index ea30372..a36d5ed 100644 --- a/library/src/main/cpp/test/test_fakelinker.cpp +++ b/fakelinker-test/src/main/cpp/test/test_fakelinker.cpp @@ -1,11 +1,10 @@ #include #include -#include +#include #include "../linker/elf_reader.h" -#include "fake_linker.h" -#include "linker_globals.h" -#include "linker_symbol.h" +#include "../linker/linker_globals.h" +#include "../linker/linker_symbol.h" using namespace fakelinker; @@ -39,6 +38,14 @@ constexpr const char *get_arch_name() { #endif } +static constexpr bool isStaticTest() { +#ifdef FAKELINKER_STATIC_TEST + return true; +#else + return false; +#endif +} + static int GetApiLevel() { char value[92] = {0}; if (__system_property_get("ro.build.version.sdk", value) < 1) @@ -49,16 +56,16 @@ static int GetApiLevel() { static void Init() { soinfo::Init(); - std::cout << "init soinfo completed" << std::endl; + LOGI("init soinfo completed"); android_namespace_t::Init(); - std::cout << "init android namespace completed" << std::endl; + LOGI("init android namespace completed"); fakelinker::linker_symbol.InitSymbolName(); init_success = fakelinker::linker_symbol.LoadSymbol(); } static void init_env() { android_api = GetApiLevel(); - std::cout << "android version: " << android_api << std::endl; + LOGI("current android api: %d", android_api); android_api_T = __ANDROID_API_T__; Init(); } @@ -69,7 +76,6 @@ TEST(Fakelinker, initTest) { ASSERT_TRUE(g_fakelinker_export.set_ld_debug_verbosity(3)) << "open linker log"; } - TEST(FakeLinker, soinfoTest) { int error; SoinfoPtr *soinfo_array; @@ -254,7 +260,7 @@ TEST(FakeLinker, namespaceTest) { for (int i = 0; i < len; ++i) { const char *name = g_fakelinker_export.android_namespace_get_name(array[i], nullptr); - std::cout << "namespace: " << name << std::endl; + LOGI("found namespace name: %s", name); if (name && new_namespace_name == name) { check_namespace = true; } @@ -290,29 +296,28 @@ CHECK_MEMBER(whitelisted_libs_); template void print_soinfo_offset() { const char *name = typeid(T).name(); - std::cout << "type: " << name << ", arch: " << get_arch_name() << std::endl; + LOGI("type: %s, arch: %s", name, get_arch_name()); if constexpr (has_member_soname_::value) { - std::cout << "\tsoname offset: " << offsetof(T, soname_) << std::endl; + LOGI("\tsoname offset: %zu", offsetof(T, soname_)); } - - std::cout << "\tchildren_ offset: " << offsetof(T, children_) << std::endl; - std::cout << "\tparents_ offset: " << offsetof(T, parents_) << std::endl; - std::cout << "\tst_dev_ offset: " << offsetof(T, st_dev_) << std::endl; - std::cout << "\tst_ino_ offset: " << offsetof(T, st_ino_) << std::endl; + LOGI("\tchildren_ offset: %zu", offsetof(T, children_)); + LOGI("\tparents_ offset: %zu", offsetof(T, parents_)); + LOGI("\tst_dev_ offset: %zu", offsetof(T, st_dev_)); + LOGI("\tst_dev_ offset: %zu", offsetof(T, st_dev_)); + LOGI("\tst_ino_ offset: %zu", offsetof(T, st_ino_)); if constexpr (has_member_ld_library_paths_::value) { - std::cout << "\tandroid_namespace ld_library_paths_ offset: " << offsetof(T, ld_library_paths_) << std::endl; + LOGI("\tandroid_namespace ld_library_paths_ offset: %zu", offsetof(T, ld_library_paths_)); } if constexpr (has_member_default_library_paths_::value) { - std::cout << "\tandroid_namespace default_library_paths_ offset: " << offsetof(T, default_library_paths_) - << std::endl; + LOGI("\tandroid_namespace default_library_paths_ offset: %zu", offsetof(T, default_library_paths_)); } if constexpr (has_member_permitted_paths_::value) { - std::cout << "\tandroid_namespace permitted_paths_ offset: " << offsetof(T, permitted_paths_) << std::endl; + LOGI("\tandroid_namespace permitted_paths_ offset: %zu", offsetof(T, permitted_paths_)); } if constexpr (has_member_whitelisted_libs_::value) { - std::cout << "\tandroid_namespace whitelisted_libs_ offset: " << offsetof(T, whitelisted_libs_) << std::endl; + LOGI("\tandroid_namespace whitelisted_libs_ offset: %zu", offsetof(T, whitelisted_libs_)); } } @@ -371,6 +376,13 @@ C_API API_PUBLIC void android_set_abort_message(const char *msg) { LOGI("only test relocate android_set_abort_message"); } +static void *dlope_library(const char *name, void *caller_address) { + if (caller_address) { + return g_fakelinker_export.call_dlopen_inside(name, RTLD_NOW, caller_address, nullptr); + } + return dlopen(name, RTLD_NOW); +} + TEST(FakeLinker, relocateTest) { auto log_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, "liblog.so", nullptr); ASSERT_TRUE(log_soinfo) << "soinfo_find"; @@ -378,10 +390,12 @@ TEST(FakeLinker, relocateTest) { auto thiz = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTAddress, nullptr, nullptr); ASSERT_TRUE(thiz) << "soinfo find"; - SoinfoAttributes attr; + SoinfoAttributes attr{}; EXPECT_EQ(g_fakelinker_export.soinfo_get_attribute(thiz, &attr), FakeLinkerError::kErrorNo); + // test liblog import ASSERT_TRUE(g_fakelinker_export.soinfo_add_to_global(thiz)); + EXPECT_TRUE(g_fakelinker_export.call_manual_relocation_by_soinfo(thiz, log_soinfo)) << "call_manual_relocation_by_soinfo"; SymbolAddress *message_addr = @@ -389,36 +403,80 @@ TEST(FakeLinker, relocateTest) { ASSERT_TRUE(message_addr) << "find import android_set_abort_message"; EXPECT_EQ(*message_addr, &android_set_abort_message) << "call_manual_relocation_by_soinfo verify"; - const char *test_library = "libssl.so"; + const char *test_library = nullptr; const char *test_function = "gettimeofday"; - void *handle = dlopen(test_library, RTLD_NOW); - ASSERT_TRUE(handle) << "dlopen " << test_library; - auto test_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, test_library, nullptr); - ASSERT_TRUE(test_soinfo) << "find " << test_library << " soinfo"; + + std::vector test_librarys = {"libssl.so", "libpcap.so", "libext2_uuid.so", + "libcups.so", "libcurl.so", "libpac.so", + "libloc_stub.so", "libavlm.so", "libbcinfo.so"}; + + void *handle = nullptr; + void *caller_address = nullptr; + AndroidNamespacePtr log_np = nullptr; + if (android_api >= __ANDROID_API_N__) { + log_np = g_fakelinker_export.android_namespace_find(NamespaceFindType::kNPSoinfo, log_soinfo, nullptr); + ASSERT_TRUE(log_np) << "find liblog.so namespace failed."; + caller_address = g_fakelinker_export.android_namespace_get_caller_address(log_np, nullptr); + ASSERT_TRUE(caller_address) << "get liblog.so namespace caller address failed"; + } + SoinfoPtr test_soinfo = nullptr; + std::string library_dir = is64BitBuild() ? "/system/lib64/" : "/system/lib/"; + for (auto name : test_librarys) { + std::string path = library_dir + name; + if (access(path.c_str(), F_OK) != 0) { + continue; + } + if (android_api >= __ANDROID_API_N__) { + test_soinfo = g_fakelinker_export.soinfo_find_in_namespace(SoinfoFindType::kSTName, name, log_np, nullptr); + } else { + test_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, name, nullptr); + } + if (!test_soinfo) { + test_library = name; + break; + } + } + ASSERT_TRUE(test_library) << "The current device cannot test library relocation, and the test library has already " + "been loaded"; + LOGW("current test so library: %s", test_library); + handle = dlope_library(test_library, caller_address); + ASSERT_TRUE(handle) << "dlopen " << test_library << " failed"; + + test_soinfo = nullptr; + if (android_api >= __ANDROID_API_N__) { + test_soinfo = g_fakelinker_export.soinfo_find_in_namespace(SoinfoFindType::kSTName, test_library, log_np, nullptr); + } else { + test_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, test_library, nullptr); + } + ASSERT_TRUE(test_soinfo) << "find " << test_library << " soinfo failed"; SymbolAddress *test_import_function = g_fakelinker_export.soinfo_get_import_symbol_address(test_soinfo, test_function, nullptr); - ASSERT_TRUE(test_import_function) << "find import"; + ASSERT_TRUE(test_import_function) << "find " << test_library << " import symbol: " << test_function; EXPECT_EQ(*test_import_function, reinterpret_cast(&gettimeofday)) << "call_manual_relocation_by_soinfo verify"; ASSERT_EQ(dlclose(handle), 0) << "dlclose"; + + // remove global g_fakelinker_export.soinfo_remove_global(thiz); - EXPECT_FALSE(g_fakelinker_export.soinfo_is_global(thiz, nullptr)) << "remove global"; - handle = dlopen(test_library, RTLD_NOW); - ASSERT_TRUE(handle) << "dlopen local " << test_library; - test_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, test_library, nullptr); - ASSERT_TRUE(test_soinfo) << "find " << test_library << " soinfo 2"; + EXPECT_FALSE(g_fakelinker_export.soinfo_is_global(thiz, nullptr)) << "remove global"; + handle = dlope_library(test_library, caller_address); + if (android_api >= __ANDROID_API_N__) { + test_soinfo = g_fakelinker_export.soinfo_find_in_namespace(SoinfoFindType::kSTName, test_library, log_np, nullptr); + } else { + test_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, test_library, nullptr); + } + ASSERT_TRUE(test_soinfo) << "find " << test_library << " soinfo failed"; test_import_function = g_fakelinker_export.soinfo_get_import_symbol_address(test_soinfo, test_function, nullptr); ASSERT_TRUE(test_import_function) << "find import"; if (android_api < __ANDROID_API_M__) { - EXPECT_EQ(*test_import_function, &gettimeofday) << "call_manual_relocation_by_soinfo verify"; + EXPECT_NE(*test_import_function, &gettimeofday) << "call_manual_relocation_by_soinfo verify"; } else { EXPECT_NE(*test_import_function, &gettimeofday) << "call_manual_relocation_by_soinfo verify"; } ASSERT_EQ(dlclose(handle), 0) << "dlclose"; - if (android_api >= __ANDROID_API_N__) { const char *default_path = is64BitBuild() ? "/system/lib64" : "/system/lib"; auto libc_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, "libc.so", nullptr); @@ -433,7 +491,6 @@ TEST(FakeLinker, relocateTest) { *reinterpret_cast(addr) = true; } } - AndroidNamespacePtr new_create_np = g_fakelinker_export.android_namespace_create("linker_test2", nullptr, default_path, 1, nullptr, libc_np, nullptr); @@ -449,7 +506,9 @@ TEST(FakeLinker, relocateTest) { handle = g_fakelinker_export.call_dlopen_inside(test_library, RTLD_NOW, caller, nullptr); ASSERT_TRUE(handle) << "call_dlopen_inside"; - test_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, test_library, nullptr); + + test_soinfo = g_fakelinker_export.soinfo_find_in_namespace(SoinfoFindType::kSTName, test_library, libc_np, nullptr); + ASSERT_TRUE(test_soinfo) << "cross namespace find soinfo"; AndroidNamespacePtr test_np = g_fakelinker_export.android_namespace_find(NamespaceFindType::kNPSoinfo, test_soinfo, nullptr); @@ -464,7 +523,7 @@ TEST(FakeLinker, relocateTest) { ASSERT_TRUE(g_fakelinker_export.soinfo_remove_global(thiz)); handle = g_fakelinker_export.call_dlopen_inside(test_library, RTLD_NOW, caller, nullptr); ASSERT_TRUE(handle) << "call_dlopen_inside"; - test_soinfo = g_fakelinker_export.soinfo_find(SoinfoFindType::kSTName, test_library, nullptr); + test_soinfo = g_fakelinker_export.soinfo_find_in_namespace(SoinfoFindType::kSTName, test_library, libc_np, nullptr); ASSERT_TRUE(test_soinfo) << "cross namespace find soinfo"; test_np = g_fakelinker_export.android_namespace_find(NamespaceFindType::kNPSoinfo, test_soinfo, nullptr); EXPECT_TRUE(test_np); @@ -477,7 +536,6 @@ TEST(FakeLinker, relocateTest) { } } - int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); print_soinfo_offset(); diff --git a/fakelinker-test/src/main/java/com/sanfengandroid/fakelinker/FakeLinker.java b/fakelinker-test/src/main/java/com/sanfengandroid/fakelinker/FakeLinker.java new file mode 100644 index 0000000..66da055 --- /dev/null +++ b/fakelinker-test/src/main/java/com/sanfengandroid/fakelinker/FakeLinker.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 fake-linker by sanfengAndroid. + * + * 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. + * + */ + +package com.sanfengandroid.fakelinker; + +public class FakeLinker { + private static final String TAG = FakeLinker.class.getCanonicalName(); + + private static native boolean entrance(String hookModulePath); + + private static native void setLogLevel(int level); + + private static native int relocationFilterSymbol(String symbolName, + boolean add); + + public static native void nativeOffset(); + + public static native void removeAllRelocationFilterSymbol(); +} diff --git a/fakelinker-test/src/main/java/com/sanfengandroid/fakelinker/test/MainActivity.kt b/fakelinker-test/src/main/java/com/sanfengandroid/fakelinker/test/MainActivity.kt new file mode 100644 index 0000000..38d9a49 --- /dev/null +++ b/fakelinker-test/src/main/java/com/sanfengandroid/fakelinker/test/MainActivity.kt @@ -0,0 +1,11 @@ +package com.sanfengandroid.fakelinker.test + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} \ No newline at end of file diff --git a/fakelinker-test/src/main/res/drawable-v24/ic_launcher_foreground.xml b/fakelinker-test/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/fakelinker-test/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/fakelinker-test/src/main/res/drawable/ic_launcher_background.xml b/fakelinker-test/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/fakelinker-test/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fakelinker-test/src/main/res/layout/activity_main.xml b/fakelinker-test/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..0b15a20 --- /dev/null +++ b/fakelinker-test/src/main/res/layout/activity_main.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/fakelinker-test/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/fakelinker-test/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/fakelinker-test/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/fakelinker-test/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/fakelinker-test/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/fakelinker-test/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/fakelinker-test/src/main/res/mipmap-hdpi/ic_launcher.webp b/fakelinker-test/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/fakelinker-test/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-mdpi/ic_launcher.webp b/fakelinker-test/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/fakelinker-test/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-xhdpi/ic_launcher.webp b/fakelinker-test/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/fakelinker-test/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/fakelinker-test/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/fakelinker-test/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/fakelinker-test/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/fakelinker-test/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/fakelinker-test/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/fakelinker-test/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/fakelinker-test/src/main/res/values-night/themes.xml b/fakelinker-test/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..a3cd83a --- /dev/null +++ b/fakelinker-test/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/fakelinker-test/src/main/res/values/colors.xml b/fakelinker-test/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/fakelinker-test/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/fakelinker-test/src/main/res/values/strings.xml b/fakelinker-test/src/main/res/values/strings.xml new file mode 100644 index 0000000..c515d56 --- /dev/null +++ b/fakelinker-test/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + FakelinkerTest + \ No newline at end of file diff --git a/fakelinker-test/src/main/res/values/themes.xml b/fakelinker-test/src/main/res/values/themes.xml new file mode 100644 index 0000000..c7f206e --- /dev/null +++ b/fakelinker-test/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/fakelinker-test/src/test/java/com/sanfengandroid/fakelinker/test/ExampleUnitTest.kt b/fakelinker-test/src/test/java/com/sanfengandroid/fakelinker/test/ExampleUnitTest.kt new file mode 100644 index 0000000..fdd1ad8 --- /dev/null +++ b/fakelinker-test/src/test/java/com/sanfengandroid/fakelinker/test/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.sanfengandroid.fakelinker.test + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index a2321eb..1dcb09e 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -26,8 +26,6 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion rootProject.targetSdk - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" externalNativeBuild { @@ -38,8 +36,7 @@ android { "-DINSTALLER_MODULE_NAME=${rootProject.installerModuleName}", "-DHOOK_LOG_LEVEL=${rootProject.logLevel}", "-DMODULE_VERSION=${rootProject.versionCode}", - "-DMODULE_VERSION_NAME=${rootProject.versionName}", - '-DFAKELINKER_TEST=ON' + "-DMODULE_VERSION_NAME=${rootProject.versionName}" } } diff --git a/library/src/main/cpp/CMakeLists.txt b/library/src/main/cpp/CMakeLists.txt index f83584b..3d010d7 100644 --- a/library/src/main/cpp/CMakeLists.txt +++ b/library/src/main/cpp/CMakeLists.txt @@ -30,9 +30,6 @@ endif() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror") -include_directories(common) -include_directories(export) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/module_config.h.in" "${CMAKE_CURRENT_SOURCE_DIR}/export/linker_version.h") message(STATUS "cmake version:${CMAKE_VERSION} @@ -68,66 +65,41 @@ set(FAKE_LINKER_SRC common/unique_memory.cpp ) -add_library( - ${LINKER_MODULE_NAME} - - SHARED - ${FAKE_LINKER_SRC} -) - -target_compile_options(${LINKER_MODULE_NAME} PRIVATE -fno-rtti -fno-exceptions) - find_library(log-lib log) -target_link_libraries( - ${LINKER_MODULE_NAME} - ${log-lib}) - -set_target_properties(${LINKER_MODULE_NAME} PROPERTIES - LINK_FLAGS_RELEASE - "${LINK_FLAGS} -Wl,--gc-sections,-s,--version-script=\"${CMAKE_CURRENT_SOURCE_DIR}/symbol.map.txt\"" - SUFFIX "${MODULE_SUFFIX}.so" -) - -if(FAKELINKER_TEST) - set(GTEST_DIR ${ANDROID_NDK}/sources/third_party/googletest) - add_library(gtest STATIC ${GTEST_DIR}/src/gtest-all.cc) - target_include_directories(gtest PRIVATE ${GTEST_DIR}) - target_include_directories(gtest PUBLIC ${GTEST_DIR}/include) - - # find_package(GTest CONFIG REQUIRED) - add_executable( - fakelinker_test - +if(FAKELINKER_BUILD_STATIC_LIB) + add_library(fakelinker_static + STATIC ${FAKE_LINKER_SRC} - test/test_elf_reader.cpp - test/test_fakelinker.cpp ) - - target_include_directories(fakelinker_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/linker) - - target_link_libraries( - fakelinker_test - PRIVATE - ${log-lib} - gtest + target_compile_options(fakelinker_static PRIVATE -fno-rtti -fno-exceptions) + target_include_directories(fakelinker_static PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/common ${CMAKE_CURRENT_SOURCE_DIR}/export) + target_link_libraries(fakelinker_static ${log-lib}) +else() + add_library(fakelinker + SHARED + ${FAKE_LINKER_SRC} ) - target_compile_options(fakelinker_test PRIVATE -DSTD_LOG) - set_target_properties(fakelinker_test PROPERTIES - RUNTIME_OUTPUT_DIRECTORY - ${CMAKE_CURRENT_SOURCE_DIR}/build/${CMAKE_BUILD_TYPE}/${CMAKE_ANDROID_ARCH_ABI}) + target_compile_options(fakelinker PRIVATE -fno-rtti -fno-exceptions) + target_link_libraries(fakelinker ${log-lib}) + target_include_directories(fakelinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/common ${CMAKE_CURRENT_SOURCE_DIR}/export) + set_target_properties(fakelinker PROPERTIES + LINK_FLAGS_RELEASE + "${LINK_FLAGS} -Wl,--gc-sections,-s,--version-script=\"${CMAKE_CURRENT_SOURCE_DIR}/symbol.map.txt\"" + SUFFIX "${MODULE_SUFFIX}.so" + OUTPUT_NAME ${LINKER_MODULE_NAME} + ) endif() if(FAKELINKER_BUILD_INSTALLER) - add_executable( - ${INSTALLER_MODULE_NAME} + add_executable(${INSTALLER_MODULE_NAME} installer/hook_installer.cpp ) - target_link_libraries( - ${INSTALLER_MODULE_NAME} + target_link_libraries(${INSTALLER_MODULE_NAME} ${log-lib}) + set_target_properties(${HOOK_INSTALL_MODULE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../assets/${CMAKE_ANDROID_ARCH_ABI}) diff --git a/library/src/main/cpp/common/macros.h b/library/src/main/cpp/common/macros.h index fd8efe8..24a8d09 100644 --- a/library/src/main/cpp/common/macros.h +++ b/library/src/main/cpp/common/macros.h @@ -84,17 +84,17 @@ static constexpr bool is64BitBuild() { } template -static inline T *align_down(T *p, size_t align) { +inline T *align_down(T *p, size_t align) { return reinterpret_cast(align_down(reinterpret_cast(p), align)); } template -static inline T *align_up(T *p, size_t align) { +inline T *align_up(T *p, size_t align) { return reinterpret_cast(align_up(reinterpret_cast(p), align)); } template -static inline T *untag_address(T *p) { +inline T *untag_address(T *p) { #if defined(__aarch64__) return reinterpret_cast(reinterpret_cast(p) & ((1ULL << 56) - 1)); #else diff --git a/library/src/main/cpp/export/fake_linker.h b/library/src/main/cpp/export/fake_linker.h index 9c1dd8e..6ea8dab 100644 --- a/library/src/main/cpp/export/fake_linker.h +++ b/library/src/main/cpp/export/fake_linker.h @@ -171,6 +171,20 @@ typedef struct { */ FunPtr(SoinfoPtr, soinfo_find, SoinfoFindType find_type, const void *param, int *out_error); + /** + * @brief Android 7.0+ finds soinfo, restricts the namespace, and returns the first matching item when the namespace + * is empty + * + * @note Required Android 7.0+ + * + * @param find_type Specifies the type of parameter + * @param param Specific parameters + * @param np Restrict namespace or nullptr + * @param[out] out_error Write error code on error exists + */ + ANDROID_GE_N FunPtr(SoinfoPtr, soinfo_find_in_namespace, SoinfoFindType find_type, const void *param, + AndroidNamespacePtr np, int *out_error); + /** * @brief Finds some properties of a specified SoinfoPtr * diff --git a/library/src/main/cpp/linker/linker_export.cpp b/library/src/main/cpp/linker/linker_export.cpp index f1c7702..cffbf8f 100644 --- a/library/src/main/cpp/linker/linker_export.cpp +++ b/library/src/main/cpp/linker/linker_export.cpp @@ -89,6 +89,44 @@ SoinfoPtr soinfo_find_impl(SoinfoFindType find_type, const void *param, int *out return nullptr; } +ANDROID_GE_N SoinfoPtr soinfo_find_in_namespace_impl(SoinfoFindType find_type, const void *param, + AndroidNamespacePtr np, int *out_error) { + CHECK_API_PTR(__ANDROID_API_N__); + if (np == nullptr) { + return soinfo_find_impl(find_type, param, out_error); + } + switch (find_type) { + case kSTAddress: { + if (!param) { + param = __builtin_return_address(0); + } + void *ret = ProxyLinker::Get().FindContainingLibrary(param); + CHECK_ERROR(ret, kErrorSoinfoNotFound); + return ret; + } + case kSTHandle: { + CHECK_API_PTR(__ANDROID_API_N__); + CHECK_PARAM_PTR(param, kErrorParameterNull); + void *ret = ProxyLinker::Get().SoinfoFromHandle(param); + CHECK_ERROR(ret, kErrorSoinfoNotFound); + return ret; + } + case kSTName: { + CHECK_PARAM_PTR(param, kErrorParameterNull); + void *ret = ProxyLinker::Get().FindSoinfoByNameInNamespace(reinterpret_cast(param), + reinterpret_cast(np)); + CHECK_ERROR(ret, kErrorSoinfoNotFound); + return ret; + } + case kSTOrig: + return const_cast(param); + default: + CHECK_PARAM_PTR(false, kErrorParameter); + return nullptr; + } + return nullptr; +} + int soinfo_get_attribute_impl(SoinfoPtr soinfo_ptr, SoinfoAttributes *attrs) { CHECK_PARAM_RET_ERROR(soinfo_ptr, kErrorSoinfoNull); CHECK_PARAM_RET_ERROR(attrs, kErrorParameterNull); @@ -407,21 +445,7 @@ ANDROID_GE_N void *android_namespace_get_caller_address_impl(AndroidNamespacePtr auto *np = static_cast(android_namespace_ptr); CHECK_PARAM_PTR(np, kErrorNpNull); - // 第一个soinfo可能是动态创建的基址可能为0,如果有全局库则还要判断命名空间 - soinfo *first_so = np->soinfo_list().find_if([&first_so, &np](soinfo *so) { - if (so->load_bias() == 0) { - return false; - } - if (so->get_primary_namespace() != np) { - return false; - } - return true; - }); - - if (first_so) { - return reinterpret_cast(first_so->load_bias()); - } - return nullptr; + return ProxyLinker::GetNamespaceCallerAddress(np); } ANDROID_GE_O int android_namespace_get_link_namespace_impl(AndroidNamespacePtr android_namespace_ptr, @@ -720,6 +744,7 @@ C_API API_PUBLIC FakeLinker g_fakelinker_export = { return init_success; }, soinfo_find_impl, + soinfo_find_in_namespace_impl, soinfo_get_attribute_impl, soinfo_to_string_impl, soinfo_query_all_impl, diff --git a/library/src/main/cpp/linker/linker_globals.cpp b/library/src/main/cpp/linker/linker_globals.cpp index 4cf6509..87246db 100644 --- a/library/src/main/cpp/linker/linker_globals.cpp +++ b/library/src/main/cpp/linker/linker_globals.cpp @@ -116,6 +116,22 @@ android_namespace_t *ProxyLinker::FindNamespaceByName(const char *name) { android_namespace_t *ProxyLinker::GetDefaultNamespace() { return linker_symbol.g_default_namespace.Get(); } +void *ProxyLinker::GetNamespaceCallerAddress(android_namespace_t *np) { + for (soinfo *si = linker_symbol.solist.Get(); si != nullptr; si = si->next()) { + if (si->get_primary_namespace() == np && si->base() > 0 && si->size() > 0) { + for (size_t i = 0; i != si->phnum(); ++i) { + const ElfW(Phdr) *phdr = &si->phdr()[i]; + if (phdr->p_type != PT_LOAD) { + continue; + } + ElfW(Addr) addr = phdr->p_vaddr + si->load_bias(); + return reinterpret_cast(addr); + } + } + } + return nullptr; +} + bool ProxyLinker::AddGlobalSoinfoToNamespace(soinfo *global, android_namespace_t *np) { if (__predict_false(global == nullptr) || __predict_false(np == nullptr)) { return false; @@ -301,7 +317,6 @@ soinfo *ProxyLinker::FindSoinfoByName(const char *name) { int delta = strlen(so_name) - len_a; if (delta == 0 && strncmp(so_name, name, len_a) == 0) { return si; - } else if (delta > 0 && strncmp(so_name + delta, name, len_a) == 0 && so_name[delta - 1] == '/') { return si; } @@ -310,6 +325,21 @@ soinfo *ProxyLinker::FindSoinfoByName(const char *name) { return nullptr; } +soinfo *ProxyLinker::FindSoinfoByNameInNamespace(const char *name, android_namespace_t *np) { + int len_a = strlen(name); + return np->soinfo_list().find_if([&](soinfo *si) { + if (const char *so_name = si->get_soname()) { + int delta = strlen(so_name) - len_a; + if (delta == 0 && strncmp(so_name, name, len_a) == 0) { + return true; + } else if (delta > 0 && strncmp(so_name + delta, name, len_a) == 0 && so_name[delta - 1] == '/') { + return true; + } + } + return false; + }); +} + soinfo *ProxyLinker::FindSoinfoByPath(const char *path) { soinfo *si = linker_symbol.solist.Get(); do { diff --git a/library/src/main/cpp/linker/linker_globals.h b/library/src/main/cpp/linker/linker_globals.h index 13a8729..6d1d2f6 100644 --- a/library/src/main/cpp/linker/linker_globals.h +++ b/library/src/main/cpp/linker/linker_globals.h @@ -26,8 +26,9 @@ class ProxyLinker { ANDROID_GE_N android_namespace_t *FindNamespaceByName(const char *name); - ANDROID_GE_N android_namespace_t *GetDefaultNamespace(); + ANDROID_GE_N static android_namespace_t *GetDefaultNamespace(); + ANDROID_GE_N static void *GetNamespaceCallerAddress(android_namespace_t *np); /* * 改变soinfo的命名空间,若np为null则添加到默认命名空间 * */ @@ -62,6 +63,8 @@ class ProxyLinker { soinfo *FindSoinfoByName(const char *name); + soinfo *FindSoinfoByNameInNamespace(const char *name, android_namespace_t *np); + soinfo *FindSoinfoByPath(const char *path); std::vector GetAllSoinfo(); diff --git a/library/src/main/cpp/linker/linker_namespaces.cpp b/library/src/main/cpp/linker/linker_namespaces.cpp index f7a5c15..cf52d15 100644 --- a/library/src/main/cpp/linker/linker_namespaces.cpp +++ b/library/src/main/cpp/linker/linker_namespaces.cpp @@ -421,7 +421,7 @@ soinfo_list_t android_namespace_t::get_global_group() { // the default namespace). soinfo_list_t android_namespace_t::get_shared_group() { - if (this == fakelinker::ProxyLinker::Get().GetDefaultNamespace()) { + if (this == fakelinker::ProxyLinker::GetDefaultNamespace()) { return get_global_group(); } soinfo_list_t shared_group; diff --git a/library/src/main/cpp/linker/linker_soinfo.cpp b/library/src/main/cpp/linker/linker_soinfo.cpp index 649a410..df94882 100644 --- a/library/src/main/cpp/linker/linker_soinfo.cpp +++ b/library/src/main/cpp/linker/linker_soinfo.cpp @@ -324,7 +324,7 @@ ElfW(Addr) __bionic_call_ifunc_resolver(ElfW(Addr) resolver_addr) { #endif } -static inline bool check_symbol_version(const ElfW(Versym) * ver_table, uint32_t sym_idx, const ElfW(Versym) verneed) { +inline bool check_symbol_version(const ElfW(Versym) * ver_table, uint32_t sym_idx, const ElfW(Versym) verneed) { if (ver_table == nullptr) return true; const uint32_t verdef = ver_table[sym_idx]; diff --git a/settings.gradle b/settings.gradle index 6d59e1f..4414598 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,4 +17,5 @@ include ':library' -include ':emualtor-testapp' +include ':emulator-testapp' +include ':fakelinker-test'