Repository: LSPosed/LSPatch Branch: master Commit: bbe8d93fb923 Files: 260 Total size: 1.1 MB Directory structure: gitextract_ca_6zfem/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── crowdin.yml │ └── main.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── apkzlib/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── android/ │ └── tools/ │ └── build/ │ └── apkzlib/ │ ├── bytestorage/ │ │ ├── AbstractCloseableByteSourceFromOutputStreamBuilder.java │ │ ├── ByteStorage.java │ │ ├── ByteStorageFactory.java │ │ ├── ChunkBasedByteStorage.java │ │ ├── ChunkBasedByteStorageFactory.java │ │ ├── ChunkBasedCloseableByteSource.java │ │ ├── CloseableByteSourceFromOutputStreamBuilder.java │ │ ├── InMemoryByteStorage.java │ │ ├── InMemoryByteStorageFactory.java │ │ ├── LimitedInputStream.java │ │ ├── LruTrackedCloseableByteSource.java │ │ ├── LruTracker.java │ │ ├── OverflowToDiskByteStorage.java │ │ ├── OverflowToDiskByteStorageFactory.java │ │ ├── SwitchableDelegateCloseableByteSource.java │ │ ├── SwitchableDelegateInputStream.java │ │ ├── TemporaryDirectory.java │ │ ├── TemporaryDirectoryFactory.java │ │ ├── TemporaryDirectoryStorage.java │ │ ├── TemporaryFile.java │ │ └── TemporaryFileCloseableByteSource.java │ ├── sign/ │ │ ├── DigestAlgorithm.java │ │ ├── ManifestGenerationExtension.java │ │ ├── SignatureAlgorithm.java │ │ ├── SigningExtension.java │ │ ├── SigningOptions.java │ │ └── package-info.java │ ├── utils/ │ │ ├── ApkZLibPair.java │ │ ├── CachedFileContents.java │ │ ├── CachedSupplier.java │ │ ├── IOExceptionConsumer.java │ │ ├── IOExceptionFunction.java │ │ ├── IOExceptionRunnable.java │ │ ├── IOExceptionWrapper.java │ │ ├── SigningBlockUtils.java │ │ └── package-info.java │ ├── zfile/ │ │ ├── ApkCreator.java │ │ ├── ApkCreatorFactory.java │ │ ├── ApkZFileCreator.java │ │ ├── ApkZFileCreatorFactory.java │ │ ├── ManifestAttributes.java │ │ ├── NativeLibrariesPackagingMode.java │ │ ├── ZFiles.java │ │ └── package-info.java │ └── zip/ │ ├── AlignmentRule.java │ ├── AlignmentRules.java │ ├── CentralDirectory.java │ ├── CentralDirectoryHeader.java │ ├── CentralDirectoryHeaderCompressInfo.java │ ├── CompressionMethod.java │ ├── CompressionResult.java │ ├── Compressor.java │ ├── DataDescriptorType.java │ ├── EncodeUtils.java │ ├── Eocd.java │ ├── EocdGroup.java │ ├── ExtraField.java │ ├── FileUseMap.java │ ├── FileUseMapEntry.java │ ├── GPFlags.java │ ├── InflaterByteSource.java │ ├── LazyDelegateByteSource.java │ ├── NestedZip.java │ ├── ProcessedAndRawByteSources.java │ ├── StoredEntry.java │ ├── StoredEntryType.java │ ├── VerifyLog.java │ ├── VerifyLogs.java │ ├── ZFile.java │ ├── ZFileExtension.java │ ├── ZFileOptions.java │ ├── Zip64Eocd.java │ ├── Zip64EocdLocator.java │ ├── Zip64ExtensibleDataSector.java │ ├── ZipField.java │ ├── ZipFieldInvariant.java │ ├── ZipFieldInvariantMaxValue.java │ ├── ZipFieldInvariantMinValue.java │ ├── ZipFieldInvariantNonNegative.java │ ├── ZipFileState.java │ ├── compress/ │ │ ├── BestAndDefaultDeflateExecutorCompressor.java │ │ ├── DeflateExecutionCompressor.java │ │ ├── ExecutorCompressor.java │ │ ├── Zip64NotSupportedException.java │ │ └── package-info.java │ └── utils/ │ ├── ByteTracker.java │ ├── CloseableByteSource.java │ ├── CloseableDelegateByteSource.java │ ├── LittleEndianUtils.java │ ├── MsDosDateTimeUtils.java │ └── RandomAccessFileUtils.java ├── build.gradle.kts ├── crowdin.yml ├── gradle/ │ ├── lspatch.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jar/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── assets/ │ └── keystore ├── manager/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules-debug.pro │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── keystore │ ├── java/ │ │ └── org/ │ │ └── lsposed/ │ │ └── lspatch/ │ │ ├── LSPApplication.kt │ │ ├── Patcher.kt │ │ ├── config/ │ │ │ ├── ConfigManager.kt │ │ │ ├── Configs.kt │ │ │ └── MyKeyStore.kt │ │ ├── database/ │ │ │ ├── LSPDatabase.kt │ │ │ ├── dao/ │ │ │ │ ├── ModuleDao.kt │ │ │ │ └── ScopeDao.kt │ │ │ └── entity/ │ │ │ ├── Module.kt │ │ │ └── Scope.kt │ │ ├── manager/ │ │ │ ├── AppBroadcastReceiver.kt │ │ │ ├── ManagerService.kt │ │ │ └── ModuleService.kt │ │ ├── ui/ │ │ │ ├── activity/ │ │ │ │ └── MainActivity.kt │ │ │ ├── component/ │ │ │ │ ├── AnywhereDropdown.kt │ │ │ │ ├── AppItem.kt │ │ │ │ ├── CenterTopBar.kt │ │ │ │ ├── LoadingDialog.kt │ │ │ │ ├── SearchBar.kt │ │ │ │ ├── SelectionColumn.kt │ │ │ │ ├── Shimmer.kt │ │ │ │ └── settings/ │ │ │ │ ├── CheckBox.kt │ │ │ │ ├── Slot.kt │ │ │ │ └── Switch.kt │ │ │ ├── page/ │ │ │ │ ├── BottomBarDestination.kt │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── LogsScreen.kt │ │ │ │ ├── ManageScreen.kt │ │ │ │ ├── NewPatchScreen.kt │ │ │ │ ├── RepoScreen.kt │ │ │ │ ├── SelectAppsScreen.kt │ │ │ │ ├── SettingsScreen.kt │ │ │ │ └── manage/ │ │ │ │ ├── AppManagePage.kt │ │ │ │ └── ModuleManagePage.kt │ │ │ ├── theme/ │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── util/ │ │ │ │ ├── CompositionProvider.kt │ │ │ │ ├── HtmlText.kt │ │ │ │ ├── MultiDelegateState.kt │ │ │ │ ├── Preview.kt │ │ │ │ └── Utils.kt │ │ │ ├── viewmodel/ │ │ │ │ ├── NewPatchViewModel.kt │ │ │ │ ├── SelectAppsViewModel.kt │ │ │ │ └── manage/ │ │ │ │ ├── AppManageViewModel.kt │ │ │ │ └── ModuleManageViewModel.kt │ │ │ └── viewstate/ │ │ │ └── ProcessingState.kt │ │ └── util/ │ │ ├── IntentSenderHelper.kt │ │ ├── LSPPackageManager.kt │ │ └── ShizukuApi.kt │ └── res/ │ ├── drawable/ │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ ├── drawable-zh-rCN/ │ │ └── ic_launcher_background.xml │ ├── drawable-zh-rTW/ │ │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26/ │ │ └── ic_launcher.xml │ ├── values/ │ │ ├── strings.xml │ │ └── strings_untranslatable.xml │ ├── values-af/ │ │ └── strings.xml │ ├── values-ar/ │ │ └── strings.xml │ ├── values-bg/ │ │ └── strings.xml │ ├── values-bn/ │ │ └── strings.xml │ ├── values-ca/ │ │ └── strings.xml │ ├── values-cs/ │ │ └── strings.xml │ ├── values-da/ │ │ └── strings.xml │ ├── values-de/ │ │ └── strings.xml │ ├── values-el/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-et/ │ │ └── strings.xml │ ├── values-fa/ │ │ └── strings.xml │ ├── values-fi/ │ │ └── strings.xml │ ├── values-fr/ │ │ └── strings.xml │ ├── values-hi/ │ │ └── strings.xml │ ├── values-hr/ │ │ └── strings.xml │ ├── values-hu/ │ │ └── strings.xml │ ├── values-in/ │ │ └── strings.xml │ ├── values-it/ │ │ └── strings.xml │ ├── values-iw/ │ │ └── strings.xml │ ├── values-ja/ │ │ └── strings.xml │ ├── values-ko/ │ │ └── strings.xml │ ├── values-ku/ │ │ └── strings.xml │ ├── values-lt/ │ │ └── strings.xml │ ├── values-nl/ │ │ └── strings.xml │ ├── values-no/ │ │ └── strings.xml │ ├── values-pl/ │ │ └── strings.xml │ ├── values-pt/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ro/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-si/ │ │ └── strings.xml │ ├── values-sk/ │ │ └── strings.xml │ ├── values-sv/ │ │ └── strings.xml │ ├── values-th/ │ │ └── strings.xml │ ├── values-tr/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ ├── values-ur/ │ │ └── strings.xml │ ├── values-vi/ │ │ └── strings.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rHK/ │ │ └── strings.xml │ └── values-zh-rTW/ │ └── strings.xml ├── meta-loader/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── org/ │ └── lsposed/ │ └── lspatch/ │ └── metaloader/ │ └── LSPAppComponentFactoryStub.java ├── patch/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── org/ │ └── lsposed/ │ └── patch/ │ ├── LSPatch.java │ └── util/ │ ├── ApkSignatureHelper.java │ ├── JavaLogger.java │ ├── Logger.java │ └── ManifestParser.java ├── patch-loader/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── org/ │ │ └── lsposed/ │ │ ├── lspatch/ │ │ │ ├── loader/ │ │ │ │ ├── LSPApplication.java │ │ │ │ ├── LSPLoader.java │ │ │ │ ├── SigBypass.java │ │ │ │ └── util/ │ │ │ │ ├── FileUtils.java │ │ │ │ └── XLog.java │ │ │ └── service/ │ │ │ ├── LocalApplicationService.java │ │ │ └── RemoteApplicationService.java │ │ └── lspd/ │ │ └── nativebridge/ │ │ └── SigBypass.java │ └── jni/ │ ├── CMakeLists.txt │ ├── api/ │ │ └── patch_main.cpp │ ├── include/ │ │ └── art/ │ │ └── runtime/ │ │ ├── jit/ │ │ │ └── profile_saver.h │ │ └── oat_file_manager.h │ └── src/ │ ├── config_impl.h │ ├── jni/ │ │ ├── bypass_sig.cpp │ │ └── bypass_sig.h │ ├── patch_loader.cpp │ └── patch_loader.h ├── settings.gradle.kts └── share/ ├── android/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── org/ │ └── lsposed/ │ └── lspatch/ │ └── util/ │ └── ModuleLoader.java └── java/ ├── .gitignore ├── build.gradle.kts └── src/ ├── main/ │ └── java/ │ └── org/ │ └── lsposed/ │ └── lspatch/ │ └── share/ │ ├── Constants.java │ └── PatchConfig.java └── template/ └── java/ └── org.lsposed.lspatch.share/ └── LSPConfig.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Set the default behavior, in case people don't have core.autocrlf set. * text=auto eol=lf # Declare files that will always have CRLF line endings on checkout. *.cmd text eol=crlf *.bat text eol=crlf # Denote all files that are truly binary and should not be modified. *.so binary *.dex binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report/反馈 Bug description: Report errors or unexpected behavior./反馈错误或异常行为。 labels: [bug] title: "[Bug] " body: - type: markdown attributes: value: | Thanks for reporting issues of LSPatch! To make it easier for us to help you please enter detailed information below. 感谢给 LSPatch 汇报问题! 为了使我们更好地帮助你,请提供以下信息。 为了防止重复汇报,标题请务必使用英文。 - type: textarea attributes: label: Steps to reproduce/复现步骤 value: | 1. 1. 1. validations: required: true - type: textarea attributes: label: Expected behaviour/预期行为 placeholder: Tell us what should happen/正常情况下应该发生什么 validations: required: true - type: textarea attributes: label: Actual behaviour/实际行为 placeholder: Tell us what happens instead/实际上发生了什么 validations: required: true - type: textarea attributes: label: Xposed Module List/Xposed 模块列表 render: Shell validations: required: true - type: input attributes: label: LSPatch version/LSPatch 版本 description: Don't use 'latest'. Specify actual version with 4 digits, otherwise your issue will be closed./不要填用“最新版”。给出四位版本号,不然 issue 会被关闭。 validations: required: true - type: input attributes: label: Android version/Android 版本 validations: required: true - type: input attributes: label: Shizuku version/Shizuku 版本 description: If you are not using Shizuku mode, input `N/A` instead./如果未使用 Shizuku 模式,请键入 `N/A`。 validations: required: true - type: checkboxes id: latest attributes: label: Version requirement/版本要求 options: - label: I am using latest debug CI version of LSPatch and enable verbose log/我正在使用最新 CI 调试版本且启用详细日志 required: true - type: textarea attributes: label: Apk file/Apk 文件 description: The apk file if patching failed / 修复失败的 apk 文件(如有) placeholder: Upload apks by clicking the bar on the bottom. /点击文本框底栏上传安装包文件。 - type: textarea attributes: label: Logs/日志 description: For usage issues, please provide the log zip saved from manager; for activation issues, please provide [bugreport](https://developer.android.com/studio/debug/bug-report). Without log, the issue will be closed. /使用问题请提供从管理器保存的日志压缩包;激活问题请提供 [bugreport](https://developer.android.google.cn/studio/debug/bug-report?hl=zh-cn) 日志。无日志提交会被关闭。 value: |
``` # Replace this line with the log / 将此行用日志替换 ```
validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask a question/提问 url: https://github.com/LSPosed/LSPatch/discussions/new?category=Q-A about: Please ask and answer questions here./如果有任何疑问请在这里提问 - name: Official Telegram Channel/官方 Telegram 频道 url: https://t.me/LSPosed about: Subscribe for notifications and releases/可以订阅通知和发行版 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ --- name: Feature request/新特性请求 description: Suggest an idea./提出建议 labels: [enhancement] title: "[Feature Request] " body: - type: textarea attributes: label: Is your feature request related to a problem?/你的请求是否与某个问题相关? placeholder: A clear and concise description of what the problem is./请清晰准确表述该问题。 validations: required: true - type: textarea attributes: label: Describe the solution you'd like/描述你想要的解决方案 placeholder: A clear and concise description of what you want to happen./请清晰准确描述新特性的预期行为 validations: required: true - type: textarea attributes: label: Additional context/其他信息 placeholder: Add any other context or screenshots about the feature request here./其他关于新特性的信息或者截图 validations: required: false ================================================ FILE: .github/workflows/crowdin.yml ================================================ name: Crowdin Action on: workflow_dispatch: push: branches: [ master ] paths: - manager/src/main/res/values/strings.xml jobs: synchronize-with-crowdin: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: crowdin action uses: crowdin/github-action@master with: upload_translations: false download_translations: false upload_sources: true config: 'crowdin.yml' crowdin_branch_name: master env: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }} ================================================ FILE: .github/workflows/main.yml ================================================ name: Android CI on: workflow_dispatch: push: branches: [ master ] pull_request: jobs: build: name: Build on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest ] env: CCACHE_COMPILERCHECK: '%compiler% -dumpmachine; %compiler% -dumpversion' CCACHE_NOHASHDIR: 'true' CCACHE_HARDLINK: 'true' CCACHE_BASEDIR: '${{ github.workspace }}' steps: - name: Checkout uses: actions/checkout@v3 with: submodules: 'recursive' fetch-depth: 0 - name: Write key if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' run: | if [ ! -z "${{ secrets.KEY_STORE }}" ]; then echo androidStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> gradle.properties echo androidKeyAlias='${{ secrets.ALIAS }}' >> gradle.properties echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties echo androidStoreFile='key.jks' >> gradle.properties echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks fi - name: Checkout libxposed/api uses: actions/checkout@v3 with: repository: libxposed/api path: libxposed/api - name: Checkout libxposed/service uses: actions/checkout@v3 with: repository: libxposed/service path: libxposed/service - name: Setup Java uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Setup Gradle uses: gradle/gradle-build-action@v2 with: gradle-home-cache-cleanup: true - name: Set up ccache uses: hendrikmuhs/ccache-action@v1.2 with: max-size: 2G key: ${{ runner.os }} restore-keys: ${{ runner.os }} save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - name: Build dependencies working-directory: libxposed run: | cd api echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties ./gradlew :api:publishApiPublicationToMavenLocal cd .. cd service echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties ./gradlew :interface:publishInterfacePublicationToMavenLocal - name: Build with Gradle run: | echo 'org.gradle.parallel=true' >> gradle.properties echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties echo 'android.native.buildOutput=verbose' >> gradle.properties ./gradlew buildAll - name: Upload Debug artifact uses: actions/upload-artifact@v3 with: name: lspatch-debug path: out/debug/* - name: Upload Release artifact uses: actions/upload-artifact@v3 with: name: lspatch-release path: out/release/* - name: Upload mappings uses: actions/upload-artifact@v3 with: name: mappings path: | patch-loader/build/outputs/mapping manager/build/outputs/mapping - name: Upload symbols uses: actions/upload-artifact@v3 with: name: symbols path: | patch-loader/build/symbols - name: Post to channel if: ${{ github.event_name != 'pull_request' && success() && github.ref == 'refs/heads/master' }} env: CHANNEL_ID: ${{ secrets.CHANNEL_ID }} DISCUSSION_ID: ${{ secrets.DISCUSSION_ID }} TOPIC_ID: ${{ secrets.TOPIC_ID }} BOT_TOKEN: ${{ secrets.BOT_TOKEN }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} COMMIT_URL: ${{ github.event.head_commit.url }} run: | if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then export jarRelease=$(find out/release -name "*.jar") export managerRelease=$(find out/release -name "*.apk") export jarDebug=$(find out/debug -name "*.jar") export managerDebug=$(find out/debug -name "*.apk") ESCAPED=`python3 -c 'import json,os,urllib.parse; msg = json.dumps(os.environ["COMMIT_MESSAGE"]); print(urllib.parse.quote(msg if len(msg) <= 1024 else json.dumps(os.environ["COMMIT_URL"])))'` curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarDebug%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerDebug%22%2C%22caption%22:${ESCAPED}%7D%5D" -F jarRelease="@$jarRelease" -F managerRelease="@$managerRelease" -F jarDebug="@$jarDebug" -F managerDebug="@$managerDebug" # curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${DISCUSSION_ID}&message_thread_id=${TOPIC_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarDebug%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerDebug%22%2C%22caption%22:${ESCAPED}%7D%5D" -F jarRelease="@$jarRelease" -F managerRelease="@$managerRelease" -F jarDebug="@$jarDebug" -F managerDebug="@$managerDebug" fi ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx /out /.idea ================================================ FILE: .gitmodules ================================================ [submodule "apksigner"] path = apksigner url = https://android.googlesource.com/platform/tools/apksig.git branch = android10-release [submodule "core"] path = core url = https://github.com/LSPosed/LSPosed.git branch = master [submodule "patch/libs/manifest-editor"] path = patch/libs/manifest-editor url = https://github.com/WindySha/ManifestEditor.git ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # LSPatch Framework [![Build](https://img.shields.io/github/actions/workflow/status/LSPosed/LSPatch/main.yml?branch=master&logo=github&label=Build&event=push)](https://github.com/LSPosed/LSPatch/actions/workflows/main.yml?query=event%3Apush+is%3Acompleted+branch%3Amaster) [![Crowdin](https://img.shields.io/badge/Localization-Crowdin-blueviolet?logo=Crowdin)](https://lsposed.crowdin.com/lspatch) [![Download](https://img.shields.io/github/v/release/LSPosed/LSPatch?color=orange&logoColor=orange&label=Download&logo=DocuSign)](https://github.com/LSPosed/LSPatch/releases/latest) [![Total](https://shields.io/github/downloads/LSPosed/LSPatch/total?logo=Bookmeter&label=Counts&logoColor=yellow&color=yellow)](https://github.com/LSPosed/LSPatch/releases) ## Introduction Rootless implementation of LSPosed framework, integrating Xposed API by inserting dex and so into the target APK. ## Supported Versions - Min: Android 9 - Max: In theory, same with [LSPosed](https://github.com/LSPosed/LSPosed#supported-versions) ## Download For stable releases, please go to [Github Releases page](https://github.com/LSPosed/LSPatch/releases) For canary build, please check [Github Actions](https://github.com/LSPosed/LSPatch/actions) Note: debug builds are only available in Github Actions ## Usage + Through jar 1. Download `lspatch.jar` 1. Run `java -jar lspatch.jar` + Through manager 1. Download and install `manager.apk` on an Android device 1. Follow the instructions of the manager app ## Translation Contributing You can contribute translation [here](https://lsposed.crowdin.com/lspatch). ## Credits - [LSPosed](https://github.com/LSPosed/LSPosed): Core framework - [Xpatch](https://github.com/WindySha/Xpatch): Fork source - [Apkzlib](https://android.googlesource.com/platform/tools/apkzlib): Repacking tool ## License LSPatch is licensed under the **GNU General Public License v3 (GPL-3)** (http://www.gnu.org/copyleft/gpl.html). ================================================ FILE: apkzlib/.gitignore ================================================ /build ================================================ FILE: apkzlib/build.gradle.kts ================================================ val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra plugins { id("java-library") } java { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility } dependencies { implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation("org.bouncycastle:bcprov-jdk15on:1.70") api("com.google.guava:guava:32.0.1-jre") api("com.android.tools.build:apksig:8.0.2") compileOnlyApi("com.google.auto.value:auto-value-annotations:1.10.1") annotationProcessor("com.google.auto.value:auto-value:1.10.1") } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/AbstractCloseableByteSourceFromOutputStreamBuilder.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.base.Preconditions; import java.io.IOException; /** * Abstract implementation of a {@link CloseableByteSourceFromOutputStreamBuilder} that simplifies * the implementation of concrete instances. It implements the state machine implied by the * interface contract and requires subclasses to implement two methods: * {@link #doWrite(byte[], int, int)} -- that actually does writing and {@link #doBuild()} that * builds the {@link CloseableByteSource]. */ abstract class AbstractCloseableByteSourceFromOutputStreamBuilder extends CloseableByteSourceFromOutputStreamBuilder { /** * Array that allows {@link #write(int)} to delegate to {@link #write(byte[], int, int)} without * having to create an array for each invocation. */ private final byte[] tempByte; /** * Has the builder been closed? If it has, then {@link #build()} may be called, but none of the * writing methods can. */ private boolean closed; /** * Has the builder been built? If this is {@code true} then {@link #closed} is also {@code true}. */ private boolean built; /** Creates a new builder. */ AbstractCloseableByteSourceFromOutputStreamBuilder() { tempByte = new byte[1]; closed = false; built = false; } @Override public void write(byte[] b, int off, int len) throws IOException { Preconditions.checkState(!closed); doWrite(b, off, len); } @Override public void write(int b) throws IOException { tempByte[0] = (byte) b; write(tempByte, 0, 1); } @Override public void close() throws IOException { closed = true; } @Override public CloseableByteSource build() throws IOException { Preconditions.checkState(!built); closed = true; built = true; return doBuild(); } /** * Same as {@link #write(byte[], int, int)}, but with the guarantee that the source has not been * built and the builder is still open. * * @param b see {@link #write(byte[], int, int)} * @param off see {@link #write(byte[], int, int)} * @param len see {@link #write(byte[], int, int)} * @throws IOException see {@link #write(byte[], int, int)} */ protected abstract void doWrite(byte[] b, int off, int len) throws IOException; /** * Builds the {@link CloseableByteSource} from the written data. This method is at most invoked * once. * * @return the new source that will contain all data written to the builder so far * @throws IOException failed to create the byte source */ protected abstract CloseableByteSource doBuild() throws IOException; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ByteStorage.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.io.ByteSource; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; /** * Interface for a storage that will temporarily save bytes. There are several factory methods to * create byte sources from several inputs, all of which may be discarded after the byte source has * been created. The data is saved in the storage and will be kept until the byte source is closed. */ public interface ByteStorage extends Closeable { /** * Creates a new byte source by fully reading an input stream. * * @param stream the input stream * @return a byte source containing the cached data from the given stream * @throws IOException failed to read the stream */ CloseableByteSource fromStream(InputStream stream) throws IOException; /** * Creates a builder that is an output stream and can create a byte source. * * @return a builder where data can be written to and a {@link CloseableByteSource} can eventually * be obtained from * @throws IOException failed to create the builder; this may happen if the builder require some * preparation such as temporary storage allocation that may fail */ CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException; /** * Creates a new byte source from another byte source. * * @param source the byte source to copy data from * @return the tracked byte source * @throws IOException failed to read data from the byte source */ CloseableByteSource fromSource(ByteSource source) throws IOException; /** * Obtains the number of bytes currently used. * * @return the number of bytes */ long getBytesUsed(); /** * Obtains the maximum number of bytes ever used by this tracker. * * @return the number of bytes */ long getMaxBytesUsed(); } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ByteStorageFactory.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import java.io.IOException; /** Factory that creates {@link ByteStorage}. */ public interface ByteStorageFactory { /** * Creates a new storage. * * @return a storage that should be closed when no longer used. */ ByteStorage create() throws IOException; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ChunkBasedByteStorage.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteSource; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; /** * Byte storage that breaks byte sources into smaller byte sources. This storage uses another * storage as a delegate and, when a source is requested, it will allocate one or more sources from * the delegate to build the requested source. */ public class ChunkBasedByteStorage implements ByteStorage { /** Size of the default chunk size. */ private static final long DEFAULT_CHUNK_SIZE_BYTES = 10 * 1024 * 1024; /** Maximum size of each chunk. */ private final long maxChunkSize; /** Byte storage where the data is actually stored. */ private final ByteStorage delegate; /** * Creates a new storage breaking sources in chunks with the default maximum size and allocating * each chunk from {@code delegate}. */ ChunkBasedByteStorage(ByteStorage delegate) { this(DEFAULT_CHUNK_SIZE_BYTES, delegate); } /** * Creates a new storage breaking sources in chunks with the maximum of {@code maxChunkSize} and * allocating each chunk from {@code delegate}. */ ChunkBasedByteStorage(long maxChunkSize, ByteStorage delegate) { this.maxChunkSize = maxChunkSize; this.delegate = delegate; } /** Obtains the byte storage chunks are allocated from. */ @VisibleForTesting // private otherwise. public ByteStorage getDelegate() { return delegate; } @Override public CloseableByteSource fromStream(InputStream stream) throws IOException { List sources = new ArrayList<>(); while (true) { LimitedInputStream limitedInput = new LimitedInputStream(stream, maxChunkSize); sources.add(delegate.fromStream(limitedInput)); if (limitedInput.isInputFinished()) { break; } } return new ChunkBasedCloseableByteSource(sources); } @Override public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException { return new AbstractCloseableByteSourceFromOutputStreamBuilder() { private final List sources = new ArrayList<>(); @Nullable private CloseableByteSourceFromOutputStreamBuilder currentBuilder = null; private long written = 0; @Override protected void doWrite(byte[] b, int off, int len) throws IOException { int actualOffset = off; int remaining = len; while (remaining > 0) { // Since we're writing data, make sure we have a builder to create the new source. if (currentBuilder == null) { currentBuilder = delegate.makeBuilder(); written = 0; } // See how much we can write without exceeding maxChunkSize in the current builder. int maxWrite = (int) Math.min(maxChunkSize - written, remaining); currentBuilder.write(b, actualOffset, maxWrite); written += maxWrite; remaining -= maxWrite; actualOffset += maxWrite; // If we've reached the end of the chunk, create the source for the part we have and reset // to builder so we start a new one if there is more data. if (written == maxChunkSize) { sources.add(currentBuilder.build()); currentBuilder = null; } } } @Override protected CloseableByteSource doBuild() throws IOException { // If we were writing a chunk, close it. if (currentBuilder != null) { sources.add(currentBuilder.build()); currentBuilder = null; } return new ChunkBasedCloseableByteSource(sources); } }; } @Override public CloseableByteSource fromSource(ByteSource source) throws IOException { List sources = new ArrayList<>(); long end = source.size(); long start = 0; while (start < end) { long chunkSize = Math.min(end - start, maxChunkSize); sources.add(delegate.fromSource(source.slice(start, chunkSize))); start += chunkSize; } return new ChunkBasedCloseableByteSource(sources); } @Override public long getBytesUsed() { return delegate.getBytesUsed(); } @Override public long getMaxBytesUsed() { return delegate.getMaxBytesUsed(); } @Override public void close() throws IOException { delegate.close(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ChunkBasedByteStorageFactory.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import java.io.IOException; import javax.annotation.Nullable; /** * {@link ByteStorageFactory} that creates {@link ByteStorage} instances that keep all data in * memory. */ public class ChunkBasedByteStorageFactory implements ByteStorageFactory { /** Factory to create the delegate storages. */ private final ByteStorageFactory delegate; /** Maximum size for chunks, if any. */ @Nullable private final Long maxChunkSize; /** Creates a new factory whose storages are created using delegates from the given factory. */ public ChunkBasedByteStorageFactory(ByteStorageFactory delegate) { this(delegate, /*maxChunkSize=*/ null); } /** * Creates a new factory whose storages use the given maximum chunk size and are created using * delegates from the given factory. */ public ChunkBasedByteStorageFactory(ByteStorageFactory delegate, @Nullable Long maxChunkSize) { this.delegate = delegate; this.maxChunkSize = maxChunkSize; } @Override public ByteStorage create() throws IOException { if (maxChunkSize == null) { return new ChunkBasedByteStorage(delegate.create()); } else { return new ChunkBasedByteStorage(maxChunkSize, delegate.create()); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ChunkBasedCloseableByteSource.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteSource; import com.google.common.io.Closer; import java.io.IOException; import java.util.List; /** * Byte source that has its data spread over several chunks, each with its own {@link * CloseableByteSource}. */ class ChunkBasedCloseableByteSource extends CloseableDelegateByteSource { /** The sources for data of all the chunks, in order. */ private final ImmutableList sources; /** Creates a new source from the given sources. */ ChunkBasedCloseableByteSource(List sources) throws IOException { super(ByteSource.concat(sources), sumSizes(sources)); this.sources = ImmutableList.copyOf(sources); } /** Computes the size of this source by summing the sizes of all sources. */ private static long sumSizes(List sources) throws IOException { long sum = 0; for (CloseableByteSource source : sources) { sum += source.size(); } return sum; } @Override protected synchronized void innerClose() throws IOException { try (Closer closer = Closer.create()) { for (CloseableByteSource source : sources) { closer.register(source); } } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/CloseableByteSourceFromOutputStreamBuilder.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import java.io.IOException; import java.io.OutputStream; /** * Output stream that creates a {@link CloseableByteSource} from the data that was written to it. * Calling {@link #close} is optional as {@link #build()} will also close the output stream. */ public abstract class CloseableByteSourceFromOutputStreamBuilder extends OutputStream { /** * Creates the source from the data that has been written to the stream. No more data can be * written to the output stream after this method has been called. * * @return a source that will provide the data that was written to the stream before this method * is invoked; where this data is stored is not specified by this interface * @throws IOException failed to build the byte source */ public abstract CloseableByteSource build() throws IOException; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/InMemoryByteStorage.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; /** Keeps track of used bytes allowing gauging memory usage. */ public class InMemoryByteStorage implements ByteStorage { /** Number of bytes currently in use. */ private long bytesUsed; /** Maximum number of bytes used. */ private long maxBytesUsed; @Override public CloseableByteSource fromStream(InputStream stream) throws IOException { byte[] data = ByteStreams.toByteArray(stream); updateUsage(data.length); return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) { @Override public synchronized void innerClose() throws IOException { super.innerClose(); updateUsage(-sizeNoException()); } }; } @Override public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); return new AbstractCloseableByteSourceFromOutputStreamBuilder() { @Override protected void doWrite(byte[] b, int off, int len) throws IOException { output.write(b, off, len); updateUsage(len); } @Override protected CloseableByteSource doBuild() throws IOException { byte[] data = output.toByteArray(); return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) { @Override protected synchronized void innerClose() throws IOException { super.innerClose(); updateUsage(-data.length); } }; } }; } @Override public CloseableByteSource fromSource(ByteSource source) throws IOException { return fromStream(source.openStream()); } /** * Updates the memory used by this tracker. * * @param delta the number of bytes to add or remove, if negative */ private synchronized void updateUsage(long delta) { bytesUsed += delta; if (maxBytesUsed < bytesUsed) { maxBytesUsed = bytesUsed; } } @Override public synchronized long getBytesUsed() { return bytesUsed; } @Override public synchronized long getMaxBytesUsed() { return maxBytesUsed; } @Override public void close() throws IOException { // Nothing to do on close. } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/InMemoryByteStorageFactory.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import java.io.IOException; /** * {@link ByteStorageFactory} that creates {@link ByteStorage} instances that keep all data in * memory. */ public class InMemoryByteStorageFactory implements ByteStorageFactory { @Override public ByteStorage create() throws IOException { return new InMemoryByteStorage(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/LimitedInputStream.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.bytestorage; import java.io.IOException; import java.io.InputStream; /** * Input stream that reads only a limited number of bytes from another input stream before reporting * EOF. When closed, this stream will not close the underlying stream. * *

If the underlying stream does not have enough data, this stream will read all available data * from the underlying stream. */ class LimitedInputStream extends InputStream { /** Where the data comes from. */ private final InputStream input; /** How many bytes remain in this stream. */ private long remaining; /** Has EOF been detected in {@link #input}? */ private boolean eofDetected; /** * Creates a new input stream. * * @param input where to read data from * @param maximum the maximum number of bytes to read from {@code input} */ LimitedInputStream(InputStream input, long maximum) { this.input = input; this.remaining = maximum; this.eofDetected = false; } @Override public int read() throws IOException { if (remaining == 0) { return -1; } int r = input.read(); if (r >= 0) { remaining--; } else { eofDetected = true; } return r; } @Override public int read(byte[] whereTo, int offset, int length) throws IOException { if (remaining == 0) { return -1; } int toRead = (int) Math.min(remaining, length); int r = input.read(whereTo, offset, toRead); if (r >= 0) { remaining -= r; } else { eofDetected = true; } return r; } /** Returns {@code true} if EOF has been detected in the {@code input} stream. */ boolean isInputFinished() { return eofDetected; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/LruTrackedCloseableByteSource.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.base.Preconditions; import java.io.IOException; import java.io.InputStream; /** * Byte source that, until switched, will keep itself in the LRU queue. The byte source will * automatically remove itself from the queue once closed or moved to disk (see {@link * #moveToDisk(ByteStorage)}. This source should not be switched explicitly or tracking will not * work. * *

The source will consider an access to be opening a stream. Every time a stream is open the * source will move itself to the top of the LRU list. */ class LruTrackedCloseableByteSource extends SwitchableDelegateCloseableByteSource { /** The tracker being used. */ private final LruTracker tracker; /** Are we still tracking usage? */ private boolean tracking; /** Has the byte source been closed? */ private boolean closed; /** Creates a new byte source based on the given source and using the provided tracker. */ LruTrackedCloseableByteSource( CloseableByteSource delegate, LruTracker tracker) throws IOException { super(delegate); this.tracker = tracker; tracker.track(this); tracking = true; closed = false; } @Override public synchronized InputStream openStream() throws IOException { Preconditions.checkState(!closed); if (tracking) { tracker.access(this); } return super.openStream(); } @Override protected synchronized void innerClose() throws IOException { closed = true; untrack(); super.innerClose(); } /** * Marks this source as not being tracked any more. May be called multiple times (only the first * one will do anything). */ private synchronized void untrack() { if (tracking) { tracking = false; tracker.untrack(this); } } /** * Moves the contents of this source to a storage. This will untrack the source and switch its * contents to a new delegate provided by {@code diskStorage}. */ synchronized void move(ByteStorage diskStorage) throws IOException { if (closed) { return; } CloseableByteSource diskSource = diskStorage.fromSource(this); untrack(); switchSource(diskSource); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/LruTracker.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.google.common.base.Preconditions; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import java.util.TreeSet; import javax.annotation.Nullable; /** * A tracker that keeps a list of the last-recently-used objects of type {@code T}. The tracker * doesn't define what LRU means, it has a method, {@link #access(Object)} that marks the object as * being accessed and moves it to the top of the queue. * *

This implementation is O(log(N)) on all operations. * *

Implementation note: we don't keep track of time. Instead we use a counter that is incremented * every time a new access is done or a new object is tracked. Because of this, each access time is * unique for each object (although it will change after each access). */ class LruTracker { /** Maps each object to its unique access time and vice-versa. */ private final BiMap objectToAccessTime; /** * Ordered set of all object's access times. This set has the same contents as {@code * objectToAccessTime.value()}. It is sorted from the highest access time (newest) to the lowest * access time (oldest). */ private final TreeSet accessTimes; /** Next access time to use for tracking or accessing. */ private int currentTime; /** Creates a new tracker without any objects. */ LruTracker() { currentTime = 1; objectToAccessTime = HashBiMap.create(); accessTimes = new TreeSet<>((i0, i1) -> i1 - i0); } /** Starts tracking an object. This object's will be the most recently used. */ synchronized void track(T object) { Preconditions.checkState(!objectToAccessTime.containsKey(object)); objectToAccessTime.put(object, currentTime); accessTimes.add(currentTime); currentTime++; } /** Stops tracking an object. */ synchronized void untrack(T object) { Preconditions.checkState(objectToAccessTime.containsKey(object)); accessTimes.remove(objectToAccessTime.get(object)); objectToAccessTime.remove(object); } /** Marks the given object as having been accessed promoting it as the most recently used. */ synchronized void access(T object) { untrack(object); track(object); } /** * Obtains the position of an object in the queue. It will be {@code 0} for the most recently used * object. */ synchronized int positionOf(T object) { Preconditions.checkState(objectToAccessTime.containsKey(object)); int lastAccess = objectToAccessTime.get(object); return accessTimes.headSet(lastAccess).size(); } /** * Obtains the last element, the one last accessed earliest. Will return empty if there are no * objects being tracked. */ @Nullable synchronized T last() { if (accessTimes.isEmpty()) { return null; } return objectToAccessTime.inverse().get(accessTimes.last()); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/OverflowToDiskByteStorage.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteSource; import java.io.IOException; import java.io.InputStream; /** * Byte storage that keeps data in memory up to a certain size. After that, older sources are moved * to disk and the newer ones served from memory. * *

Once unloaded to disk, sources are not reloaded into memory as that would be in direct * conflict with the filesystem's caching and the costs would probably outweight the benefits. * *

The maximum memory used by storage is actually larger than the maximum provided. It may exceed * the limit by the size of one source. That is because sources are always loaded into memory before * the storage decides to flush them to disk. */ public class OverflowToDiskByteStorage implements ByteStorage { /** Size of the default memory cache. */ private static final long DEFAULT_MEMORY_CACHE_BYTES = 50 * 1024 * 1024; /** In-memory storage. */ private final InMemoryByteStorage memoryStorage; /** Disk-based storage. */ @VisibleForTesting // private otherwise. final TemporaryDirectoryStorage diskStorage; /** Tracker that keeps all memory sources. */ private final LruTracker memorySourcesTracker; /** Maximum amount of data to keep in memory. */ private final long memoryCacheSize; /** Maximum amount of data used. */ private long maxBytesUsed; /** * Creates a new byte storage with the default memory cache using the provided temporary directory * to write data that overflows the memory size. * * @param temporaryDirectoryFactory the factory used to create a temporary directory where to * overflow to; the created directory will be closed when the {@link * OverflowToDiskByteStorage} object is closed * @throws IOException failed to create the temporary directory */ public OverflowToDiskByteStorage(TemporaryDirectoryFactory temporaryDirectoryFactory) throws IOException { this(DEFAULT_MEMORY_CACHE_BYTES, temporaryDirectoryFactory); } /** * Creates a new byte storage with the given memory cache size using the provided temporary * directory to write data that overflows the memory size. * * @param memoryCacheSize the in-memory cache; a value of {@link 0} will effectively disable * in-memory caching * @param temporaryDirectoryFactory the factory used to create a temporary directory where to * overflow to; the created directory will be closed when the {@link * OverflowToDiskByteStorage} object is closed * @throws IOException failed to create the temporary directory */ public OverflowToDiskByteStorage( long memoryCacheSize, TemporaryDirectoryFactory temporaryDirectoryFactory) throws IOException { memoryStorage = new InMemoryByteStorage(); diskStorage = new TemporaryDirectoryStorage(temporaryDirectoryFactory); this.memoryCacheSize = memoryCacheSize; this.memorySourcesTracker = new LruTracker<>(); } @Override public CloseableByteSource fromStream(InputStream stream) throws IOException { CloseableByteSource memSource = new LruTrackedCloseableByteSource(memoryStorage.fromStream(stream), memorySourcesTracker); checkMaxUsage(); reviewSources(); return memSource; } @Override public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException { CloseableByteSourceFromOutputStreamBuilder memBuilder = memoryStorage.makeBuilder(); return new AbstractCloseableByteSourceFromOutputStreamBuilder() { @Override protected void doWrite(byte[] b, int off, int len) throws IOException { memBuilder.write(b, off, len); } @Override protected CloseableByteSource doBuild() throws IOException { CloseableByteSource memSource = new LruTrackedCloseableByteSource(memBuilder.build(), memorySourcesTracker); checkMaxUsage(); reviewSources(); return memSource; } }; } @Override public CloseableByteSource fromSource(ByteSource source) throws IOException { CloseableByteSource memSource = new LruTrackedCloseableByteSource(memoryStorage.fromSource(source), memorySourcesTracker); checkMaxUsage(); reviewSources(); return memSource; } @Override public synchronized long getBytesUsed() { return memoryStorage.getBytesUsed() + diskStorage.getBytesUsed(); } @Override public synchronized long getMaxBytesUsed() { return maxBytesUsed; } /** Checks if we have reached a new high of data usage and set it. */ private synchronized void checkMaxUsage() { if (getBytesUsed() > maxBytesUsed) { maxBytesUsed = getBytesUsed(); } } /** Checks if any of the sources needs to be written to disk or loaded into memory. */ private synchronized void reviewSources() throws IOException { // Move data from memory to disk until we have at most memoryCacheSize bytes in memory. while (memoryStorage.getBytesUsed() > memoryCacheSize) { LruTrackedCloseableByteSource last = memorySourcesTracker.last(); if (last != null) { LruTrackedCloseableByteSource lastSource = last; lastSource.move(diskStorage); } } } /** Obtains the number of bytes stored in memory. */ public long getMemoryBytesUsed() { return memoryStorage.getBytesUsed(); } /** Obtains the maximum number of bytes ever stored in memory. */ public long getMaxMemoryBytesUsed() { return memoryStorage.getMaxBytesUsed(); } /** Obtains the number of bytes stored in disk. */ public long getDiskBytesUsed() { return diskStorage.getBytesUsed(); } /** Obtains the maximum number of bytes ever stored in disk. */ public long getMaxDiskBytesUsed() { return diskStorage.getMaxBytesUsed(); } @Override public void close() throws IOException { memoryStorage.close(); diskStorage.close(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/OverflowToDiskByteStorageFactory.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import java.io.IOException; import javax.annotation.Nullable; /** * {@link ByteStorageFactory} that creates instances of {@link ByteStorage} that will keep some data * in memory and will overflow to disk when necessary. */ public class OverflowToDiskByteStorageFactory implements ByteStorageFactory { /** How much data we want to keep in cache? If {@code null} then we want the default value. */ @Nullable private final Long memoryCacheSizeInBytes; /** Factory that creates temporary directories. */ private final TemporaryDirectoryFactory temporaryDirectoryFactory; /** * Creates a new factory with an optional in-memory size and a temporary directory for overflow. * * @param temporaryDirectoryFactory a factory that creates temporary directories that will be used * for overflow of the {@link ByteStorage} instances created by this factory */ public OverflowToDiskByteStorageFactory(TemporaryDirectoryFactory temporaryDirectoryFactory) { this(null, temporaryDirectoryFactory); } /** * Creates a new factory with an optional in-memory size and a temporary directory for overflow. * * @param memoryCacheSizeInBytes how many bytes to keep in memory? If {@code null} then a default * value will be used * @param temporaryDirectoryFactory a factory that creates temporary directories that will be used * for overflow of the {@link ByteStorage} instances created by this factory */ public OverflowToDiskByteStorageFactory( Long memoryCacheSizeInBytes, TemporaryDirectoryFactory temporaryDirectoryFactory) { this.memoryCacheSizeInBytes = memoryCacheSizeInBytes; this.temporaryDirectoryFactory = temporaryDirectoryFactory; } @Override public ByteStorage create() throws IOException { if (memoryCacheSizeInBytes == null) { return new OverflowToDiskByteStorage(temporaryDirectoryFactory); } else { return new OverflowToDiskByteStorage(memoryCacheSizeInBytes, temporaryDirectoryFactory); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/SwitchableDelegateCloseableByteSource.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.io.Closer; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; /** * Byte source that delegates to another byte source that can be switched dynamically. * *

This byte source encloses another byte source (the delegate) and allows switching the * delegate. Switching is done transparently for the user (as long as the new byte source represents * the same data) maintaining all open streams working, but now streaming from the new source. */ class SwitchableDelegateCloseableByteSource extends CloseableByteSource { /** The current delegate. */ private CloseableByteSource delegate; /** Has the byte source been closed? */ private boolean closed; /** * Streams that have been opened, but not yet closed. These are all the streams that have to be * switched when we switch delegates. */ private final List nonClosedStreams; /** Creates a new source using {@code source} as delegate. */ SwitchableDelegateCloseableByteSource(CloseableByteSource source) { this.delegate = source; nonClosedStreams = new ArrayList<>(); } @Override protected synchronized void innerClose() throws IOException { closed = true; try (Closer closer = Closer.create()) { for (SwitchableDelegateInputStream stream : nonClosedStreams) { closer.register(stream); } nonClosedStreams.clear(); } delegate.close(); } @Override public synchronized InputStream openStream() throws IOException { SwitchableDelegateInputStream stream = new SwitchableDelegateInputStream(delegate.openStream()) { // Can't have a lock on the stream while we synchronize the removal of nonClosedStreams // because it can deadlock when called in parallel with switchSource as the lock order is // reversed. The lack of synchronization is OK because we don't access any data on the // stream anyway until super.close() is called. @SuppressWarnings("UnsynchronizedOverridesSynchronized") @Override public void close() throws IOException { // Remove the stream on close. synchronized (SwitchableDelegateCloseableByteSource.this) { nonClosedStreams.remove(this); } super.close(); } }; nonClosedStreams.add(stream); return stream; } /** * Switches the current source for {@code source}. All streams are kept valid. The current source * is closed. * *

If the current source has already been closed, {@code source} will also be closed and * nothing else is done. * *

Otherwise, as long as it is possible to open enough input streams from {@code source} to * replace all current input streams, the source if changed. Any errors while closing input * streams (which happens during switching -- see {@link * SwitchableDelegateInputStream#switchStream(InputStream)}) or closing the old source are * reported as thrown {@code IOException} */ synchronized void switchSource(CloseableByteSource source) throws IOException { if (source == delegate) { return; } if (closed) { source.close(); return; } List switchStreams = new ArrayList<>(); for (int i = 0; i < nonClosedStreams.size(); i++) { switchStreams.add(source.openStream()); } CloseableByteSource oldDelegate = delegate; delegate = source; // A bit of trickery. We want to call switchStream for all streams. switchStream will // successfully switch the stream even if it throws an exception (if it does, it means it // failed to close the old stream). So we want to continue switching and recording all // exceptions. Closer() has that logic already so we register each stream switch as a close // operation. try (Closer closer = Closer.create()) { for (int i = 0; i < nonClosedStreams.size(); i++) { SwitchableDelegateInputStream nonClosedStream = nonClosedStreams.get(i); InputStream switchStream = switchStreams.get(i); closer.register(() -> nonClosedStream.switchStream(switchStream)); } closer.register(oldDelegate); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/SwitchableDelegateInputStream.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.io.InputStream; /** * Input stream that delegates to another input stream, but can switch transparently the source * input stream. * *

Given a set of input streams that return the same data, this input stream will read from one * and allow switching to read from other streams continuing from the offset that was initially * read. The result is only meaningful if all streams read the same data. * *

This class allows transparently to switch between different implementations of the underlying * streams (memory, disk, etc.) while transparently providing data to users. It does not support * marking and it is multi-thread safe. */ class SwitchableDelegateInputStream extends InputStream { /** The input stream that is currently providing data. */ private InputStream delegate; /** * Current offset in the input stream. We keep track of this to allow skipping data when switching * input streams. */ private long currentOffset; /** Have we reached the end of stream? */ @VisibleForTesting // private otherwise. boolean endOfStreamReached; /** * If a switch has occurred, how many bytes still need to be skipped in the input stream to * continue reading from the same position? */ private long needsSkipping; SwitchableDelegateInputStream(InputStream delegate) { this.delegate = delegate; currentOffset = 0; endOfStreamReached = false; needsSkipping = 0; } /** * Skips data in the input stream if it has been switched and there is data to skip. Will fail if * we can't skip all the data. */ private void skipDataIfNeeded() throws IOException { while (needsSkipping > 0) { long skipped = delegate.skip(needsSkipping); if (skipped == 0) { throw new IOException("Skipping InputStream after switching failed"); } needsSkipping -= skipped; } } /** Same as {@link #increaseOffset(long)}. */ private int increaseOffset(int amount) { return (int) increaseOffset((long) amount); } /** * Increases the current offset after reading. {@code amount} will indicate how many bytes we have * read. It {@code -1} then we know we've reached the end of the stream and {@link * #endOfStreamReached} is set to {@code true}. */ private long increaseOffset(long amount) { if (amount > 0) { currentOffset += amount; } if (amount == -1) { endOfStreamReached = true; } return amount; } @Override public synchronized int read(byte[] b) throws IOException { if (endOfStreamReached) { return -1; } skipDataIfNeeded(); return increaseOffset(delegate.read(b)); } @Override public synchronized int read(byte[] b, int off, int len) throws IOException { if (endOfStreamReached) { return -1; } skipDataIfNeeded(); return increaseOffset(delegate.read(b, off, len)); } @Override public synchronized int read() throws IOException { if (endOfStreamReached) { return -1; } skipDataIfNeeded(); int r = delegate.read(); if (r == -1) { endOfStreamReached = true; } else { increaseOffset(1); } return r; } @Override public synchronized long skip(long n) throws IOException { if (endOfStreamReached) { return 0; } skipDataIfNeeded(); return increaseOffset(delegate.skip(n)); } @Override public synchronized int available() throws IOException { if (endOfStreamReached) { return 0; } skipDataIfNeeded(); return delegate.available(); } @Override public synchronized void close() throws IOException { endOfStreamReached = true; delegate.close(); } @Override public void mark(int readlimit) { // We don't support marking. } @Override public void reset() throws IOException { throw new IOException("Mark not supported"); } @Override public boolean markSupported() { return false; } /** * Switches the stream used. * *

The stream that is currently in use and the new stream will be used in further operations. * If this stream has already reached the end, {@code newStream} will be closed immediately and no * other action is taken. If the stream has not reached the end, any exception reported is due to * closing the stream currently in use, the new stream is not affected and this stream can still * be used to read from {@code newStream}. */ synchronized void switchStream(InputStream newStream) throws IOException { if (newStream == delegate) { return; } try (InputStream oldDelegate = delegate) { delegate = newStream; needsSkipping = currentOffset; } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryDirectory.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.google.common.annotations.VisibleForTesting; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; /** * A temporary directory is a directory that creates temporary files. Upon close, all temporary * files are removed. Whether the directory itself is removed is dependent on the actual * implementation. */ public interface TemporaryDirectory extends Closeable { /** * Creates a new file in the directory. This method returns a new file that deleted, recreated, * read and written freely by the caller. No assumptions are made on the contents of this file * except that it will be deleted it if it still exists when the temporary directory is closed. */ File newFile() throws IOException; /** Obtains the directory, only useful for tests. */ @VisibleForTesting // private otherwise. File getDirectory(); /** * Creates a new temporary directory in the system's temporary directory. All files created will * be created in this directory. The directory will be deleted (as long as all the files in it) * when closed. */ static TemporaryDirectory newSystemTemporaryDirectory() throws IOException { Path tempDir = Files.createTempDirectory("tempdir_"); TemporaryFile tempDirFile = new TemporaryFile(tempDir.toFile()); return new TemporaryDirectory() { @Override public File newFile() throws IOException { return Files.createTempFile(tempDir, "temp_", ".data").toFile(); } @Override public File getDirectory() { return tempDir.toFile(); } @Override public void close() throws IOException { tempDirFile.close(); } }; } /** * Creates a new temporary directory that uses a fixed directory. * * @param directory the directory that will be returned; this directory won't be deleted when the * {@link TemporaryDirectory} objects are closed * @return a {@link TemporaryDirectory} that will create files in {@code directory} */ static TemporaryDirectory fixed(File directory) { return new TemporaryDirectory() { @Override public File newFile() throws IOException { return Files.createTempFile(directory.toPath(), "temp_", ".data").toFile(); } @Override public File getDirectory() { return directory; } @Override public void close() throws IOException {} }; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryDirectoryFactory.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import java.io.File; import java.io.IOException; /** * Factory that creates temporary directories. {@link * TemporaryDirectory#newSystemTemporaryDirectory()} conforms to this interface. */ public interface TemporaryDirectoryFactory { /** * Creates a new temporary directory. * * @return the new temporary directory that should be closed when finished * @throws IOException failed to create the temporary directory */ TemporaryDirectory make() throws IOException; /** * Obtains a factory that creates temporary directories using {@link * TemporaryDirectory#fixed(File)}. * * @param directory the directory where all temporary files will be created * @return a factory that creates instances of {@link TemporaryDirectory} that creates all files * inside {@code directory} */ static TemporaryDirectoryFactory fixed(File directory) { return () -> TemporaryDirectory.fixed(directory); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryDirectoryStorage.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; /** * Byte storage that keeps all byte sources as files in a temporary directory. Each data stored is * stored as a new file. The file is deleted as soon as the byte source is closed. */ public class TemporaryDirectoryStorage implements ByteStorage { /** Temporary directory to use. */ @VisibleForTesting // private otherwise. final TemporaryDirectory temporaryDirectory; /** Number of bytes currently used. */ private long bytesUsed; /** Maximum number of bytes used. */ private long maxBytesUsed; /** * Creates a new storage using the provided temporary directory. * * @param temporaryDirectoryFactory a factory used to create the directory to use for temporary * files; this directory will be closed when the {@link TemporaryDirectoryStorage} is closed. * @throws IOException failed to create the temporary directory */ public TemporaryDirectoryStorage(TemporaryDirectoryFactory temporaryDirectoryFactory) throws IOException { this.temporaryDirectory = temporaryDirectoryFactory.make(); } @Override public CloseableByteSource fromStream(InputStream stream) throws IOException { File temporaryFile = temporaryDirectory.newFile(); try (FileOutputStream output = new FileOutputStream(temporaryFile)) { ByteStreams.copy(stream, output); } long size = temporaryFile.length(); incrementBytesUsed(size); return new TemporaryFileCloseableByteSource(temporaryFile, () -> incrementBytesUsed(-size)); } @Override public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException { File temporaryFile = temporaryDirectory.newFile(); return new AbstractCloseableByteSourceFromOutputStreamBuilder() { private final FileOutputStream output = new FileOutputStream(temporaryFile); @Override protected void doWrite(byte[] b, int off, int len) throws IOException { output.write(b, off, len); incrementBytesUsed(len); } @Override protected CloseableByteSource doBuild() throws IOException { output.close(); long size = temporaryFile.length(); return new TemporaryFileCloseableByteSource(temporaryFile, () -> incrementBytesUsed(-size)); } }; } @Override public CloseableByteSource fromSource(ByteSource source) throws IOException { try (InputStream stream = source.openStream()) { return fromStream(stream); } } @Override public synchronized long getBytesUsed() { return bytesUsed; } @Override public synchronized long getMaxBytesUsed() { return maxBytesUsed; } /** Increments the byte counter by the given amount (decrements if {@code amount} is negative). */ private synchronized void incrementBytesUsed(long amount) { bytesUsed += amount; if (bytesUsed > maxBytesUsed) { maxBytesUsed = bytesUsed; } } @Override public void close() throws IOException { temporaryDirectory.close(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryFile.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.google.common.base.Preconditions; import java.io.Closeable; import java.io.File; import java.io.IOException; /** * A temporary file or directory. Wraps a file or directory and deletes it (recursively, if it is a * directory) when closed. */ public class TemporaryFile implements Closeable { /** Has the file or directory represented by {@link #file} been deleted? */ private boolean deleted; /** * The file or directory that will be deleted on close. May no longer exist if {@link #deleted} is * {@code true}. */ private final File file; /** * Creates a new wrapper around the given file. The file or directory {@code file} will be deleted * (recursively, if it is a directory) on close. */ public TemporaryFile(File file) { deleted = false; this.file = file; } /** Obtains the file or directory this temporary file refers to. */ public File getFile() { Preconditions.checkState(!deleted, "File already deleted"); return file; } @Override public void close() throws IOException { if (deleted) { return; } deleted = true; deleteFile(file); } /** Deletes a file or directory if it exists. */ private void deleteFile(File file) throws IOException { if (file.isDirectory()) { File[] contents = file.listFiles(); if (contents != null) { for (File subFile : contents) { deleteFile(subFile); } } } if (file.exists() && !file.delete()) { throw new IOException("Failed to delete '" + file.getAbsolutePath() + "'"); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryFileCloseableByteSource.java ================================================ package com.android.tools.build.apkzlib.bytestorage; import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource; import com.google.common.io.Files; import java.io.File; import java.io.IOException; /** * Closeable byte source that uses a temporary file to store its contents. The file is deleted when * the byte source is closed. */ class TemporaryFileCloseableByteSource extends CloseableDelegateByteSource { /** Temporary file backing the byte source. */ private final TemporaryFile temporaryFile; /** Callback to notify when the byte source is closed. */ private final Runnable closeCallback; /** * Creates a new byte source based on the given file. The provided callback is executed when the * source is deleted. There is no guarantee about which thread invokes the callback (it is the * thread that closes the source). */ TemporaryFileCloseableByteSource(File file, Runnable closeCallback) { super(Files.asByteSource(file), file.length()); temporaryFile = new TemporaryFile(file); this.closeCallback = closeCallback; } @Override protected synchronized void innerClose() throws IOException { super.innerClose(); temporaryFile.close(); closeCallback.run(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/DigestAlgorithm.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.sign; /** Message digest algorithms. */ public enum DigestAlgorithm { /** * SHA-1 digest. * *

Android 2.3 (API Level 9) to 4.2 (API Level 17) (inclusive) do not support SHA-2 JAR * signatures. * *

Moreover, platforms prior to API Level 18, without the additional Digest-Algorithms * attribute, only support SHA or SHA1 algorithm names in .SF and MANIFEST.MF attributes. */ SHA1("SHA1", "SHA-1"), /** SHA-256 digest. */ SHA256("SHA-256", "SHA-256"); /** * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and {@link * SignatureAlgorithm#ECDSA}. */ public static final int API_SHA_256_RSA_AND_ECDSA = 18; /** * API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s. * *

Before that, SHA256 can only be used with RSA and ECDSA. */ public static final int API_SHA_256_ALL_ALGORITHMS = 21; /** Name of algorithm for message digest. */ public final String messageDigestName; /** Name of attribute in signature file with the manifest digest. */ public final String manifestAttributeName; /** Name of attribute in entry (both manifest and signature file) with the entry's digest. */ public final String entryAttributeName; /** * Creates a digest algorithm. * * @param attributeName attribute name in the signature file * @param messageDigestName name of algorithm for message digest */ DigestAlgorithm(String attributeName, String messageDigestName) { this.messageDigestName = messageDigestName; this.entryAttributeName = attributeName + "-Digest"; this.manifestAttributeName = attributeName + "-Digest-Manifest"; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/ManifestGenerationExtension.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.sign; import com.android.tools.build.apkzlib.utils.CachedSupplier; import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; import com.android.tools.build.apkzlib.utils.IOExceptionWrapper; import com.android.tools.build.apkzlib.zfile.ManifestAttributes; import com.android.tools.build.apkzlib.zip.StoredEntry; import com.android.tools.build.apkzlib.zip.ZFile; import com.android.tools.build.apkzlib.zip.ZFileExtension; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.jar.Attributes; import java.util.jar.Manifest; import javax.annotation.Nullable; /** * Extension to {@link ZFile} that will generate a manifest. The extension will register * automatically with the {@link ZFile}. * *

Creating this extension will ensure a manifest for the zip exists. This extension will * generate a manifest if one does not exist and will update an existing manifest, if one does * exist. The extension will also provide access to the manifest so that others may update the * manifest. * *

Apart from standard manifest elements, this extension does not handle any particular manifest * features such as signing or adding custom attributes. It simply generates a plain manifest and * provides infrastructure so that other extensions can add data in the manifest. * *

The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()} * notification is received, meaning all manifest manipulation is done in-memory. */ public class ManifestGenerationExtension { /** Name of META-INF directory. */ private static final String META_INF_DIR = "META-INF"; /** Name of the manifest file. */ static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF"; /** Who should be reported as the manifest builder. */ private final String builtBy; /** Who should be reported as the manifest creator. */ private final String createdBy; /** The file this extension is attached to. {@code null} if not yet registered. */ @Nullable private ZFile zFile; /** The zip file's manifest. */ private final Manifest manifest; /** * Byte representation of the manifest. There is no guarantee that two writes of the java's {@code * Manifest} object will yield the same byte array (there is no guaranteed order of entries in the * manifest). * *

Because we need the byte representation of the manifest to be stable if there are no changes * to the manifest, we cannot rely on {@code Manifest} to generate the byte representation every * time we need the byte representation. * *

This cache will ensure that we will request one byte generation from the {@code Manifest} * and will cache it. All further requests of the manifest's byte representation will receive the * same byte array. */ private final CachedSupplier manifestBytes; /** * Has the current manifest been changed and not yet flushed? If {@link #dirty} is {@code true}, * then {@link #manifestBytes} should not be valid. This means that marking the manifest as dirty * should also invalidate {@link #manifestBytes}. To avoid breaking the invariant, instead of * setting {@link #dirty}, {@link #markDirty()} should be called. */ private boolean dirty; /** The extension to register with the {@link ZFile}. {@code null} if not registered. */ @Nullable private ZFileExtension extension; /** * Creates a new extension. This will not register the extension with the provided {@link ZFile}. * Until {@link #register(ZFile)} is invoked, this extension is not used. * * @param builtBy who built the manifest? * @param createdBy who created the manifest? */ public ManifestGenerationExtension(String builtBy, String createdBy) { this.builtBy = builtBy; this.createdBy = createdBy; manifest = new Manifest(); dirty = false; manifestBytes = new CachedSupplier<>( () -> { ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); try { manifest.write(outBytes); } catch (IOException e) { throw new IOExceptionWrapper(e); } return outBytes.toByteArray(); }); } /** * Marks the manifest as being dirty, i.e., its data has changed since it was last read * and/or written. */ private void markDirty() { dirty = true; manifestBytes.reset(); } /** * Registers the extension with the {@link ZFile} provided in the constructor. * * @param zFile the zip file to add the extension to * @throws IOException failed to analyze the zip */ public void register(ZFile zFile) throws IOException { Preconditions.checkState(extension == null, "register() has already been invoked."); this.zFile = zFile; rebuildManifest(); extension = new ZFileExtension() { @Nullable @Override public IOExceptionRunnable beforeUpdate() { return ManifestGenerationExtension.this::updateManifest; } }; this.zFile.addZFileExtension(extension); } /** Rebuilds the zip file's manifest, if it needs changes. */ private void rebuildManifest() throws IOException { Verify.verifyNotNull(zFile, "zFile == null"); StoredEntry manifestEntry = zFile.get(MANIFEST_NAME); if (manifestEntry != null) { /* * Read the manifest entry in the zip file. Make sure we store these byte sequence * because writing the manifest may not generate the same byte sequence, which may * trigger an unnecessary re-sign of the jar. */ manifest.clear(); byte[] manifestBytes = manifestEntry.read(); manifest.read(new ByteArrayInputStream(manifestBytes)); this.manifestBytes.precomputed(manifestBytes); } Attributes mainAttributes = manifest.getMainAttributes(); String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION); if (currentVersion == null) { setMainAttribute( ManifestAttributes.MANIFEST_VERSION, ManifestAttributes.CURRENT_MANIFEST_VERSION); } else { if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) { throw new IOException("Unsupported manifest version: " + currentVersion + "."); } } /* * We "blindly" override all other main attributes. */ setMainAttribute(ManifestAttributes.BUILT_BY, builtBy); setMainAttribute(ManifestAttributes.CREATED_BY, createdBy); } /** * Sets the value of a main attribute. * * @param attribute the attribute * @param value the value */ private void setMainAttribute(String attribute, String value) { Attributes mainAttributes = manifest.getMainAttributes(); String current = mainAttributes.getValue(attribute); if (!value.equals(current)) { mainAttributes.putValue(attribute, value); markDirty(); } } /** * Updates the manifest in the zip file, if it has been changed. * * @throws IOException failed to update the manifest */ private void updateManifest() throws IOException { Verify.verifyNotNull(zFile, "zFile == null"); if (!dirty) { return; } zFile.add(MANIFEST_NAME, new ByteArrayInputStream(manifestBytes.get())); dirty = false; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/SignatureAlgorithm.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.sign; import java.security.NoSuchAlgorithmException; /** Signature algorithm. */ public enum SignatureAlgorithm { /** RSA algorithm. */ RSA("RSA", 1, "withRSA"), /** ECDSA algorithm. */ ECDSA("EC", 18, "withECDSA"), /** DSA algorithm. */ DSA("DSA", 1, "withDSA"); /** Name of the private key as reported by {@code PrivateKey}. */ public final String keyAlgorithm; /** Minimum SDK version that allows this signature. */ public final int minSdkVersion; /** Suffix appended to digest algorithm to obtain signature algorithm. */ public final String signatureAlgorithmSuffix; /** * Creates a new signature algorithm. * * @param keyAlgorithm the name as reported by {@code PrivateKey} * @param minSdkVersion minimum SDK version that allows this signature * @param signatureAlgorithmSuffix suffix for signature name with used with a digest */ SignatureAlgorithm(String keyAlgorithm, int minSdkVersion, String signatureAlgorithmSuffix) { this.keyAlgorithm = keyAlgorithm; this.minSdkVersion = minSdkVersion; this.signatureAlgorithmSuffix = signatureAlgorithmSuffix; } /** * Obtains the signature algorithm that corresponds to a private key name applicable to a SDK * version. * * @param keyAlgorithm the named referred in the {@code PrivateKey} * @param minSdkVersion minimum SDK version to run * @return the algorithm that has {@link #keyAlgorithm} equal to {@code keyAlgorithm} * @throws NoSuchAlgorithmException if no algorithm was found for the given private key; an * algorithm was found but is not applicable to the given SDK version */ public static SignatureAlgorithm fromKeyAlgorithm(String keyAlgorithm, int minSdkVersion) throws NoSuchAlgorithmException { for (SignatureAlgorithm alg : values()) { if (alg.keyAlgorithm.equalsIgnoreCase(keyAlgorithm)) { if (alg.minSdkVersion > minSdkVersion) { throw new NoSuchAlgorithmException( "Signatures with " + keyAlgorithm + " keys are not supported on minSdkVersion " + minSdkVersion + ". They are supported only for minSdkVersion >= " + alg.minSdkVersion); } return alg; } } throw new NoSuchAlgorithmException("Signing with " + keyAlgorithm + " keys is not supported"); } /** * Obtains the name of the signature algorithm when used with a digest algorithm. * * @param digestAlgorithm the digest algorithm to use * @return the name of the signature algorithm */ public String signatureAlgorithmName(DigestAlgorithm digestAlgorithm) { return digestAlgorithm.messageDigestName.replace("-", "") + signatureAlgorithmSuffix; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/SigningExtension.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.sign; import com.android.apksig.ApkSignerEngine; import com.android.apksig.ApkVerifier; import com.android.apksig.DefaultApkSignerEngine; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; import com.android.tools.build.apkzlib.utils.SigningBlockUtils; import com.android.tools.build.apkzlib.zip.StoredEntry; import com.android.tools.build.apkzlib.zip.ZFile; import com.android.tools.build.apkzlib.zip.ZFileExtension; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.primitives.Bytes; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; /** * {@link ZFile} extension which signs the APK. * *

This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK * Signature Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters * to this extension's constructor. */ public class SigningExtension { private static final int MAX_READ_CHUNK_SIZE = 65536; // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine. // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile // extension events/callbacks. // // The main issue leading to additional complexity in this class is that the current build // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that // matter) for incremental builds. Thus: // * ZFile extension receives no events for JAR entries already in the APK whereas // ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this // class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries // already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR // entries which the incremental build session did not touch. // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed // from it whereas ApkSignerEngine produces no output only if it has already produced a signed // APK and no changes have since been made to it. This class addresses this issue by checking // in its "register" method whether the APK is correctly signed and, only if that's the case, // doesn't modify the APK unless a JAR entry is added to it or removed from it after // "register". /** APK signer which performs most of the heavy lifting. */ private final ApkSignerEngine signer; /** Names of APK entries which have been processed by {@link #signer}. */ private final Set signerProcessedOutputEntryNames = new HashSet<>(); /** Signing block Id for SDK dependency block. */ static final int DEPENDENCY_INFO_BLOCK_ID = 0x504b4453; /** SDK dependencies of the APK */ @Nullable private byte[] sdkDependencyData; /** * Cached contents of the most recently output APK Signing Block or {@code null} if the block * hasn't yet been output. */ @Nullable private byte[] cachedApkSigningBlock; /** * {@code true} if signatures may need to be output, {@code false} if there's no need to output * signatures. This is used in an optimization where we don't modify the APK if it's already * signed and if no JAR entries have been added to or removed from the file. */ private boolean dirty; /** The extension registered with the {@link ZFile}. {@code null} if not registered. */ @Nullable private ZFileExtension extension; /** The file this extension is attached to. {@code null} if not yet registered. */ @Nullable private ZFile zFile; /** A buffer used to read data from entries to feed to digests */ private final Supplier digestBuffer = Suppliers.memoize(() -> new byte[MAX_READ_CHUNK_SIZE]); /** An object that has all necessary information to sign the zip file and verify its signature */ private final SigningOptions options; public SigningExtension(SigningOptions opts) throws InvalidKeyException { DefaultApkSignerEngine.SignerConfig signerConfig = new DefaultApkSignerEngine.SignerConfig.Builder( "CERT", opts.getKey(), opts.getCertificates()) .build(); signer = new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), opts.getMinSdkVersion()) .setOtherSignersSignaturesPreserved(false) .setV1SigningEnabled(opts.isV1SigningEnabled()) .setV2SigningEnabled(opts.isV2SigningEnabled()) .setV3SigningEnabled(false) .setCreatedBy("1.0 (Android)") .build(); if (opts.getSdkDependencyData() != null) { sdkDependencyData = opts.getSdkDependencyData(); } if (opts.getExecutor() != null) { signer.setExecutor(opts.getExecutor()); } this.options = opts; } public void register(ZFile zFile) throws NoSuchAlgorithmException, IOException { Preconditions.checkState(extension == null, "register() already invoked"); this.zFile = zFile; switch (options.getValidation()) { case ALWAYS_VALIDATE: dirty = !isCurrentSignatureAsRequested(); break; case ASSUME_VALID: if (options.isV1SigningEnabled()) { Set entryNames = ImmutableSet.copyOf( Iterables.transform( zFile.entries(), e -> e.getCentralDirectoryHeader().getName())); StoredEntry manifestEntry = zFile.get(ManifestGenerationExtension.MANIFEST_NAME); Preconditions.checkNotNull( manifestEntry, "No manifest found in apk for incremental build with enabled v1 signature"); signerProcessedOutputEntryNames.addAll( this.signer.initWith(manifestEntry.read(), entryNames)); } dirty = false; break; case ASSUME_INVALID: dirty = true; break; } extension = new ZFileExtension() { @Override public IOExceptionRunnable added(StoredEntry entry, @Nullable StoredEntry replaced) { return () -> onZipEntryOutput(entry); } @Override public IOExceptionRunnable removed(StoredEntry entry) { String entryName = entry.getCentralDirectoryHeader().getName(); return () -> onZipEntryRemovedFromOutput(entryName); } @Override public IOExceptionRunnable beforeUpdate() throws IOException { return () -> onOutputZipReadyForUpdate(); } @Override public void entriesWritten() throws IOException { onOutputZipEntriesWritten(); } @Override public void closed() { onOutputClosed(); } }; this.zFile.addZFileExtension(extension); } /** * Returns {@code true} if the APK's signatures are as requested by parameters to this signing * extension. */ private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException { ApkVerifier.Result result; try { result = new ApkVerifier.Builder(zFile.asDataSource()) .setMinCheckedPlatformVersion(options.getMinSdkVersion()) .build() .verify(); } catch (ApkFormatException e) { // Malformed APK return false; } if (!result.isVerified()) { // Signature(s) did not verify return false; } if ((result.isVerifiedUsingV1Scheme() != options.isV1SigningEnabled()) || (result.isVerifiedUsingV2Scheme() != options.isV2SigningEnabled())) { // APK isn't signed with exactly the schemes we want it to be signed return false; } List verifiedSignerCerts = result.getSignerCertificates(); if (verifiedSignerCerts.size() != 1) { // APK is not signed by exactly one signer return false; } byte[] expectedEncodedCert; byte[] actualEncodedCert; try { expectedEncodedCert = options.getCertificates().get(0).getEncoded(); actualEncodedCert = verifiedSignerCerts.get(0).getEncoded(); } catch (CertificateEncodingException e) { // Failed to encode signing certificates return false; } if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) { // APK is signed by a wrong signer return false; } // APK is signed the way we want it to be signed return true; } private void onZipEntryOutput(StoredEntry entry) throws IOException { setDirty(); String entryName = entry.getCentralDirectoryHeader().getName(); // This event may arrive after the entry has already been deleted. In that case, we don't // report the addition of the entry to ApkSignerEngine. if (entry.isDeleted()) { return; } ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = signer.outputJarEntry(entryName); signerProcessedOutputEntryNames.add(entryName); if (inspectEntryRequest != null) { try (InputStream inputStream = new BufferedInputStream(entry.open())) { copyStreamToDataSink(inputStream, inspectEntryRequest.getDataSink()); } inspectEntryRequest.done(); } } private void copyStreamToDataSink(InputStream inputStream, DataSink dataSink) throws IOException { int bytesRead; byte[] buffer = digestBuffer.get(); while ((bytesRead = inputStream.read(buffer)) > 0) { dataSink.consume(buffer, 0, bytesRead); } } private void onZipEntryRemovedFromOutput(String entryName) { setDirty(); signer.outputJarEntryRemoved(entryName); signerProcessedOutputEntryNames.remove(entryName); } private void onOutputZipReadyForUpdate() throws IOException { if (!dirty) { return; } // Notify signer engine about ZIP entries that have appeared in the output without the // engine knowing. Also identify ZIP entries which disappeared from the output without the // engine knowing. Set unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames); for (StoredEntry entry : zFile.entries()) { String entryName = entry.getCentralDirectoryHeader().getName(); unprocessedRemovedEntryNames.remove(entryName); if (!signerProcessedOutputEntryNames.contains(entryName)) { // Signer engine is not yet aware that this entry is in the output onZipEntryOutput(entry); } } // Notify signer engine about entries which disappeared from the output without the engine // knowing for (String entryName : unprocessedRemovedEntryNames) { onZipEntryRemovedFromOutput(entryName); } // Check whether we need to output additional JAR entries which comprise the v1 signature ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest; try { addV1SignatureRequest = signer.outputJarEntries(); } catch (Exception e) { throw new IOException("Failed to generate v1 signature", e); } if (addV1SignatureRequest == null) { return; } // We need to output additional JAR entries which comprise the v1 signature List v1SignatureEntries = new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries()); // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by // ManifestGenerationExtension. for (int i = 0; i < v1SignatureEntries.size(); i++) { ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i); String name = entry.getName(); if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) { continue; } if (i != 0) { v1SignatureEntries.remove(i); v1SignatureEntries.add(0, entry); } break; } // Output the JAR entries comprising the v1 signature for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) { String name = entry.getName(); byte[] data = entry.getData(); zFile.add(name, new ByteArrayInputStream(data)); } addV1SignatureRequest.done(); } private void onOutputZipEntriesWritten() throws IOException { if (!dirty) { return; } // Check whether we should output an APK Signing Block which contains v2 signatures byte[] apkSigningBlock; byte[] centralDirBytes = zFile.getCentralDirectoryBytes(); byte[] eocdBytes = zFile.getEocdBytes(); ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest; // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we // cache the block to speed things up. The cached block is invalidated by any changes to the // file (as reported to this extension). if (cachedApkSigningBlock != null) { apkSigningBlock = cachedApkSigningBlock; addV2SignatureRequest = null; } else { DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes)); DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes)); long zipEntriesSizeBytes = zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(); DataSource zipEntries = zFile.asDataSource(0, zipEntriesSizeBytes); try { addV2SignatureRequest = signer.outputZipSections2(zipEntries, centralDir, eocd); } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | ApkFormatException | IOException e) { throw new IOException("Failed to generate v2 signature", e); } if (addV2SignatureRequest != null) { apkSigningBlock = addV2SignatureRequest.getApkSigningBlock(); if (sdkDependencyData != null) { apkSigningBlock = SigningBlockUtils.addToSigningBlock( apkSigningBlock, sdkDependencyData, DEPENDENCY_INFO_BLOCK_ID); } apkSigningBlock = Bytes.concat( new byte[addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock()], apkSigningBlock); } else { apkSigningBlock = new byte[0]; if (sdkDependencyData != null) { apkSigningBlock = SigningBlockUtils.addToSigningBlock( apkSigningBlock, sdkDependencyData, DEPENDENCY_INFO_BLOCK_ID); int paddingSize = ApkSigningBlockUtils.generateApkSigningBlockPadding( zipEntries, /* apkSigningBlockPaddingSupported */ true) .getSecond(); apkSigningBlock = Bytes.concat(new byte[paddingSize], apkSigningBlock); } } cachedApkSigningBlock = apkSigningBlock; } // Insert the APK Signing Block into the output right before the ZIP Central Directory and // accordingly update the start offset of ZIP Central Directory in ZIP End of Central // Directory. zFile.directWrite( zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(), apkSigningBlock); zFile.setExtraDirectoryOffset(apkSigningBlock.length); if (addV2SignatureRequest != null) { addV2SignatureRequest.done(); } } private void onOutputClosed() { if (!dirty) { return; } signer.outputDone(); dirty = false; } private void setDirty() { dirty = true; cachedApkSigningBlock = null; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/SigningOptions.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.sign; import com.android.apksig.util.RunnablesExecutor; import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.security.PrivateKey; import java.security.cert.X509Certificate; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** A class that contains data to initialize SigningExtension. */ @AutoValue public abstract class SigningOptions { /** An implementation of builder pattern to create a {@link SigningOptions} object. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder setKey(@Nonnull PrivateKey key); public abstract Builder setCertificates(@Nonnull ImmutableList certs); public abstract Builder setCertificates(X509Certificate... certs); public abstract Builder setV1SigningEnabled(boolean enabled); public abstract Builder setV2SigningEnabled(boolean enabled); public abstract Builder setMinSdkVersion(int version); public abstract Builder setValidation(@Nonnull Validation validation); public abstract Builder setExecutor(@Nullable RunnablesExecutor executor); public abstract Builder setSdkDependencyData(@Nullable byte[] sdkDependencyData); abstract SigningOptions autoBuild(); public SigningOptions build() { SigningOptions options = autoBuild(); Preconditions.checkArgument(options.getMinSdkVersion() >= 0, "minSdkVersion < 0"); Preconditions.checkArgument( !options.getCertificates().isEmpty(), "There should be at least one certificate in SigningOptions"); return options; } } public static Builder builder() { return new AutoValue_SigningOptions.Builder() .setV1SigningEnabled(false) .setV2SigningEnabled(false) .setValidation(Validation.ALWAYS_VALIDATE); } /** {@link PrivateKey} used to sign the archive. */ public abstract PrivateKey getKey(); /** * A list of the {@link X509Certificate}s to embed in the signed APKs. The first * element of the list must be the certificate associated with the private key. */ public abstract ImmutableList getCertificates(); /** Shows whether signing with JAR Signature Scheme (aka v1 signing) is enabled. */ public abstract boolean isV1SigningEnabled(); /** Shows whether signing with APK Signature Scheme v2 (aka v2 signing) is enabled. */ public abstract boolean isV2SigningEnabled(); /** Minimum SDK version supported. */ public abstract int getMinSdkVersion(); /** Strategy of package signature validation */ public abstract Validation getValidation(); @Nullable public abstract RunnablesExecutor getExecutor(); /** SDK dependencies of the APK */ @SuppressWarnings("mutable") @Nullable public abstract byte[] getSdkDependencyData(); public enum Validation { /** Always perform signature validation */ ALWAYS_VALIDATE, /** * Assume the signature is valid without validation i.e. don't resign if no files changed */ ASSUME_VALID, /** Assume the signature is invalid without validation i.e. unconditionally resign */ ASSUME_INVALID, } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/package-info.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * The {@code sign} package provides extensions for the {@code zip} package that allow: * *

    *
  • Adding a {@code MANIFEST.MF} file to a zip making a jar. *
  • Signing a jar. *
  • Fully signing a jar using v2 apk signature. *
* *

Because the {@code zip} package is completely independent of the {@code sign} package, the * actual coordination between the two is complex. The {@code sign} package works by registering * extensions with the {@code zip} package. These extensions are notified in changes made in the zip * and will change the zip file itself. * *

The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will ensure the zip * has a manifest file and is, therefore, a valid jar. The {@link * com.android.apkzlib.sign.SigningExtension} extension will ensure the jar is signed. * *

The extension mechanism used is the one provided in the {@code zip} package (see {@link * com.android.apkzlib.zip.ZFile} and {@link com.android.apkzlib.zip.ZFileExtension}. Building the * zip and then operating the extensions is not done sequentially, as we don't want to build a zip * and then sign it. We want to build a zip that is automatically signed. Extension are basically * observers that register on the zip and are notified when things happen in the zip. They will then * modify the zip accordingly. * *

The zip file notifies extensions in 4 critical moments: when a file is added or removed from * the zip, when the zip is about to be flushed to disk and when the zip's entries have been flushed * but the central directory not. At these moments, the extensions can act to update the zip in any * way they need. * *

To see how this works, consider the manifest generation extension: when the extension is * created, it checks the zip file to see if there is a manifest. If a manifest exists and does not * need updating, it does not change anything, otherwise it generates a new manifest for the zip * file. At this point, the extension could write the manifest to the zip, but we opted not to. It * would be irrelevant anyway as the zip will only be written when flushed. * *

Now, when the {@code ZFile} notifies the extension that it is about to start writing the zip * file, the manifest extension, if it has noted that the manifest needs to be rewritten, will -- * before the {@code ZFile} actually writes anything -- modify the zip and add or replace the * existing manifest file. So, process-wise, the zip is written only once with the correct manifest. * The flow is as follows (if only the manifest generation extension was added to the {@code * ZFile}): * *

    *
  1. {@code ZFile.update()} is called. *
  2. {@code ZFile} calls {@code beforeUpdate()} for all {@code ZFileExtensions} registered, in * this case, only the instance of the anonymous inner class generated in the {@code * ManifestGenerationExtension} constructor is invoked. *
  3. {@code ManifestGenerationExtension.updateManifest()} is called. *
  4. If the manifest does not need to be updated, {@code updateManifest()} returns immediately. *
  5. If the manifest needs updating, {@code ZFile.add()} is invoked to add or replace the * manifest. *
  6. {@code ManifestGenerationExtension.updateManifest()} returns. *
  7. {@code ZFile.update()} continues and writes the zip file, containing the manifest. *
  8. The zip is finally written with an updated manifest. *
* *

To generate a signed apk, we need to add a second extension, the {@code SigningExtension}. * This extension will also register listeners with the {@code ZFile}. * *

In this case the flow would be (starting a bit earlier for clarity and assuming a package task * in the build process): * *

    *
  1. Package task creates a {@code ZFile} on the target apk (or non-existing file, if there is * no target apk in the output directory). *
  2. Package task configures the {@code ZFile} with alignment rules. *
  3. Package task creates a {@code ManifestGenerationExtension}. *
  4. Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}. *
  5. The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid * manifest. No changes are done to the {@code ZFile}. *
  6. Package task creates a {@code SigningExtension}. *
  7. Package task registers the {@code SigningExtension} with the {@code ZFile}. *
  8. The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile} and * look at the {@code ZFile} to see if there is a valid signature file. *
  9. If there are changes to the digital signature file needed, these are marked internally in * the extension. If there are changes needed to the digests, the manifest is updated (by * calling {@code ManifestGenerationExtension}.
    * (note that this point, the apk file, if any existed, has not been touched, the manifest * is only updated in memory and the digests of all files in the apk, if any, have been * computed and stored in memory only; the digital signature of the {@code SF} file has not * been computed.) *
  10. The Package task now adds all files to the {@code ZFile}. *
  11. For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added} * method of all registered extensions. *
  12. The {@code ManifestGenerationExtension} ignores added invocations. *
  13. The {@code SigningExtension} computes the digest for the added file and stores them in the * manifest.
    * (when all files are added to the apk, all digests are computed and the manifest is * updated but only in memory; the apk file has not been touched; also note that {@code ZFile} * has not actually written anything to disk at this point, all files added are kept in * memory). *
  14. Package task calls {@code ZFile.update()} to update the apk. *
  15. {@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is * done before anything is written. In this case both the {@code ManifestGenerationExtension} * and {@code SigningExtension} are invoked. *
  16. The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new * manifest, unless nothing has changed, in which case it does nothing. *
  17. The {@code SigningExtension} will add the SF file (unless nothing has changed), will * compute the digital signature of the SF file and write it to the {@code ZFile}.
    * (note that the order by which the {@code ManifestGenerationExtension} and {@code * SigningExtension} are called is non-deterministic; however, this is not a problem because * the manifest is already computed by the {@code ManifestGenerationExtension} at this time * and the {@code SigningExtension} will obtain the manifest data from the {@code * ManifestGenerationExtension} and not from the {@code ZFile}; this means that the {@code SF} * file may be added to the {@code ZFile} before the {@code MF} file, but that is * irrelevant.) *
  18. Once both extensions have finished doing the {@code beforeUpdate()} method, the {@code * ZFile.update()} method continues. *
  19. {@code ZFile.update()} writes all changes and new entries to the zip file. *
  20. {@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all registered * extensions. {@code SigningExtension} will kick in at this point, if v2 signature has * changed. *
  21. {@code ZFile} writes the central directory and EOCD. *
  22. {@code ZFile.update()} returns control to the package task. *
  23. The package task finishes. *
* * (*) There is a number of optimizations if we're adding files from another {@code ZFile}, * which is the case when we add the output of aapt to the apk. In particular, files from the aapt * are ignored if they are already in the apk (same name, same CRC32) and also files copied from the * aapt's output are not recompressed (the binary compressed data is directly copied to the * zip). * *

If there are no changes to the {@code ZFile} made by the package task and the file's manifest * and v1 signatures are correct, neither the {@code ManifestGenerationExtension} nor the {@code * SigningExtension} will not do anything on the {@code beforeUpdate()} and the {@code ZFile} won't * even be open for writing. * *

This implementation provides perfect incremental updates. * *

Additionally, by adding/removing extensions we can configure what type of apk we want: * *

    *
  • No SigningExtension => Aligned, unsigned apk. *
  • SigningExtension => Aligned, signed apk. *
* * So, by configuring which extensions to add, the package task can decide what type of apk we want. */ package com.android.apkzlib.sign; ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/ApkZLibPair.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.utils; /** Pair implementation to use with the {@code apkzlib} library. */ public class ApkZLibPair { /** First value. */ public T1 v1; /** Second value. */ public T2 v2; /** * Creates a new pair. * * @param v1 the first value * @param v2 the second value */ public ApkZLibPair(T1 v1, T2 v2) { this.v1 = v1; this.v2 = v2; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/CachedFileContents.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.utils; import com.google.common.base.Objects; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; import com.google.common.io.Files; import java.io.File; import java.io.IOException; import javax.annotation.Nullable; /** * A cache for file contents. The cache allows closing a file and saving in memory its contents (or * some related information). It can then be used to check if the contents are still valid at some * later time. Typical usage flow is: * *

* *

{@code
 * Object fileRepresentation = // ...
 * File toWrite = // ...
 * // Write file contents and update in memory representation
 * CachedFileContents contents = new CachedFileContents(toWrite);
 * contents.closed(fileRepresentation);
 *
 * // Later, when data is needed:
 * if (contents.isValid()) {
 *     fileRepresentation = contents.getCache();
 * } else {
 *     // Re-read the file and recreate the file representation
 * }
 * }
 *
 * @param  the type of cached contents
 */
public class CachedFileContents {

  /** The file. */
  private final File file;

  /** Time when last closed (time when {@link #closed(Object)} was invoked). */
  private long lastClosed;

  /** Size of the file when last closed. */
  private long size;

  /** Hash of the file when closed. {@code null} if hashing failed for some reason. */
  @Nullable private HashCode hash;

  /** Cached data associated with the file. */
  @Nullable private T cache;

  /**
   * Creates a new contents. When the file is written, {@link #closed(Object)} should be invoked to
   * set the cache.
   *
   * @param file the file
   */
  public CachedFileContents(File file) {
    this.file = file;
  }

  /**
   * Should be called when the file's contents are set and the file closed. This will save the cache
   * and register the file's timestamp to later detect if it has been modified.
   *
   * 

This method can be called as many times as the file has been written. * * @param cache an optional cache to save */ public void closed(@Nullable T cache) { this.cache = cache; lastClosed = file.lastModified(); size = file.length(); hash = hashFile(); } /** * Are the cached contents still valid? If this method determines that the file has been modified * since the last time {@link #closed(Object)} was invoked. * * @return are the cached contents still valid? If this method returns {@code false}, the cache is * cleared */ public boolean isValid() { boolean valid = true; if (!file.exists()) { valid = false; } if (valid && file.lastModified() != lastClosed) { valid = false; } if (valid && file.length() != size) { valid = false; } if (valid && !Objects.equal(hash, hashFile())) { valid = false; } if (!valid) { cache = null; } return valid; } /** * Obtains the cached data set with {@link #closed(Object)} if the file has not been modified * since {@link #closed(Object)} was invoked. * * @return the last cached data or {@code null} if the file has been modified since {@link * #closed(Object)} has been invoked */ @Nullable public T getCache() { return cache; } /** * Computes the hashcode of the cached file. * * @return the hash code */ @Nullable private HashCode hashFile() { try { return Files.asByteSource(file).hash(Hashing.crc32()); } catch (IOException e) { return null; } } /** * Obtains the file used for caching. * * @return the file; this file always exists and contains the old (cached) contents of the file */ public File getFile() { return file; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/CachedSupplier.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.utils; import com.google.common.base.Supplier; /** * Supplier that will cache a computed value and always supply the same value. It can be used to * lazily compute data. For example: * *

{@code
 * CachedSupplier value = new CachedSupplier<>(() -> {
 *     Integer result;
 *     // Do some expensive computation.
 *     return result;
 * });
 *
 * if (a) {
 *     // We need the result of the expensive computation.
 *     Integer r = value.get();
 * }
 *
 * if (b) {
 *     // We also need the result of the expensive computation.
 *     Integer r = value.get();
 * }
 *
 * // If neither a nor b are true, we avoid doing the computation at all.
 * }
*/ public class CachedSupplier { /** * The cached data, {@code null} if computation resulted in {@code null}. It is also {@code null} * if the cached data has not yet been computed. */ private T cached; /** Is the current data in {@link #cached} valid? */ private boolean valid; /** Actual supplier of data, if computation is needed. */ private final Supplier supplier; /** Creates a new supplier. */ public CachedSupplier(Supplier supplier) { valid = false; this.supplier = supplier; } /** * Obtains the value. * * @return the value, either cached (if one exists) or computed */ public synchronized T get() { if (!valid) { cached = supplier.get(); valid = true; } return cached; } /** * Resets the cache forcing a {@code get()} on the supplier next time {@link #get()} is invoked. */ public synchronized void reset() { cached = null; valid = false; } /** * In some cases, we may be able to precompute the cache value (or load it from somewhere we had * previously stored it). This method allows the cache value to be loaded. * *

If this method is invoked, then an invocation of {@link #get()} will not trigger an * invocation of the supplier provided in the constructor. * * @param t the new cache contents; will replace any currently cache content, if one exists */ public synchronized void precomputed(T t) { cached = t; valid = true; } /** * Checks if the contents of the cache are valid. * * @return are there valid contents in the cache? */ public synchronized boolean isValid() { return valid; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionConsumer.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.utils; import java.io.IOException; import javax.annotation.Nullable; /** Consumer that can throw an {@link IOException}. */ public interface IOExceptionConsumer { /** * Performs an operation on the given input. * * @param input the input */ void accept(@Nullable T input) throws IOException; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionFunction.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.utils; import com.google.common.base.Function; import java.io.IOException; import javax.annotation.Nullable; /** Function that can throw an I/O Exception */ public interface IOExceptionFunction { /** * Applies the function to the given input. * * @param input the input * @return the function result */ @Nullable T apply(@Nullable F input) throws IOException; /** * Wraps a function that may throw an IO Exception throwing an {@link IOExceptionWrapper}. * * @param f the function */ static Function asFunction(IOExceptionFunction f) { return i -> { try { return f.apply(i); } catch (IOException e) { throw new IOExceptionWrapper(e); } }; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionRunnable.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.utils; import java.io.IOException; /** Runnable that can throw I/O exceptions. */ public interface IOExceptionRunnable { /** * Runs the runnable. * * @throws IOException failed to run */ void run() throws IOException; /** * Wraps a runnable that may throw an IO Exception throwing an {@code UncheckedIOException}. * * @param r the runnable */ static Runnable asRunnable(IOExceptionRunnable r) { return () -> { try { r.run(); } catch (IOException e) { throw new IOExceptionWrapper(e); } }; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionWrapper.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.utils; import java.io.IOException; /** * Runtime exception used to encapsulate an IO Exception. This is used to allow throwing I/O * exceptions in functional interfaces that do not allow it and catching the exception afterwards. */ public class IOExceptionWrapper extends RuntimeException { /** * Creates a new exception. * * @param e the I/O exception to encapsulate */ public IOExceptionWrapper(IOException e) { super(e); } @Override public IOException getCause() { return (IOException) super.getCause(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/SigningBlockUtils.java ================================================ /* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.utils; import static java.nio.ByteOrder.LITTLE_ENDIAN; import com.android.apksig.apk.ApkSigningBlockNotFoundException; import com.android.apksig.apk.ApkUtils; import com.android.apksig.apk.ApkUtils.ApkSigningBlock; import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.util.Pair; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import com.android.apksig.zip.ZipFormatException; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import javax.annotation.Nullable; /** Generates and appends a new block to APK v2 Signature block. */ public final class SigningBlockUtils { private static final int MAGIC_NUM_BYTES = 16; private static final int BLOCK_LENGTH_NUM_BYTES = 8; static final int SIZE_OF_BLOCK_NUM_BYTES = 8; static final int BLOCK_ID_NUM_BYTES = 4; static final int ANDROID_COMMON_PAGE_ALIGNMENT_NUM_BYTES = 4096; static final int VERITY_PADDING_BLOCK_ID = 0x42726577; /** * Generates a new block with the given block value and block id, and appends it to the signing * block. * * @param signingBlock Block containing v2 signature and (optionally) padding block or null. * @param blockValue byte array containing block value of the new block or null. * @param blockId block id of the new block. * @return APK v2 block with signatures and the new block. If {@code blockValue} is null the * {@code signingBlock} is returned without any modification. If {@code signingBlock} is null, * a new signature block is created containing the new block and, optionally, padding block. */ public static byte[] addToSigningBlock(byte[] signingBlock, byte[] blockValue, int blockId) throws IOException { if (blockValue == null || blockValue.length == 0) { return signingBlock; } if (signingBlock == null || signingBlock.length == 0) { return createSigningBlock(blockValue, blockId); } return appendToSigningBlock(signingBlock, blockValue, blockId); } /** * Adds a new block to the signature block and a padding block, if required. * * @param signingBlock APK v2 signing block containing : length prefix, signers (can include * padding block), length postfix and APK sig v2 block magic. * @param blockValue byte array containing block value of the new block. * @param blockId block id of the new block. * @return APK v2 signing block containing : length prefix, signers including the new block (may * include padding block as well), length postfix and APK sig v2 block magic. */ private static byte[] appendToSigningBlock(byte[] signingBlock, byte[] blockValue, int blockId) throws IOException { ImmutableList> entries = ImmutableList.>builder() .addAll(extractAllSigners(DataSources.asDataSource(ByteBuffer.wrap(signingBlock)))) .add(Pair.of(blockValue, blockId)) .build(); return ApkSigningBlockUtils.generateApkSigningBlock(entries); } /** * Generate APK sig v2 block containing a block composed of the provided block value and id, and * (optionally) padding block. */ private static byte[] createSigningBlock(byte[] blockValue, int blockId) { return ApkSigningBlockUtils.generateApkSigningBlock( ImmutableList.of(Pair.of(blockValue, blockId))); } /** * Extracts all signing block entries except padding block. * * @param signingBlock APK v2 signing block containing: length prefix, signers (can include * padding block), length postfix and APK sig v2 block magic. * @return list of block entry value and block entry id pairs. */ private static ImmutableList> extractAllSigners(DataSource signingBlock) throws IOException { long wholeBlockSize = signingBlock.size(); // Take the segment of the existing signing block without the length prefix (8 bytes) // at the beginning and the length and magic (24 bytes) at the end, so it is just the sequence // of length prefix id value pairs. DataSource lengthPrefixedIdValuePairsSource = signingBlock.slice( SIZE_OF_BLOCK_NUM_BYTES, wholeBlockSize - 2 * SIZE_OF_BLOCK_NUM_BYTES - MAGIC_NUM_BYTES); final int lengthAndIdByteCount = BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES; ByteBuffer lengthAndId = ByteBuffer.allocate(lengthAndIdByteCount).order(LITTLE_ENDIAN); ImmutableList.Builder> idValuePairs = ImmutableList.builder(); for (int index = 0; index <= lengthPrefixedIdValuePairsSource.size() - lengthAndIdByteCount; ) { lengthPrefixedIdValuePairsSource.copyTo(index, lengthAndIdByteCount, lengthAndId); lengthAndId.flip(); int blockLength = Ints.checkedCast(lengthAndId.getLong()); int id = lengthAndId.getInt(); lengthAndId.clear(); if (id != VERITY_PADDING_BLOCK_ID) { int blockValueSize = blockLength - BLOCK_ID_NUM_BYTES; ByteBuffer blockValue = ByteBuffer.allocate(blockValueSize); lengthPrefixedIdValuePairsSource.copyTo( index + BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES, blockValueSize, blockValue); idValuePairs.add(Pair.of(blockValue.array(), id)); } index += blockLength + BLOCK_LENGTH_NUM_BYTES; } return idValuePairs.build(); } /** * Extract a block with the given id from the APK. If there is more than one block with the same * ID, the first block will be returned. If there are no block with the give id, {@code null} will * be returned. * * @param apk APK file * @param blockId id of the block to be extracted. */ @Nullable public static ByteBuffer extractBlock(File apk, int blockId) throws IOException, ZipFormatException, ApkSigningBlockNotFoundException { try (RandomAccessFile file = new RandomAccessFile(apk, "r")) { DataSource apkDataSource = DataSources.asDataSource(file); ApkSigningBlock signingBlockInfo = ApkUtils.findApkSigningBlock(apkDataSource, ApkUtils.findZipSections(apkDataSource)); DataSource wholeV2Block = signingBlockInfo.getContents(); final int lengthAndIdByteCount = BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES; DataSource signingBlock = wholeV2Block.slice( SIZE_OF_BLOCK_NUM_BYTES, wholeV2Block.size() - SIZE_OF_BLOCK_NUM_BYTES - MAGIC_NUM_BYTES); ByteBuffer lengthAndId = ByteBuffer.allocate(lengthAndIdByteCount).order(ByteOrder.LITTLE_ENDIAN); for (int index = 0; index <= signingBlock.size() - lengthAndIdByteCount; ) { signingBlock.copyTo(index, lengthAndIdByteCount, lengthAndId); lengthAndId.flip(); int blockLength = (int) lengthAndId.getLong(); int id = lengthAndId.getInt(); lengthAndId.flip(); if (id == blockId) { ByteBuffer block = ByteBuffer.allocate(blockLength - BLOCK_ID_NUM_BYTES); signingBlock.copyTo( index + lengthAndIdByteCount, blockLength - BLOCK_ID_NUM_BYTES, block); block.flip(); return block; } index += blockLength + BLOCK_LENGTH_NUM_BYTES; } return null; } } private SigningBlockUtils() {} } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/package-info.java ================================================ /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** Utilities to work with {@code apkzlib}. */ package com.android.tools.build.apkzlib.utils; ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreator.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zfile; import com.google.common.base.Function; import com.google.common.base.Predicate; import java.io.Closeable; import java.io.File; import java.io.IOException; import javax.annotation.Nullable; /** Creates or updates APKs based on provided entries. */ public interface ApkCreator extends Closeable { /** * Copies the content of a Jar/Zip archive into the receiver archive. * *

An optional predicate allows to selectively choose which files to copy over and an option * function allows renaming the files as they are copied. * * @param zip the zip to copy data from * @param transform an optional transform to apply to file names before copying them * @param isIgnored an optional filter or {@code null} to mark which out files should not be * added, even through they are on the zip; if {@code transform} is specified, then this * predicate applies after transformation * @throws IOException I/O error */ void writeZip( File zip, @Nullable Function transform, @Nullable Predicate isIgnored) throws IOException; /** * Writes a new {@link File} into the archive. If a file already existed with the given path, it * should be replaced. * * @param inputFile the {@link File} to write. * @param apkPath the filepath inside the archive. * @throws IOException I/O error */ void writeFile(File inputFile, String apkPath) throws IOException; /** * Deletes a file in a given path. * * @param apkPath the path to remove * @throws IOException failed to remove the entry */ void deleteFile(String apkPath) throws IOException; /** Returns true if the APK will be rewritten on close. */ boolean hasPendingChangesWithWait() throws IOException; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreatorFactory.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zfile; import com.android.tools.build.apkzlib.sign.SigningOptions; import com.google.auto.value.AutoValue; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import java.io.File; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** Factory that creates instances of {@link ApkCreator}. */ public interface ApkCreatorFactory { /** * Creates an {@link ApkCreator} with a given output location, and signing information. * * @param creationData the information to create the APK */ ApkCreator make(CreationData creationData); /** * Data structure with the required information to initiate the creation of an APK. See {@link * ApkCreatorFactory#make(CreationData)}. */ @AutoValue abstract class CreationData { /** An implementation of builder pattern to create a {@link CreationData} object. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder setApkPath(@Nonnull File apkPath); public abstract Builder setSigningOptions(@Nonnull SigningOptions signingOptions); public abstract Builder setBuiltBy(@Nullable String buildBy); public abstract Builder setCreatedBy(@Nullable String createdBy); public abstract Builder setNativeLibrariesPackagingMode( NativeLibrariesPackagingMode packagingMode); public abstract Builder setNoCompressPredicate(Predicate predicate); public abstract Builder setIncremental(boolean incremental); abstract CreationData autoBuild(); public CreationData build() { CreationData data = autoBuild(); Preconditions.checkArgument(data.getApkPath() != null, "Output apk path is not set"); return data; } } public static Builder builder() { return new AutoValue_ApkCreatorFactory_CreationData.Builder() .setBuiltBy(null) .setCreatedBy(null) .setNoCompressPredicate(s -> false) .setIncremental(false); } /** * Obtains the path where the APK should be located. If the path already exists, then the APK * may be updated instead of re-created. * * @return the path that may already exist or not */ public abstract File getApkPath(); /** * Obtains the data used to sign the APK. * * @return the SigningOptions */ @Nonnull public abstract Optional getSigningOptions(); /** * Obtains the "built-by" text for the APK. * * @return the text or {@code null} if the default should be used */ @Nullable public abstract String getBuiltBy(); /** * Obtains the "created-by" text for the APK. * * @return the text or {@code null} if the default should be used */ @Nullable public abstract String getCreatedBy(); /** Returns the packaging policy that the {@link ApkCreator} should use for native libraries. */ public abstract NativeLibrariesPackagingMode getNativeLibrariesPackagingMode(); /** Returns the predicate to decide which file paths should be uncompressed. */ public abstract Predicate getNoCompressPredicate(); /** * Returns if this apk build is incremental. * * As mentioned in {@link getApkPath} description, we may already have an existing apk in place. * This is the case when e.g. building APK via build system and this is not the first build. * In that case the build is called incremental and internal APK data might be reused speeding * the build up. */ public abstract boolean isIncremental(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreator.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zfile; import com.android.tools.build.apkzlib.zip.AlignmentRule; import com.android.tools.build.apkzlib.zip.AlignmentRules; import com.android.tools.build.apkzlib.zip.StoredEntry; import com.android.tools.build.apkzlib.zip.ZFile; import com.android.tools.build.apkzlib.zip.ZFileOptions; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.io.Closer; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import javax.annotation.Nullable; /** {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK. */ class ApkZFileCreator implements ApkCreator { /** Suffix for native libraries. */ private static final String NATIVE_LIBRARIES_SUFFIX = ".so"; /** Shared libraries are alignment at 4096 boundaries. */ private static final AlignmentRule SO_RULE = AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096); /** The zip file. */ private final ZFile zip; /** Has the zip file been closed? */ private boolean closed; /** Predicate defining which files should not be compressed. */ private final Predicate noCompressPredicate; /** * Creates a new creator. * * @param creationData the data needed to create the APK * @param options zip file options * @throws IOException failed to create the zip */ ApkZFileCreator(ApkCreatorFactory.CreationData creationData, ZFileOptions options) throws IOException { switch (creationData.getNativeLibrariesPackagingMode()) { case COMPRESSED: noCompressPredicate = creationData.getNoCompressPredicate(); break; case UNCOMPRESSED_AND_ALIGNED: Predicate baseNoCompressPredicate = creationData.getNoCompressPredicate(); noCompressPredicate = name -> baseNoCompressPredicate.apply(name) || name.endsWith(NATIVE_LIBRARIES_SUFFIX); options.setAlignmentRule(AlignmentRules.compose(SO_RULE, options.getAlignmentRule())); break; default: throw new AssertionError(); } // In case of incremental build we can skip validation since we generated the previous apk and // we trust ourselves options.setSkipValidation(creationData.isIncremental()); zip = ZFiles.apk( creationData.getApkPath(), options, creationData.getSigningOptions(), creationData.getBuiltBy(), creationData.getCreatedBy()); closed = false; } @Override public void writeZip( File zip, @Nullable Function transform, @Nullable Predicate isIgnored) throws IOException { Preconditions.checkState(!closed, "closed == true"); Preconditions.checkArgument(zip.isFile(), "!zip.isFile()"); Closer closer = Closer.create(); try { ZFile toMerge = closer.register(ZFile.openReadWrite(zip)); Predicate ignorePredicate; if (isIgnored == null) { ignorePredicate = s -> false; } else { ignorePredicate = isIgnored; } // Files that *must* be uncompressed in the result should not be merged and should be // added after. This is just very slightly less efficient than ignoring just the ones // that were compressed and must be uncompressed, but it is a lot simpler :) Predicate noMergePredicate = v -> ignorePredicate.apply(v) || noCompressPredicate.apply(v); this.zip.mergeFrom(toMerge, noMergePredicate); for (StoredEntry toMergeEntry : toMerge.entries()) { String path = toMergeEntry.getCentralDirectoryHeader().getName(); if (noCompressPredicate.apply(path) && !ignorePredicate.apply(path)) { // This entry *must* be uncompressed so it was ignored in the merge and should // now be added to the apk. try (InputStream ignoredData = toMergeEntry.open()) { this.zip.add(path, ignoredData, false); } } } } catch (Throwable t) { throw closer.rethrow(t); } finally { closer.close(); } } @Override public void writeFile(File inputFile, String apkPath) throws IOException { Preconditions.checkState(!closed, "closed == true"); boolean mayCompress = !noCompressPredicate.apply(apkPath); Closer closer = Closer.create(); try { FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile)); zip.add(apkPath, inputFileStream, mayCompress); } catch (IOException e) { throw closer.rethrow(e, IOException.class); } catch (Throwable t) { throw closer.rethrow(t); } finally { closer.close(); } } @Override public void deleteFile(String apkPath) throws IOException { Preconditions.checkState(!closed, "closed == true"); StoredEntry entry = zip.get(apkPath); if (entry != null) { entry.delete(); } } @Override public boolean hasPendingChangesWithWait() throws IOException { return zip.hasPendingChangesWithWait(); } @Override public void close() throws IOException { if (closed) { return; } zip.close(); closed = true; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreatorFactory.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zfile; import com.android.tools.build.apkzlib.utils.IOExceptionWrapper; import com.android.tools.build.apkzlib.zip.ZFileOptions; import java.io.IOException; /** Creates instances of {@link ApkZFileCreator}. */ public class ApkZFileCreatorFactory implements ApkCreatorFactory { /** Options for the {@link ZFileOptions} to use in all APKs. */ private final ZFileOptions options; /** * Creates a new factory. * * @param options the options to use for all instances created */ public ApkZFileCreatorFactory(ZFileOptions options) { this.options = options; } @Override public ApkCreator make(CreationData creationData) { try { return new ApkZFileCreator(creationData, options); } catch (IOException e) { throw new IOExceptionWrapper(e); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ManifestAttributes.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zfile; /** Java manifest attributes and some default values. */ public interface ManifestAttributes { /** Manifest attribute with the built by information. */ String BUILT_BY = "Built-By"; /** Manifest attribute with the created by information. */ String CREATED_BY = "Created-By"; /** Manifest attribute with the manifest version. */ String MANIFEST_VERSION = "Manifest-Version"; /** Manifest attribute value with the manifest version. */ String CURRENT_MANIFEST_VERSION = "1.0"; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/NativeLibrariesPackagingMode.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zfile; /** Describes how native libs should be packaged. */ public enum NativeLibrariesPackagingMode { /** Native libs are packaged as any other file. */ COMPRESSED, /** * Native libs are packaged uncompressed and page-aligned, so they can be mapped into memory at * runtime. * *

Support for this mode was added in Android 23, it only works if the {@code * extractNativeLibs} attribute is set in the manifest. */ UNCOMPRESSED_AND_ALIGNED; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ZFiles.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zfile; import com.android.tools.build.apkzlib.sign.ManifestGenerationExtension; import com.android.tools.build.apkzlib.sign.SigningExtension; import com.android.tools.build.apkzlib.sign.SigningOptions; import com.android.tools.build.apkzlib.zip.AlignmentRule; import com.android.tools.build.apkzlib.zip.AlignmentRules; import com.android.tools.build.apkzlib.zip.ZFile; import com.android.tools.build.apkzlib.zip.ZFileOptions; import com.google.common.base.Optional; import java.io.File; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import javax.annotation.Nullable; /** Factory for {@link ZFile}s that are specifically configured to be APKs, AARs, ... */ public class ZFiles { /** By default all non-compressed files are alignment at 4 byte boundaries.. */ private static final AlignmentRule APK_DEFAULT_RULE = AlignmentRules.constant(4); /** Default build by string. */ private static final String DEFAULT_BUILD_BY = "Generated-by-ADT"; /** Default created by string. */ private static final String DEFAULT_CREATED_BY = "Generated-by-ADT"; /** * Creates a new zip file configured as an apk, based on a given file. * * @param f the file, if this path does not represent an existing path, will create a {@link * ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is * invoked) * @param options the options to create the {@link ZFile} * @return the zip file * @throws IOException failed to create the zip file */ public static ZFile apk(File f, ZFileOptions options) throws IOException { options.setAlignmentRule(AlignmentRules.compose(options.getAlignmentRule(), APK_DEFAULT_RULE)); return ZFile.openReadWrite(f, options); } /** * Creates a new zip file configured as an apk, based on a given file. * * @param f the file, if this path does not represent an existing path, will create a {@link * ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is * invoked) * @param options the options to create the {@link ZFile} * @param signingOptions the options to sign the apk * @param builtBy who to mark as builder in the manifest * @param createdBy who to mark as creator in the manifest * @return the zip file * @throws IOException failed to create the zip file */ public static ZFile apk( File f, ZFileOptions options, Optional signingOptions, @Nullable String builtBy, @Nullable String createdBy) throws IOException { return apk( f, options, signingOptions, builtBy, createdBy, options.getAlwaysGenerateJarManifest()); } /** * Creates a new zip file configured as an apk, based on a given file. * * @param f the file, if this path does not represent an existing path, will create a {@link * ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is * invoked) * @param options the options to create the {@link ZFile} * @param signingOptions the options to sign the apk * @param builtBy who to mark as builder in the manifest * @param createdBy who to mark as creator in the manifest * @param writeManifest a migration parameter that forces keeping (useless) manifest.mf file in * apk file in order to prevent breaking changes. Clients of the previous interface will still * get apk with manifest.mf because the flag is true by default * @return the zip file * @throws IOException failed to create the zip file * @deprecated Use ZFileOptions.setAlwaysGenerateJarManifest() instead. */ @Deprecated // This method can be removed once ZFileOptions.getAlwaysGenerateJarManifest() is on Maven. public static ZFile apk( File f, ZFileOptions options, Optional signingOptions, @Nullable String builtBy, @Nullable String createdBy, boolean writeManifest) throws IOException { ZFile zfile = apk(f, options); if ((signingOptions.isPresent() && signingOptions.get().isV1SigningEnabled()) || writeManifest) { if (builtBy == null) { builtBy = DEFAULT_BUILD_BY; } if (createdBy == null) { createdBy = DEFAULT_CREATED_BY; } ManifestGenerationExtension manifestExt = new ManifestGenerationExtension(builtBy, createdBy); manifestExt.register(zfile); } if (signingOptions.isPresent()) { SigningOptions signOptions = signingOptions.get(); try { new SigningExtension(signOptions).register(zfile); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new IOException("Failed to create signature extensions", e); } } return zfile; } private ZFiles() {} } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/package-info.java ================================================ /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** The {@code zfile} package contains */ package com.android.tools.build.apkzlib.zfile; ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRule.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** An alignment rule defines how to a file should be aligned in a zip, based on its name. */ public interface AlignmentRule { /** Alignment value of files that do not require alignment. */ int NO_ALIGNMENT = 1; /** * Obtains the alignment this rule computes for a given path. * * @param path the path in the zip file * @return the alignment value, always greater than {@code 0}; if this rule places no restrictions * on the provided path, then {@link AlignmentRule#NO_ALIGNMENT} is returned */ int alignment(String path); } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRules.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.google.common.base.Preconditions; /** Factory for instances of {@link AlignmentRule}. */ public final class AlignmentRules { private AlignmentRules() {} /** * A rule that defines a constant alignment for all files. * * @param alignment the alignment * @return the rule */ public static AlignmentRule constant(int alignment) { Preconditions.checkArgument(alignment > 0, "alignment <= 0"); return (String path) -> alignment; } /** * A rule that defines constant alignment for all files with a certain suffix, placing no * restrictions on other files. * * @param suffix the suffix * @param alignment the alignment for paths that match the provided suffix * @return the rule */ public static AlignmentRule constantForSuffix(String suffix, int alignment) { Preconditions.checkArgument(!suffix.isEmpty(), "suffix.isEmpty()"); Preconditions.checkArgument(alignment > 0, "alignment <= 0"); return (String path) -> path.endsWith(suffix) ? alignment : AlignmentRule.NO_ALIGNMENT; } /** * A rule that applies other rules in order. * * @param rules all rules to be tried; the first rule that does not return {@link * AlignmentRule#NO_ALIGNMENT} will define the alignment for a path; if there are no rules * that return a value different from {@link AlignmentRule#NO_ALIGNMENT}, then {@link * AlignmentRule#NO_ALIGNMENT} is returned * @return the composition rule */ public static AlignmentRule compose(AlignmentRule... rules) { return (String path) -> { for (AlignmentRule r : rules) { int align = r.alignment(path); if (align != AlignmentRule.NO_ALIGNMENT) { return align; } } return AlignmentRule.NO_ALIGNMENT; }; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectory.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.bytestorage.ByteStorage; import com.android.tools.build.apkzlib.utils.CachedSupplier; import com.android.tools.build.apkzlib.utils.IOExceptionWrapper; import com.android.tools.build.apkzlib.zip.utils.MsDosDateTimeUtils; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /** Representation of the central directory of a zip archive. */ class CentralDirectory { /** Field in the central directory with the central directory signature. */ private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x02014b50, "Signature"); /** Field in the central directory with the "made by" code. */ private static final ZipField.F2 F_MADE_BY = new ZipField.F2(F_SIGNATURE.endOffset(), "Made by", new ZipFieldInvariantNonNegative()); /** Field in the central directory with the minimum version required to extract the entry. */ @VisibleForTesting static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2( F_MADE_BY.endOffset(), "Version to extract", new ZipFieldInvariantNonNegative()); /** Field in the central directory with the GP bit flag. */ private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(), "GP bit"); /** * Field in the central directory with the code of the compression method. See {@link * CompressionMethod#fromCode(long)}. */ private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(), "Method"); /** * Field in the central directory with the last modification time in MS-DOS format (see {@link * MsDosDateTimeUtils#packTime(long)}). */ private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(), "Last modification time"); /** * Field in the central directory with the last modification date in MS-DOS format. See {@link * MsDosDateTimeUtils#packDate(long)}. */ private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(), "Last modification date"); /** * Field in the central directory with the CRC32 checksum of the entry. This will be zero for * directories and files with no content. */ private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), "CRC32"); /** * Field in the central directory with the entry's compressed size, i.e., the file on the * archive. This will be the same as the uncompressed size if the method is {@link * CompressionMethod#STORE}. */ private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(), "Compressed size", new ZipFieldInvariantNonNegative()); /** * Field in the central directory with the entry's uncompressed size, i.e., the size the * file will have when extracted from the zip. This will be zero for directories and empty files * and will be the same as the compressed size if the method is {@link CompressionMethod#STORE}. */ private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4( F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative()); /** * Field in the central directory with the length of the file name. The file name is stored after * the offset field ({@link #F_OFFSET}). The number of characters in the file name are stored in * this field. */ private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2( F_UNCOMPRESSED_SIZE.endOffset(), "File name length", new ZipFieldInvariantNonNegative()); /** * Field in the central directory with the length of the extra field. The extra field is stored * after the file name ({@link #F_FILE_NAME_LENGTH}). The contents of this field are partially * defined in the zip specification but we do not parse it. */ private static final ZipField.F2 F_EXTRA_FIELD_LENGTH = new ZipField.F2( F_FILE_NAME_LENGTH.endOffset(), "Extra field length", new ZipFieldInvariantNonNegative()); /** * Field in the central directory with the length of the comment. The comment is stored after the * extra field ({@link #F_EXTRA_FIELD_LENGTH}). We do not parse the comment. */ private static final ZipField.F2 F_COMMENT_LENGTH = new ZipField.F2( F_EXTRA_FIELD_LENGTH.endOffset(), "Comment length", new ZipFieldInvariantNonNegative()); /** * Number of the disk where the central directory starts. Because we do not support multi-file * archives, this field has to have value {@code 0}. */ private static final ZipField.F2 F_DISK_NUMBER_START = new ZipField.F2(F_COMMENT_LENGTH.endOffset(), 0, "Disk start"); /** Internal attributes. This field can only contain one bit set, the {@link #ASCII_BIT}. */ private static final ZipField.F2 F_INTERNAL_ATTRIBUTES = new ZipField.F2(F_DISK_NUMBER_START.endOffset(), "Int attributes"); /** External attributes. This field is ignored. */ private static final ZipField.F4 F_EXTERNAL_ATTRIBUTES = new ZipField.F4(F_INTERNAL_ATTRIBUTES.endOffset(), "Ext attributes"); /** * Offset into the archive where the entry starts. This is the offset to the local header (see * {@link StoredEntry} for information on the local header), not to the file data itself. The file * data, if there is any, will be stored after the local header. */ private static final ZipField.F4 F_OFFSET = new ZipField.F4( F_EXTERNAL_ATTRIBUTES.endOffset(), "Offset", new ZipFieldInvariantNonNegative()); /** Maximum supported version to extract. */ private static final int MAX_VERSION_TO_EXTRACT = 20; /** * Bit that can be set on the internal attributes stating that the file is an ASCII file. We don't * do anything with this information, but we check that nothing unexpected appears in the internal * attributes. */ private static final int ASCII_BIT = 1; /** Contains all entries in the directory mapped from their names. */ private final Map entries; /** The file where this directory belongs to. */ private final ZFile file; /** Supplier that provides a byte representation of the central directory. */ private final CachedSupplier bytesSupplier; /** Verify log for the central directory. */ private final VerifyLog verifyLog; /** * Creates a new, empty, central directory, for a given zip file. * * @param file the file */ CentralDirectory(ZFile file) { entries = Maps.newHashMap(); this.file = file; bytesSupplier = new CachedSupplier<>(this::computeByteRepresentation); verifyLog = file.getVerifyLog(); } /** * Reads the central directory data from a zip file, parses it, and creates the in-memory * structure representing the directory. * * @param bytes the data of the central directory; the directory is read from the buffer's current * position; when this method terminates, the buffer's position is the first byte after the * directory * @param count the number of entries expected in the central directory (usually read from the * {@link Eocd}). * @param file the zip file this central directory belongs to * @param storage the storage used to generate sources with entry data * @return the central directory * @throws IOException failed to read data from the zip, or the central directory is corrupted or * has unsupported features */ static CentralDirectory makeFromData(ByteBuffer bytes, long count, ZFile file, ByteStorage storage) throws IOException { Preconditions.checkNotNull(bytes, "bytes == null"); Preconditions.checkArgument(count >= 0, "count < 0"); CentralDirectory directory = new CentralDirectory(file); for (long i = 0; i < count; i++) { try { directory.readEntry(bytes, storage); } catch (IOException e) { throw new IOException( "Failed to read directory entry index " + i + " (total " + "directory bytes read: " + bytes.position() + ").", e); } } return directory; } /** * Creates a new central directory from the entries. This is used to build a new central directory * from entries in the zip file. * * @param entries the entries in the zip file * @param file the zip file itself * @return the created central directory */ static CentralDirectory makeFromEntries(Set entries, ZFile file) { CentralDirectory directory = new CentralDirectory(file); for (StoredEntry entry : entries) { CentralDirectoryHeader cdr = entry.getCentralDirectoryHeader(); Preconditions.checkArgument( !directory.entries.containsKey(cdr.getName()), "Duplicate filename"); directory.entries.put(cdr.getName(), entry); } return directory; } /** * Reads the next entry from the central directory and adds it to {@link #entries}. * * @param bytes the central directory's data, positioned starting at the beginning of the next * entry to read; when finished, the buffer's position will be at the first byte after the * entry * @param storage the storage used to generate sources to store entry data * @throws IOException failed to read the directory entry, either because of an I/O error, because * it is corrupt or contains unsupported features */ private void readEntry(ByteBuffer bytes, ByteStorage storage) throws IOException { F_SIGNATURE.verify(bytes); long madeBy = F_MADE_BY.read(bytes); long versionNeededToExtract = F_VERSION_EXTRACT.read(bytes); verifyLog.verify( versionNeededToExtract <= MAX_VERSION_TO_EXTRACT, "Ignored unknown version needed to extract in zip directory entry: %s.", versionNeededToExtract); long gpBit = F_GP_BIT.read(bytes); GPFlags flags = GPFlags.from(gpBit); long methodCode = F_METHOD.read(bytes); CompressionMethod method = CompressionMethod.fromCode(methodCode); verifyLog.verify(method != null, "Unknown method in zip directory entry: %s.", methodCode); long lastModTime; long lastModDate; if (file.areTimestampsIgnored()) { lastModTime = 0; lastModDate = 0; F_LAST_MOD_TIME.skip(bytes); F_LAST_MOD_DATE.skip(bytes); } else { lastModTime = F_LAST_MOD_TIME.read(bytes); lastModDate = F_LAST_MOD_DATE.read(bytes); } long crc32 = F_CRC32.read(bytes); long compressedSize = F_COMPRESSED_SIZE.read(bytes); long uncompressedSize = F_UNCOMPRESSED_SIZE.read(bytes); int fileNameLength = Ints.checkedCast(F_FILE_NAME_LENGTH.read(bytes)); int extraFieldLength = Ints.checkedCast(F_EXTRA_FIELD_LENGTH.read(bytes)); int fileCommentLength = Ints.checkedCast(F_COMMENT_LENGTH.read(bytes)); F_DISK_NUMBER_START.verify(bytes, verifyLog); long internalAttributes = F_INTERNAL_ATTRIBUTES.read(bytes); verifyLog.verify( (internalAttributes & ~ASCII_BIT) == 0, "Ignored invalid internal attributes: %s.", internalAttributes); long externalAttributes = F_EXTERNAL_ATTRIBUTES.read(bytes); long entryOffset = F_OFFSET.read(bytes); long remainingSize = (long) fileNameLength + extraFieldLength + fileCommentLength; if (bytes.remaining() < fileNameLength + extraFieldLength + fileCommentLength) { throw new IOException( "Directory entry should have " + remainingSize + " bytes remaining (name = " + fileNameLength + ", extra = " + extraFieldLength + ", comment = " + fileCommentLength + "), but it has " + bytes.remaining() + "."); } byte[] encodedFileName = new byte[fileNameLength]; bytes.get(encodedFileName); String fileName = EncodeUtils.decode(encodedFileName, flags); byte[] extraField = new byte[extraFieldLength]; bytes.get(extraField); byte[] fileCommentField = new byte[fileCommentLength]; bytes.get(fileCommentField); /* * Tricky: to create a CentralDirectoryHeader we need the future that will hold the result * of the compress information. But, to actually create the result of the compress * information we need the CentralDirectoryHeader */ ListenableFuture compressInfo = Futures.immediateFuture( new CentralDirectoryHeaderCompressInfo(method, compressedSize, versionNeededToExtract)); CentralDirectoryHeader centralDirectoryHeader = new CentralDirectoryHeader( fileName, encodedFileName, uncompressedSize, compressInfo, flags, file, lastModTime, lastModDate); centralDirectoryHeader.setMadeBy(madeBy); centralDirectoryHeader.setLastModTime(lastModTime); centralDirectoryHeader.setLastModDate(lastModDate); centralDirectoryHeader.setCrc32(crc32); centralDirectoryHeader.setInternalAttributes(internalAttributes); centralDirectoryHeader.setExternalAttributes(externalAttributes); centralDirectoryHeader.setOffset(entryOffset); centralDirectoryHeader.setExtraFieldNoNotify(new ExtraField(extraField)); centralDirectoryHeader.setComment(fileCommentField); StoredEntry entry; try { entry = new StoredEntry(centralDirectoryHeader, file, null, storage); } catch (IOException e) { throw new IOException("Failed to read stored entry '" + fileName + "'.", e); } if (entries.containsKey(fileName)) { verifyLog.log("File file contains duplicate file '" + fileName + "'."); } entries.put(fileName, entry); } /** * Obtains all the entries in the central directory. * * @return all entries on a non-modifiable map */ Map getEntries() { return ImmutableMap.copyOf(entries); } /** * Obtains whether the Central Directory contains any files with Zip64 file extensions. * *

At the present time, files in the Zip64 format are not supported, so this method returns * false. * * @return false, as Zip64 formatted files are not supported */ boolean containsZip64Files() { return false; } /** * Obtains the byte representation of the central directory. * * @return a byte array containing the whole central directory * @throws IOException failed to write the byte array */ byte[] toBytes() throws IOException { return bytesSupplier.get(); } /** * Computes the byte representation of the central directory. * * @return a byte array containing the whole central directory * @throws UncheckedIOException failed to write the byte array */ private byte[] computeByteRepresentation() { List sorted = Lists.newArrayList(entries.values()); Collections.sort(sorted, StoredEntry.COMPARE_BY_NAME); CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[entries.size()]; CentralDirectoryHeaderCompressInfo[] compressInfos = new CentralDirectoryHeaderCompressInfo[entries.size()]; byte[][] encodedFileNames = new byte[entries.size()][]; byte[][] extraFields = new byte[entries.size()][]; byte[][] comments = new byte[entries.size()][]; try { /* * First collect all the data and compute the total size of the central directory. */ int idx = 0; int total = 0; for (StoredEntry entry : sorted) { cdhs[idx] = entry.getCentralDirectoryHeader(); compressInfos[idx] = cdhs[idx].getCompressionInfoWithWait(); encodedFileNames[idx] = cdhs[idx].getEncodedFileName(); extraFields[idx] = new byte[cdhs[idx].getExtraField().size()]; cdhs[idx].getExtraField().write(ByteBuffer.wrap(extraFields[idx])); comments[idx] = cdhs[idx].getComment(); total += F_OFFSET.endOffset() + encodedFileNames[idx].length + extraFields[idx].length + comments[idx].length; idx++; } ByteBuffer out = ByteBuffer.allocate(total); for (idx = 0; idx < entries.size(); idx++) { F_SIGNATURE.write(out); F_MADE_BY.write(out, cdhs[idx].getMadeBy()); F_VERSION_EXTRACT.write(out, compressInfos[idx].getVersionExtract()); F_GP_BIT.write(out, cdhs[idx].getGpBit().getValue()); F_METHOD.write(out, compressInfos[idx].getMethod().methodCode); if (file.areTimestampsIgnored()) { F_LAST_MOD_TIME.write(out, 0); F_LAST_MOD_DATE.write(out, 0); } else { F_LAST_MOD_TIME.write(out, cdhs[idx].getLastModTime()); F_LAST_MOD_DATE.write(out, cdhs[idx].getLastModDate()); } F_CRC32.write(out, cdhs[idx].getCrc32()); F_COMPRESSED_SIZE.write(out, compressInfos[idx].getCompressedSize()); F_UNCOMPRESSED_SIZE.write(out, cdhs[idx].getUncompressedSize()); F_FILE_NAME_LENGTH.write(out, cdhs[idx].getEncodedFileName().length); F_EXTRA_FIELD_LENGTH.write(out, cdhs[idx].getExtraField().size()); F_COMMENT_LENGTH.write(out, cdhs[idx].getComment().length); F_DISK_NUMBER_START.write(out); F_INTERNAL_ATTRIBUTES.write(out, cdhs[idx].getInternalAttributes()); F_EXTERNAL_ATTRIBUTES.write(out, cdhs[idx].getExternalAttributes()); F_OFFSET.write(out, cdhs[idx].getOffset()); out.put(encodedFileNames[idx]); out.put(extraFields[idx]); out.put(comments[idx]); } return out.array(); } catch (IOException e) { throw new IOExceptionWrapper(e); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeader.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.zip.utils.MsDosDateTimeUtils; import com.google.common.base.Verify; import java.io.IOException; import java.util.Arrays; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; /** * The Central Directory Header contains information about files stored in the zip. Instances of * this class contain information for files that already are in the zip and, for which the data was * read from the Central Directory. But some instances of this class are used for new files. Because * instances of this class can refer to files not yet on the zip, some of the fields may not be * filled in, or may be filled in with default values. * *

Because compression decision is done lazily, some data is stored with futures. */ public class CentralDirectoryHeader implements Cloneable { /** * Default "version made by" field: upper byte needs to be 0 to set to MS-DOS compatibility. Lower * byte can be anything, really. We use 18 because aapt uses 17 :) */ private static final int DEFAULT_VERSION_MADE_BY = 0x0018; private static final byte[] EMPTY_COMMENT = new byte[0]; /** Name of the file. */ private final String name; /** CRC32 of the data. 0 if not yet computed. */ private long crc32; /** Size of the file uncompressed. 0 if the file has no data. */ private long uncompressedSize; /** Code of the program that made the zip. We actually don't care about this. */ private long madeBy; /** General-purpose bit flag. */ private GPFlags gpBit; /** Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packTime(long)}). */ private long lastModTime; /** Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packDate(long)}). */ private long lastModDate; /** * Extra data field contents. This field follows a specific structure according to the * specification. */ private ExtraField extraField; /** File comment. */ private byte[] comment; /** File internal attributes. */ private long internalAttributes; /** File external attributes. */ private long externalAttributes; /** * Offset in the file where the data is located. This will be -1 if the header corresponds to a * new file that is not yet written in the zip and, therefore, has no written data. */ private long offset; /** Encoded file name. */ private byte[] encodedFileName; /** Compress information that may not have been computed yet due to lazy compression. */ private final Future compressInfo; /** The file this header belongs to. */ private final ZFile file; /** * Creates data for a file. * * @param name the file name * @param encodedFileName the encoded file name, this array will be owned by the header * @param uncompressedSize the uncompressed file size * @param compressInfo computation that defines the compression information * @param flags flags used in the entry * @param zFile the file this header belongs to */ CentralDirectoryHeader( String name, byte[] encodedFileName, long uncompressedSize, Future compressInfo, GPFlags flags, ZFile zFile) { this( name, encodedFileName, uncompressedSize, compressInfo, flags, zFile, MsDosDateTimeUtils.packCurrentTime(), MsDosDateTimeUtils.packCurrentDate()); } CentralDirectoryHeader( String name, byte[] encodedFileName, long uncompressedSize, Future compressInfo, GPFlags flags, ZFile zFile, long currentTime, long currentDate) { this.name = name; this.uncompressedSize = uncompressedSize; crc32 = 0; /* * Set sensible defaults for the rest. */ madeBy = DEFAULT_VERSION_MADE_BY; gpBit = flags; lastModTime = currentTime; lastModDate = currentDate; extraField = ExtraField.EMPTY; comment = EMPTY_COMMENT; internalAttributes = 0; externalAttributes = 0; offset = -1; this.encodedFileName = encodedFileName; this.compressInfo = compressInfo; file = zFile; } public CentralDirectoryHeader link(String name, byte[] encodedFileName, GPFlags flags, ZFile file) { var newData = new CentralDirectoryHeader(name, encodedFileName, uncompressedSize, compressInfo, flags, file, lastModTime, lastModDate); newData.extraField = extraField; newData.offset = -1; newData.internalAttributes = internalAttributes; newData.externalAttributes = externalAttributes; newData.comment = comment; newData.madeBy = madeBy; newData.crc32 = crc32; return newData; } /** * Obtains the name of the file. * * @return the name */ public String getName() { return name; } /** * Obtains the size of the uncompressed file. * * @return the size of the file */ public long getUncompressedSize() { return uncompressedSize; } /** * Obtains the CRC32 of the data. * * @return the CRC32, 0 if not yet computed */ public long getCrc32() { return crc32; } /** * Sets the CRC32 of the data. * * @param crc32 the CRC 32 */ void setCrc32(long crc32) { this.crc32 = crc32; } /** * Obtains the code of the program that made the zip. * * @return the code */ public long getMadeBy() { return madeBy; } /** * Sets the code of the progtram that made the zip. * * @param madeBy the code */ void setMadeBy(long madeBy) { this.madeBy = madeBy; } /** * Obtains the general-purpose bit flag. * * @return the bit flag */ public GPFlags getGpBit() { return gpBit; } /** * Obtains the last modification time of the entry. * * @return the last modification time in MS-DOS format (see {@link * MsDosDateTimeUtils#packTime(long)}) */ public long getLastModTime() { return lastModTime; } /** * Sets the last modification time of the entry. * * @param lastModTime the last modification time in MS-DOS format (see {@link * MsDosDateTimeUtils#packTime(long)}) */ void setLastModTime(long lastModTime) { this.lastModTime = lastModTime; } /** * Obtains the last modification date of the entry. * * @return the last modification date in MS-DOS format (see {@link * MsDosDateTimeUtils#packDate(long)}) */ public long getLastModDate() { return lastModDate; } /** * Sets the last modification date of the entry. * * @param lastModDate the last modification date in MS-DOS format (see {@link * MsDosDateTimeUtils#packDate(long)}) */ void setLastModDate(long lastModDate) { this.lastModDate = lastModDate; } /** * Obtains the data in the extra field. * * @return the data (returns an empty array if there is none) */ public ExtraField getExtraField() { return extraField; } /** * Sets the data in the extra field. * * @param extraField the data to set */ public void setExtraField(ExtraField extraField) { setExtraFieldNoNotify(extraField); file.centralDirectoryChanged(); } /** * Sets the data in the extra field, but does not notify {@link ZFile}. This method is invoked * when the {@link ZFile} knows the extra field is being set. * * @param extraField the data to set */ void setExtraFieldNoNotify(ExtraField extraField) { this.extraField = extraField; } /** * Obtains the entry's comment. * * @return the comment (returns an empty array if there is no comment) */ public byte[] getComment() { return comment; } /** * Sets the entry's comment. * * @param comment the comment */ void setComment(byte[] comment) { this.comment = comment; } /** * Obtains the entry's internal attributes. * * @return the entry's internal attributes */ public long getInternalAttributes() { return internalAttributes; } /** * Sets the entry's internal attributes. * * @param internalAttributes the entry's internal attributes */ void setInternalAttributes(long internalAttributes) { this.internalAttributes = internalAttributes; } /** * Obtains the entry's external attributes. * * @return the entry's external attributes */ public long getExternalAttributes() { return externalAttributes; } /** * Sets the entry's external attributes. * * @param externalAttributes the entry's external attributes */ void setExternalAttributes(long externalAttributes) { this.externalAttributes = externalAttributes; } /** * Obtains the offset in the zip file where this entry's data is. * * @return the offset or {@code -1} if the file has no data in the zip and, therefore, data is * stored in memory */ public long getOffset() { return offset; } /** * Sets the offset in the zip file where this entry's data is. * * @param offset the offset or {@code -1} if the file is new and has no data in the zip yet */ void setOffset(long offset) { this.offset = offset; } /** * Obtains the encoded file name. * * @return the encoded file name */ public byte[] getEncodedFileName() { return encodedFileName; } /** Resets the deferred CRC flag in the GP flags. */ void resetDeferredCrc() { /* * We actually create a new set of flags. Since the only information we care about is the * UTF-8 encoding, we'll just create a brand new object. */ gpBit = GPFlags.make(gpBit.isUtf8FileName()); } @Override protected CentralDirectoryHeader clone() throws CloneNotSupportedException { CentralDirectoryHeader cdr = (CentralDirectoryHeader) super.clone(); cdr.extraField = extraField; cdr.comment = Arrays.copyOf(comment, comment.length); cdr.encodedFileName = Arrays.copyOf(encodedFileName, encodedFileName.length); return cdr; } /** * Obtains the future with the compression information. * * @return the information */ public Future getCompressionInfo() { return compressInfo; } /** * Equivalent to {@code getCompressionInfo().get()} but masking the possible exceptions and * guaranteeing non-{@code null} return. * * @return the result of the future * @throws IOException failed to get the information */ public CentralDirectoryHeaderCompressInfo getCompressionInfoWithWait() throws IOException { try { CentralDirectoryHeaderCompressInfo info = getCompressionInfo().get(); Verify.verifyNotNull(info, "info == null"); return info; } catch (InterruptedException e) { throw new IOException("Interrupted while waiting for compression information.", e); } catch (ExecutionException e) { throw new IOException("Execution of compression failed.", e); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** * Information stored in the {@link CentralDirectoryHeader} that is related to compression and may * need to be computed lazily. */ public class CentralDirectoryHeaderCompressInfo { /** Version of zip file that only supports stored files. */ public static final long VERSION_WITH_STORE_FILES_ONLY = 10L; /** Version of zip file that only supports directories and deflated files. */ public static final long VERSION_WITH_DIRECTORIES_AND_DEFLATE = 20L; /** Version of zip file that only supports ZIP64 format extensions */ public static final long VERSION_WITH_ZIP64_EXTENSIONS = 45L; /** Version of zip file that uses central file encryption and version 2 of the Zip64 EOCD */ public static final long VERSION_WITH_CENTRAL_FILE_ENCRYPTION = 62L; /** The compression method. */ private final CompressionMethod method; /** Size of the file compressed. 0 if the file has no data. */ private final long compressedSize; /** Version needed to extract the zip. */ private final long versionExtract; /** * Creates new compression information for the central directory header. * * @param method the compression method * @param compressedSize the compressed size * @param versionToExtract minimum version to extract (typically {@link * #VERSION_WITH_STORE_FILES_ONLY} or {@link #VERSION_WITH_DIRECTORIES_AND_DEFLATE}) */ public CentralDirectoryHeaderCompressInfo( CompressionMethod method, long compressedSize, long versionToExtract) { this.method = method; this.compressedSize = compressedSize; versionExtract = versionToExtract; } /** * Creates new compression information for the central directory header. * * @param header the header this information relates to * @param method the compression method * @param compressedSize the compressed size */ public CentralDirectoryHeaderCompressInfo( CentralDirectoryHeader header, CompressionMethod method, long compressedSize) { this.method = method; this.compressedSize = compressedSize; if (header.getName().endsWith("/") || method == CompressionMethod.DEFLATE) { /* * Directories and compressed files only in version 2.0. */ versionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE; } else { versionExtract = VERSION_WITH_STORE_FILES_ONLY; } } /** * Obtains the compression data size. * * @return the compressed data size */ public long getCompressedSize() { return compressedSize; } /** * Obtains the compression method. * * @return the compression method */ public CompressionMethod getMethod() { return method; } /** * Obtains the minimum version for extract. * * @return the minimum version */ long getVersionExtract() { return versionExtract; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CompressionMethod.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import javax.annotation.Nullable; /** Enumeration with all known compression methods. */ public enum CompressionMethod { /** STORE method: data is stored without any compression. */ STORE(0), /** DEFLATE method: data is stored compressed using the DEFLATE algorithm. */ DEFLATE(8); /** Code, within the zip file, that identifies this compression method. */ int methodCode; /** * Creates a new compression method. * * @param methodCode the code used in the zip file that identifies the compression method */ CompressionMethod(int methodCode) { this.methodCode = methodCode; } /** * Obtains the compression method that corresponds to the provided code. * * @param code the code * @return the method or {@code null} if no method has the provided code */ @Nullable static CompressionMethod fromCode(long code) { for (CompressionMethod method : values()) { if (method.methodCode == code) { return method; } } return null; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CompressionResult.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; /** Result of compressing data. */ public class CompressionResult { /** The compression method used. */ private final CompressionMethod compressionMethod; /** The resulting data. */ private final CloseableByteSource source; /** * Size of the compressed source. Kept because {@code source.size()} can throw {@code * IOException}. */ private final long mSize; /** * Creates a new compression result. * * @param source the data source * @param method the compression method */ public CompressionResult(CloseableByteSource source, CompressionMethod method, long size) { compressionMethod = method; this.source = source; mSize = size; } /** * Obtains the compression method. * * @return the compression method */ public CompressionMethod getCompressionMethod() { return compressionMethod; } /** * Obtains the compressed data. * * @return the data, the resulting array should not be modified */ public CloseableByteSource getSource() { return source; } /** * Obtains the size of the compression result. * * @return the size */ public long getSize() { return mSize; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Compressor.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.bytestorage.ByteStorage; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.util.concurrent.ListenableFuture; /** * A compressor is capable of, well, compressing data. Data is read from an {@code ByteSource}. * Compressors are asynchronous: compressing results in a {@code ListenableFuture} that will contain * the compression result. */ public interface Compressor { /** * Compresses an entry source. * * @param source the source to compress * @param storage a byte storage from where the compressor can obtain byte sources to work * @return a future that will eventually contain the compression result */ ListenableFuture compress(CloseableByteSource source, ByteStorage storage); } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/DataDescriptorType.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** * Type of data descriptor that an entry has. Data descriptors are used if the CRC and sizing data * is not known when the data is being written and cannot be placed in the file's local header. In * those cases, after the file data itself, a data descriptor is placed after the entry's contents. * *

While the zip specification says the data descriptor should be used but it is optional. We * record also whether the data descriptor contained the 4-byte signature at the start of the block * or not. */ public enum DataDescriptorType { /** The entry has no data descriptor. */ NO_DATA_DESCRIPTOR(0), /** The entry has a data descriptor that does not contain a signature. */ DATA_DESCRIPTOR_WITHOUT_SIGNATURE(12), /** The entry has a data descriptor that contains a signature. */ DATA_DESCRIPTOR_WITH_SIGNATURE(16); /** The number of bytes the data descriptor spans. */ public int size; /** * Creates a new data descriptor. * * @param size the number of bytes the data descriptor spans */ DataDescriptorType(int size) { this.size = size; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/EncodeUtils.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CodingErrorAction; /** Utilities to encode and decode file names in zips. */ public class EncodeUtils { /** Utility class: no constructor. */ private EncodeUtils() { /* * Nothing to do. */ } /** * Decodes a file name. * * @param bytes the raw data buffer to read from * @param length the number of bytes in the raw data buffer containing the string to decode * @param flags the zip entry flags * @return the decode file name */ public static String decode(ByteBuffer bytes, int length, GPFlags flags) throws IOException { if (bytes.remaining() < length) { throw new IOException( "Only " + bytes.remaining() + " bytes exist in the buffer, but " + "length is " + length + "."); } byte[] stringBytes = new byte[length]; bytes.get(stringBytes); return decode(stringBytes, flags); } /** * Decodes a file name. * * @param data the raw data * @param flags the zip entry flags * @return the decode file name */ public static String decode(byte[] data, GPFlags flags) { return decode(data, flagsCharset(flags)); } /** * Decodes a file name. * * @param data the raw data * @param charset the charset to use * @return the decode file name */ private static String decode(byte[] data, Charset charset) { try { return charset .newDecoder() .onMalformedInput(CodingErrorAction.REPORT) .decode(ByteBuffer.wrap(data)) .toString(); } catch (CharacterCodingException e) { // If we're trying to decode ASCII, try UTF-8. Otherwise, revert to the default // behavior (usually replacing invalid characters). if (charset.equals(US_ASCII)) { return decode(data, UTF_8); } else { return charset.decode(ByteBuffer.wrap(data)).toString(); } } } /** * Encodes a file name. * * @param name the name to encode * @param flags the zip entry flags * @return the encoded file name */ public static byte[] encode(String name, GPFlags flags) { Charset charset = flagsCharset(flags); ByteBuffer bytes = charset.encode(name); byte[] result = new byte[bytes.remaining()]; bytes.get(result); return result; } /** * Obtains the charset to encode and decode zip entries, given a set of flags. * * @param flags the flags * @return the charset to use */ private static Charset flagsCharset(GPFlags flags) { if (flags.isUtf8FileName()) { return UTF_8; } else { return US_ASCII; } } /** * Checks if some text may be encoded using ASCII. * * @param text the text to check * @return can it be encoded using ASCII? */ public static boolean canAsciiEncode(String text) { return US_ASCII.newEncoder().canEncode(text); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.utils.CachedSupplier; import com.android.tools.build.apkzlib.utils.IOExceptionWrapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import com.google.common.primitives.Ints; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.ByteBuffer; /** End Of Central Directory record in a zip file. */ class Eocd { /** Max total records that can be specified by the standard EOCD. */ static final long MAX_TOTAL_RECORDS = 0xFFFFL; /** Max size of the Central Directory that can be specified by the standard EOCD. */ static final long MAX_CD_SIZE = 0xFFFFFFFFL; /** Max offset of the Central Directory that can be specified by the standard EOCD. */ static final long MAX_CD_OFFSET = 0xFFFFFFFFL; /** Field in the record: the record signature, fixed at this value by the specification. */ private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x06054b50, "EOCD signature"); /** * Field in the record: the number of the disk where the EOCD is located. It has to be zero * because we do not support multi-file archives. */ private static final ZipField.F2 F_NUMBER_OF_DISK = new ZipField.F2(F_SIGNATURE.endOffset(), 0, "Number of this disk"); /** * Field in the record: the number of the disk where the Central Directory starts. Has to be zero * because we do not support multi-file archives. */ private static final ZipField.F2 F_DISK_CD_START = new ZipField.F2(F_NUMBER_OF_DISK.endOffset(), 0, "Disk where CD starts"); /** * Field in the record: the number of entries in the Central Directory on this disk. Because we do * not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}. */ private static final ZipField.F2 F_RECORDS_DISK = new ZipField.F2( F_DISK_CD_START.endOffset(), "Record on disk count", new ZipFieldInvariantNonNegative()); /** * Field in the record: the total number of entries in the Central Directory. This value will be * {@link #MAX_TOTAL_RECORDS} if the file is in the Zip64 format, and the Central Directory holds * at least {@link #MAX_TOTAL_RECORDS} entries. */ private static final ZipField.F2 F_RECORDS_TOTAL = new ZipField.F2( F_RECORDS_DISK.endOffset(), "Total records", new ZipFieldInvariantNonNegative(), new ZipFieldInvariantMaxValue(Integer.MAX_VALUE)); /** * Field in the record: number of bytes of the Central Directory. This is not private because it * is required in unit tests. This value will be {@link #MAX_CD_SIZE} if the file is in the Zip64 * format, and the Central Directory is at least {@link #MAX_CD_SIZE} bytes. */ @VisibleForTesting static final ZipField.F4 F_CD_SIZE = new ZipField.F4( F_RECORDS_TOTAL.endOffset(), "Directory size", new ZipFieldInvariantNonNegative()); /** * Field in the record: offset, from the archive start, where the Central Directory starts. This * is not private because it is required in unit tests. This value will be {@link #MAX_CD_OFFSET} * if the file is in the Zip64 format, and the Central Directory is at least * {@link #MAX_CD_OFFSET} bytes. */ @VisibleForTesting static final ZipField.F4 F_CD_OFFSET = new ZipField.F4( F_CD_SIZE.endOffset(), "Directory offset", new ZipFieldInvariantNonNegative()); /** * Field in the record: number of bytes of the file comment (located at the end of the EOCD * record). */ private static final ZipField.F2 F_COMMENT_SIZE = new ZipField.F2( F_CD_OFFSET.endOffset(), "File comment size", new ZipFieldInvariantNonNegative()); /** Number of entries in the central directory. */ private final long totalRecords; /** Offset from the beginning of the archive where the Central Directory is located. */ private final long directoryOffset; /** Number of bytes of the Central Directory. */ private final long directorySize; /** Contents of the EOCD comment. */ private final byte[] comment; /** Supplier of the byte representation of the EOCD. */ private final CachedSupplier byteSupplier; /** * Creates a new EOCD, reading it from a byte source. This method will parse the byte source and * obtain the EOCD. It will check that the byte source starts with the EOCD signature. * * @param bytes the byte buffer with the EOCD data; when this method finishes, the byte buffer's * position will have moved to the end of the EOCD * @throws IOException failed to read information or the EOCD data is corrupt or invalid */ Eocd(ByteBuffer bytes) throws IOException { /* * Read the EOCD record. */ F_SIGNATURE.verify(bytes); F_NUMBER_OF_DISK.verify(bytes); F_DISK_CD_START.verify(bytes); long totalRecords1 = F_RECORDS_DISK.read(bytes); long totalRecords2 = F_RECORDS_TOTAL.read(bytes); long directorySize = F_CD_SIZE.read(bytes); long directoryOffset = F_CD_OFFSET.read(bytes); int commentSize = Ints.checkedCast(F_COMMENT_SIZE.read(bytes)); /* * Some sanity checks. */ if (totalRecords1 != totalRecords2) { throw new IOException( "Zip states records split in multiple disks, which is not " + "supported."); } Verify.verify(totalRecords1 <= Integer.MAX_VALUE); totalRecords = Ints.checkedCast(totalRecords1); this.directorySize = directorySize; this.directoryOffset = directoryOffset; if (bytes.remaining() < commentSize) { throw new IOException( "Corrupt EOCD record: not enough data for comment (comment " + "size is " + commentSize + ")."); } comment = new byte[commentSize]; bytes.get(comment); byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); } /** * Creates a new EOCD. This is used when generating an EOCD for an Central Directory that has just * been generated. The EOCD will be generated without any comment. * * @param totalRecords total number of records in the directory * @param directoryOffset offset, since beginning of archive, where the Central Directory is * located * @param directorySize number of bytes of the Central Directory * @param comment the EOCD comment */ Eocd(long totalRecords, long directoryOffset, long directorySize, byte[] comment) { Preconditions.checkArgument(totalRecords >= 0, "totalRecords < 0"); Preconditions.checkArgument(directoryOffset >= 0, "directoryOffset < 0"); Preconditions.checkArgument(directorySize >= 0, "directorySize < 0"); this.totalRecords = totalRecords; this.directoryOffset = directoryOffset; this.directorySize = directorySize; this.comment = comment; byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); } /** * Obtains the number of records in the Central Directory. * * @return the number of records */ long getTotalRecords() { return totalRecords; } /** * Obtains the offset since the beginning of the zip archive where the Central Directory is * located. * * @return the offset where the Central Directory is located */ long getDirectoryOffset() { return directoryOffset; } /** * Obtains the size of the Central Directory. * * @return the number of bytes that make up the Central Directory */ long getDirectorySize() { return directorySize; } /** * Obtains the size of the EOCD. * * @return the size, in bytes, of the EOCD */ long getEocdSize() { return (long) F_COMMENT_SIZE.endOffset() + comment.length; } /** * Generates the EOCD data. * * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes * @throws IOException failed to generate the EOCD data */ byte[] toBytes() throws IOException { return byteSupplier.get(); } /** * Obtains the comment in the EOCD. * * @return the comment exactly as it is represented in the file (no encoding conversion is * done) */ byte[] getComment() { byte[] commentCopy = new byte[comment.length]; System.arraycopy(comment, 0, commentCopy, 0, comment.length); return commentCopy; } /** * Computes the byte representation of the EOCD. * * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes * @throws UncheckedIOException failed to generate the EOCD data */ private byte[] computeByteRepresentation() { ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length); try { F_SIGNATURE.write(out); F_NUMBER_OF_DISK.write(out); F_DISK_CD_START.write(out); F_RECORDS_DISK.write(out, totalRecords); F_RECORDS_TOTAL.write(out, totalRecords); F_CD_SIZE.write(out, directorySize); F_CD_OFFSET.write(out, directoryOffset); F_COMMENT_SIZE.write(out, comment.length); out.put(comment); return out.array(); } catch (IOException e) { throw new IOExceptionWrapper(e); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/EocdGroup.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.utils.IOExceptionWrapper; import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import com.google.common.primitives.Ints; import java.io.IOException; import java.nio.ByteBuffer; import javax.annotation.Nullable; /** * The collection of all data stored in all End of Central Directory records in the zip file. The * {@code EOCDGroup} is meant to collect and manage all the information about the {@link Eocd}, * {@link Zip64EocdLocator}, and the {@link Zip64Eocd} in one place. */ public class EocdGroup { /** Minimum size the EOCD can have. */ private static final int MIN_EOCD_SIZE = 22; /** Maximum size for the EOCD. */ private static final int MAX_EOCD_COMMENT_SIZE = 65535; /** How many bytes to look back from the end of the file to look for the EOCD signature. */ private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE; /** Signature of the Zip64 EOCD locator record. */ private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50; /** Signature of the EOCD record. */ private static final long EOCD_SIGNATURE = 0x06054b50; /** * The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the one * that exists on disk is no longer valid (because the zip has been changed). * *

If the EOCD is deleted because the zip has been changed and the old EOCD was no longer * valid, then {@link #eocdComment} will contain the comment saved from the EOCD. */ @Nullable private FileUseMapEntry eocdEntry; /** * The EOCD locator entry. Will be {@code null} if there is no EOCD (because the zip is new), * the EOCD on disk is no longer valid (because the zip has been changed), or the zip file is not * in Zip64 format (There are no values in the EOCD that overflow or any files with Zip64 * extended information.) * *

If this value is {@code nonnull} then the EOCD exists and is in Zip64 format (i.e. * both {@link #eocdEntry} and {@link #eocd64Entry} will be {@code nonnull}). */ @Nullable private FileUseMapEntry eocd64Locator; /** * The Zip64 EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new), * the EOCD on disk is no longer valid (because the zip has been changed), or the zip file is not * in Zip64 format (There are no values in the EOCD that overflow or any files with Zip64 * extended information.) * *

If this value is {@code nonnull} then the EOCD exists and is in Zip64 format (i.e. * both {@link #eocdEntry} and {@link #eocd64Locator} will be {@code nonnull}). */ @Nullable private FileUseMapEntry eocd64Entry; /** * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure. This * may happen, for example, if the zip has been changed and the Central Directory and EOCD have * been deleted (in-memory). In that case, this field will save the comment to place on the EOCD * once it is created. * *

This field will only be non-{@code null} if there is no in-memory EOCD structure * (i.e., {@link #eocdEntry} is {@code null}, If there is an {@link #eocdEntry}, then the * comment will be there instead of being in this field. */ @Nullable private byte[] eocdComment; /** * This field contains the extensible data sector in the zip's Zip64 EOCD if there is no EOCD * in-memory. This may happen if the zip has been modified and the Central Directory and EOCD have * been deleted (in-memory). In that case, this field will save the data sector to place in the * Zip64 EOCD once it is created. * *

This field will only be non-{@code null} if there is no in-memory EOCD structure * (i.e., {@link #eocdEntry} is {@code null}, If there is an {@link #eocdEntry}, then the * data sector will be in the {@link #eocd64Entry} instead of being in this field. */ @Nullable private Zip64ExtensibleDataSector eocdDataSector; /** * Specifies whether the Zip64 Eocd will be in Version 2 or Version 1 format when it is * constructed. */ private boolean useVersion2Header; /** The zip file to which this EOCD record belongs. */ private final ZFile file; /** The in-memory map of the pieces of the zip-file. */ private final FileUseMap map; /** The zip file's log. */ private final VerifyLog verifyLog; /** * Constructs an empty EOCD group, which will have no in-memory EOCD structure. * * @param file The zip file to which this EOCD record belongs. * @param map he in-memory map of the zip file. */ EocdGroup(ZFile file, FileUseMap map) { eocd64Entry = null; eocd64Locator = null; eocdEntry = null; eocdComment = new byte[0]; eocdDataSector = new Zip64ExtensibleDataSector(); this.file = file; this.map = map; this.verifyLog = file.getVerifyLog(); useVersion2Header = false; } /** * Attempts to read the EOCD record into the {@link EocdGroup} from disk specified by * {@link #file}. It will populate the in-memory EOCD structure (i.e. {@link #eocdEntry}), * including the Zip64 EOCD record and locator if applicable. * * @param fileLength The length of the file on disk, used to help find the EOCD record. * @throws IOException Failed to read the EOCD. */ void readRecord(long fileLength) throws IOException { /* * Read the last part of the zip into memory. If we don't find the EOCD signature by then, * the file is corrupt. */ int lastToRead = LAST_BYTES_TO_READ; if (lastToRead > fileLength) { lastToRead = Ints.checkedCast(fileLength); } byte[] last = new byte[lastToRead]; file.directFullyRead(fileLength - lastToRead, last); /* * Start endIdx at the first possible location where the signature can be located and then * move backwards. Because the EOCD must have at least MIN_EOCD size, the first byte of the * signature (and first byte of the EOCD) must be located at last.length - MIN_EOCD_SIZE. * * Because the EOCD signature may exist in the file comment, when we find a signature we * will try to read the Eocd. If we fail, we continue searching for the signature. However, * we will keep the last exception in case we don't find any signature. */ Eocd eocd = null; int foundEocdSignatureIdx = -1; IOException errorFindingSignature = null; long eocdStart = -1; for (int endIdx = last.length - MIN_EOCD_SIZE; endIdx >= 0 && foundEocdSignatureIdx == -1; endIdx--) { ByteBuffer potentialLocator = ByteBuffer.wrap(last, endIdx, 4); if (LittleEndianUtils.readUnsigned4Le(potentialLocator) == EOCD_SIGNATURE) { /* * We found a signature. Try to read the EOCD record. */ foundEocdSignatureIdx = endIdx; ByteBuffer eocdBytes = ByteBuffer.wrap(last, foundEocdSignatureIdx, last.length - foundEocdSignatureIdx); try { eocd = new Eocd(eocdBytes); eocdStart = fileLength - lastToRead + foundEocdSignatureIdx; /* * Make sure the EOCD takes the whole file up to the end. Log an error if it * doesn't. */ if (eocdStart + eocd.getEocdSize() != fileLength) { verifyLog.log( "EOCD starts at " + eocdStart + " and has " + eocd.getEocdSize() + " bytes, but file ends at " + fileLength + "."); } } catch (IOException e) { if (errorFindingSignature != null) { e.addSuppressed(errorFindingSignature); } errorFindingSignature = e; foundEocdSignatureIdx = -1; eocd = null; } } } if (foundEocdSignatureIdx == -1) { throw new IOException( "EOCD signature not found in the last " + lastToRead + " bytes of the file.", errorFindingSignature); } Verify.verify(eocdStart >= 0); eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd); /* * Look for the Zip64 central directory locator. If we find it, then this file is a Zip64 * file and we need to read both the Zip64 EOCD locator and Zip64 EOCD */ long zip64LocatorStart = eocdStart - Zip64EocdLocator.LOCATOR_SIZE; if (zip64LocatorStart >= 0) { byte[] possibleZip64Locator = new byte[Zip64EocdLocator.LOCATOR_SIZE]; file.directFullyRead(zip64LocatorStart, possibleZip64Locator); if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator)) == ZIP64_EOCD_LOCATOR_SIGNATURE) { /* found the locator. Read it into memory. */ Zip64EocdLocator locator = new Zip64EocdLocator(ByteBuffer.wrap(possibleZip64Locator)); eocd64Locator = map.add( zip64LocatorStart, zip64LocatorStart + locator.getSize(), locator); /* Find the size of the Zip64 EOCD by reading its size field */ byte[] zip64EocdSizeHolder = new byte[8]; file.directFullyRead( locator.getZ64EocdOffset() + Zip64Eocd.SIZE_OFFSET, zip64EocdSizeHolder); long zip64EocdSize = LittleEndianUtils.readUnsigned8Le(ByteBuffer.wrap(zip64EocdSizeHolder)) + Zip64Eocd.TRUE_SIZE_DIFFERENCE; /* read the Zip64 EOCD into memory */ byte[] zip64EocdBytes = new byte[Ints.checkedCast(zip64EocdSize)]; file.directFullyRead(locator.getZ64EocdOffset(), zip64EocdBytes); Zip64Eocd zip64Eocd = new Zip64Eocd(ByteBuffer.wrap(zip64EocdBytes)); useVersion2Header = zip64Eocd.getVersionToExtract() >= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION; long zip64EocdEnd = locator.getZ64EocdOffset() + zip64EocdSize; if (zip64EocdEnd != zip64LocatorStart) { String msg = "Zip64 EOCD record is stored in [" + locator.getZ64EocdOffset() + " - " + zip64EocdEnd + "] and EOCD starts at " + zip64LocatorStart + "."; /* * If there is an empty space between the Zip64 EOCD and the EOCD locator, we proceed * logging an error. If the Zip64 EOCD ends after the start of the EOCD locator (and * therefore, they overlap), throw an exception. */ if (zip64EocdEnd > zip64LocatorStart) { throw new IOException(msg); } else { verifyLog.log(msg); } } eocd64Entry = map.add( locator.getZ64EocdOffset(), zip64EocdEnd, zip64Eocd); } } } /** * Computes the EOCD record from the given Central Directory entry in memory. This will populate * the EOCD in-memory and possibly the Zip64 EOCD and Locator if applicable. * * @param directoryEntry The entry to create the EOCD record from. * @param extraDirectoryOffset The offset between the last local entry and the Central Directory. * This will be preserved by the EOCD if the Central Directory is empty. * @throws IOException Failed to create the EOCD record. */ void computeRecord( @Nullable FileUseMapEntry directoryEntry, long extraDirectoryOffset) throws IOException { long dirStart; long dirSize; long dirNumEntries; if (directoryEntry != null) { dirStart = directoryEntry.getStart(); dirSize = directoryEntry.getSize(); dirNumEntries = directoryEntry.getStore().getEntries().size(); } else { // if we do not have a directory, then we must leave any required offset. dirStart = extraDirectoryOffset; dirSize = 0; dirNumEntries = 0; } /* * We need a Zip64 EOCD if any value overflows or if Zip64 file extensions are used as stated * in the Zip Specification. */ boolean useZip64Eocd = dirStart > Eocd.MAX_CD_OFFSET || dirSize > Eocd.MAX_CD_SIZE || dirNumEntries > Eocd.MAX_TOTAL_RECORDS || (directoryEntry != null && directoryEntry.getStore().containsZip64Files()); /* construct the Zip64 EOCD and locator first, as they come before the standard EOCD */ if (useZip64Eocd) { Verify.verify(eocdDataSector != null); Zip64Eocd zip64Eocd = new Zip64Eocd(dirNumEntries, dirStart, dirSize, useVersion2Header, eocdDataSector); eocdDataSector = null; byte[] zip64EocdBytes = zip64Eocd.toBytes(); long zip64Offset = map.size(); map.extend(zip64Offset + zip64EocdBytes.length); eocd64Entry = map.add(zip64Offset, zip64Offset + zip64EocdBytes.length, zip64Eocd); Zip64EocdLocator locator = new Zip64EocdLocator(eocd64Entry.getStart()); byte[] locatorBytes = locator.toBytes(); long locatorOffset = map.size(); map.extend(locatorOffset + locatorBytes.length); eocd64Locator = map.add(locatorOffset, locatorOffset + locatorBytes.length, locator); } /* add the EOCD to the end of the file */ Verify.verify(eocdComment != null); Eocd eocd = new Eocd( Math.min(dirNumEntries, Eocd.MAX_TOTAL_RECORDS), Math.min(dirStart, Eocd.MAX_CD_OFFSET), Math.min(dirSize, Eocd.MAX_CD_SIZE), eocdComment); eocdComment = null; byte[] eocdBytes = eocd.toBytes(); long eocdOffset = map.size(); map.extend(eocdOffset + eocdBytes.length); eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd); } /** * Writes the entire EOCD record to the end of the file. The EOCDGroup must not be empty * ({@link #isEmpty()}) by being populated by a call to * {@link #computeRecord(FileUseMapEntry, long)}, and the Central Directory must already be * written to the file. If the CentralDirectory has not written, then {@link #file} should have * no entries. * * @throws IOException Failed to write the EOCD record. */ void appendToFile() throws IOException { Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); if (eocd64Entry != null) { Zip64Eocd zip64Eocd = eocd64Entry.getStore(); Preconditions.checkNotNull(zip64Eocd); Zip64EocdLocator locator = eocd64Locator.getStore(); Preconditions.checkNotNull(locator); file.directWrite(eocd64Entry.getStart(), zip64Eocd.toBytes()); file.directWrite(eocd64Locator.getStart(), locator.toBytes()); } Eocd eocd = eocdEntry.getStore(); Preconditions.checkNotNull(eocd, "eocd == null"); byte[] eocdBytes = eocd.toBytes(); long eocdOffset = eocdEntry.getStart(); file.directWrite(eocdOffset, eocdBytes); } /** * Obtains the byte array representation of the EOCD. The EOCD must have already been computed for * this method to be invoked. * * @return The byte representation of the EOCD. * @throws IOException Failed to obtain the byte representation of the EOCD. */ byte[] getEocdBytes() throws IOException { Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); Eocd eocd = eocdEntry.getStore(); Preconditions.checkNotNull(eocd, "eocd == null"); return eocd.toBytes(); } /** * Obtains the byte array representation of the Zip64 EOCD Locator. The EOCD record must already * have been computed for this method to be invoked. * * @return The byte representation of the Zip64 EOCD Locator, or null if the EOCD record is not * in Zip64 format. * @throws IOException Failed to obtain the byte representation of the EOCD Locator. */ @VisibleForTesting @Nullable byte[] getEocdLocatorBytes() throws IOException { Preconditions.checkNotNull(eocdEntry); if (eocd64Locator == null) { return null; } return eocd64Locator.getStore().toBytes(); } /** * Obtains the byte array representation of the Zip64 EOCD. The EOCD record must already * have been computed for this method to be invoked. * * @return The byte representation of the Zip64 EOCD, or null if the EOCD record is not * in Zip64 format. * @throws IOException Failed to obtain the byte representation of the Zip64 EOCD. */ @VisibleForTesting @Nullable byte[] getZ64EocdBytes() throws IOException { Preconditions.checkNotNull(eocdEntry); if (eocd64Entry == null) { return null; } return eocd64Entry.getStore().toBytes(); } /** * Checks whether the EOCD record is presently in-memory. (i.e. the EOCD was either read * from disk and is still valid, or has been computed from the Central Directory). * * @return True iff the EOCD record is in-memory. */ boolean isEmpty() { return eocdEntry == null; } /** * Sets whether or not the EOCD record should use the Version 1 or Version 2 of the Zip64 EOCD * (iff the file needs a Zip64 record). The EOCD record should not be in-memory when trying to set * this value, and the EOCD will need to be recomputed to have any affect. * * @param useVersion2Header True if the Version 2 header is to be used, and false for the Version * 1 header. */ void setUseVersion2Header(boolean useVersion2Header) { verifyLog.verify(eocdEntry == null, "eocdEntry != null"); this.useVersion2Header = useVersion2Header; } /** * Specifies if the EOCD Group will be using a Version 2 Zip64 EOCD record or a Version 1 record * if the file needs to be in Zip64 format. * * @return True if the Version 2 record will be used, and false if the Version 1 record will be * used. */ boolean usingVersion2Header() { return useVersion2Header; } /** * Removes the EOCD record from memory. */ void deleteRecord() { if (eocdEntry != null) { map.remove(eocdEntry); Eocd eocd = eocdEntry.getStore(); Verify.verify(eocd != null); eocdComment = eocd.getComment(); eocdEntry = null; } if (eocd64Locator != null) { Verify.verify(eocd64Entry != null); eocdDataSector = eocd64Entry.getStore().getExtraFields(); map.remove(eocd64Locator); map.remove(eocd64Entry); eocd64Locator = null; eocd64Entry = null; } else { eocdDataSector = new Zip64ExtensibleDataSector(); } } /** * Sets the EOCD comment. * * @param comment The new comment; no conversion is done, these exact bytes will be placed in the * EOCD comment. * @throws IllegalArgumentException If the comment corrupts the ZipFile by having a valid EOCD * record in it. */ void setEocdComment(byte[] comment) { if (comment.length > MAX_EOCD_COMMENT_SIZE) { throw new IllegalArgumentException( "EOCD comment size (" + comment.length + ") is larger than the maximum allowed (" + MAX_EOCD_COMMENT_SIZE + ")"); } // Check if the EOCD signature appears anywhere in the comment we need to check if it // is valid. for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) { // Remember: little endian... ByteBuffer potentialSignature = ByteBuffer.wrap(comment, i, 4); try { if (LittleEndianUtils.readUnsigned4Le(potentialSignature) == EOCD_SIGNATURE) { // We found a possible EOCD signature at position i. Try to read it. ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i); try { new Eocd(bytes); // If a valid record is found in the comment then this corrupts the Zip file record // as we look for the EOCD at the back of the file (where the comment is) first. throw new IllegalArgumentException( "Position " + i + " of the comment contains a valid EOCD record."); } catch (IOException e) { // Fine, this is an invalid record. Move along... } } } catch (IOException e) { throw new IOExceptionWrapper(e); } } deleteRecord(); eocdComment = new byte[comment.length]; System.arraycopy(comment, 0, eocdComment, 0, comment.length); } /** * Returns the start of the EOCD record location in the file or -1 if the EOCD is not in memory. * * @return The start of the record. */ long getOffset() { if (eocdEntry == null) { return -1; } return getRecordStart(); } /** * Gets the comment in the EOCD. * * @return The comment exactly as it was encoded in the EOCD, no encoding is done. */ byte[] getEocdComment() { if (eocdEntry == null) { Verify.verify(eocdComment != null); byte[] eocdCommentCopy = eocdComment.clone(); return eocdCommentCopy; } Eocd eocd = eocdEntry.getStore(); Verify.verify(eocd != null); return eocd.getComment(); } /** * Gets the size of the central directory as specified from the EOCD record. The EOCD must be in * memory before this method is invoked. * * @return The directory's size. */ long getDirectorySize() { Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); Eocd eocd = eocdEntry.getStore(); if (eocd64Entry != null && eocd.getDirectorySize() == Eocd.MAX_CD_SIZE) { return eocd64Entry.getStore().getDirectorySize(); } else { return eocd.getDirectorySize(); } } /** * Gets the offset of the Central Directory from the start of the archive as specified from the * EOCD record. The EOCD must be in memory before this method is invoked. * * @return The offset of the start of the Central Directory. */ long getDirectoryOffset() { Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); Eocd eocd = eocdEntry.getStore(); if (eocd64Entry != null && eocd.getDirectoryOffset() == Eocd.MAX_CD_OFFSET) { return eocd64Entry.getStore().getDirectoryOffset(); } else { return eocd.getDirectoryOffset(); } } /** * Gets the total number of entries in the Central Directory as specified from the EOCD record. * The EOCD must be in memory before this method is invoked. * * @return The total number of records in the Central Directory. */ long getTotalDirectoryRecords() { Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); Eocd eocd = eocdEntry.getStore(); if (eocd64Entry != null && eocd.getTotalRecords() == Eocd.MAX_TOTAL_RECORDS) { return eocd64Entry.getStore().getTotalRecords(); } return eocd.getTotalRecords(); } /** * Returns the start of the EOCD record from the start of the archive. This will be the same as * the start of the standard EOCD in a Zip32 file or in a Zip64 file will be the start of the * Zip64 Eocd record. The EOCD must be in memory for this method to be invoked. * * @return The start of the entire EOCD record. */ long getRecordStart() { Verify.verify(eocdEntry != null, "eocdEntry == null"); if (eocd64Entry != null) { return eocd64Entry.getStart(); } return eocdEntry.getStart(); } /** * Returns the total size of the EOCD record. This will be the same as the standard EOCD size for * a Zip32 file or in a Zip64 file will be the start of the Zip64 record to the end of the * standard EOCD. the EOCD must be in memory for this method to be invoked. * * @return The total size of the EOCD record. */ public long getRecordSize() { if (eocd64Entry != null) { Verify.verify(eocdEntry != null); return eocdEntry.getEnd() - eocd64Entry.getStart(); } if (eocdEntry == null) { return -1; } return eocdEntry.getSize(); } /** * Returns the Zip64 Extensible Data Sector, or {@code null} if the EOCD record is not in the * Zip64 format. The EOCD must be in memory for this method to be invoked. * * @return The Extensible data sector, or {@code null} if none exists. */ @Nullable public Zip64ExtensibleDataSector getExtensibleData() { Verify.verify(eocdEntry != null); if (eocd64Entry != null) { return eocd64Entry.getStore().getExtraFields(); } return null; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ExtraField.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; /** * Contains an extra field. * *

According to the zip specification, the extra field is composed of a sequence of fields. This * class provides a way to access, parse and modify that information. * *

The zip specification calls fields to the fields inside the extra field. Because this * terminology is confusing, we use segment to refer to a part of the extra field. Each * segment is represented by an instance of {@link Segment} and contains a header ID and data. * *

Each instance of {@link ExtraField} is immutable. The extra field of a particular entry can be * changed by creating a new instanceof {@link ExtraField} and pass it to {@link * StoredEntry#setLocalExtra(ExtraField)}. * *

Instances of {@link ExtraField} can be created directly from the list of segments in it or * from the raw byte data. If created from the raw byte data, the data will only be parsed on * demand. So, if neither {@link #getSegments()} nor {@link #getSingleSegment(int)} is invoked, the * extra field will not be parsed. This guarantees low performance impact of the using the extra * field unless its contents are needed. */ public class ExtraField { public static final ExtraField EMPTY = new ExtraField(); /** Header ID for field with zip alignment. */ static final int ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = 0xd935; /** Header ID for field with linking entry. */ static final int LINKING_ENTRY_EXTRA_DATA_FIELD_HEADER_ID = 0x2333; /** * The field's raw data, if it is known. Either this variable or {@link #segments} must be * non-{@code null}. */ @Nullable private final byte[] rawData; /** * The list of field's segments. Will be populated if the extra field is created based on a list * of segments; will also be populated after parsing if the extra field is created based on the * raw bytes. */ @Nullable private ImmutableList segments; /** * Creates an extra field based on existing raw data. * * @param rawData the raw data; will not be parsed unless needed */ public ExtraField(byte[] rawData) { this.rawData = rawData; segments = null; } /** Creates a new extra field with no segments. */ public ExtraField() { rawData = null; segments = ImmutableList.of(); } /** * Creates a new extra field with the given segments. * * @param segments the segments */ public ExtraField(ImmutableList segments) { rawData = null; this.segments = segments; } /** * Obtains all segments in the extra field. * * @return all segments * @throws IOException failed to parse the extra field */ public ImmutableList getSegments() throws IOException { if (segments == null) { parseSegments(); } Preconditions.checkNotNull(segments); return segments; } /** * Obtains the only segment with the provided header ID. * * @param headerId the header ID * @return the segment found or {@code null} if no segment contains the provided header ID * @throws IOException there is more than one header with the provided header ID */ @Nullable public Segment getSingleSegment(int headerId) throws IOException { List found = new ArrayList<>(); for (Segment s : getSegments()) { if (s.getHeaderId() == headerId) { found.add(s); } } if (found.isEmpty()) { return null; } else if (found.size() == 1) { return found.get(0); } else { throw new IOException(found.size() + " segments with header ID " + headerId + "found"); } } /** * Parses the raw data and generates all segments in {@link #segments}. * * @throws IOException failed to parse the data */ private void parseSegments() throws IOException { Preconditions.checkNotNull(rawData); Preconditions.checkState(segments == null); List segments = new ArrayList<>(); ByteBuffer buffer = ByteBuffer.wrap(rawData); while (buffer.remaining() > 0) { int headerId = LittleEndianUtils.readUnsigned2Le(buffer); int dataSize = LittleEndianUtils.readUnsigned2Le(buffer); if (dataSize < 0) { throw new IOException( "Invalid data size for extra field segment with header ID " + headerId + ": " + dataSize); } byte[] data = new byte[dataSize]; if (buffer.remaining() < dataSize) { throw new IOException( "Invalid data size for extra field segment with header ID " + headerId + ": " + dataSize + " (only " + buffer.remaining() + " bytes are available)"); } buffer.get(data); SegmentFactory factory = identifySegmentFactory(headerId); Segment seg = factory.make(headerId, data); segments.add(seg); } this.segments = ImmutableList.copyOf(segments); } /** * Obtains the size of the extra field. * * @return the size */ public int size() { if (rawData != null) { return rawData.length; } else { Preconditions.checkNotNull(segments); int sz = 0; for (Segment s : segments) { sz += s.size(); } return sz; } } /** * Writes the extra field to the given output buffer. * * @param out the output buffer to write the field; exactly {@link #size()} bytes will be written * @throws IOException failed to write the extra fields */ public void write(ByteBuffer out) throws IOException { if (rawData != null) { out.put(rawData); } else { Preconditions.checkNotNull(segments); for (Segment s : segments) { s.write(out); } } } /** * Identifies the factory to create the segment with the provided header ID. * * @param headerId the header ID * @return the segmnet factory that creates segments with the given header */ private static SegmentFactory identifySegmentFactory(int headerId) { if (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) { return AlignmentSegment::new; } return RawDataSegment::new; } /** * Field inside the extra field. A segment contains a header ID and data. Specific types of * segments implement this interface. */ public interface Segment { /** * Obtains the segment's header ID. * * @return the segment's header ID */ int getHeaderId(); /** * Obtains the size of the segment including the header ID. * * @return the number of bytes needed to write the segment */ int size(); /** * Writes the segment to a buffer. * * @param out the buffer where to write the segment to; exactly {@link #size()} bytes will be * written * @throws IOException failed to write segment data */ void write(ByteBuffer out) throws IOException; } /** Factory that creates a segment. */ interface SegmentFactory { /** * Creates a new segment. * * @param headerId the header ID * @param data the segment's data * @return the created segment * @throws IOException failed to create the segment from the data */ Segment make(int headerId, byte[] data) throws IOException; } /** * Segment of raw data: this class represents a general segment containing an array of bytes as * data. */ public static class RawDataSegment implements Segment { /** Header ID. */ private final int headerId; /** Data in the segment. */ private final byte[] data; /** * Creates a new raw data segment. * * @param headerId the header ID * @param data the segment data */ RawDataSegment(int headerId, byte[] data) { this.headerId = headerId; this.data = data; } @Override public int getHeaderId() { return headerId; } @Override public void write(ByteBuffer out) throws IOException { LittleEndianUtils.writeUnsigned2Le(out, headerId); LittleEndianUtils.writeUnsigned2Le(out, data.length); out.put(data); } @Override public int size() { return 4 + data.length; } } /** * Segment with information on an alignment: this segment contains information on how an entry * should be aligned and contains zero-filled data to force alignment. * *

An alignment segment contains the header ID, the size of the data, the alignment value and * zero bytes to pad */ public static class AlignmentSegment implements Segment { /** Minimum size for an alignment segment. */ public static final int MINIMUM_SIZE = 6; /** The alignment value. */ private int alignment; /** How many bytes of padding are in this segment? */ private int padding; /** * Creates a new alignment segment. * * @param alignment the alignment value * @param totalSize how many bytes should this segment take? */ public AlignmentSegment(int alignment, int totalSize) { Preconditions.checkArgument(alignment > 0, "alignment <= 0"); Preconditions.checkArgument(totalSize >= MINIMUM_SIZE, "totalSize < MINIMUM_SIZE"); /* * We have 6 bytes of fixed data: header ID (2 bytes), data size (2 bytes), alignment * value (2 bytes). */ this.alignment = alignment; padding = totalSize - MINIMUM_SIZE; } /** * Creates a new alignment segment from extra data. * * @param headerId the header ID * @param data the segment data * @throws IOException failed to create the segment from the data */ public AlignmentSegment(int headerId, byte[] data) throws IOException { Preconditions.checkArgument(headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); ByteBuffer dataBuffer = ByteBuffer.wrap(data); alignment = LittleEndianUtils.readUnsigned2Le(dataBuffer); if (alignment <= 0) { throw new IOException("Invalid alignment in alignment field: " + alignment); } padding = data.length - 2; } @Override public void write(ByteBuffer out) throws IOException { LittleEndianUtils.writeUnsigned2Le(out, ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); LittleEndianUtils.writeUnsigned2Le(out, padding + 2); LittleEndianUtils.writeUnsigned2Le(out, alignment); out.put(new byte[padding]); } @Override public int size() { return padding + 6; } @Override public int getHeaderId() { return ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID; } } public static class LinkingEntrySegment implements Segment { private final StoredEntry linkingEntry; private int dataOffset = -1; private long zipOffset = -1; public LinkingEntrySegment(StoredEntry linkingEntry) throws IOException { Preconditions.checkArgument(linkingEntry.isLinkingEntry(), "linkingEntry is not a linking entry"); this.linkingEntry = linkingEntry; } @Override public int getHeaderId() { return LINKING_ENTRY_EXTRA_DATA_FIELD_HEADER_ID; } @Override public int size() { return linkingEntry.isDummyEntry() ? 0 : linkingEntry.getLocalHeaderSize() + 4; } public void setOffset(int dataOffset, long zipOffset) { this.dataOffset = dataOffset; this.zipOffset = zipOffset; } @Override public void write(ByteBuffer out) throws IOException { if (dataOffset < 0 || zipOffset < 0) { throw new IOException("linking entry has wrong offset"); } if (!linkingEntry.isDummyEntry()) { LittleEndianUtils.writeUnsigned2Le(out, LINKING_ENTRY_EXTRA_DATA_FIELD_HEADER_ID); LittleEndianUtils.writeUnsigned2Le(out, linkingEntry.getLocalHeaderSize()); var offset = out.position(); linkingEntry.writeData(out, dataOffset - linkingEntry.getLocalHeaderSize() - offset); linkingEntry.replaceSourceFromZip(offset + zipOffset); } else { linkingEntry.replaceSourceFromZip(zipOffset + dataOffset + linkingEntry.getNestedOffset()); } } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMap.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.annotation.Nullable; /** * The file use map keeps track of which parts of the zip file are used which parts are not. It * essentially maintains an ordered set of entries ({@link FileUseMapEntry}). Each entry either has * some data (an entry, the Central Directory, the EOCD) or is a free entry. * *

For example: [0-95, "foo/"][95-260, "xpto"][260-310, free][310-360, Central Directory] * [360-390,EOCD] * *

There are a few invariants in this structure: * *

    *
  • there are no gaps between map entries; *
  • the map is fully covered up to its size; *
  • there are no two free entries next to each other; this is guaranteed by coalescing the * entries upon removal (see {@link #coalesce(FileUseMapEntry)}); *
  • all free entries have a minimum size defined in the constructor, with the possible * exception of the last one *
*/ class FileUseMap { /** * Size of the file according to the map. This should always match the last entry in {@code #map}. */ private long size; /** * Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #size}. If * {@link #size} is zero then this set is empty. This is the only situation in which the map will * be empty. */ private final TreeSet> map; /** * Tree with all free blocks ordered by size. This is essentially a view over {@link #map} * containing only the free blocks, but in a different order. */ private final TreeSet> freeBySize; private final TreeSet> freeByStart; /** If defined, defines the minimum size for a free entry. */ private int mMinFreeSize; /** * Creates a new, empty file map. * * @param size the size of the file * @param minFreeSize minimum size of a free entry */ FileUseMap(long size, int minFreeSize) { Preconditions.checkArgument(size >= 0, "size < 0"); Preconditions.checkArgument(minFreeSize >= 0, "minFreeSize < 0"); this.size = size; map = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START); freeBySize = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE); freeByStart = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START); mMinFreeSize = minFreeSize; if (size > 0) { internalAdd(FileUseMapEntry.makeFree(0, size)); } } /** * Adds an entry to the internal structures. * * @param entry the entry to add */ private void internalAdd(FileUseMapEntry entry) { map.add(entry); if (entry.isFree()) { freeBySize.add(entry); freeByStart.add(entry); } } /** * Removes an entry from the internal structures. * * @param entry the entry to remove */ private void internalRemove(FileUseMapEntry entry) { boolean wasRemoved = map.remove(entry); Preconditions.checkState(wasRemoved, "entry not in map"); if (entry.isFree()) { freeBySize.remove(entry); freeByStart.remove(entry); } } /** * Adds a new file to the map. The interval specified by {@code entry} must fit inside an empty * entry in the map. That entry will be replaced by entry and additional free entries will be * added before and after if needed to make sure no spaces exist on the map. * * @param entry the entry to add */ private void add(FileUseMapEntry entry) { Preconditions.checkArgument(entry.getStart() < size, "entry.getStart() >= size"); Preconditions.checkArgument(entry.getEnd() <= size, "entry.getEnd() > size"); Preconditions.checkArgument(!entry.isFree(), "entry.isFree()"); FileUseMapEntry container = findContainer(entry); Verify.verify(container.isFree(), "!container.isFree()"); Set> replacements = split(container, entry); internalRemove(container); for (FileUseMapEntry r : replacements) { internalAdd(r); } } /** * Adds a new file to the map. The interval specified by ({@code start}, {@code end}) must fit * inside an empty entry in the map. That entry will be replaced by entry and additional free * entries will be added before and after if needed to make sure no spaces exist on the map. * *

The entry cannot extend beyong the end of the map. If necessary, extend the map using {@link * #extend(long)}. * * @param start the start of this entry * @param end the end of the entry * @param store extra data to store with the entry * @param the type of data to store in the entry * @return the new entry */ FileUseMapEntry add(long start, long end, T store) { Preconditions.checkArgument(start >= 0, "start < 0"); Preconditions.checkArgument(end > start, "end < start"); FileUseMapEntry entry = FileUseMapEntry.makeUsed(start, end, store); add(entry); return entry; } /** * Removes a file from the map, replacing it with an empty one that is then coalesced with * neighbors (if the neighbors are free). * * @param entry the entry */ void remove(FileUseMapEntry entry) { Preconditions.checkState(map.contains(entry), "!map.contains(entry)"); Preconditions.checkArgument(!entry.isFree(), "entry.isFree()"); internalRemove(entry); FileUseMapEntry replacement = FileUseMapEntry.makeFree(entry.getStart(), entry.getEnd()); internalAdd(replacement); coalesce(replacement); } /** * Finds the entry that fully contains the given one. It is assumed that one exists. * * @param entry the entry whose container we're looking for * @return the container */ private FileUseMapEntry findContainer(FileUseMapEntry entry) { FileUseMapEntry container = map.floor(entry); Verify.verifyNotNull(container); Verify.verify(container.getStart() <= entry.getStart()); Verify.verify(container.getEnd() >= entry.getEnd()); return container; } /** * Splits a container to add an entry, adding new free entries before and after the provided entry * if needed. * * @param container the container entry, a free entry that is in {@link #map} that that encloses * {@code entry} * @param entry the entry that will be used to split {@code container} * @return a set of non-overlapping entries that completely covers {@code container} and that * includes {@code entry} */ private static Set> split( FileUseMapEntry container, FileUseMapEntry entry) { Preconditions.checkArgument(container.isFree(), "!container.isFree()"); long farStart = container.getStart(); long start = entry.getStart(); long end = entry.getEnd(); long farEnd = container.getEnd(); Verify.verify(farStart <= start, "farStart > start"); Verify.verify(start < end, "start >= end"); Verify.verify(farEnd >= end, "farEnd < end"); Set> result = Sets.newHashSet(); if (farStart < start) { result.add(FileUseMapEntry.makeFree(farStart, start)); } result.add(entry); if (end < farEnd) { result.add(FileUseMapEntry.makeFree(end, farEnd)); } return result; } /** * Coalesces a free entry replacing it and neighboring free entries with a single, larger entry. * This method does nothing if {@code entry} does not have free neighbors. * * @param entry the free entry to coalesce with neighbors */ private void coalesce(FileUseMapEntry entry) { Preconditions.checkArgument(entry.isFree(), "!entry.isFree()"); FileUseMapEntry prevToMerge = null; long start = entry.getStart(); if (start > 0) { /* * See if we have a previous entry to merge with this one. */ prevToMerge = map.floor(FileUseMapEntry.makeFree(start - 1, start)); Verify.verifyNotNull(prevToMerge); if (!prevToMerge.isFree()) { prevToMerge = null; } } FileUseMapEntry nextToMerge = null; long end = entry.getEnd(); if (end < size) { /* * See if we have a next entry to merge with this one. */ nextToMerge = map.ceiling(FileUseMapEntry.makeFree(end, end + 1)); Verify.verifyNotNull(nextToMerge); if (!nextToMerge.isFree()) { nextToMerge = null; } } if (prevToMerge == null && nextToMerge == null) { return; } long newStart = start; if (prevToMerge != null) { newStart = prevToMerge.getStart(); internalRemove(prevToMerge); } long newEnd = end; if (nextToMerge != null) { newEnd = nextToMerge.getEnd(); internalRemove(nextToMerge); } internalRemove(entry); internalAdd(FileUseMapEntry.makeFree(newStart, newEnd)); } /** Truncates map removing the top entry if it is free and reducing the map's size. */ void truncate() { if (size == 0) { return; } /* * Find the last entry. */ FileUseMapEntry last = map.last(); Verify.verifyNotNull(last, "last == null"); if (last.isFree()) { internalRemove(last); size = last.getStart(); } } /** * Obtains the size of the map. * * @return the size */ long size() { return size; } /** * Obtains the largest used offset in the map. This will be size of the map after truncation. * * @return the size of the file discounting the last block if it is empty */ long usedSize() { if (size == 0) { return 0; } /* * Find the last entry to see if it is an empty entry. If it is, we need to remove its size * from the returned value. */ FileUseMapEntry last = map.last(); Verify.verifyNotNull(last, "last == null"); if (last.isFree()) { return last.getStart(); } else { Verify.verify(last.getEnd() == size); return size; } } /** * Extends the map to guarantee it has at least {@code size} bytes. If the current size is as * large as {@code size}, this method does nothing. * * @param size the new size of the map that cannot be smaller that the current size */ void extend(long size) { Preconditions.checkArgument(size >= this.size, "size < size"); if (this.size == size) { return; } FileUseMapEntry newBlock = FileUseMapEntry.makeFree(this.size, size); internalAdd(newBlock); this.size = size; coalesce(newBlock); } /** * Locates a free area in the map with at least {@code size} bytes such that {@code ((start + * alignOffset) % align == 0} and such that the free space before {@code start} is not smaller * than the minimum free entry size. This method will follow the algorithm specified by {@code * alg}. * *

If no free contiguous block exists in the map that can hold the provided size then the first * free index at the end of the map is provided. This means that the map may need to be extended * before data can be added. * * @param size the size of the contiguous area requested * @param alignOffset an offset to which alignment needs to be computed (see method description) * @param align alignment at the offset (see method description) * @param alg which algorithm to use * @return the location of the contiguous area; this may be located at the end of the map */ long locateFree(long size, long alignOffset, long align, PositionAlgorithm alg) { Preconditions.checkArgument(size > 0, "size <= 0"); FileUseMapEntry minimumSizedEntry = FileUseMapEntry.makeFree(0, size); SortedSet> matches; switch (alg) { case BEST_FIT: matches = freeBySize.tailSet(minimumSizedEntry); break; case FIRST_FIT: matches = freeByStart; break; default: throw new AssertionError(); } FileUseMapEntry best = null; long bestExtraSize = 0; for (FileUseMapEntry curr : matches) { /* * We don't care about blocks that aren't free. */ if (!curr.isFree()) { continue; } /* * Compute any extra size we need in this block to make sure we verify the alignment. * There must be a better to do this... */ long extraSize; if (align == 0) { extraSize = 0; } else { extraSize = (align - ((curr.getStart() + alignOffset) % align)) % align; } /* * We can't leave than mMinFreeSize before. So if the extraSize is less than * mMinFreeSize, we have to increase it by 'align' as many times as needed. For * example, if mMinFreeSize is 20, align 4 and extraSize is 5. We need to increase it * to 21 (5 + 4 * 4) */ if (extraSize > 0 && extraSize < mMinFreeSize) { int addAlignBlocks = Ints.checkedCast((mMinFreeSize - extraSize + align - 1) / align); extraSize += addAlignBlocks * align; } /* * We don't care about blocks where we don't fit in. */ if (curr.getSize() < (size + extraSize)) { continue; } /* * We don't care about blocks that leave less than the minimum size after. There are * two exceptions: (1) this is the last block and (2) the next block is free in which * case, after coalescing, the free block with have at least the minimum size. */ long emptySpaceLeft = curr.getSize() - (size + extraSize); if (emptySpaceLeft > 0 && emptySpaceLeft < mMinFreeSize) { FileUseMapEntry next = map.higher(curr); if (next != null && !next.isFree()) { continue; } } /* * We don't care about blocks that are bigger than the best so far (otherwise this * wouldn't be a best-fit algorithm). */ if (best != null && best.getSize() < curr.getSize()) { continue; } best = curr; bestExtraSize = extraSize; /* * If we're doing first fit, we don't want to search for a better one :) */ if (alg == PositionAlgorithm.FIRST_FIT) { break; } } /* * If no entry that could hold size is found, get the first free byte. */ long firstFree = this.size; if (best == null && !map.isEmpty()) { FileUseMapEntry last = map.last(); if (last.isFree()) { firstFree = last.getStart(); } } /* * We're done: either we found something or we didn't, in which the new entry needs to * be added to the end of the map. */ if (best == null) { long extra = (align - ((firstFree + alignOffset) % align)) % align; /* * If adding this entry at the end would create a space smaller than the minimum, * push it for 'align' bytes forward. */ if (extra > 0) { if (extra < mMinFreeSize) { extra += align * (((mMinFreeSize - extra) + (align - 1)) / align); } } return firstFree + extra; } else { return best.getStart() + bestExtraSize; } } /** * Obtains all free areas of the map, excluding any trailing free area. * * @return all free areas, an empty set if there are no free areas; the areas are returned in file * order, that is, if area {@code x} starts before area {@code y}, then area {@code x} will be * stored before area {@code y} in the list */ List> getFreeAreas() { List> freeAreas = Lists.newArrayList(); for (FileUseMapEntry area : map) { if (area.isFree() && area.getEnd() != size) { freeAreas.add(area); } } return freeAreas; } /** * Obtains the entry that is located before the one provided. * * @param entry the map entry to get the previous one for; must belong to the map * @return the entry before the provided one, {@code null} if {@code entry} is the first entry in * the map */ @Nullable FileUseMapEntry before(FileUseMapEntry entry) { Preconditions.checkNotNull(entry, "entry == null"); return map.lower(entry); } /** * Obtains the entry that is located after the one provided. * * @param entry the map entry to get the next one for; must belong to the map * @return the entry after the provided one, {@code null} if {@code entry} is the last entry in * the map */ @Nullable FileUseMapEntry after(FileUseMapEntry entry) { Preconditions.checkNotNull(entry, "entry == null"); return map.higher(entry); } /** * Obtains the entry at the given offset. * * @param offset the offset to look for * @return the entry found or {@code null} if there is no entry (not even a free one) at the given * offset */ @Nullable FileUseMapEntry at(long offset) { Preconditions.checkArgument(offset >= 0, "offset < 0"); Preconditions.checkArgument(offset < size, "offset >= size"); FileUseMapEntry entry = map.floor(FileUseMapEntry.makeFree(offset, offset + 1)); if (entry == null) { return null; } Verify.verify(entry.getStart() <= offset); Verify.verify(entry.getEnd() > offset); return entry; } @Override public String toString() { StringBuilder builder = new StringBuilder(); boolean first = true; for (FileUseMapEntry entry : map) { if (first) { first = false; } else { builder.append(", "); } builder.append(entry.getStart()); builder.append(" - "); builder.append(entry.getEnd()); builder.append(": "); builder.append(entry.getStore()); } return builder.toString(); } /** Algorithms used to position entries in blocks. */ public enum PositionAlgorithm { /** Best fit: finds the smallest free block that can receive the entry. */ BEST_FIT, /** First fit: finds the first free block that can receive the entry. */ FIRST_FIT } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMapEntry.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.primitives.Ints; import java.util.Comparator; import javax.annotation.Nullable; /** * Represents an entry in the {@link FileUseMap}. Each entry contains an interval of bytes. The end * of the interval is exclusive. * *

Entries can either be free or used. Used entries must store an object. Free entries * do not store anything. * *

File map entries are used to keep track of which parts of a file map are used and not. * * @param the type of data stored */ class FileUseMapEntry { /** Comparator that compares entries by their start date. */ public static final Comparator> COMPARE_BY_START = (o1, o2) -> Ints.saturatedCast(o1.getStart() - o2.getStart()); /** Comparator that compares entries by their size. */ public static final Comparator> COMPARE_BY_SIZE = (o1, o2) -> Ints.saturatedCast(o1.getSize() - o2.getSize()); /** The first byte in the entry. */ private final long start; /** The first byte no longer in the entry. */ private final long end; /** The stored data. If {@code null} then this entry represents a free entry. */ @Nullable private final T store; /** * Creates a new map entry. * * @param start the start of the entry * @param end the end of the entry (first byte no longer in the entry) * @param store the data to store in the entry or {@code null} if this is a free entry */ private FileUseMapEntry(long start, long end, @Nullable T store) { Preconditions.checkArgument(start >= 0, "start < 0"); Preconditions.checkArgument(end > start, "end <= start"); this.start = start; this.end = end; this.store = store; } /** * Creates a new free entry. * * @param start the start of the entry * @param end the end of the entry (first byte no longer in the entry) * @return the entry */ public static FileUseMapEntry makeFree(long start, long end) { return new FileUseMapEntry<>(start, end, null); } /** * Creates a new used entry. * * @param start the start of the entry * @param end the end of the entry (first byte no longer in the entry) * @param store the data to store in the entry * @param the type of data to store in the entry * @return the entry */ public static FileUseMapEntry makeUsed(long start, long end, T store) { Preconditions.checkNotNull(store, "store == null"); return new FileUseMapEntry<>(start, end, store); } /** * Obtains the first byte in the entry. * * @return the first byte in the entry (if the same value as {@link #getEnd()} then the entry is * empty and contains no data) */ long getStart() { return start; } /** * Obtains the first byte no longer in the entry. * * @return the first byte no longer in the entry */ long getEnd() { return end; } /** * Obtains the size of the entry. * * @return the number of bytes contained in the entry */ long getSize() { return end - start; } /** * Determines if this is a free entry. * * @return is this entry free? */ boolean isFree() { return store == null; } /** * Obtains the data stored in the entry. * * @return the data stored or {@code null} if this entry is a free entry */ @Nullable T getStore() { return store; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("start", start) .add("end", end) .add("store", store) .toString(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/GPFlags.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import java.io.IOException; /** * General purpose bit flags. Contains the encoding of the zip's general purpose bits. * *

We don't really care about the method bit(s). These are bits 1 and 2. Here are the values: * *

    *
  • 0 (00): Normal (-en) compression option was used. *
  • 1 (01): Maximum (-exx/-ex) compression option was used. *
  • 2 (10): Fast (-ef) compression option was used. *
  • 3 (11): Super Fast (-es) compression option was used. *
*/ class GPFlags { /** Is the entry encrypted? */ private static final int BIT_ENCRYPTION = 1; /** Has CRC computation been deferred and, therefore, does a data description block exist? */ private static final int BIT_DEFERRED_CRC = (1 << 3); /** Is enhanced deflating used? */ private static final int BIT_ENHANCED_DEFLATING = (1 << 4); /** Does the entry contain patched data? */ private static final int BIT_PATCHED_DATA = (1 << 5); /** Is strong encryption used? */ private static final int BIT_STRONG_ENCRYPTION = (1 << 6) | (1 << 13); /** * If this bit is set the filename and comment fields for this file must be encoded using UTF-8. */ private static final int BIT_EFS = (1 << 11); /** Unused bits. */ private static final int BIT_UNUSED = (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 14) | (1 << 15); /** Bit flag value. */ private final long value; /** Has the CRC computation beeen deferred? */ private boolean deferredCrc; /** Is the file name encoded in UTF-8? */ private boolean utf8FileName; /** * Creates a new flags object. * * @param value the value of the bit mask */ private GPFlags(long value) { this.value = value; deferredCrc = ((value & BIT_DEFERRED_CRC) != 0); utf8FileName = ((value & BIT_EFS) != 0); } /** * Obtains the flags value. * * @return the value of the bit mask */ public long getValue() { return value; } /** * Is the CRC computation deferred? * * @return is the CRC computation deferred? */ public boolean isDeferredCrc() { return deferredCrc; } /** * Is the file name encoded in UTF-8? * * @return is the file name encoded in UTF-8? */ public boolean isUtf8FileName() { return utf8FileName; } /** * Creates a new bit mask. * * @param utf8Encoding should UTF-8 encoding be used? * @return the new bit mask */ static GPFlags make(boolean utf8Encoding) { long flags = 0; if (utf8Encoding) { flags |= BIT_EFS; } return new GPFlags(flags); } /** * Creates the flag information from a byte. This method will also validate that only supported * options are defined in the flag. * * @param bits the bit mask * @return the created flag information * @throws IOException unsupported options are used in the bit mask */ static GPFlags from(long bits) throws IOException { if ((bits & BIT_ENCRYPTION) != 0) { throw new IOException("Zip files with encrypted of entries not supported."); } if ((bits & BIT_ENHANCED_DEFLATING) != 0) { throw new IOException("Enhanced deflating not supported."); } if ((bits & BIT_PATCHED_DATA) != 0) { throw new IOException("Compressed patched data not supported."); } if ((bits & BIT_STRONG_ENCRYPTION) != 0) { throw new IOException("Strong encryption not supported."); } if ((bits & BIT_UNUSED) != 0) { throw new IOException( "Unused bits set in directory entry. Weird. I don't know what's " + "going on."); } if ((bits & 0xffffffff00000000L) != 0) { throw new IOException("Unsupported bits after 32."); } return new GPFlags(bits); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/InflaterByteSource.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.SequenceInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; /** * Byte source that inflates another byte source. It assumed the inner byte source has deflated * data. */ public class InflaterByteSource extends CloseableByteSource { /** The stream factory for the deflated data. */ private final CloseableByteSource deflatedSource; /** * Creates a new source. * * @param byteSource the factory for deflated data */ public InflaterByteSource(CloseableByteSource byteSource) { deflatedSource = byteSource; } @Override public InputStream openStream() throws IOException { /* * The extra byte is a dummy byte required by the inflater. Weirdo. * (see the java.util.Inflater documentation). Looks like a hack... * "Oh, I need an extra dummy byte to allow for some... err... optimizations..." */ ByteArrayInputStream hackByte = new ByteArrayInputStream(new byte[] {0}); return new InflaterInputStream( new SequenceInputStream(deflatedSource.openStream(), hackByte), new Inflater(true)); } @Override public void innerClose() throws IOException { deflatedSource.close(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/LazyDelegateByteSource.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.io.ByteProcessor; import com.google.common.io.ByteSink; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; import com.google.common.util.concurrent.ListenableFuture; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.concurrent.ExecutionException; /** * {@code ByteSource} that delegates all operations to another {@code ByteSource}. The other byte * source, the delegate, may be computed lazily. */ public class LazyDelegateByteSource extends CloseableByteSource { /** Byte source where we delegate operations to. */ private final ListenableFuture delegate; /** * Creates a new byte source that delegates operations to the provided source. * * @param delegate the source that will receive all operations */ public LazyDelegateByteSource(ListenableFuture delegate) { this.delegate = delegate; } /** * Obtains the delegate future. * * @return the delegate future, that may be computed or not */ public ListenableFuture getDelegate() { return delegate; } /** * Obtains the byte source, waiting for the future to be computed. * * @return the byte source * @throws IOException failed to compute the future :) */ private CloseableByteSource get() throws IOException { try { CloseableByteSource r = delegate.get(); if (r == null) { throw new IOException("Delegate byte source computation resulted in null."); } return r; } catch (InterruptedException e) { throw new IOException("Interrupted while waiting for byte source computation.", e); } catch (ExecutionException e) { throw new IOException("Failed to compute byte source.", e); } } @Override public CharSource asCharSource(Charset charset) { try { return get().asCharSource(charset); } catch (IOException e) { throw new RuntimeException(e); } } @Override public InputStream openBufferedStream() throws IOException { return get().openBufferedStream(); } @Override public ByteSource slice(long offset, long length) { try { return get().slice(offset, length); } catch (IOException e) { throw new RuntimeException(e); } } @Override public boolean isEmpty() throws IOException { return get().isEmpty(); } @Override public long size() throws IOException { return get().size(); } @Override public long copyTo(OutputStream output) throws IOException { return get().copyTo(output); } @Override public long copyTo(ByteSink sink) throws IOException { return get().copyTo(sink); } @Override public byte[] read() throws IOException { return get().read(); } @Override public T read(ByteProcessor processor) throws IOException { return get().read(processor); } @Override public HashCode hash(HashFunction hashFunction) throws IOException { return get().hash(hashFunction); } @Override public boolean contentEquals(ByteSource other) throws IOException { return get().contentEquals(other); } @Override public InputStream openStream() throws IOException { return get().openStream(); } @Override public void innerClose() throws IOException { get().close(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/NestedZip.java ================================================ package com.android.tools.build.apkzlib.zip; import com.google.common.collect.Maps; import java.io.File; import java.io.IOException; import java.util.Map; public class NestedZip extends ZFile { final ZFile target; final StoredEntry entry; public interface NameCallback { String getName(ZFile file) throws IOException; } public NestedZip(NameCallback name, ZFile target, File src, boolean mayCompress) throws IOException { super(src, new ZFileOptions(), true); this.target = target; this.entry = target.add(name.getName(this), directOpen(0, directSize()), mayCompress); } /** * @return true if lfh is consistent with cdh otherwise inconsistent */ public boolean addFileLink(StoredEntry srcEntry, String dstName) throws IOException { if (srcEntry == null) throw new IOException("Entry " + srcEntry + " does not exist in nested zip"); var srcName = srcEntry.getCentralDirectoryHeader().getName(); var offset = srcEntry.getCentralDirectoryHeader().getOffset() + srcEntry.getLocalHeaderSize(); if (srcName.equals(dstName)) { target.addNestedLink(entry, dstName, srcEntry, srcEntry.getCentralDirectoryHeader().getOffset(), true); return true; } else if (offset < MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE) { target.addNestedLink(entry, dstName, srcEntry, offset, false); return true; } return false; } public boolean addFileLink(String srcName, String dstName) throws IOException { var srcEntry = get(srcName); return addFileLink(srcEntry, dstName); } public ZFile getTarget() { return target; } public StoredEntry getEntry() { return entry; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ProcessedAndRawByteSources.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.io.Closer; import java.io.Closeable; import java.io.IOException; /** * Container that has two bytes sources: one representing raw data and another processed data. In * case of compression, the raw data is the compressed data and the processed data is the * uncompressed data. It is valid for a RaP ("Raw-and-Processed") to contain the same byte sources * for both processed and raw data. */ public class ProcessedAndRawByteSources implements Closeable { /** The processed byte source. */ private final CloseableByteSource processedSource; /** The processed raw source. */ private final CloseableByteSource rawSource; /** * Creates a new container. * * @param processedSource the processed source * @param rawSource the raw source */ public ProcessedAndRawByteSources( CloseableByteSource processedSource, CloseableByteSource rawSource) { this.processedSource = processedSource; this.rawSource = rawSource; } /** * Obtains a byte source that read the processed contents of the entry. * * @return a byte source */ public CloseableByteSource getProcessedByteSource() { return processedSource; } /** * Obtains a byte source that reads the raw contents of an entry. This is the data that is * ultimately stored in the file and, in the case of compressed files, is the same data in the * source returned by {@link #getProcessedByteSource()}. * * @return a byte source */ public CloseableByteSource getRawByteSource() { return rawSource; } @Override public void close() throws IOException { Closer closer = Closer.create(); closer.register(processedSource); closer.register(rawSource); closer.close(); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntry.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.bytestorage.ByteStorage; import com.android.tools.build.apkzlib.bytestorage.CloseableByteSourceFromOutputStreamBuilder; import com.android.tools.build.apkzlib.utils.IOExceptionWrapper; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.base.Verify; import com.google.common.io.ByteStreams; import com.google.common.primitives.Ints; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Comparator; import javax.annotation.Nullable; /** * A stored entry represents a file in the zip. The entry may or may not be written to the zip file. * *

Stored entries provide the operations that are related to the files themselves, not to the * zip. It is through the {@code StoredEntry} class that entries can be deleted ({@link #delete()}, * open ({@link #open()}) or realigned ({@link #realign()}). * *

Entries are not created directly. They are created using {@link ZFile#add(String, InputStream, * boolean)} and obtained from the zip file using {@link ZFile#get(String)} or {@link * ZFile#entries()}. * *

Most of the data in the an entry is in the Central Directory Header. This includes the name, * compression method, file compressed and uncompressed sizes, CRC32 checksum, etc. The CDH can be * obtained using the {@link #getCentralDirectoryHeader()} method. */ public class StoredEntry { /** Comparator that compares instances of {@link StoredEntry} by their names. */ static final Comparator COMPARE_BY_NAME = (o1, o2) -> { if (o1 == null && o2 == null) { return 0; } if (o1 == null) { return -1; } if (o2 == null) { return 1; } String name1 = o1.getCentralDirectoryHeader().getName(); String name2 = o2.getCentralDirectoryHeader().getName(); return name1.compareTo(name2); }; /** Signature of the data descriptor. */ private static final int DATA_DESC_SIGNATURE = 0x08074b50; /** Local header field: signature. */ private static final ZipField.F4 F_LOCAL_SIGNATURE = new ZipField.F4(0, 0x04034b50, "Signature"); /** Local header field: version to extract, should match the CDH's. */ @VisibleForTesting static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2( F_LOCAL_SIGNATURE.endOffset(), "Version to extract", new ZipFieldInvariantNonNegative()); /** Local header field: GP bit flag, should match the CDH's. */ private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(), "GP bit flag"); /** Local header field: compression method, should match the CDH's. */ private static final ZipField.F2 F_METHOD = new ZipField.F2( F_GP_BIT.endOffset(), "Compression method", new ZipFieldInvariantNonNegative()); /** Local header field: last modification time, should match the CDH's. */ private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(), "Last modification time"); /** Local header field: last modification time, should match the CDH's. */ private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(), "Last modification date"); /** Local header field: CRC32 checksum, should match the CDH's. 0 if there is no data. */ private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), "CRC32"); /** Local header field: compressed size, size the data takes in the zip file. */ private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(), "Compressed size", new ZipFieldInvariantNonNegative()); /** Local header field: uncompressed size, size the data takes after extraction. */ private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4( F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative()); /** Local header field: length of the file name. */ private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2( F_UNCOMPRESSED_SIZE.endOffset(), "@File name length", new ZipFieldInvariantNonNegative()); /** Local header filed: length of the extra field. */ private static final ZipField.F2 F_EXTRA_LENGTH = new ZipField.F2( F_FILE_NAME_LENGTH.endOffset(), "Extra length", new ZipFieldInvariantNonNegative()); /** Local header size (fixed part, not counting file name or extra field). */ static final int FIXED_LOCAL_FILE_HEADER_SIZE = F_EXTRA_LENGTH.endOffset(); /** Type of entry. */ private final StoredEntryType type; /** The central directory header with information about the file. */ private final CentralDirectoryHeader cdh; /** The file this entry is associated with */ private final ZFile file; /** Has this entry been deleted? */ private boolean deleted; /** Extra field specified in the local directory. */ private ExtraField localExtra; /** Type of data descriptor associated with the entry. */ private Supplier dataDescriptorType; /** * Source for this entry's data. If this entry is a directory, this source has to have zero size. */ private ProcessedAndRawByteSources source; /** Verify log for the entry. */ private final VerifyLog verifyLog; /** Storage used to create buffers when loading storage into memory. */ private final ByteStorage storage; /** Entry it is linking to. */ private final StoredEntry linkedEntry; /** Offset of the nested link. */ private final long nestedOffset; /** Dummy entry won't be written to file. */ private final boolean dummy; /** * Creates a new stored entry. * * @param header the header with the entry information; if the header does not contain an offset * it means that this entry is not yet written in the zip file * @param file the zip file containing the entry * @param source the entry's data source; it can be {@code null} only if the source can be read * from the zip file, that is, if {@code header.getOffset()} is non-negative * @throws IOException failed to create the entry */ StoredEntry( CentralDirectoryHeader header, ZFile file, @Nullable ProcessedAndRawByteSources source, ByteStorage storage) throws IOException { this(header, file, source, storage, null, 0, false); } StoredEntry( String name, ZFile file, ByteStorage storage, StoredEntry linkedEntry, StoredEntry nestedEntry, long nestedOffset, boolean dummy) throws IOException { this((nestedEntry == null ? linkedEntry: nestedEntry).linkingCentralDirectoryHeader(name, file), file, (nestedEntry == null ? linkedEntry : nestedEntry).getSource(), storage, linkedEntry, nestedOffset, dummy); } private CentralDirectoryHeader linkingCentralDirectoryHeader(String name, ZFile file) { boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name); GPFlags flags = GPFlags.make(encodeWithUtf8); return cdh.link(name, EncodeUtils.encode(name, flags), flags, file); } private StoredEntry( CentralDirectoryHeader header, ZFile file, @Nullable ProcessedAndRawByteSources source, ByteStorage storage, StoredEntry linkedEntry, long nestedOffset, boolean dummy) throws IOException { cdh = header; this.file = file; deleted = false; verifyLog = file.makeVerifyLog(); this.storage = storage; this.linkedEntry = linkedEntry; this.nestedOffset = nestedOffset; this.dummy = dummy; if (header.getOffset() >= 0) { readLocalHeader(); Preconditions.checkArgument( source == null, "Source was defined but contents already exist on file."); /* * Since the file is already in the zip, dynamically create a source that will read * the file from the zip when needed. The assignment is not really needed, but we * would get a warning because of the @NotNull otherwise. */ this.source = createSourceFromZip(cdh.getOffset()); } else { /* * There is no local extra data for new files. */ localExtra = new ExtraField(); Preconditions.checkNotNull(source, "Source was not defined, but contents are not on file."); this.source = source; } /* * It seems that zip utilities store directories as names ending with "/". * This seems to be respected by all zip utilities although I could not find there anywhere * in the specification. */ if (cdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) { type = StoredEntryType.DIRECTORY; verifyLog.verify( this.source.getProcessedByteSource().isEmpty(), "Directory source is not empty."); verifyLog.verify(cdh.getCrc32() == 0, "Directory has CRC32 = %s.", cdh.getCrc32()); verifyLog.verify( cdh.getUncompressedSize() == 0, "Directory has uncompressed size = %s.", cdh.getUncompressedSize()); /* * Some clever (OMG!) tools, like jar will actually try to compress the directory * contents and generate a 2 byte compressed data. Of course, the uncompressed size is * zero and we're just wasting space. */ long compressedSize = cdh.getCompressionInfoWithWait().getCompressedSize(); verifyLog.verify( compressedSize == 0 || compressedSize == 2, "Directory has compressed size = %s.", compressedSize); } else { type = StoredEntryType.FILE; } /* * By default we assume there is no data descriptor unless the CRC is marked as deferred * in the header's GP Bit. */ dataDescriptorType = Suppliers.ofInstance(DataDescriptorType.NO_DATA_DESCRIPTOR); if (header.getGpBit().isDeferredCrc()) { /* * If the deferred CRC bit exists, then we have an extra descriptor field. This extra * field may have a signature. */ Verify.verify( header.getOffset() >= 0, "Files that are not on disk cannot have the " + "deferred CRC bit set."); dataDescriptorType = Suppliers.memoize( () -> { try { return readDataDescriptorRecord(); } catch (IOException e) { throw new IOExceptionWrapper( new IOException("Failed to read data descriptor record.", e)); } }); } } /** * Obtains the size of the local header of this entry. * * @return the local header size in bytes */ public int getLocalHeaderSize() { Preconditions.checkState(!deleted, "deleted"); return FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getEncodedFileName().length + localExtra.size(); } /** * Obtains the size of the whole entry on disk, including local header and data descriptor. This * method will wait until compression information is complete, if needed. * * @return the number of bytes * @throws IOException failed to get compression information */ long getInFileSize() throws IOException { Preconditions.checkState(!deleted, "deleted"); return cdh.getCompressionInfoWithWait().getCompressedSize() + getLocalHeaderSize() + dataDescriptorType.get().size; } /** * Obtains a stream that allows reading from the entry. * * @return a stream that will return as many bytes as the uncompressed entry size * @throws IOException failed to open the stream */ public InputStream open() throws IOException { return source.getProcessedByteSource().openStream(); } /** * Obtains the contents of the file. * * @return a byte array with the contents of the file (uncompressed if the file was compressed) * @throws IOException failed to read the file */ public byte[] read() throws IOException { try (InputStream is = new BufferedInputStream(open())) { return ByteStreams.toByteArray(is); } } /** * Obtains the contents of the file in an existing buffer. * * @param bytes buffer to read the file contents in. * @return the number of bytes read * @throws IOException failed to read the file. */ public int read(byte[] bytes) throws IOException { if (bytes.length < getCentralDirectoryHeader().getUncompressedSize()) { throw new RuntimeException( "Buffer to small while reading {}" + getCentralDirectoryHeader().getName()); } try (InputStream is = new BufferedInputStream(open())) { return ByteStreams.read(is, bytes, 0, bytes.length); } } /** * Obtains the type of entry. * * @return the type of entry */ public StoredEntryType getType() { Preconditions.checkState(!deleted, "deleted"); return type; } /** * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. To * eventually write updates to disk, {@link ZFile#update()} must be called. * * @throws IOException failed to delete the entry * @throws IllegalStateException if the zip file was open in read-only mode */ public void delete() throws IOException { delete(true); } /** * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. To * eventually write updates to disk, {@link ZFile#update()} must be called. * * @param notify should listeners be notified of the deletion? This will only be {@code false} if * the entry is being removed as part of a replacement * @throws IOException failed to delete the entry * @throws IllegalStateException if the zip file was open in read-only mode */ void delete(boolean notify) throws IOException { Preconditions.checkState(!deleted, "deleted"); file.delete(this, notify); deleted = true; source.close(); } /** Returns {@code true} if this entry has been deleted/replaced. */ public boolean isDeleted() { return deleted; } /** * Obtains the CDH associated with this entry. * * @return the CDH */ public CentralDirectoryHeader getCentralDirectoryHeader() { return cdh; } /** * Reads the file's local header and verifies that it matches the Central Directory Header * provided in the constructor. This method should only be called if the entry already exists on * disk; new entries do not have local headers. * *

This method will define the {@link #localExtra} field that is only defined in the local * descriptor. * * @throws IOException failed to read the local header */ private void readLocalHeader() throws IOException { byte[] localHeader = new byte[FIXED_LOCAL_FILE_HEADER_SIZE]; file.directFullyRead(cdh.getOffset(), localHeader); CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); ByteBuffer bytes = ByteBuffer.wrap(localHeader); F_LOCAL_SIGNATURE.verify(bytes); F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract(), verifyLog); F_GP_BIT.verify(bytes, cdh.getGpBit().getValue(), verifyLog); F_METHOD.verify(bytes, compressInfo.getMethod().methodCode, verifyLog); if (file.areTimestampsIgnored()) { F_LAST_MOD_TIME.skip(bytes); F_LAST_MOD_DATE.skip(bytes); } else { F_LAST_MOD_TIME.verify(bytes, cdh.getLastModTime(), verifyLog); F_LAST_MOD_DATE.verify(bytes, cdh.getLastModDate(), verifyLog); } /* * If CRC-32, compressed size and uncompressed size are deferred, their values in Local * File Header must be ignored and their actual values must be read from the Data * Descriptor following the contents of this entry. See readDataDescriptorRecord(). */ if (cdh.getGpBit().isDeferredCrc()) { F_CRC32.skip(bytes); F_COMPRESSED_SIZE.skip(bytes); F_UNCOMPRESSED_SIZE.skip(bytes); } else { F_CRC32.verify(bytes, cdh.getCrc32(), verifyLog); F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize(), verifyLog); F_UNCOMPRESSED_SIZE.verify(bytes, cdh.getUncompressedSize(), verifyLog); } F_FILE_NAME_LENGTH.verify(bytes, cdh.getEncodedFileName().length); long extraLength = F_EXTRA_LENGTH.read(bytes); long fileNameStart = cdh.getOffset() + F_EXTRA_LENGTH.endOffset(); byte[] fileNameData = new byte[cdh.getEncodedFileName().length]; file.directFullyRead(fileNameStart, fileNameData); String fileName = EncodeUtils.decode(fileNameData, cdh.getGpBit()); if (!fileName.equals(cdh.getName())) { verifyLog.log( String.format( "Central directory reports file as being named '%s' but local header" + "reports file being named '%s'.", cdh.getName(), fileName)); } long localExtraStart = fileNameStart + cdh.getEncodedFileName().length; byte[] localExtraRaw = new byte[Ints.checkedCast(extraLength)]; file.directFullyRead(localExtraStart, localExtraRaw); localExtra = new ExtraField(localExtraRaw); } /** * Reads the data descriptor record. This method can only be invoked once it is established that a * data descriptor does exist. It will read the data descriptor and check that the data described * there matches the data provided in the Central Directory. * *

This method will set the {@link #dataDescriptorType} field to the appropriate type of data * descriptor record. * * @throws IOException failed to read the data descriptor record */ private DataDescriptorType readDataDescriptorRecord() throws IOException { CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); long ddStart = cdh.getOffset() + FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getName().length() + localExtra.size() + compressInfo.getCompressedSize(); byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size]; file.directFullyRead(ddStart, ddData); ByteBuffer ddBytes = ByteBuffer.wrap(ddData); ZipField.F4 signatureField = new ZipField.F4(0, "Data descriptor signature"); int cpos = ddBytes.position(); long sig = signatureField.read(ddBytes); DataDescriptorType result; if (sig == DATA_DESC_SIGNATURE) { result = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE; } else { result = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE; ddBytes.position(cpos); } ZipField.F4 crc32Field = new ZipField.F4(0, "CRC32"); ZipField.F4 compressedField = new ZipField.F4(crc32Field.endOffset(), "Compressed size"); ZipField.F4 uncompressedField = new ZipField.F4(compressedField.endOffset(), "Uncompressed size"); crc32Field.verify(ddBytes, cdh.getCrc32(), verifyLog); compressedField.verify(ddBytes, compressInfo.getCompressedSize(), verifyLog); uncompressedField.verify(ddBytes, cdh.getUncompressedSize(), verifyLog); return result; } /** * Creates a new source that reads data from the zip. * * @param zipOffset the offset into the zip file where the data is, must be non-negative * @throws IOException failed to close the old source * @return the created source */ private ProcessedAndRawByteSources createSourceFromZip(final long zipOffset) throws IOException { Preconditions.checkArgument(zipOffset >= 0, "zipOffset < 0"); final CentralDirectoryHeaderCompressInfo compressInfo; try { compressInfo = cdh.getCompressionInfoWithWait(); } catch (IOException e) { throw new RuntimeException( "IOException should never occur here because compression " + "information should be immediately available if reading from zip.", e); } /* * Create a source that will return whatever is on the zip file. */ CloseableByteSource rawContents = new CloseableByteSource() { @Override public long size() throws IOException { return compressInfo.getCompressedSize(); } @Override public InputStream openStream() throws IOException { Preconditions.checkState(!deleted, "deleted"); long dataStart = zipOffset + getLocalHeaderSize(); long dataEnd = dataStart + compressInfo.getCompressedSize(); file.openReadOnlyIfClosed(); return file.directOpen(dataStart, dataEnd); } @Override protected void innerClose() throws IOException { /* * Nothing to do here. */ } }; return createSourcesFromRawContents(rawContents); } /** * Creates a {@link ProcessedAndRawByteSources} from the raw data source . The processed source * will either inflate or do nothing depending on the compression information that, at this point, * should already be available * * @param rawContents the raw data to create the source from * @return the sources for this entry */ private ProcessedAndRawByteSources createSourcesFromRawContents(CloseableByteSource rawContents) { CentralDirectoryHeaderCompressInfo compressInfo; try { compressInfo = cdh.getCompressionInfoWithWait(); } catch (IOException e) { throw new RuntimeException( "IOException should never occur here because compression " + "information should be immediately available if creating from raw " + "contents.", e); } CloseableByteSource contents; /* * If the contents are deflated, wrap that source in an inflater source so we get the * uncompressed data. */ if (compressInfo.getMethod() == CompressionMethod.DEFLATE) { contents = new InflaterByteSource(rawContents); } else { contents = rawContents; } return new ProcessedAndRawByteSources(contents, rawContents); } /** * Replaces {@link #source} with one that reads file data from the zip file. * * @param zipFileOffset the offset in the zip file where data is written; must be non-negative * @throws IOException failed to replace the source */ void replaceSourceFromZip(long zipFileOffset) throws IOException { Preconditions.checkArgument(zipFileOffset >= 0, "zipFileOffset < 0"); ProcessedAndRawByteSources oldSource = source; source = createSourceFromZip(zipFileOffset); cdh.setOffset(zipFileOffset); if (!isLinkingEntry()) oldSource.close(); } /** * Loads all data in memory and replaces {@link #source} with one that contains all the data in * memory. * *

If the entry's contents are already in memory, this call does nothing. * * @throws IOException failed to replace the source */ void loadSourceIntoMemory() throws IOException { if (cdh.getOffset() == -1) { /* * No offset in the CDR means data has not been written to disk which, in turn, * means data is already loaded into memory. */ return; } CloseableByteSourceFromOutputStreamBuilder rawBuilder = storage.makeBuilder(); try (InputStream input = source.getRawByteSource().openStream()) { ByteStreams.copy(input, rawBuilder); } CloseableByteSource newRaw = rawBuilder.build(); ProcessedAndRawByteSources newSources = createSourcesFromRawContents(newRaw); try (ProcessedAndRawByteSources oldSource = source) { source = newSources; cdh.setOffset(-1); } } /** * Obtains the source data for this entry. This method can only be called for files, it cannot be * called for directories. * * @return the entry source */ ProcessedAndRawByteSources getSource() { return source; } /** * Obtains the type of data descriptor used in the entry. * * @return the type of data descriptor */ public DataDescriptorType getDataDescriptorType() { return dataDescriptorType.get(); } /** * Removes the data descriptor, if it has one and resets the data descriptor bit in the central * directory header. * * @return was the data descriptor remove? */ boolean removeDataDescriptor() { if (dataDescriptorType.get() == DataDescriptorType.NO_DATA_DESCRIPTOR) { return false; } dataDescriptorType = Suppliers.ofInstance(DataDescriptorType.NO_DATA_DESCRIPTOR); cdh.resetDeferredCrc(); return true; } /** * Obtains the local header data. * * @param buffer a buffer to write header data to * @return the header data size * @throws IOException failed to get header byte data */ int toHeaderData(byte[] buffer) throws IOException { Preconditions.checkArgument( buffer.length >= F_EXTRA_LENGTH.endOffset() + cdh.getEncodedFileName().length + localExtra.size(), "Buffer should be at least the header size"); ByteBuffer out = ByteBuffer.wrap(buffer); writeData(out); return out.position(); } private void writeData(ByteBuffer out) throws IOException { writeData(out, 0); } void writeData(ByteBuffer out, int extraOffset) throws IOException { Preconditions.checkArgument(extraOffset >= 0 , "extraOffset < 0"); CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); F_LOCAL_SIGNATURE.write(out); F_VERSION_EXTRACT.write(out, compressInfo.getVersionExtract()); F_GP_BIT.write(out, cdh.getGpBit().getValue()); F_METHOD.write(out, compressInfo.getMethod().methodCode); if (file.areTimestampsIgnored()) { F_LAST_MOD_TIME.write(out, 0); F_LAST_MOD_DATE.write(out, 0); } else { F_LAST_MOD_TIME.write(out, cdh.getLastModTime()); F_LAST_MOD_DATE.write(out, cdh.getLastModDate()); } F_CRC32.write(out, cdh.getCrc32()); F_COMPRESSED_SIZE.write(out, compressInfo.getCompressedSize()); F_UNCOMPRESSED_SIZE.write(out, cdh.getUncompressedSize()); F_FILE_NAME_LENGTH.write(out, cdh.getEncodedFileName().length); F_EXTRA_LENGTH.write(out, localExtra.size() + extraOffset + nestedOffset); out.put(cdh.getEncodedFileName()); localExtra.write(out); } /** * Requests that this entry be realigned. If this entry is already aligned according to the rules * in {@link ZFile} then this method does nothing. Otherwise it will move the file's data into * memory and place it in a different area of the zip. * * @return has this file been changed? Note that if the entry has not yet been written on the * file, realignment does not count as a change as nothing needs to be updated in the file; * also, if the entry has been changed, this object may have been marked as deleted and a new * stored entry may need to be fetched from the file * @throws IOException failed to realign the entry; the entry may no longer exist in the zip file */ public boolean realign() throws IOException { Preconditions.checkState(!deleted, "Entry has been deleted."); if (isLinkingEntry()) return true; return file.realign(this); } public boolean isLinkingEntry() { return linkedEntry != null; } public boolean isDummyEntry() { return dummy; } public long getNestedOffset() { return nestedOffset; } /** * Obtains the contents of the local extra field. * * @return the contents of the local extra field */ public ExtraField getLocalExtra() { return localExtra; } /** * Sets the contents of the local extra field. * * @param localExtra the contents of the local extra field * @throws IOException failed to update the zip file */ public void setLocalExtra(ExtraField localExtra) throws IOException { boolean resized = setLocalExtraNoNotify(localExtra); file.localHeaderChanged(this, resized); } /** * Sets the contents of the local extra field, does not notify the {@link ZFile} of the change. * This is used internally when the {@link ZFile} itself wants to change the local extra and * doesn't need the callback. * * @param localExtra the contents of the local extra field * @return has the local header size changed? * @throws IOException failed to load the file */ boolean setLocalExtraNoNotify(ExtraField localExtra) throws IOException { boolean sizeChanged; /* * Make sure we load into memory. * * If we change the size of the local header, the actual start of the file changes * according to our in-memory structures so, if we don't read the file now, we won't be * able to load it later :) * * But, even if the size doesn't change, we need to read it force the entry to be * rewritten otherwise the changes in the local header aren't written. Of course this case * may be optimized with some extra complexity added :) */ loadSourceIntoMemory(); if (this.localExtra.size() != localExtra.size()) { sizeChanged = true; } else { sizeChanged = false; } this.localExtra = localExtra; return sizeChanged; } /** * Obtains the verify log for the entry. * * @return the verify log */ public VerifyLog getVerifyLog() { return verifyLog; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntryType.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** Type of stored entry. */ public enum StoredEntryType { /** Entry is a file. */ FILE, /** Entry is a directory. */ DIRECTORY } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLog.java ================================================ /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.google.common.collect.ImmutableList; /** * The verify log contains verification messages. It is used to capture validation issues with a zip * file or with parts of a zip file. */ public interface VerifyLog { /** * Logs a message. * * @param message the message to verify */ void log(String message); /** * Obtains all save logged messages. * * @return the logged messages */ ImmutableList getLogs(); /** * Performs verification of a non-critical condition, logging a message if the condition is not * verified. * * @param condition the condition * @param message the message to write if {@code condition} is {@code false}. * @param args arguments for formatting {@code message} using {@code String.format} */ default void verify(boolean condition, String message, Object... args) { if (!condition) { log(String.format(message, args)); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLogs.java ================================================ /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; /** Factory for verification logs. */ final class VerifyLogs { private VerifyLogs() {} /** * Creates a {@link VerifyLog} that ignores all messages logged. * * @return the log */ static VerifyLog devNull() { return new VerifyLog() { @Override public void log(String message) {} @Override public ImmutableList getLogs() { return ImmutableList.of(); } }; } /** * Creates a {@link VerifyLog} that stores all log messages. * * @return the log */ static VerifyLog unlimited() { return new VerifyLog() { /** All saved messages. */ private final List messages = new ArrayList<>(); @Override public void log(String message) { messages.add(message); } @Override public ImmutableList getLogs() { return ImmutableList.copyOf(messages); } }; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZFile.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import com.android.tools.build.apkzlib.bytestorage.ByteStorage; import com.android.tools.build.apkzlib.utils.CachedFileContents; import com.android.tools.build.apkzlib.utils.IOExceptionFunction; import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; import com.android.tools.build.apkzlib.zip.compress.Zip64NotSupportedException; import com.android.tools.build.apkzlib.zip.utils.ByteTracker; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource; import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Supplier; import com.google.common.base.Verify; import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; import com.google.common.io.ByteSource; import com.google.common.io.Closer; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import javax.annotation.Nullable; /** * The {@code ZFile} provides the main interface for interacting with zip files. A {@code ZFile} can * be created on a new file or in an existing file. Once created, files can be added or removed from * the zip file. * *

Changes in the zip file are always deferred. Any change requested is made in memory and * written to disk only when {@link #update()} or {@link #close()} is invoked. * *

Zip files are open initially in read-only mode and will switch to read-write when needed. This * is done automatically. Because modifications to the file are done in-memory, the zip file can be * manipulated when closed. When invoking {@link #update()} or {@link #close()} the zip file will be * reopen and changes will be written. However, the zip file cannot be modified outside the control * of {@code ZFile}. So, if a {@code ZFile} is closed, modified outside and then a file is added or * removed from the zip file, when reopening the zip file, {@link ZFile} will detect the outside * modification and will fail. * *

In memory manipulation means that files added to the zip file are kept in memory until written * to disk. This provides much faster operation and allows better zip file allocation (see below). * It may, however, increase the memory footprint of the application. When adding large files, if * memory consumption is a concern, a call to {@link #update()} will actually write the file to disk * and discard the memory buffer. Information about allocation can be obtained from a {@link * ByteTracker} that can be given to the file on creation. * *

{@code ZFile} keeps track of allocation inside of the zip file. If a file is deleted, its * space is marked as freed and will be reused for an added file if it fits in the space. Allocation * of files to empty areas is done using a best fit algorithm. When adding a file, if it * doesn't fit in any free area, the zip file will be extended. * *

{@code ZFile} provides a fast way to merge data from another zip file (see {@link * #mergeFrom(ZFile, Predicate)}) avoiding recompression and copying of equal files. When merging, * patterns of files may be provided that are ignored. This allows handling special files in the * merging process, such as files in {@code META-INF}. * *

When adding files to the zip file, unless files are explicitly required to be stored, files * will be deflated. However, deflating will not occur if the deflated file is larger then the * stored file, e.g. if compression would yield a bigger file. See {@link Compressor} for * details on how compression works. * *

Because {@code ZFile} was designed to be used in a build system and not as general-purpose zip * utility, it is very strict (and unforgiving) about the zip format and unsupported features. * *

{@code ZFile} supports alignment. Alignment means that file data (not entries -- the * local header must be discounted) must start at offsets that are multiple of a number -- the * alignment. Alignment is defined by an alignment rules ({@link AlignmentRule} in the {@link * ZFileOptions} object used to create the {@link ZFile}. * *

When a file is added to the zip, the alignment rules will be checked and alignment will be * honored when positioning the file in the zip. This means that unused spaces in the zip may be * generated as a result. However, alignment of existing entries will not be changed. * *

Entries can be realigned individually (see {@link StoredEntry#realign()} or the full zip file * may be realigned (see {@link #realign()}). When realigning the full zip entries that are already * aligned will not be affected. * *

Because realignment may cause files to move in the zip, realignment is done in-memory meaning * that files that need to change location will moved to memory and will only be flushed when either * {@link #update()} or {@link #close()} are called. * *

Alignment only applies to filed that are forced to be uncompressed. This is because alignment * is used to allow mapping files in the archive directly into memory and compressing defeats the * purpose of alignment. * *

Manipulating zip files with {@link ZFile} may yield zip files with empty spaces between files. * This happens in two situations: (1) if alignment is required, files may be shifted to conform to * the request alignment leaving an empty space before the previous file, and (2) if a file is * removed or replaced with a file that does not fit the space it was in. By default, {@link ZFile} * does not do any special processing in these situations. Files are indexed by their offsets from * the central directory and empty spaces can exist in the zip file. * *

However, it is possible to tell {@link ZFile} to use the extra field in the local header to do * cover the empty spaces. This is done by setting {@link * ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} to {@code true}. This has the advantage * of leaving no gaps between entries in the zip, as required by some tools like Oracle's {code jar} * tool. However, setting this option will destroy the contents of the file's extra field. * *

Activating {@link ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} may lead to * virtual files being added to the zip file. Since extra field is limited to 64k, it is not * possible to cover any space bigger than that using the extra field. In those cases, virtual * files are added to the file. A virtual file is a file that exists in the actual zip data, but * is not referenced from the central directory. A zip-compliant utility should ignore these files. * However, zip utilities that expect the zip to be a stream, such as Oracle's jar, will find these * files instead of considering the zip to be corrupt. * *

{@code ZFile} support sorting zip files. Sorting (done through the {@link #sortZipContents()} * method) is a process by which all files are re-read into memory, if not already in memory, * removed from the zip and re-added in alphabetical order, respecting alignment rules. So, in * general, file {@code b} will come after file {@code a} unless file {@code a} is subject to * alignment that forces an empty space before that can be occupied by {@code b}. Sorting can be * used to minimize the changes between two zips. * *

Sorting in {@code ZFile} can be done manually or automatically. Manual sorting is done by * invoking {@link #sortZipContents()}. Automatic sorting is done by setting the {@link * ZFileOptions#getAutoSortFiles()} option when creating the {@code ZFile}. Automatic sorting * invokes {@link #sortZipContents()} immediately when doing an {@link #update()} after all * extensions have processed the {@link ZFileExtension#beforeUpdate()}. This has the guarantee that * files added by extensions will be sorted, something that does not happen if the invocation is * sequential, i.e., {@link #sortZipContents()} called before {@link #update()}. The drawback * of automatic sorting is that sorting will happen every time {@link #update()} is called and the * file is dirty having a possible penalty in performance. * *

To allow whole-apk signing, the {@code ZFile} allows the central directory location to be * offset by a fixed amount. This amount can be set using the {@link #setExtraDirectoryOffset(long)} * method. Setting a non-zero value will add extra (unused) space in the zip file before the central * directory. This value can be changed at any time and it will force the central directory * rewritten when the file is updated or closed. * *

{@code ZFile} provides an extension mechanism to allow objects to register with the file and * be notified when changes to the file happen. This should be used to add extra features to the zip * file while providing strong decoupling. See {@link ZFileExtension}, {@link * ZFile#addZFileExtension(ZFileExtension)} and {@link ZFile#removeZFileExtension(ZFileExtension)}. * *

This class is not thread-safe. Neither are any of the classes associated with * it in this package, except when otherwise noticed. */ public class ZFile implements Closeable { /** * The file separator in paths in the zip file. This is fixed by the zip specification (section * 4.4.17). */ public static final char SEPARATOR = '/'; /** Minimum size the EOCD can have. */ private static final int MIN_EOCD_SIZE = 22; /** Number of bytes of the Zip64 EOCD locator record. */ private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; /** Maximum size for the EOCD. */ private static final int MAX_EOCD_COMMENT_SIZE = 65535; /** How many bytes to look back from the end of the file to look for the EOCD signature. */ private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE; /** Signature of the Zip64 EOCD locator record. */ private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50; /** Signature of the EOCD record. */ private static final byte[] EOCD_SIGNATURE = new byte[] {0x06, 0x05, 0x4b, 0x50}; /** Size of buffer for I/O operations. */ private static final int IO_BUFFER_SIZE = 1024 * 1024; /** * When extensions request re-runs, we do maximum number of cycles until we decide to stop and * flag a infinite recursion problem. */ private static final int MAXIMUM_EXTENSION_CYCLE_COUNT = 10; /** * Minimum size for the extra field when we have to add one. We rely on the alignment segment to * do that so the minimum size for the extra field is the minimum size of an alignment segment. */ protected static final int MINIMUM_EXTRA_FIELD_SIZE = ExtraField.AlignmentSegment.MINIMUM_SIZE; /** * Maximum size of the extra field. * *

Theoretically, this is (1 << 16) - 1 = 65535 and not (1 < 15) -1 = 32767. However, due to * http://b.android.com/221703, we need to keep this limited. */ protected static final int MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE = (1 << 15) - 1; /** File zip file. */ protected final File file; /** * The random access file used to access the zip file. This will be {@code null} if and only if * {@link #state} is {@link ZipFileState#CLOSED}. */ @Nullable private RandomAccessFile raf; /** * The map containing the in-memory contents of the zip file. It keeps track of which parts of the * zip file are used and which are not. */ private final FileUseMap map; /** * The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the one * that exists on disk is no longer valid (because the zip has been changed). * *

If the EOCD is deleted because the zip has been changed and the old EOCD was no longer * valid, then {@link #eocdComment} will contain the comment saved from the EOCD. */ @Nullable private FileUseMapEntry eocdEntry; /** * The Central Directory entry. Will be {@code null} if there is no Central Directory (because the * zip is new) or because the one that exists on disk is no longer valid (because the zip has been * changed). */ @Nullable private FileUseMapEntry directoryEntry; /** * All entries in the zip file. It includes in-memory changes and may not reflect what is written * on disk. Only entries that have been compressed are in this list. */ private final Map> entries; /** * Entries added to the zip file, but that are not yet compressed. When compression is done, these * entries are eventually moved to {@link #entries}. uncompressedEntries is a list because entries * need to be kept in the order by which they were added. It allows adding multiple files with the * same name and getting the right notifications on which files replaced which. * *

Files are placed in this list in {@link #add(StoredEntry)} method. This method will keep * files here temporarily and move then to {@link #entries} when the data is available. * *

Moving files out of this list to {@link #entries} is done by {@link * #processAllReadyEntries()}. */ private final List uncompressedEntries; /** LSPatch * */ private final List linkingEntries; /** Current state of the zip file. */ private ZipFileState state; /** * Are the in-memory changes that have not been written to the zip file? * *

This might be false, but will become true after {@link #processAllReadyEntriesWithWait()} is * called if there are {@link #uncompressedEntries} compressing in the background. */ private boolean dirty; /** * Non-{@code null} only if the file is currently closed. Used to detect if the zip is modified * outside this object's control. If the file has never been written, this will be {@code null} * even if it is closed. */ @Nullable private CachedFileContents closedControl; /** The alignment rule. */ private final AlignmentRule alignmentRule; /** Extensions registered with the file. */ private final List extensions; /** * When notifying extensions, extensions may request that some runnables are executed. This list * collects all runnables by the order they were requested. Together with {@link #isNotifying}, it * is used to avoid reordering notifications. */ private final List toRun; /** * {@code true} when {@link #notify(com.android.tools.build.apkzlib.utils.IOExceptionFunction)} is * notifying extensions. Used to avoid reordering notifications. */ private boolean isNotifying; /** * An extra offset for the central directory location. {@code 0} if the central directory should * be written in its standard location. */ private long extraDirectoryOffset; /** Should all timestamps be zeroed when reading / writing the zip? */ private boolean noTimestamps; /** Compressor to use. */ private final Compressor compressor; /** Byte storage to use. */ private final ByteStorage storage; /** Use the zip entry's "extra field" field to cover empty space in the zip file? */ private boolean coverEmptySpaceUsingExtraField; /** Should files be automatically sorted when updating? */ private boolean autoSortFiles; /** Verify log factory to use. */ private final Supplier verifyLogFactory; /** Verify log to use. */ private final VerifyLog verifyLog; /** Should skip expensive validation? */ private final boolean skipValidation; /** * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure. This * may happen, for example, if the zip has been changed and the Central Directory and EOCD have * been deleted (in-memory). In that case, this field will save the comment to place on the EOCD * once it is created. * *

This field will only be non-{@code null} if there is no in-memory EOCD structure * (i.e., {@link #eocdEntry} is {@code null}). If there is an {@link #eocdEntry}, then the * comment will be there instead of being in this field. */ @Nullable private byte[] eocdComment; /** Is the file in read-only mode? In read-only mode no changes are allowed. */ private boolean readOnly; /** * Creates a new zip file. If the zip file does not exist, then no file is created at this point * and {@code ZFile} will contain an empty structure. However, an (empty) zip file will be created * if either {@link #update()} or {@link #close()} are used. If a zip file exists, it will be * parsed and read. * * @param file the zip file * @throws IOException some file exists but could not be read * @deprecated use {@link ZFile#openReadOnly(File)} or {@link ZFile#openReadWrite(File)} */ @Deprecated public ZFile(File file) throws IOException { this(file, new ZFileOptions()); } /** * Creates a new zip file. If the zip file does not exist, then no file is created at this point * and {@code ZFile} will contain an empty structure. However, an (empty) zip file will be created * if either {@link #update()} or {@link #close()} are used. If a zip file exists, it will be * parsed and read. * * @param file the zip file * @param options configuration options * @throws IOException some file exists but could not be read * @deprecated use {@link ZFile#openReadOnly(File, ZFileOptions)} or {@link * ZFile#openReadWrite(File, ZFileOptions)} */ @Deprecated public ZFile(File file, ZFileOptions options) throws IOException { this(file, options, false); } /** * Creates a new zip file. If the zip file does not exist, then no file is created at this point * and {@code ZFile} will contain an empty structure. However, an (empty) zip file will be created * if either {@link #update()} or {@link #close()} are used. If a zip file exists, it will be * parsed and read. * * @param file the zip file * @param options configuration options * @param readOnly should the file be open in read-only mode? If {@code true} then the file must * exist and no methods can be invoked that could potentially change the file * @throws IOException some file exists but could not be read * @deprecated use {@link ZFile#openReadOnly(File, ZFileOptions)} or {@link * ZFile#openReadWrite(File, ZFileOptions)} */ @Deprecated public ZFile(File file, ZFileOptions options, boolean readOnly) throws IOException { this.file = file; map = new FileUseMap( 0, options.getCoverEmptySpaceUsingExtraField() ? MINIMUM_EXTRA_FIELD_SIZE : 0); this.readOnly = readOnly; dirty = false; closedControl = null; alignmentRule = options.getAlignmentRule(); extensions = Lists.newArrayList(); toRun = Lists.newArrayList(); noTimestamps = options.getNoTimestamps(); storage = options.getStorageFactory().create(); compressor = options.getCompressor(); coverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField(); autoSortFiles = options.getAutoSortFiles(); verifyLogFactory = options.getVerifyLogFactory(); verifyLog = verifyLogFactory.get(); skipValidation = options.getSkipValidation(); /* * These two values will be overwritten by openReadOnlyIfClosed() below if the file exists. */ state = ZipFileState.CLOSED; raf = null; if (file.exists()) { openReadOnlyIfClosed(); } else if (readOnly) { throw new IOException("File does not exist but read-only mode requested"); } else { dirty = true; } entries = Maps.newHashMap(); uncompressedEntries = Lists.newArrayList(); linkingEntries = Lists.newArrayList(); extraDirectoryOffset = 0; try { if (state != ZipFileState.CLOSED) { // TODO: to be removed completely once Zip64 is fully supported final long MAX_ENTRY_SIZE = 0xFFFFFFFFL; // 2^32-1 long rafSize = raf.length(); if (rafSize > MAX_ENTRY_SIZE) { throw new IOException("File exceeds size limit of " + MAX_ENTRY_SIZE + "."); } map.extend(raf.length()); readData(); } // If we don't have an EOCD entry, set the comment to empty. if (eocdEntry == null) { eocdComment = new byte[0]; } // Notify the extensions if the zip file has been open. if (state != ZipFileState.CLOSED) { notify(ZFileExtension::open); } } catch (Zip64NotSupportedException e) { throw e; } catch (IOException e) { throw new IOException("Failed to read zip file '" + file.getAbsolutePath() + "'.", e); } catch (IllegalStateException | IllegalArgumentException | VerifyException e) { throw new RuntimeException( "Internal error when trying to read zip file '" + file.getAbsolutePath() + "'.", e); } } /** * Old name of {@link #openReadOnlyIfClosed()}, method kept for backwards compatibility only. * * @deprecated use {@link #openReadOnlyIfClosed()} if necessary to ensure a {@link ZFile} is open * and readable */ @Deprecated public void openReadOnly() throws IOException { openReadOnlyIfClosed(); } /** * Opens a new {@link ZFile} from the given file in read-only mode. * * @param file the file to open * @return the created file * @throws IOException failed to read the file */ public static ZFile openReadOnly(File file) throws IOException { return openReadOnly(file, new ZFileOptions()); } /** * Opens a new {@link ZFile} from the given file in read-only mode. * * @param file the file to open * @param options the options to use to open the file; because the file is open read-only, many of * these options won't have any effect * @return the created file * @throws IOException failed to read the file */ public static ZFile openReadOnly(File file, ZFileOptions options) throws IOException { return new ZFile(file, options, true); } /** * Opens a new {@link ZFile} from the given file in read-write mode. Opening a file in read-write * mode may force the file to be written even if no changes are made. For example, differences in * signature will force the file to be written. Use {@link #openReadOnly(File, ZFileOptions)} to * open a file and ensure it won't be written. * *

The file will be created if it doesn't exist. If the file exists, it must be a valid zip * archive. * * @param file the file to open * @return the created file * @throws IOException failed to read the file */ public static ZFile openReadWrite(File file) throws IOException { return openReadWrite(file, new ZFileOptions()); } /** * Opens a new {@link ZFile} from the given file in read-write mode. Opening a file in read-write * mode may force the file to be written even if no changes are made. For example, differences in * signature will force the file to be written. Use {@link #openReadOnly(File, ZFileOptions)} to * open a file and ensure it won't be written. * *

The file will be created if it doesn't exist. If the file exists, it must be a valid zip * archive. * * @param file the file to open * @param options the options to use to open the file * @return the created file * @throws IOException failed to read the file */ public static ZFile openReadWrite(File file, ZFileOptions options) throws IOException { return new ZFile(file, options, false); } public boolean getSkipValidation() { return skipValidation; } /** * Obtains all entries in the file. Entries themselves may be or not written in disk. However, all * of them can be open for reading. * * @return all entries in the zip */ public Set entries() { Map entries = Maps.newHashMap(); for (FileUseMapEntry mapEntry : this.entries.values()) { StoredEntry entry = mapEntry.getStore(); Preconditions.checkNotNull(entry, "Entry at %s is null", mapEntry.getStart()); entries.put(entry.getCentralDirectoryHeader().getName(), entry); } /* * mUncompressed may override mEntriesReady as we may not have yet processed all * entries. */ for (StoredEntry uncompressed : uncompressedEntries) { entries.put(uncompressed.getCentralDirectoryHeader().getName(), uncompressed); } for (StoredEntry linking: linkingEntries) { entries.put(linking.getCentralDirectoryHeader().getName(), linking); } return Sets.newHashSet(entries.values()); } /** * Obtains an entry at a given path in the zip. * * @param path the path * @return the entry at the path or {@code null} if none exists */ @Nullable public StoredEntry get(String path) { /* * The latest entries are the last ones in uncompressed and they may eventually override * files in entries. */ for (StoredEntry stillUncompressed : Lists.reverse(uncompressedEntries)) { if (stillUncompressed.getCentralDirectoryHeader().getName().equals(path)) { return stillUncompressed; } } FileUseMapEntry found = entries.get(path); if (found == null) { return null; } return found.getStore(); } /** * Reads all the data in the zip file, except the contents of the entries themselves. This method * will populate the directory and maps in the instance variables. * * @throws IOException failed to read the zip file */ private void readData() throws IOException { Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); Preconditions.checkNotNull(raf, "raf == null"); readEocd(); readCentralDirectory(); /* * Go over all files and create the usage map, verifying there is no overlap in the files. */ long entryEndOffset; long directoryStartOffset; if (directoryEntry != null) { CentralDirectory directory = directoryEntry.getStore(); Preconditions.checkNotNull(directory, "Central directory is null"); entryEndOffset = 0; for (StoredEntry entry : directory.getEntries().values()) { long start = entry.getCentralDirectoryHeader().getOffset(); long end = start + entry.getInFileSize(); /* * If isExtraAlignmentBlock(entry.getLocalExtra()) is true, we know the entry * has an extra field that is solely used for alignment. This means the * actual entry could start at start + extra.length and leave space before. * * But, if we did this here, we would be modifying the zip file and that is * weird because we're just opening it for reading. * * The downside is that we will never reuse that space. Maybe one day ZFile * can be clever enough to remove the local extra when we start modifying the zip * file. */ Verify.verify(start >= 0, "start < 0"); Verify.verify(end < map.size(), "end >= map.size()"); FileUseMapEntry found = map.at(start); Verify.verifyNotNull(found); // We've got a problem if the found entry is not free or is a free entry but // doesn't cover the whole file. if (!found.isFree() || found.getEnd() < end) { if (found.isFree()) { found = map.after(found); Verify.verify(found != null && !found.isFree()); } Object foundEntry = found.getStore(); Verify.verify(foundEntry != null); // Obtains a custom description of an entry. IOExceptionFunction describe = e -> String.format( "'%s' (offset: %d, size: %d)", e.getCentralDirectoryHeader().getName(), e.getCentralDirectoryHeader().getOffset(), e.getInFileSize()); String overlappingEntryDescription; if (foundEntry instanceof StoredEntry) { StoredEntry foundStored = (StoredEntry) foundEntry; overlappingEntryDescription = describe.apply(foundStored); } else { overlappingEntryDescription = "Central Directory / EOCD: " + found.getStart() + " - " + found.getEnd(); } throw new IOException( "Cannot read entry " + describe.apply(entry) + " because it overlaps with " + overlappingEntryDescription); } FileUseMapEntry mapEntry = map.add(start, end, entry); entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); if (end > entryEndOffset) { entryEndOffset = end; } } directoryStartOffset = directoryEntry.getStart(); } else { /* * No directory means an empty zip file. Use the start of the EOCD to compute * an existing offset. */ Verify.verifyNotNull(eocdEntry); Preconditions.checkNotNull(eocdEntry, "EOCD is null"); directoryStartOffset = eocdEntry.getStart(); entryEndOffset = 0; } /* * Check if there is an extra central directory offset. If there is, save it. Note that * we can't call extraDirectoryOffset() because that would mark the file as dirty. */ long extraOffset = directoryStartOffset - entryEndOffset; Verify.verify(extraOffset >= 0, "extraOffset (%s) < 0", extraOffset); extraDirectoryOffset = extraOffset; } /** * Finds the EOCD marker and reads it. It will populate the {@link #eocdEntry} variable. * * @throws IOException failed to read the EOCD */ private void readEocd() throws IOException { Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); Preconditions.checkNotNull(raf, "raf == null"); /* * Read the last part of the zip into memory. If we don't find the EOCD signature by then, * the file is corrupt. */ int lastToRead = LAST_BYTES_TO_READ; if (lastToRead > raf.length()) { lastToRead = Ints.checkedCast(raf.length()); } byte[] last = new byte[lastToRead]; directFullyRead(raf.length() - lastToRead, last); /* * Start endIdx at the first possible location where the signature can be located and then * move backwards. Because the EOCD must have at least MIN_EOCD size, the first byte of the * signature (and first byte of the EOCD) must be located at last.length - MIN_EOCD_SIZE. * * Because the EOCD signature may exist in the file comment, when we find a signature we * will try to read the Eocd. If we fail, we continue searching for the signature. However, * we will keep the last exception in case we don't find any signature. */ Eocd eocd = null; int foundEocdSignature = -1; IOException errorFindingSignature = null; long eocdStart = -1; for (int endIdx = last.length - MIN_EOCD_SIZE; endIdx >= 0 && foundEocdSignature == -1; endIdx--) { /* * Remember: little endian... */ if (last[endIdx] == EOCD_SIGNATURE[3] && last[endIdx + 1] == EOCD_SIGNATURE[2] && last[endIdx + 2] == EOCD_SIGNATURE[1] && last[endIdx + 3] == EOCD_SIGNATURE[0]) { /* * We found a signature. Try to read the EOCD record. */ foundEocdSignature = endIdx; ByteBuffer eocdBytes = ByteBuffer.wrap(last, foundEocdSignature, last.length - foundEocdSignature); try { eocd = new Eocd(eocdBytes); eocdStart = raf.length() - lastToRead + foundEocdSignature; /* * Make sure the EOCD takes the whole file up to the end. Log an error if it * doesn't. */ if (eocdStart + eocd.getEocdSize() != raf.length()) { verifyLog.log( "EOCD starts at " + eocdStart + " and has " + eocd.getEocdSize() + " bytes, but file ends at " + raf.length() + "."); } } catch (IOException e) { if (errorFindingSignature != null) { e.addSuppressed(errorFindingSignature); } errorFindingSignature = e; foundEocdSignature = -1; eocd = null; } } } if (foundEocdSignature == -1) { throw new IOException( "EOCD signature not found in the last " + lastToRead + " bytes of the file.", errorFindingSignature); } Verify.verify(eocdStart >= 0); /* * Look for the Zip64 central directory locator. If we find it, then this file is a Zip64 * file and we do not support it. */ long zip64LocatorStart = eocdStart - ZIP64_EOCD_LOCATOR_SIZE; if (zip64LocatorStart >= 0) { byte[] possibleZip64Locator = new byte[4]; directFullyRead(zip64LocatorStart, possibleZip64Locator); if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator)) == ZIP64_EOCD_LOCATOR_SIGNATURE) { throw new Zip64NotSupportedException( "Zip64 EOCD locator found but Zip64 format is not supported."); } } eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd); } /** * Reads the zip's central directory and populates the {@link #directoryEntry} variable. This * method can only be called after the EOCD has been read. If the central directory is empty (if * there are no files on the zip archive), then {@link #directoryEntry} will be set to {@code * null}. * * @throws IOException failed to read the central directory */ private void readCentralDirectory() throws IOException { Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); Preconditions.checkNotNull(eocdEntry.getStore(), "eocdEntry.getStore() == null"); Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); Preconditions.checkNotNull(raf, "raf == null"); Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); Eocd eocd = eocdEntry.getStore(); long dirSize = eocd.getDirectorySize(); if (dirSize > Integer.MAX_VALUE) { throw new IOException("Cannot read central directory with size " + dirSize + "."); } long centralDirectoryEnd = eocd.getDirectoryOffset() + dirSize; if (centralDirectoryEnd != eocdEntry.getStart()) { String msg = "Central directory is stored in [" + eocd.getDirectoryOffset() + " - " + (centralDirectoryEnd - 1) + "] and EOCD starts at " + eocdEntry.getStart() + "."; /* * If there is an empty space between the central directory and the EOCD, we proceed * logging an error. If the central directory ends after the start of the EOCD (and * therefore, they overlap), throw an exception. */ if (centralDirectoryEnd > eocdEntry.getSize()) { throw new IOException(msg); } else { verifyLog.log(msg); } } byte[] directoryData = new byte[Ints.checkedCast(dirSize)]; directFullyRead(eocd.getDirectoryOffset(), directoryData); CentralDirectory directory = CentralDirectory.makeFromData( ByteBuffer.wrap(directoryData), eocd.getTotalRecords(), this, storage); if (eocd.getDirectorySize() > 0) { directoryEntry = map.add( eocd.getDirectoryOffset(), eocd.getDirectoryOffset() + eocd.getDirectorySize(), directory); } } /** * Opens a portion of the zip for reading. The zip must be open for this method to be invoked. * Note that if the zip has not been updated, the individual zip entries may not have been written * yet. * * @param start the index within the zip file to start reading * @param end the index within the zip file to end reading (the actual byte pointed by * end will not be read) * @return a stream that will read the portion of the file; no decompression is done, data is * returned as is * @throws IOException failed to open the zip file */ public InputStream directOpen(final long start, final long end) throws IOException { Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); Preconditions.checkNotNull(raf, "raf == null"); Preconditions.checkArgument(start >= 0, "start < 0"); Preconditions.checkArgument(end >= start, "end < start"); Preconditions.checkArgument(end <= raf.length(), "end > raf.length()"); return new InputStream() { private long mCurr = start; @Override public int read() throws IOException { if (mCurr == end) { return -1; } byte[] b = new byte[1]; int r = directRead(mCurr, b); if (r > 0) { mCurr++; return b[0]; } else { return -1; } } @Override public int read(byte[] b, int off, int len) throws IOException { Preconditions.checkNotNull(b, "b == null"); Preconditions.checkArgument(off >= 0, "off < 0"); Preconditions.checkArgument(off <= b.length, "off > b.length"); Preconditions.checkArgument(len >= 0, "len < 0"); Preconditions.checkArgument(off + len <= b.length, "off + len > b.length"); long availableToRead = end - mCurr; long toRead = Math.min(len, availableToRead); if (toRead == 0) { return -1; } if (toRead > Integer.MAX_VALUE) { throw new IOException("Cannot read " + toRead + " bytes."); } int r = directRead(mCurr, b, off, Ints.checkedCast(toRead)); if (r > 0) { mCurr += r; } return r; } }; } /** * Deletes an entry from the zip. This method does not actually delete anything on disk. It just * changes in-memory structures. Use {@link #update()} to update the contents on disk. * * @param entry the entry to delete * @param notify should listeners be notified of the deletion? This will only be {@code false} if * the entry is being removed as part of a replacement * @throws IOException failed to delete the entry * @throws IllegalStateException if open in read-only mode */ void delete(final StoredEntry entry, boolean notify) throws IOException { checkNotInReadOnlyMode(); String path = entry.getCentralDirectoryHeader().getName(); FileUseMapEntry mapEntry = entries.get(path); Preconditions.checkNotNull(mapEntry, "mapEntry == null"); Preconditions.checkArgument(entry == mapEntry.getStore(), "entry != mapEntry.getStore()"); dirty = true; map.remove(mapEntry); entries.remove(path); if (notify) { notify(ext -> ext.removed(entry)); } } /** * Checks that the file is not in read-only mode. * * @throws IllegalStateException if the file is in read-only mode */ private void checkNotInReadOnlyMode() { if (readOnly) { throw new IllegalStateException("Illegal operation in read only model"); } } /** * Updates the file writing new entries and removing deleted entries. This will force reopening * the file as read/write if the file wasn't open in read/write mode. * * @throws IOException failed to update the file; this exception may have been thrown by the * compressor but only reported here */ public void update() throws IOException { checkNotInReadOnlyMode(); /* * Process all background stuff before calling in the extensions. */ processAllReadyEntriesWithWait(); notify(ZFileExtension::beforeUpdate); /* * Process all background stuff that may be leftover by the extensions. */ processAllReadyEntriesWithWait(); if (dirty) { writeAllFilesToZip(); } // Even if no files were modified, we still need to recompute the central directory and EOCD // in case they have been modified by any extension. recomputeAndWriteCentralDirectoryAndEocd(); // If there are no changes to the file, we may get here without even opening the zip as a // RandomAccessFile. In that case, don't try to change the size since we're sure there are no // changes. if (raf != null) { // Ensure we make the zip have the right size (only useful if shrinking), mark the zip as // no longer dirty and notify all extensions. if (raf.length() != map.size()) { raf.setLength(map.size()); } } // Regardless of whether the zip was dirty or not, we're sure it isn't now. dirty = false; notify( ext -> { ext.updated(); return null; }); } /** * Writes all files to the zip, sorting/packing if necessary. The central directory and EOCD are * deleted. When this method finishes, all entries have been written to the file and are properly * aligned. */ private void writeAllFilesToZip() throws IOException { reopenRw(); /* * At this point, no more files can be added. We may need to repack to remove extra * empty spaces or sort. If we sort, we don't need to repack as sorting forces the * zip file to be as compact as possible. */ if (autoSortFiles) { sortZipContents(); } else { packIfNecessary(); } /* * We're going to change the file so delete the central directory and the EOCD as they * will have to be rewritten. */ deleteDirectoryAndEocd(); map.truncate(); /* * If we need to use the extra field to cover empty spaces, we do the processing here. */ if (coverEmptySpaceUsingExtraField) { /* We will go over all files in the zip and check whether there is empty space before * them. If there is, then we will move the entry to the beginning of the empty space * (covering it) and extend the extra field with the size of the empty space. */ for (FileUseMapEntry entry : new HashSet<>(entries.values())) { StoredEntry storedEntry = entry.getStore(); Preconditions.checkNotNull(storedEntry, "Entry at %s is null", entry.getStart()); FileUseMapEntry before = map.before(entry); if (before == null || !before.isFree()) { continue; } /* * We have free space before the current entry. However, we do know that it can * be covered by the extra field, because both sortZipContents() and * packIfNecessary() guarantee it. */ int localExtraSize = storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize()); Verify.verify(localExtraSize <= MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE); /* * Move file back in the zip. */ storedEntry.loadSourceIntoMemory(); long newStart = before.getStart(); long newSize = entry.getSize() + before.getSize(); /* * Remove the entry. */ String name = storedEntry.getCentralDirectoryHeader().getName(); map.remove(entry); Verify.verify(entry == entries.remove(name)); /* * Make a list will all existing segments in the entry's extra field, but remove * the alignment field, if it exists. Also, sum the size of all kept extra field * segments. */ ImmutableList currentSegments; try { currentSegments = storedEntry.getLocalExtra().getSegments(); } catch (IOException e) { /* * Parsing current segments has failed. This means the contents of the extra * field are not valid. We'll continue discarding the existing segments. */ currentSegments = ImmutableList.of(); } List extraFieldSegments = new ArrayList<>(); int newExtraFieldSize = 0; for (ExtraField.Segment segment : currentSegments) { if (segment.getHeaderId() != ExtraField.ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) { extraFieldSegments.add(segment); newExtraFieldSize += segment.size(); } } int spaceToFill = Ints.checkedCast( before.getSize() + storedEntry.getLocalExtra().size() - newExtraFieldSize); extraFieldSegments.add( new ExtraField.AlignmentSegment(chooseAlignment(storedEntry), spaceToFill)); storedEntry.setLocalExtraNoNotify(new ExtraField(ImmutableList.copyOf(extraFieldSegments))); entries.put(name, map.add(newStart, newStart + newSize, storedEntry)); /* * Reset the offset to force the file to be rewritten. */ storedEntry.getCentralDirectoryHeader().setOffset(-1); } } /* * Write new files in the zip. We identify new files because they don't have an offset * in the zip where they are written although we already know, by their location in the * file map, where they will be written to. * * Before writing the files, we sort them in the order they are written in the file so that * writes are made in order on disk. * This is, however, unlikely to optimize anything relevant given the way the Operating * System does caching, but it certainly won't hurt :) */ TreeMap, StoredEntry> toWriteToStore = new TreeMap<>(FileUseMapEntry.COMPARE_BY_START); for (FileUseMapEntry entry : entries.values()) { StoredEntry entryStore = entry.getStore(); Preconditions.checkNotNull(entryStore, "Entry at %s is null", entry.getStart()); if (entryStore.getCentralDirectoryHeader().getOffset() == -1) { toWriteToStore.put(entry, entryStore); } } /* * Add all free entries to the set. */ for (FileUseMapEntry freeArea : map.getFreeAreas()) { toWriteToStore.put(freeArea, null); } /* * Write everything to file. */ byte[] chunk = new byte[IO_BUFFER_SIZE]; for (FileUseMapEntry fileUseMapEntry : toWriteToStore.keySet()) { StoredEntry entry = toWriteToStore.get(fileUseMapEntry); if (entry == null) { int size = Ints.checkedCast(fileUseMapEntry.getSize()); directWrite(fileUseMapEntry.getStart(), new byte[size]); } else { writeEntry(entry, fileUseMapEntry.getStart(), chunk); } } } /** * Recomputes the central directory and EOCD and notifies extensions that all entries have been * written. Extensions may further modify the archive and this may require the directory and EOCD * to be recomputed several times. * *

This method finishes when the central directory and EOCD have both been computed and written * to the zip file and all extensions have been notified using {@link * ZFileExtension#entriesWritten()}. */ private void recomputeAndWriteCentralDirectoryAndEocd() throws IOException { boolean changedAnything = false; boolean hasCentralDirectory; int extensionBugDetector = MAXIMUM_EXTENSION_CYCLE_COUNT; do { // Try to compute the central directory and EOCD. Computing the central directory may end // with directoryEntry == null if there are no entries in the zip. if (directoryEntry == null) { reopenRw(); changedAnything = true; computeCentralDirectory(); } if (eocdEntry == null) { // It is fine to call computeEocd even if directoryEntry == null as long as the zip has // no files. reopenRw(); changedAnything = true; computeEocd(); } hasCentralDirectory = (directoryEntry != null); notify( ext -> { ext.entriesWritten(); return null; }); if ((--extensionBugDetector) == 0) { throw new IOException( "Extensions keep resetting the central directory. This is " + "probably a bug."); } } while ((hasCentralDirectory && directoryEntry == null) || eocdEntry == null); if (changedAnything) { reopenRw(); appendCentralDirectory(); appendEocd(); } } /** * Reorganizes the zip so that there are no gaps between files bigger than {@link * #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #coverEmptySpaceUsingExtraField} is set to * {@code true}. * *

Essentially, this makes sure we can cover any empty space with the extra field, given that * the local extra field is limited to {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE}. If an entry * is too far from the previous one, it is removed and re-added. * * @throws IOException failed to repack */ private void packIfNecessary() throws IOException { if (!coverEmptySpaceUsingExtraField) { return; } SortedSet> entriesByLocation = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START); entriesByLocation.addAll(entries.values()); for (FileUseMapEntry entry : entriesByLocation) { StoredEntry storedEntry = entry.getStore(); Preconditions.checkNotNull(storedEntry, "Entry at %s is null", entry.getStart()); FileUseMapEntry before = map.before(entry); if (before == null || !before.isFree()) { continue; } int localExtraSize = storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize()); if (localExtraSize > MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE) { /* * This entry is too far from the previous one. Remove it and re-add it to the * zip file. */ reAdd(storedEntry, PositionHint.LOWEST_OFFSET); } } } /** * Removes a stored entry from the zip and adds it back again. This will force the entry to be * loaded into memory and repositioned in the zip file. It will also mark the archive as being * dirty. * * @param entry the entry * @param positionHint hint to where the file should be positioned when re-adding * @throws IOException failed to load the entry into memory */ private void reAdd(StoredEntry entry, PositionHint positionHint) throws IOException { String name = entry.getCentralDirectoryHeader().getName(); FileUseMapEntry mapEntry = entries.get(name); Preconditions.checkNotNull(mapEntry); Preconditions.checkState(mapEntry.getStore() == entry); entry.loadSourceIntoMemory(); map.remove(mapEntry); entries.remove(name); FileUseMapEntry positioned = positionInFile(entry, positionHint); entries.put(name, positioned); dirty = true; } /** * Invoked from {@link StoredEntry} when entry has changed in a way that forces the local header * to be rewritten * * @param entry the entry that changed * @param resized was the local header resized? * @throws IOException failed to load the entry into memory */ void localHeaderChanged(StoredEntry entry, boolean resized) throws IOException { dirty = true; if (resized) { reAdd(entry, PositionHint.ANYWHERE); } } /** Invoked when the central directory has changed and needs to be rewritten. */ void centralDirectoryChanged() { dirty = true; deleteDirectoryAndEocd(); } /** Updates the file and closes it. */ @Override public void close() throws IOException { // We need to make sure to release raf, otherwise we end up locking the file on // Windows. Use try-with-resources to handle exception suppressing. try (Closeable ignored = this::innerClose) { if (!readOnly) { update(); } storage.close(); } notify( ext -> { ext.closed(); return null; }); } /** * Removes the Central Directory and EOCD from the file. This will free space for new entries as * well as allowing the zip file to be truncated if files have been removed. * *

This method does not mark the zip as dirty. */ private void deleteDirectoryAndEocd() { if (directoryEntry != null) { map.remove(directoryEntry); directoryEntry = null; } if (eocdEntry != null) { map.remove(eocdEntry); Eocd eocd = eocdEntry.getStore(); Verify.verify(eocd != null); eocdComment = eocd.getComment(); eocdEntry = null; } } /** * Writes an entry's data in the zip file. This includes everything: the local header and the data * itself. After writing, the entry is updated with the offset and its source replaced with a * source that reads from the zip file. * * @param entry the entry to write * @param offset the offset at which the entry should be written * @throws IOException failed to write the entry */ private void writeEntry(StoredEntry entry, long offset, byte[] chunk) throws IOException { Preconditions.checkArgument( entry.getDataDescriptorType() == DataDescriptorType.NO_DATA_DESCRIPTOR, "Cannot write entries with a data " + "descriptor."); Preconditions.checkNotNull(raf, "raf == null"); Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); int r; // Put header data to the beginning of buffer // LSPatch: write extra entries in the extra field if it's a linking int localHeaderSize = entry.getLocalHeaderSize(); for (var segment : entry.getLocalExtra().getSegments()) { if (segment instanceof ExtraField.LinkingEntrySegment) { ((ExtraField.LinkingEntrySegment) segment).setOffset(localHeaderSize, offset); } } int readOffset = entry.toHeaderData(chunk); assert localHeaderSize == readOffset; long writeOffset = offset; try (InputStream is = entry.getSource().getRawByteSource().openStream()) { while ((r = is.read(chunk, readOffset, chunk.length - readOffset)) >= 0 || readOffset > 0) { int toWrite = (r == -1 ? 0 : r) + readOffset; directWrite(writeOffset, chunk, 0, toWrite); writeOffset += toWrite; readOffset = 0; } } /* * Set the entry's offset and create the entry source. */ entry.replaceSourceFromZip(offset); } /** * Computes the central directory. The central directory must not have been computed yet. When * this method finishes, the central directory has been computed {@link #directoryEntry}, unless * the directory is empty in which case {@link #directoryEntry} is left as {@code null}. Nothing * is written to disk as a result of this method's invocation. * * @throws IOException failed to append the central directory */ private void computeCentralDirectory() throws IOException { Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); Preconditions.checkNotNull(raf, "raf == null"); Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); Set newStored = Sets.newHashSet(); for (FileUseMapEntry mapEntry : entries.values()) { newStored.add(mapEntry.getStore()); } newStored.addAll(linkingEntries); /* * Make sure we truncate the map before computing the central directory's location since * the central directory is the last part of the file. */ map.truncate(); CentralDirectory newDirectory = CentralDirectory.makeFromEntries(newStored, this); byte[] newDirectoryBytes = newDirectory.toBytes(); long directoryOffset = map.size() + extraDirectoryOffset; map.extend(directoryOffset + newDirectoryBytes.length); if (newDirectoryBytes.length > 0) { directoryEntry = map.add(directoryOffset, directoryOffset + newDirectoryBytes.length, newDirectory); } } /** * Writes the central directory to the end of the zip file. {@link #directoryEntry} may be {@code * null} only if there are no files in the archive. * * @throws IOException failed to append the central directory */ private void appendCentralDirectory() throws IOException { Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); Preconditions.checkNotNull(raf, "raf == null"); if (entries.isEmpty()) { Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); return; } Preconditions.checkNotNull(directoryEntry, "directoryEntry != null"); CentralDirectory newDirectory = directoryEntry.getStore(); Preconditions.checkNotNull(newDirectory, "newDirectory != null"); byte[] newDirectoryBytes = newDirectory.toBytes(); long directoryOffset = directoryEntry.getStart(); /* * It is fine to seek beyond the end of file. Seeking beyond the end of file will not extend * the file. Even if we do not have any directory data to write, the extend() call below * will force the file to be extended leaving exactly extraDirectoryOffset bytes empty at * the beginning. */ directWrite(directoryOffset, newDirectoryBytes); } /** * Obtains the byte array representation of the central directory. The central directory must have * been already computed. If there are no entries in the zip, the central directory will be empty. * * @return the byte representation, or an empty array if there are no entries in the zip * @throws IOException failed to compute the central directory byte representation */ public byte[] getCentralDirectoryBytes() throws IOException { if (entries.isEmpty()) { Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); return new byte[0]; } Preconditions.checkNotNull(directoryEntry, "directoryEntry == null"); CentralDirectory cd = directoryEntry.getStore(); Preconditions.checkNotNull(cd, "cd == null"); return cd.toBytes(); } /** * Computes the EOCD. This creates a new {@link #eocdEntry}. The central directory must already be * written. If {@link #directoryEntry} is {@code null}, then the zip file must not have any * entries. * * @throws IOException failed to write the EOCD */ private void computeEocd() throws IOException { Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); Preconditions.checkNotNull(raf, "raf == null"); if (directoryEntry == null) { Preconditions.checkState(entries.isEmpty(), "directoryEntry == null && !entries.isEmpty()"); } long dirStart; long dirSize = 0; if (directoryEntry != null) { CentralDirectory directory = directoryEntry.getStore(); Preconditions.checkNotNull(directory, "Central directory is null"); dirStart = directoryEntry.getStart(); dirSize = directoryEntry.getSize(); Verify.verify(directory.getEntries().size() == entries.size() + linkingEntries.size()); } else { /* * If we do not have a directory, then we must leave any requested offset empty. */ dirStart = extraDirectoryOffset; } Verify.verify(eocdComment != null); Eocd eocd = new Eocd(entries.size() + linkingEntries.size(), dirStart, dirSize, eocdComment); eocdComment = null; byte[] eocdBytes = eocd.toBytes(); long eocdOffset = map.size(); map.extend(eocdOffset + eocdBytes.length); eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd); } /** * Writes the EOCD to the end of the zip file. This creates a new {@link #eocdEntry}. The central * directory must already be written. If {@link #directoryEntry} is {@code null}, then the zip * file must not have any entries. * * @throws IOException failed to write the EOCD */ private void appendEocd() throws IOException { Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); Preconditions.checkNotNull(raf, "raf == null"); Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); Eocd eocd = eocdEntry.getStore(); Preconditions.checkNotNull(eocd, "eocd == null"); byte[] eocdBytes = eocd.toBytes(); long eocdOffset = eocdEntry.getStart(); directWrite(eocdOffset, eocdBytes); } /** * Obtains the byte array representation of the EOCD. The EOCD must have already been computed for * this method to be invoked. * * @return the byte representation of the EOCD * @throws IOException failed to obtain the byte representation of the EOCD */ public byte[] getEocdBytes() throws IOException { Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); Eocd eocd = eocdEntry.getStore(); Preconditions.checkNotNull(eocd, "eocd == null"); return eocd.toBytes(); } /** * Closes the file, if it is open. * * @throws IOException failed to close the file */ private void innerClose() throws IOException { if (state == ZipFileState.CLOSED) { return; } Verify.verifyNotNull(raf, "raf == null"); raf.close(); raf = null; state = ZipFileState.CLOSED; if (closedControl == null) { closedControl = new CachedFileContents<>(file); } closedControl.closed(null); } /** * If the zip file is closed, opens it in read-only mode. If it is already open, does nothing. In * general, it is not necessary to directly invoke this method. However, if directly reading the * zip file using, for example {@link #directRead(long, byte[])}, then this method needs to be * called. * * @throws IOException failed to open the file */ public void openReadOnlyIfClosed() throws IOException { if (state != ZipFileState.CLOSED) { return; } state = ZipFileState.OPEN_RO; raf = new RandomAccessFile(file, "r"); } /** * Opens (or reopens) the zip file as read-write. This method will ensure that {@link #raf} is not * null and open for writing. * * @throws IOException failed to open the file, failed to close it or the file was closed and has * been modified outside the control of this object */ private void reopenRw() throws IOException { // We an never open a file RW in read-only mode. We should never get this far, though. Verify.verify(!readOnly); if (state == ZipFileState.OPEN_RW) { return; } boolean wasClosed; if (state == ZipFileState.OPEN_RO) { /* * ReadAccessFile does not have a way to reopen as RW so we have to close it and * open it again. */ innerClose(); wasClosed = false; } else { wasClosed = true; } Verify.verify(state == ZipFileState.CLOSED, "state != ZpiFileState.CLOSED"); Verify.verify(raf == null, "raf != null"); if (closedControl != null && !closedControl.isValid()) { throw new IOException( "File '" + file.getAbsolutePath() + "' has been modified " + "by an external application."); } raf = new RandomAccessFile(file, "rw"); state = ZipFileState.OPEN_RW; /* * Now that we've open the zip and are ready to write, clear out any data descriptors * in the zip since we don't need them and they take space in the archive. */ for (StoredEntry entry : entries()) { dirty |= entry.removeDataDescriptor(); } if (wasClosed) { notify(ZFileExtension::open); } } /** * Equivalent to call {@link #add(String, InputStream, boolean)} using {@code true} as {@code * mayCompress}. * * @param name the file name (i.e., path); paths should be defined using slashes and the * name should not end in slash * @param stream the source for the file's data * @throws IOException failed to read the source data * @throws IllegalStateException if the file is in read-only mode */ public void add(String name, InputStream stream) throws IOException { checkNotInReadOnlyMode(); add(name, stream, true); } /** * Adds a file to the archive. * *

Adding the file will not update the archive immediately. Updating will only happen when the * {@link #update()} method is invoked. * *

Adding a file with the same name as an existing file will replace that file in the archive. * * @param name the file name (i.e., path); paths should be defined using slashes and the * name should not end in slash * @param stream the source for the file's data * @param mayCompress can the file be compressed? This flag will be ignored if the alignment rules * force the file to be aligned, in which case the file will not be compressed. * @throws IOException failed to read the source data * @throws IllegalStateException if the file is in read-only mode */ public StoredEntry add(String name, InputStream stream, boolean mayCompress) throws IOException { return add(name, storage.fromStream(stream), mayCompress); } /** * Adds a file to the archive. * *

Adding the file will not update the archive immediately. Updating will only happen when the * {@link #update()} method is invoked. * *

Adding a file with the same name as an existing file will replace that file in the archive. * * @param name the file name (i.e., path); paths should be defined using slashes and the * name should not end in slash * @param source the source for the file's data * @param mayCompress can the file be compressed? This flag will be ignored if the alignment rules * force the file to be aligned, in which case the file will not be compressed. * @throws IOException failed to read the source data * @throws IllegalStateException if the file is in read-only mode */ public void add(String name, ByteSource source, boolean mayCompress) throws IOException { Optional sizeBytes = source.sizeIfKnown(); if (!sizeBytes.isPresent()) { throw new IllegalArgumentException("Can only add ByteSources with known size"); } add(name, new CloseableDelegateByteSource(source, sizeBytes.get()), mayCompress); } private StoredEntry add(String name, CloseableByteSource source, boolean mayCompress) throws IOException { checkNotInReadOnlyMode(); /* * Clean pending background work, if needed. */ processAllReadyEntries(); return add(makeStoredEntry(name, source, mayCompress)); } public void addLink(StoredEntry linkedEntry, String dstName) throws IOException { addNestedLink(linkedEntry, dstName, null, 0L, false); } void addNestedLink(StoredEntry linkedEntry, String dstName, StoredEntry nestedEntry, long nestedOffset, boolean dummy) throws IOException { Preconditions.checkArgument(linkedEntry != null, "linkedEntry is null"); Preconditions.checkArgument(linkedEntry.getCentralDirectoryHeader().getOffset() < 0, "linkedEntry is not new file"); Preconditions.checkArgument(!linkedEntry.isLinkingEntry(), "linkedEntry is a linking entry"); var linkingEntry = new StoredEntry(dstName, this, storage, linkedEntry, nestedEntry, nestedOffset, dummy); linkingEntries.add(linkingEntry); linkedEntry.setLocalExtraNoNotify(new ExtraField(ImmutableList.builder().add(linkedEntry.getLocalExtra().getSegments().toArray(new ExtraField.Segment[0])).add(new ExtraField.LinkingEntrySegment(linkingEntry)).build())); reAdd(linkedEntry, PositionHint.LOWEST_OFFSET); } public NestedZip addNestedZip(NestedZip.NameCallback name, File src, boolean mayCompress) throws IOException { return new NestedZip(name, this, src, mayCompress); } /** * Adds a {@link StoredEntry} to the zip. The entry is not immediately added to {@link #entries} * because data may not yet be available. Instead, it is placed under {@link #uncompressedEntries} * and later moved to {@link #processAllReadyEntries()} when done. * *

This method invokes {@link #processAllReadyEntries()} to move the entry if it has already * been computed so, if there is no delay in compression, and no more files are in waiting queue, * then the entry is added to {@link #entries} immediately. * * @param newEntry the entry to add * @throws IOException failed to process this entry (or a previous one whose future only completed * now) */ private StoredEntry add(final StoredEntry newEntry) throws IOException { uncompressedEntries.add(newEntry); processAllReadyEntries(); return newEntry; } /** * Creates a stored entry. This does not add the entry to the zip file, it just creates the {@link * StoredEntry} object. * * @param name the name of the entry * @param source the source with the entry's data * @param mayCompress can the entry be compressed? * @return the created entry * @throws IOException failed to create the entry */ private StoredEntry makeStoredEntry(String name, CloseableByteSource source, boolean mayCompress) throws IOException { long crc32 = source.hash(Hashing.crc32()).padToLong(); boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name); SettableFuture compressInfo = SettableFuture.create(); GPFlags flags = GPFlags.make(encodeWithUtf8); CentralDirectoryHeader newFileData = new CentralDirectoryHeader( name, EncodeUtils.encode(name, flags), source.size(), compressInfo, flags, this); newFileData.setCrc32(crc32); /* * Create the new entry and sets its data source. Offset should be set to -1 automatically * because this is a new file. With offset set to -1, StoredEntry does not try to verify the * local header. Since this is a new file, there is no local header and not checking it is * what we want to happen. */ Verify.verify(newFileData.getOffset() == -1); return new StoredEntry( newFileData, this, createSources(mayCompress, source, compressInfo, newFileData), storage); } /** * Creates the processed and raw sources for an entry. * * @param mayCompress can the entry be compressed? * @param source the entry's data (uncompressed) * @param compressInfo the compression info future that will be set when the raw entry is created * and the {@link CentralDirectoryHeaderCompressInfo} object can be created * @param newFileData the central directory header for the new file * @return the sources whose data may or may not be already defined * @throws IOException failed to create the raw sources */ private ProcessedAndRawByteSources createSources( boolean mayCompress, CloseableByteSource source, SettableFuture compressInfo, CentralDirectoryHeader newFileData) throws IOException { if (mayCompress) { ListenableFuture result = compressor.compress(source, storage); Futures.addCallback( result, new FutureCallback() { @Override public void onSuccess(CompressionResult result) { compressInfo.set( new CentralDirectoryHeaderCompressInfo( newFileData, result.getCompressionMethod(), result.getSize())); } @Override public void onFailure(Throwable t) { compressInfo.setException(t); } }, MoreExecutors.directExecutor()); ListenableFuture compressedByteSourceFuture = Futures.transform(result, CompressionResult::getSource, MoreExecutors.directExecutor()); LazyDelegateByteSource compressedByteSource = new LazyDelegateByteSource(compressedByteSourceFuture); return new ProcessedAndRawByteSources(source, compressedByteSource); } else { compressInfo.set( new CentralDirectoryHeaderCompressInfo( newFileData, CompressionMethod.STORE, source.size())); return new ProcessedAndRawByteSources(source, source); } } /** * Moves all ready entries from {@link #uncompressedEntries} to {@link #entries}. It will stop as * soon as entry whose future has not been completed is found. * * @throws IOException the exception reported in the future computation, if any, or failed to add * a file to the archive */ private void processAllReadyEntries() throws IOException { /* * Many things can happen during addToEntries(). Because addToEntries() fires * notifications to extensions, other files can be added, removed, etc. Ee are *not* * guaranteed that new stuff does not get into uncompressedEntries: add() will still work * and will add new entries in there. * * However -- important -- processReadyEntries() may be invoked during addToEntries() * because of the extension mechanism. This means that stuff *can* be removed from * uncompressedEntries and moved to entries during addToEntries(). */ while (!uncompressedEntries.isEmpty()) { StoredEntry next = uncompressedEntries.get(0); CentralDirectoryHeader cdh = next.getCentralDirectoryHeader(); Future compressionInfo = cdh.getCompressionInfo(); if (!compressionInfo.isDone()) { /* * First entry in queue is not yet complete. We can't do anything else. */ return; } uncompressedEntries.remove(0); try { compressionInfo.get(); } catch (InterruptedException e) { throw new IOException( "Impossible I/O exception: get for already computed " + "future throws InterruptedException", e); } catch (ExecutionException e) { throw new IOException("Failed to obtain compression information for entry", e); } addToEntries(next); } } /** * Waits until {@link #uncompressedEntries} is empty. * * @throws IOException the exception reported in the future computation, if any, or failed to add * a file to the archive */ private void processAllReadyEntriesWithWait() throws IOException { processAllReadyEntries(); while (!uncompressedEntries.isEmpty()) { /* * Wait for the first future to complete and then try again. Keep looping until we're * done. */ StoredEntry first = uncompressedEntries.get(0); CentralDirectoryHeader cdh = first.getCentralDirectoryHeader(); cdh.getCompressionInfoWithWait(); processAllReadyEntries(); } } /** * Adds a new file to {@link #entries}. This is actually added to the zip and its space allocated * in the {@link #map}. * * @param newEntry the new entry to add * @throws IOException failed to add the file */ private void addToEntries(final StoredEntry newEntry) throws IOException { Preconditions.checkArgument( newEntry.getDataDescriptorType() == DataDescriptorType.NO_DATA_DESCRIPTOR, "newEntry has data descriptor"); /* * If there is a file with the same name in the archive, remove it. We remove it by * calling delete() on the entry (this is the public API to remove a file from the archive). * StoredEntry.delete() will call {@link ZFile#delete(StoredEntry, boolean)} to perform * data structure cleanup. */ FileUseMapEntry toReplace = entries.get(newEntry.getCentralDirectoryHeader().getName()); final StoredEntry replaceStore; if (toReplace != null) { replaceStore = toReplace.getStore(); Preconditions.checkNotNull( replaceStore, "File to replace at %s is null", toReplace.getStart()); replaceStore.delete(false); } else { replaceStore = null; } FileUseMapEntry fileUseMapEntry = positionInFile(newEntry, PositionHint.ANYWHERE); entries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry); dirty = true; notify(ext -> ext.added(newEntry, replaceStore)); } /** * Finds a location in the zip where this entry will be added to and create the map entry. This * method cannot be called if there is already a map entry for the given entry (if you do that, * then you're doing something wrong somewhere). * *

This may delete the central directory and EOCD (if it deletes one, it deletes the other) if * there is no space before the central directory. Otherwise, the file would be added after the * central directory. This would force a new central directory to be written when updating the * file and would create a hole in the zip. Me no like holes. Holes are evil. * * @param entry the entry to place in the zip * @param positionHint hint to where the file should be positioned * @return the position in the file where the entry should be placed */ private FileUseMapEntry positionInFile(StoredEntry entry, PositionHint positionHint) throws IOException { deleteDirectoryAndEocd(); long size = entry.getInFileSize(); int localHeaderSize = entry.getLocalHeaderSize(); int alignment = chooseAlignment(entry); FileUseMap.PositionAlgorithm algorithm; switch (positionHint) { case LOWEST_OFFSET: algorithm = FileUseMap.PositionAlgorithm.FIRST_FIT; break; case ANYWHERE: algorithm = FileUseMap.PositionAlgorithm.BEST_FIT; break; default: throw new AssertionError(); } long newOffset = map.locateFree(size, localHeaderSize, alignment, algorithm); long newEnd = newOffset + entry.getInFileSize(); if (newEnd > map.size()) { map.extend(newEnd); } return map.add(newOffset, newEnd, entry); } /** * Determines what is the alignment value of an entry. * * @param entry the entry * @return the alignment value, {@link AlignmentRule#NO_ALIGNMENT} if there is no alignment * required for the entry * @throws IOException failed to determine the alignment */ private int chooseAlignment(StoredEntry entry) throws IOException { CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader(); CentralDirectoryHeaderCompressInfo compressionInfo = cdh.getCompressionInfoWithWait(); boolean isCompressed = compressionInfo.getMethod() != CompressionMethod.STORE; if (isCompressed) { return AlignmentRule.NO_ALIGNMENT; } else { return alignmentRule.alignment(cdh.getName()); } } /** * Adds all files from another zip file, maintaining their compression. Files specified in * src that are already on this file will replace the ones in this file. However, if * their sizes and checksums are equal, they will be ignored. * *

This method will not perform any changes in itself, it will only update in-memory data * structures. To actually write the zip file, invoke either {@link #update()} or {@link * #close()}. * * @param src the source archive * @param ignoreFilter predicate that, if {@code true}, identifies files in src that * should be ignored by merging; merging will behave as if these files were not there * @throws IOException failed to read from src or write on the output * @throws IllegalStateException if the file is in read-only mode */ public void mergeFrom(ZFile src, Predicate ignoreFilter) throws IOException { checkNotInReadOnlyMode(); for (StoredEntry fromEntry : src.entries()) { if (ignoreFilter.apply(fromEntry.getCentralDirectoryHeader().getName())) { continue; } boolean replaceCurrent = true; String path = fromEntry.getCentralDirectoryHeader().getName(); FileUseMapEntry currentEntry = entries.get(path); if (currentEntry != null) { long fromSize = fromEntry.getCentralDirectoryHeader().getUncompressedSize(); long fromCrc = fromEntry.getCentralDirectoryHeader().getCrc32(); StoredEntry currentStore = currentEntry.getStore(); Preconditions.checkNotNull(currentStore, "Entry at %s is null", currentEntry.getStart()); long currentSize = currentStore.getCentralDirectoryHeader().getUncompressedSize(); long currentCrc = currentStore.getCentralDirectoryHeader().getCrc32(); if (fromSize == currentSize && fromCrc == currentCrc) { replaceCurrent = false; } } if (replaceCurrent) { CentralDirectoryHeader fromCdr = fromEntry.getCentralDirectoryHeader(); CentralDirectoryHeaderCompressInfo fromCompressInfo = fromCdr.getCompressionInfoWithWait(); CentralDirectoryHeader newFileData; try { /* * We make two changes in the central directory from the file to merge: * we reset the offset to force the entry to be written and we reset the * deferred CRC bit as we don't need the extra stuff after the file. It takes * space and is totally useless. */ newFileData = fromCdr.clone(); newFileData.setOffset(-1); newFileData.resetDeferredCrc(); } catch (CloneNotSupportedException e) { throw new IOException("Failed to clone CDR.", e); } /* * Read the data (read directly the compressed source if there is one). */ ProcessedAndRawByteSources fromSource = fromEntry.getSource(); InputStream fromInput = fromSource.getRawByteSource().openStream(); long sourceSize = fromSource.getRawByteSource().size(); if (sourceSize > Integer.MAX_VALUE) { throw new IOException("Cannot read source with " + sourceSize + " bytes."); } byte[] data = new byte[Ints.checkedCast(sourceSize)]; int read = 0; while (read < data.length) { int r = fromInput.read(data, read, data.length - read); Verify.verify(r >= 0, "There should be at least 'size' bytes in the stream."); read += r; } /* * Build the new source and wrap it around an inflater source if data came from * a compressed source. */ CloseableByteSource rawContents = storage.fromSource(fromSource.getRawByteSource()); CloseableByteSource processedContents; if (fromCompressInfo.getMethod() == CompressionMethod.DEFLATE) { //noinspection IOResourceOpenedButNotSafelyClosed processedContents = new InflaterByteSource(rawContents); } else { processedContents = rawContents; } ProcessedAndRawByteSources newSource = new ProcessedAndRawByteSources(processedContents, rawContents); /* * Add will replace any current entry with the same name. */ StoredEntry newEntry = new StoredEntry(newFileData, this, newSource, storage); add(newEntry); } } } /** * Forcibly marks this zip file as touched, forcing it to be updated when {@link #update()} or * {@link #close()} are invoked. * * @throws IllegalStateException if the file is in read-only mode */ public void touch() { checkNotInReadOnlyMode(); dirty = true; } /** * Wait for any background tasks to finish and report any errors. In general this method does not * need to be invoked directly as errors from background tasks are reported during {@link * #add(String, InputStream, boolean)}, {@link #update()} and {@link #close()}. However, if * required for some purposes, e.g., ensuring all notifications have been done to * extensions, then this method may be called. It will wait for all background tasks to complete. * * @throws IOException some background work failed */ public void finishAllBackgroundTasks() throws IOException { processAllReadyEntriesWithWait(); } /** * Realigns all entries in the zip. This is equivalent to call {@link StoredEntry#realign()} for * all entries in the zip file. * * @return has any entry been changed? Note that for entries that have not yet been written on the * file, realignment does not count as a change as nothing needs to be updated in the file; * entries that have been updated may have been recreated and the existing references outside * of {@code ZFile} may refer to {@link StoredEntry}s that are no longer valid * @throws IOException failed to realign the zip; some entries in the zip may have been lost due * to the I/O error * @throws IllegalStateException if the file is in read-only mode */ public boolean realign() throws IOException { checkNotInReadOnlyMode(); boolean anyChanges = false; for (StoredEntry entry : entries()) { anyChanges |= entry.realign(); } if (anyChanges) { dirty = true; } return anyChanges; } /** * Realigns a stored entry, if necessary. Realignment is done by removing and re-adding the file * if it was not aligned. * * @param entry the entry to realign * @return has the entry been changed? Note that if the entry has not yet been written on the * file, realignment does not count as a change as nothing needs to be updated in the file * @throws IOException failed to read/write an entry; the entry may no longer exist in the file */ boolean realign(StoredEntry entry) throws IOException { FileUseMapEntry mapEntry = entries.get(entry.getCentralDirectoryHeader().getName()); Verify.verify(entry == mapEntry.getStore()); long currentDataOffset = mapEntry.getStart() + entry.getLocalHeaderSize(); int expectedAlignment = chooseAlignment(entry); long misalignment = currentDataOffset % expectedAlignment; if (misalignment == 0) { /* * Good. File is aligned properly. */ return false; } if (entry.getCentralDirectoryHeader().getOffset() == -1) { /* * File is not aligned but it is not written. We do not really need to do much other * than find another place in the map. */ map.remove(mapEntry); long newStart = map.locateFree( mapEntry.getSize(), entry.getLocalHeaderSize(), expectedAlignment, FileUseMap.PositionAlgorithm.BEST_FIT); mapEntry = map.add(newStart, newStart + entry.getInFileSize(), entry); entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); /* * Just for safety. We're modifying the in-memory structures but the file should * already be marked as dirty. */ Verify.verify(dirty); return false; } /* * Get the entry data source, but check if we have a compressed one (we don't want to * inflate and deflate). */ CentralDirectoryHeaderCompressInfo compressInfo = entry.getCentralDirectoryHeader().getCompressionInfoWithWait(); ProcessedAndRawByteSources source = entry.getSource(); CentralDirectoryHeader clonedCdh; try { clonedCdh = entry.getCentralDirectoryHeader().clone(); } catch (CloneNotSupportedException e) { Verify.verify(false); return false; } /* * We make two changes in the central directory when realigning: * we reset the offset to force the entry to be written and we reset the * deferred CRC bit as we don't need the extra stuff after the file. It takes * space and is totally useless and we may need the extra space to realign the entry... */ clonedCdh.setOffset(-1); clonedCdh.resetDeferredCrc(); CloseableByteSource rawContents = storage.fromSource(source.getRawByteSource()); CloseableByteSource processedContents; if (compressInfo.getMethod() == CompressionMethod.DEFLATE) { //noinspection IOResourceOpenedButNotSafelyClosed processedContents = new InflaterByteSource(rawContents); } else { processedContents = rawContents; } ProcessedAndRawByteSources newSource = new ProcessedAndRawByteSources(processedContents, rawContents); /* * Add the new file. This will replace the existing one. */ StoredEntry newEntry = new StoredEntry(clonedCdh, this, newSource, storage); add(newEntry); return true; } /** * Adds an extension to this zip file. * * @param extension the listener to add * @throws IllegalStateException if the file is in read-only mode */ public void addZFileExtension(ZFileExtension extension) { checkNotInReadOnlyMode(); extensions.add(extension); } /** * Removes an extension from this zip file. * * @param extension the listener to remove * @throws IllegalStateException if the file is in read-only mode */ public void removeZFileExtension(ZFileExtension extension) { checkNotInReadOnlyMode(); extensions.remove(extension); } /** * Notifies all extensions, collecting their execution requests and running them. * * @param function the function to apply to all listeners, it will generally invoke the * notification method on the listener and return the result of that invocation * @throws IOException failed to process some extensions */ private void notify(IOExceptionFunction function) throws IOException { for (ZFileExtension fl : Lists.newArrayList(extensions)) { IOExceptionRunnable r = function.apply(fl); if (r != null) { toRun.add(r); } } if (!isNotifying) { isNotifying = true; try { while (!toRun.isEmpty()) { IOExceptionRunnable r = toRun.remove(0); r.run(); } } finally { isNotifying = false; } } } /** * Directly writes data in the zip file. Incorrect use of this method may corrupt the zip * file. Invoking this method may force the zip to be reopened in read/write mode. * * @param offset the offset at which data should be written * @param data the data to write, may be an empty array * @param start start offset in {@code data} where data to write is located * @param count number of bytes of data to write * @throws IOException failed to write the data * @throws IllegalStateException if the file is in read-only mode */ public void directWrite(long offset, byte[] data, int start, int count) throws IOException { checkNotInReadOnlyMode(); Preconditions.checkArgument(offset >= 0, "offset < 0"); Preconditions.checkArgument(start >= 0, "start >= 0"); Preconditions.checkArgument(count >= 0, "count >= 0"); if (data.length == 0) { return; } Preconditions.checkArgument(start <= data.length, "start > data.length"); Preconditions.checkArgument(start + count <= data.length, "start + count > data.length"); reopenRw(); Preconditions.checkNotNull(raf, "raf == null"); raf.seek(offset); raf.write(data, start, count); } /** * Same as {@code directWrite(offset, data, 0, data.length)}. * * @param offset the offset at which data should be written * @param data the data to write, may be an empty array * @throws IOException failed to write the data * @throws IllegalStateException if the file is in read-only mode */ public void directWrite(long offset, byte[] data) throws IOException { directWrite(offset, data, 0, data.length); } /** * Returns the current size (in bytes) of the underlying file. * * @throws IOException if an I/O error occurs */ public long directSize() throws IOException { /* * Only force a reopen if the file is closed. */ if (raf == null) { reopenRw(); Preconditions.checkNotNull(raf, "raf == null"); } return raf.length(); } /** * Directly reads data from the zip file. Invoking this method may force the zip to be reopened in * read/write mode. * * @param offset the offset at which data should be written * @param data the array where read data should be stored * @param start start position in the array where to write data to * @param count how many bytes of data can be written * @return how many bytes of data have been written or {@code -1} if there are no more bytes to be * read * @throws IOException failed to write the data */ public int directRead(long offset, byte[] data, int start, int count) throws IOException { Preconditions.checkArgument(start >= 0, "start >= 0"); Preconditions.checkArgument(count >= 0, "count >= 0"); Preconditions.checkArgument(start <= data.length, "start > data.length"); Preconditions.checkArgument(start + count <= data.length, "start + count > data.length"); return directRead(offset, ByteBuffer.wrap(data, start, count)); } /** * Directly reads data from the zip file. Invoking this method may force the zip to be reopened in * read/write mode. * * @param offset the offset from which data should be read * @param dest the output buffer to fill with data from the {@code offset}. * @return how many bytes of data have been written or {@code -1} if there are no more bytes to be * read * @throws IOException failed to write the data */ public int directRead(long offset, ByteBuffer dest) throws IOException { Preconditions.checkArgument(offset >= 0, "offset < 0"); if (!dest.hasRemaining()) { return 0; } /* * Only force a reopen if the file is closed. */ if (raf == null) { reopenRw(); Preconditions.checkNotNull(raf, "raf == null"); } raf.seek(offset); return raf.getChannel().read(dest); } /** * Same as {@code directRead(offset, data, 0, data.length)}. * * @param offset the offset at which data should be read * @param data receives the read data, may be an empty array * @throws IOException failed to read the data */ public int directRead(long offset, byte[] data) throws IOException { return directRead(offset, data, 0, data.length); } /** * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all the * requested data. * * @param offset the offset at which to start reading * @param data the array that receives the data read * @throws IOException failed to read some data or there is not enough data to read */ public void directFullyRead(long offset, byte[] data) throws IOException { directFullyRead(offset, ByteBuffer.wrap(data)); } /** * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read * all the requested data. * * @param offset the offset at which to start reading * @param dest the output buffer to fill with data * @throws IOException failed to read some data or there is not enough data to read */ public void directFullyRead(long offset, ByteBuffer dest) throws IOException { Preconditions.checkArgument(offset >= 0, "offset < 0"); if (!dest.hasRemaining()) { return; } /* * Only force a reopen if the file is closed. */ if (raf == null) { reopenRw(); Preconditions.checkNotNull(raf, "raf == null"); } FileChannel fileChannel = raf.getChannel(); while (dest.hasRemaining()) { fileChannel.position(offset); int chunkSize = fileChannel.read(dest); if (chunkSize == -1) { throw new EOFException("Failed to read " + dest.remaining() + " more bytes: premature EOF"); } offset += chunkSize; } } /** * Adds all files and directories recursively. * *

Equivalent to calling {@link #addAllRecursively(File, Predicate)} using a predicate that * always returns {@code true} * * @param file a file or directory; if it is a directory, all files and directories will be added * recursively * @throws IOException failed to some (or all ) of the files * @throws IllegalStateException if the file is in read-only mode */ public void addAllRecursively(File file) throws IOException { checkNotInReadOnlyMode(); addAllRecursively(file, f -> true); } /** * Adds all files and directories recursively. * * @param file a file or directory; if it is a directory, all files and directories will be added * recursively * @param mayCompress a function that decides whether files may be compressed * @throws IOException failed to some (or all ) of the files * @throws IllegalStateException if the file is in read-only mode */ public void addAllRecursively(File file, Predicate mayCompress) throws IOException { checkNotInReadOnlyMode(); addAllRecursively(file, file, mayCompress); } /** * Adds all files and directories recursively. * * @param file a file or directory; if it is a directory, all files and directories will be added * recursively * @param base the file/directory to compute the relative path from * @param mayCompress a function that decides whether files may be compressed * @throws IOException failed to some (or all ) of the files * @throws IllegalStateException if the file is in read-only mode */ private void addAllRecursively(File file, File base, Predicate mayCompress) throws IOException { // If we're just adding a file, do not compute a relative path, but rather use the file name // as path. String path = Objects.equal(file, base) ? file.getName() : base.toURI().relativize(file.toURI()).getPath(); /* * The case of file.isFile() is different because if file.isFile() we will add it to the * zip in the root. However, if file.isDirectory() we won't add it and add its children. */ if (file.isFile()) { boolean mayCompressFile = mayCompress.apply(file); try (Closer closer = Closer.create()) { FileInputStream fileInput = closer.register(new FileInputStream(file)); add(path, fileInput, mayCompressFile); } return; } else if (file.isDirectory()) { // Add an entry for the directory, unless it is the base. if (!file.equals(base)) { try (Closer closer = Closer.create()) { InputStream stream = closer.register(new ByteArrayInputStream(new byte[0])); add(path, stream, false); } } // Add recursively. File[] directoryContents = file.listFiles(); if (directoryContents != null) { Arrays.sort(directoryContents, (f0, f1) -> f0.getName().compareTo(f1.getName())); for (File subFile : directoryContents) { addAllRecursively(subFile, base, mayCompress); } } } } /** * Obtains the offset at which the central directory exists, or at which it will be written if the * zip file were to be flushed immediately. * * @return the offset, in bytes, where the central directory is or will be written; this value * includes any extra offset for the central directory */ public long getCentralDirectoryOffset() { if (directoryEntry != null) { return directoryEntry.getStart(); } /* * If there are no entries, the central directory is written at the start of the file. */ if (entries.isEmpty()) { return extraDirectoryOffset; } /* * The Central Directory is written after all entries. This will be at the end of the file * if the */ return map.usedSize() + extraDirectoryOffset; } /** * Obtains the size of the central directory, if the central directory is written in the zip file. * * @return the size of the central directory or {@code -1} if the central directory has not been * computed */ public long getCentralDirectorySize() { if (directoryEntry != null) { return directoryEntry.getSize(); } if (entries.isEmpty()) { return 0; } return 1; } /** * Obtains the offset of the EOCD record, if the EOCD has been written to the file. * * @return the offset of the EOCD or {@code -1} if none exists yet */ public long getEocdOffset() { if (eocdEntry == null) { return -1; } return eocdEntry.getStart(); } /** * Obtains the size of the EOCD record, if the EOCD has been written to the file. * * @return the size of the EOCD of {@code -1} it none exists yet */ public long getEocdSize() { if (eocdEntry == null) { return -1; } return eocdEntry.getSize(); } /** * Obtains the comment in the EOCD. * * @return the comment exactly as it was encoded in the EOCD, no encoding conversion is done */ public byte[] getEocdComment() { if (eocdEntry == null) { Verify.verify(eocdComment != null); byte[] eocdCommentCopy = new byte[eocdComment.length]; System.arraycopy(eocdComment, 0, eocdCommentCopy, 0, eocdComment.length); return eocdCommentCopy; } Eocd eocd = eocdEntry.getStore(); Verify.verify(eocd != null); return eocd.getComment(); } /** * Sets the comment in the EOCD. * * @param comment the new comment; no conversion is done, these exact bytes will be placed in the * EOCD comment * @throws IllegalStateException if file is in read-only mode */ public void setEocdComment(byte[] comment) { checkNotInReadOnlyMode(); if (comment.length > MAX_EOCD_COMMENT_SIZE) { throw new IllegalArgumentException( "EOCD comment size (" + comment.length + ") is larger than the maximum allowed (" + MAX_EOCD_COMMENT_SIZE + ")"); } // Check if the EOCD signature appears anywhere in the comment we need to check if it // is valid. for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) { // Remember: little endian... if (comment[i] == EOCD_SIGNATURE[3] && comment[i + 1] == EOCD_SIGNATURE[2] && comment[i + 2] == EOCD_SIGNATURE[1] && comment[i + 3] == EOCD_SIGNATURE[0]) { // We found a possible EOCD signature at position i. Try to read it. ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i); try { new Eocd(bytes); throw new IllegalArgumentException( "Position " + i + " of the comment contains a valid EOCD record."); } catch (IOException e) { // Fine, this is an invalid record. Move along... } } } deleteDirectoryAndEocd(); eocdComment = new byte[comment.length]; System.arraycopy(comment, 0, eocdComment, 0, comment.length); dirty = true; } /** * Sets an extra offset for the central directory. See class description for details. Changing * this value will mark the file as dirty and force a rewrite of the central directory when * updated. * * @param offset the offset or {@code 0} to write the central directory at its current location * @throws IllegalStateException if file is in read-only mode */ public void setExtraDirectoryOffset(long offset) { checkNotInReadOnlyMode(); Preconditions.checkArgument(offset >= 0, "offset < 0"); if (extraDirectoryOffset != offset) { extraDirectoryOffset = offset; deleteDirectoryAndEocd(); dirty = true; } } /** * Obtains the extra offset for the central directory. See class description for details. * * @return the offset or {@code 0} if no offset is set */ public long getExtraDirectoryOffset() { return extraDirectoryOffset; } /** * Obtains whether this {@code ZFile} is ignoring timestamps. * * @return are the timestamps being ignored? */ public boolean areTimestampsIgnored() { return noTimestamps; } /** * Sorts all files in the zip. This will force all files to be loaded and will wait for all * background tasks to complete. Sorting files is never done implicitly and will operate in memory * only (maybe reading files from the zip disk into memory, if needed). It will leave the zip in * dirty state, requiring a call to {@link #update()} to force the entries to be written to disk. * * @throws IOException failed to load or move a file in the zip * @throws IllegalStateException if file is in read-only mode */ public void sortZipContents() throws IOException { checkNotInReadOnlyMode(); reopenRw(); processAllReadyEntriesWithWait(); Verify.verify(uncompressedEntries.isEmpty()); SortedSet sortedEntries = Sets.newTreeSet(StoredEntry.COMPARE_BY_NAME); for (FileUseMapEntry fmEntry : entries.values()) { StoredEntry entry = fmEntry.getStore(); Preconditions.checkNotNull(entry); sortedEntries.add(entry); entry.loadSourceIntoMemory(); map.remove(fmEntry); } entries.clear(); for (StoredEntry entry : sortedEntries) { String name = entry.getCentralDirectoryHeader().getName(); FileUseMapEntry positioned = positionInFile(entry, PositionHint.LOWEST_OFFSET); entries.put(name, positioned); } dirty = true; } /** * Obtains the filesystem path to the zip file. * * @return the file that may or may not exist (depending on whether something existed there before * the zip was created and on whether the zip has been updated or not) */ public File getFile() { return file; } public DataSource asDataSource() throws IOException { if (raf == null) { reopenRw(); Preconditions.checkNotNull(raf, "raf == null"); } return DataSources.asDataSource(this.raf); } public DataSource asDataSource(long offset, long size) throws IOException { if (raf == null) { reopenRw(); Preconditions.checkNotNull(raf, "raf == null"); } return DataSources.asDataSource(this.raf, offset, size); } /** * Creates a new verify log. * * @return the new verify log */ VerifyLog makeVerifyLog() { VerifyLog log = verifyLogFactory.get(); Preconditions.checkNotNull(log, "log == null"); return log; } /** * Obtains the zip file's verify log. * * @return the verify log */ VerifyLog getVerifyLog() { return verifyLog; } /** * Are there in-memory changes that have not been written to the zip file? * *

Waits for all pending processing which may make changes. */ public boolean hasPendingChangesWithWait() throws IOException { processAllReadyEntriesWithWait(); return dirty; } /** * Obtains the storage used by the zip to store data. * * @return the storage object that should only be used to query data; using this storage for any * purposes other than statistics may have undefined results */ public ByteStorage getStorage() { return storage; } /** Hint to where files should be positioned. */ enum PositionHint { /** File may be positioned anywhere, caller doesn't care. */ ANYWHERE, /** File should be positioned at the lowest offset possible. */ LOWEST_OFFSET } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZFileExtension.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; import java.io.IOException; import javax.annotation.Nullable; /** * An extension of a {@link ZFile}. Extensions are notified when files are open, updated, closed and * when files are added or removed from the zip. These notifications are received after the zip has * been updated in memory for open, when files are added or removed and when the zip has been * updated on disk or closed. * *

An extension is also notified before the file is updated, allowing it to modify the file * before the update happens. If it does, then all extensions are notified of the changes on the zip * file. Because the order of the notifications is preserved, all extensions are notified in the * same order. For example, if two extensions E1 and E2 are registered and they both add a file at * update time, this would be the flow: * *

    *
  • E1 receives {@code beforeUpdate} notification. *
  • E1 adds file F1 to the zip (notifying the addition is suspended because another * notification is in progress). *
  • E2 receives {@code beforeUpdate} notification. *
  • E2 adds file F2 to the zip (notifying the addition is suspended because another * notification is in progress). *
  • E1 is notified F1 was added. *
  • E2 is notified F1 was added. *
  • E1 is notified F2 was added. *
  • E2 is notified F2 was added. *
  • (zip file is updated on disk) *
  • E1 is notified the zip was updated. *
  • E2 is notified the zip was updated. *
* *

An extension should not modify the zip file when notified of changes. If allowed, this would * break event notification order in case multiple extensions are registered with the zip file. To * allow performing changes to the zip file, all notification method return a {@code * IOExceptionRunnable} that is invoked when {@link ZFile} has finished notifying all extensions. */ public abstract class ZFileExtension { /** * The zip file has been open and the zip's contents have been read. The default implementation * does nothing and returns {@code null}. * * @return an optional runnable to run when notification of all listeners has ended * @throws IOException failed to process the event */ @Nullable public IOExceptionRunnable open() throws IOException { return null; } /** * The zip will be updated. This method allows the extension to register changes to the zip file * before the file is written. The default implementation does nothing and returns {@code null}. * *

After this notification is received, the extension will receive further {@link * #added(StoredEntry, StoredEntry)} and {@link #removed(StoredEntry)} notifications if it or * other extensions add or remove files before update. * *

When no more files are updated, the {@link #entriesWritten()} notification is sent. * * @return an optional runnable to run when notification of all listeners has ended * @throws IOException failed to process the event */ @Nullable public IOExceptionRunnable beforeUpdate() throws IOException { return null; } /** * This notification is sent when all entries have been written in the file but the central * directory and the EOCD have not yet been written. No entries should be added, removed or * updated during this notification. If this method forces an update of either the central * directory or EOCD, then this method will be invoked again for all extensions with the new * central directory and EOCD. * *

After this notification, {@link #updated()} is sent. * * @throws IOException failed to process the event */ public void entriesWritten() throws IOException {} /** * The zip file has been updated on disk. The default implementation does nothing. * * @throws IOException failed to perform update tasks */ public void updated() throws IOException {} /** * The zip file has been closed. Note that if {@link ZFile#close()} requires that the zip file be * updated (because it had in-memory changes), {@link #updated()} will be called before this * method. The default implementation does nothing. */ public void closed() {} /** * A new entry has been added to the zip, possibly replacing an entry in there. The default * implementation does nothing and returns {@code null}. * * @param entry the entry that was added * @param replaced the entry that was replaced, if any * @return an optional runnable to run when notification of all listeners has ended */ @Nullable public IOExceptionRunnable added(StoredEntry entry, @Nullable StoredEntry replaced) { return null; } /** * An entry has been removed from the zip. This method is not invoked for entries that have been * replaced. Those entries are notified using replaced in {@link #added(StoredEntry, * StoredEntry)}. The default implementation does nothing and returns {@code null}. * * @param entry the entry that was deleted * @return an optional runnable to run when notification of all listeners has ended */ @Nullable public IOExceptionRunnable removed(StoredEntry entry) { return null; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZFileOptions.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.bytestorage.ByteStorageFactory; import com.android.tools.build.apkzlib.bytestorage.ChunkBasedByteStorageFactory; import com.android.tools.build.apkzlib.bytestorage.OverflowToDiskByteStorageFactory; import com.android.tools.build.apkzlib.bytestorage.TemporaryDirectory; import com.android.tools.build.apkzlib.zip.compress.DeflateExecutionCompressor; import com.android.tools.build.apkzlib.zip.utils.ByteTracker; import com.google.common.base.Supplier; import java.util.zip.Deflater; /** Options to create a {@link ZFile}. */ public class ZFileOptions { /** The storage to use. */ private ByteStorageFactory storageFactory; /** The compressor to use. */ private Compressor compressor; /** Should timestamps be zeroed? */ private boolean noTimestamps; /** The alignment rule to use. */ private AlignmentRule alignmentRule; /** Should the extra field be used to cover empty space? */ private boolean coverEmptySpaceUsingExtraField; /** Should files be automatically sorted before update? */ private boolean autoSortFiles; /** * Skip expensive validation during {@link ZFile} creation? * *

During incremental build we are absolutely sure that the zip file is valid, so we do not * have to spend time verifying different fields (some of these checks are relatively expensive * and should be skipped if possible for performance) */ private boolean skipValidation; /** Factory creating verification logs to use. */ private Supplier verifyLogFactory; /** * Whether to always generate the MANIFEST.MF file regardless whether the APK will be signed with * v1 signing scheme (i.e. jar signing). */ private boolean alwaysGenerateJarManifest; /** Creates a new options object. All options are set to their defaults. */ public ZFileOptions() { storageFactory = new ChunkBasedByteStorageFactory( new OverflowToDiskByteStorageFactory(TemporaryDirectory::newSystemTemporaryDirectory)); compressor = new DeflateExecutionCompressor(Runnable::run, Deflater.DEFAULT_COMPRESSION); alignmentRule = AlignmentRules.compose(); verifyLogFactory = VerifyLogs::devNull; // We set this to true because many utilities stream the zip and expect no space between entries // in the zip file. coverEmptySpaceUsingExtraField = true; skipValidation = false; // True by default for backwards compatibility. alwaysGenerateJarManifest = true; } /** * Obtains the ZFile's byte storage factory. * * @return the factory used to create byte storages used to store data */ public ByteStorageFactory getStorageFactory() { return storageFactory; } @Deprecated public ByteTracker getTracker() { return new ByteTracker(); } /** * Sets the byte storage factory to use. * * @param storage the factory to use to create storage for new instances of {@link ZFile} created * for these options. */ public ZFileOptions setStorageFactory(ByteStorageFactory storage) { this.storageFactory = storage; return this; } /** * Obtains the compressor to use. * * @return the compressor */ public Compressor getCompressor() { return compressor; } /** * Sets the compressor to use. * * @param compressor the compressor */ public ZFileOptions setCompressor(Compressor compressor) { this.compressor = compressor; return this; } /** * Obtains whether timestamps should be zeroed. * * @return should timestamps be zeroed? */ public boolean getNoTimestamps() { return noTimestamps; } /** * Sets whether timestamps should be zeroed. * * @param noTimestamps should timestamps be zeroed? */ public ZFileOptions setNoTimestamps(boolean noTimestamps) { this.noTimestamps = noTimestamps; return this; } /** * Obtains the alignment rule. * * @return the alignment rule */ public AlignmentRule getAlignmentRule() { return alignmentRule; } /** * Sets the alignment rule. * * @param alignmentRule the alignment rule */ public ZFileOptions setAlignmentRule(AlignmentRule alignmentRule) { this.alignmentRule = alignmentRule; return this; } /** * Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for an * explanation on using the extra field for covering empty spaces. * * @return should the extra field be used to cover empty spaces? */ public boolean getCoverEmptySpaceUsingExtraField() { return coverEmptySpaceUsingExtraField; } /** * Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an * explanation on using the extra field for covering empty spaces. * * @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces? */ public ZFileOptions setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) { this.coverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField; return this; } /** * Obtains whether files should be automatically sorted before updating the zip file. See {@link * ZFile} for an explanation on automatic sorting. * * @return should the file be automatically sorted? */ public boolean getAutoSortFiles() { return autoSortFiles; } /** * Sets whether files should be automatically sorted before updating the zip file. See {@link * ZFile} for an explanation on automatic sorting. * * @param autoSortFiles should the file be automatically sorted? */ public ZFileOptions setAutoSortFiles(boolean autoSortFiles) { this.autoSortFiles = autoSortFiles; return this; } /** * Sets the verification log factory. * * @param verifyLogFactory verification log factory */ public ZFileOptions setVerifyLogFactory(Supplier verifyLogFactory) { this.verifyLogFactory = verifyLogFactory; return this; } /** * Obtains the verification log factory. By default, the verification log doesn't store anything * and will always return an empty log. * * @return the verification log factory */ public Supplier getVerifyLogFactory() { return verifyLogFactory; } /** * Sets whether expensive validation should be skipped during {@link ZFile} creation * * @param skipValidation during creation? */ public ZFileOptions setSkipValidation(boolean skipValidation) { this.skipValidation = skipValidation; return this; } /** * Gets whether expensive validation should be performed during {@link ZFile} creation * * @return skip verification during creation? */ public boolean getSkipValidation() { return skipValidation; } /** * Sets whether to always generate the MANIFEST.MF file, regardless whether the APK is signed with * v1 signing scheme. */ public ZFileOptions setAlwaysGenerateJarManifest(boolean alwaysGenerateJarManifest) { this.alwaysGenerateJarManifest = alwaysGenerateJarManifest; return this; } /** Returns whether the MANIFEST.MF file should always be generated. */ public boolean getAlwaysGenerateJarManifest() { return alwaysGenerateJarManifest; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Zip64Eocd.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.utils.CachedSupplier; import com.android.tools.build.apkzlib.utils.IOExceptionWrapper; import com.google.common.primitives.Ints; import java.io.IOException; import java.nio.ByteBuffer; /** * Zip64 End of Central Directory record in a zip file. */ public class Zip64Eocd { /** * Default "version made by" field: upper byte needs to be 0 to set to MS-DOS compatibility. Lower * byte can be anything, really. We use 0x18 because aapt uses 0x17 :) */ private static final int DEFAULT_VERSION_MADE_BY = 0x0018; /** * Minimum size that can be stored in the {@link #F_EOCD_SIZE} field of the record. */ private static final int MIN_EOCD_SIZE = 44; /** Field in the record: the record signature, fixed at this value by the specification */ private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x06064b50, "Zip64 EOCD signature"); /** * Field in the record: the size of the central directory record, not including the first 12 * bytes of data (the signature and this size information). Therefore this variable should be: * * size = sizeOfFixedFields + sizeOfVariableData - 12 * * as specified by the zip specification. */ private static final ZipField.F8 F_EOCD_SIZE = new ZipField.F8( F_SIGNATURE.endOffset(), "Zip64 EOCD size", new ZipFieldInvariantMinValue(MIN_EOCD_SIZE)); /** Field in the record: ID program that made the zip (we don't actually use this). */ private static final ZipField.F2 F_MADE_BY = new ZipField.F2(F_EOCD_SIZE.endOffset(), "Made by", new ZipFieldInvariantNonNegative()); /** * Field in the record: Version needed to extract the Zip. We expect this value to be at least * {@link CentralDirectoryHeaderCompressInfo#VERSION_WITH_ZIP64_EXTENSIONS}. This value also * determines whether we are using Version 1 or Version 2 of the Zip64 EOCD record. */ private static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2( F_MADE_BY.endOffset(), "Version to extract", new ZipFieldInvariantMinValue( CentralDirectoryHeaderCompressInfo.VERSION_WITH_ZIP64_EXTENSIONS)); /** * Field in the record: the number of disk where the Zip64 EOCD is located. It must be zero * as multi-file archives are not supported. */ private static final ZipField.F4 F_NUMBER_OF_DISK = new ZipField.F4(F_VERSION_EXTRACT.endOffset(), 0, "Number of this disk"); /** * Field in the record: the number of the disk where the central directory resides. This must be * zero as multi-file archives are not supported. */ private static final ZipField.F4 F_DISK_CD_START = new ZipField.F4(F_NUMBER_OF_DISK.endOffset(), 0, "Disk where CD starts"); /** * Field in the record: the number of entries in the Central Directory on this disk. Because we do * not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL} */ private static final ZipField.F8 F_RECORDS_DISK = new ZipField.F8( F_DISK_CD_START.endOffset(), "Record on disk count", new ZipFieldInvariantNonNegative()); /** Field in the record: the total number of entries in the Central Directory. */ private static final ZipField.F8 F_RECORDS_TOTAL = new ZipField.F8( F_RECORDS_DISK.endOffset(), "Total records", new ZipFieldInvariantNonNegative()); /** Field in the record: number of bytes of the Central Directory. */ private static final ZipField.F8 F_CD_SIZE = new ZipField.F8( F_RECORDS_TOTAL.endOffset(), "Directory size", new ZipFieldInvariantNonNegative()); /** Field in the record: offset, from the archive start, where the Central Directory starts. */ private static final ZipField.F8 F_CD_OFFSET = new ZipField.F8( F_CD_SIZE.endOffset(), "Directory offset", new ZipFieldInvariantNonNegative()); /** * Field in Version 2 of the record: The compression method used for the Central Directory in the * given Zip file. Although we do support version 2 of the Zip64 EOCD, we presently do not support * any compression method, and thus this value must be zero. */ private static final ZipField.F2 F_V2_CD_COMPRESSION_METHOD = new ZipField.F2( F_CD_OFFSET.endOffset(), 0, "Version 2: Directory Compression method"); /** * Field in Version 2 of the record: The compressed size of the Central Directory. As Compression * is not supported for the CD, this value should always be the same as * {@link #F_V2_CD_UNCOMPRESSED_SIZE}. */ private static final ZipField.F8 F_V2_CD_COMPRESSED_SIZE = new ZipField.F8( F_V2_CD_COMPRESSION_METHOD.endOffset(), "Version 2: Directory Compressed Size", new ZipFieldInvariantNonNegative()); /** Field in Version 2 of the record: The uncompressed size of the Central Directory. */ private static final ZipField.F8 F_V2_CD_UNCOMPRESSED_SIZE = new ZipField.F8( F_V2_CD_COMPRESSED_SIZE.endOffset(), "Version 2: Directory Uncompressed Size", new ZipFieldInvariantNonNegative()); /** * Field in Version 2 of the record: The ID for the type of encryption used to encrypt the Central * directory. Since Central Directory encryption is not supported, this value has to be zero. */ private static final ZipField.F2 F_V2_CD_ENCRYPTION_ID = new ZipField.F2( F_V2_CD_UNCOMPRESSED_SIZE.endOffset(), 0, "Version 2: Directory Encryption"); /** * Field in Version 2 of the record: The length of the encryption key for the encryption of the * Central Directory given by {@link #F_V2_CD_ENCRYPTION_ID}. Since encryption of the Central * Directory is not supported, this value has to be zero. */ private static final ZipField.F2 F_V2_CD_ENCRYPTION_KEY_LENGTH = new ZipField.F2( F_V2_CD_ENCRYPTION_ID.endOffset(), 0, "Version 2: Directory Encryption key length"); /** * Field in Version 2 of the record: The flags for the encryption method used on the Central * Directory. As encryption of the Central Directory is not supported, this value has to be zero. */ private static final ZipField.F2 F_V2_CD_ENCRYPTION_FLAGS = new ZipField.F2( F_V2_CD_ENCRYPTION_KEY_LENGTH.endOffset(), 0, "Version 2: Directory Encryption Flags"); /** * Field in Version 2 of the record: ID of the algorithm used to hash the Central Directory data. * Hashing of the Central Directory is not supported, so this value has to be zero. */ private static final ZipField.F2 F_V2_HASH_ID = new ZipField.F2( F_V2_CD_ENCRYPTION_FLAGS.endOffset(), 0, "Version 2: Hash algorithm ID"); /** * Field in Version 2 of the record: Length of the data for the hash of the Central Directory. * Hashing of the Central Directory is not supported, so this value has to be zero. */ private static final ZipField.F2 F_V2_HASH_LENGTH = new ZipField.F2( F_V2_HASH_ID.endOffset(), 0, "Version 2: Hash length"); /** The location of the Zip64 size field relative to the start of the Zip64 EOCD. */ public static final int SIZE_OFFSET = F_EOCD_SIZE.offset(); /** * The difference between the size in the size field and the true size of the Zip64 EOCD. The size * field in the EOCD does not consider the size field and the identifier field when calculating * the size of the Zip64 EOCD record. */ public static final int TRUE_SIZE_DIFFERENCE = F_EOCD_SIZE.endOffset(); /** Code of the program that made the zip. We actually don't care about this. */ private final long madeBy; /** Version needed to extract the zip. */ private final long versionToExtract; /** Number of entries in the Central Directory. */ private final long totalRecords; /** Offset from the beginning of the archive where the Central Directory is located. */ private final long directoryOffset; /** Number of bytes of the Central Directory. */ private final long directorySize; /** The variable extra fields at the end of the Zip64 EOCD (in both Version 1 and 2). */ private final Zip64ExtensibleDataSector extraFields; /** Supplier of the byte representation of the Zip64 EOCD. */ private final CachedSupplier byteSupplier; /** * Creates a Zip64Eocd record from the given information from the central directory record. * * @param totalRecords the number of entries in the central directory. * @param directoryOffset the offset of the central directory from the start of the archive. * @param directorySize the size (in bytes) of the central directory record. * @param useVersion2 whether we want to use Version 2 of the Zip64 EOCD. * @param dataSector the extensible data sector. */ Zip64Eocd( long totalRecords, long directoryOffset, long directorySize, boolean useVersion2, Zip64ExtensibleDataSector dataSector) { this.madeBy = DEFAULT_VERSION_MADE_BY; this.totalRecords = totalRecords; this.directorySize = directorySize; this.directoryOffset = directoryOffset; this.versionToExtract = useVersion2 ? CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION : CentralDirectoryHeaderCompressInfo.VERSION_WITH_ZIP64_EXTENSIONS; extraFields = dataSector; byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); } /** * Creates a Zip64 EOCD from the given byte information. It does verify that the record starts * with the correct header information. * * @param bytes the bytes to be read as a Zip64 EOCD * @throws IOException the bytes could not be read as a Zip64 EOCD */ Zip64Eocd(ByteBuffer bytes) throws IOException { F_SIGNATURE.verify(bytes); long eocdSize = F_EOCD_SIZE.read(bytes); long madeBy = F_MADE_BY.read(bytes); long versionToExtract = F_VERSION_EXTRACT.read(bytes); F_NUMBER_OF_DISK.verify(bytes); F_DISK_CD_START.verify(bytes); long totalRecords1 = F_RECORDS_DISK.read(bytes); long totalRecords2 = F_RECORDS_TOTAL.read(bytes); long directorySize = F_CD_SIZE.read(bytes); long directoryOffset = F_CD_OFFSET.read(bytes); long sizeOfFixedFields = F_CD_OFFSET.endOffset(); // sanity checks for Version 1 fields. if (totalRecords1 != totalRecords2) { throw new IOException( "Zip states records split in multiple disks, which is not supported"); } // read Version 2 fields if necessary if (versionToExtract >= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION) { if (eocdSize < F_V2_HASH_LENGTH.endOffset() - F_EOCD_SIZE.endOffset()) { throw new IOException( "Zip states the size of Zip64 EOCD is too small for version 2 format."); } F_V2_CD_COMPRESSION_METHOD.verify(bytes); long compressedSize = F_V2_CD_COMPRESSED_SIZE.read(bytes); long uncompressedSize = F_V2_CD_UNCOMPRESSED_SIZE.read(bytes); F_V2_CD_ENCRYPTION_ID.verify(bytes); F_V2_CD_ENCRYPTION_KEY_LENGTH.verify(bytes); F_V2_CD_ENCRYPTION_FLAGS.verify(bytes); F_V2_HASH_ID.verify(bytes); F_V2_HASH_LENGTH.verify(bytes); sizeOfFixedFields = F_V2_HASH_LENGTH.endOffset(); // sanity checks for version 2 fields. if (compressedSize != uncompressedSize) { throw new IOException( "Zip states Central Directory Compression is used, which is not supported"); } directorySize = uncompressedSize; } this.madeBy = madeBy; this.versionToExtract = versionToExtract; this.totalRecords = totalRecords1; this.directorySize = directorySize; this.directoryOffset = directoryOffset; long extensibleDataSize = eocdSize - (sizeOfFixedFields - F_EOCD_SIZE.endOffset()); if (extensibleDataSize > Integer.MAX_VALUE) { throw new IOException("Extensible data of size: " + extensibleDataSize + "not supported"); } byte[] rawData = new byte[Ints.checkedCast(extensibleDataSize)]; bytes.get(rawData); extraFields = new Zip64ExtensibleDataSector(rawData); byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); } /** * The size of the fixed field in the Zip64 EOCD. This vaue may be different if we are using a * version 1 or version 2 record. * * @return the size of the fixed fields. */ private int sizeOfFixedFields() { return versionToExtract >= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION ? F_V2_HASH_LENGTH.endOffset() : F_CD_OFFSET.endOffset(); } /** * Gets the size (in bytes) of the Zip64 EOCD record. * * @return the size of the record. */ public int size() { return sizeOfFixedFields() + extraFields.size(); } public long getTotalRecords() { return totalRecords; } public long getDirectorySize() { return directorySize; } public long getDirectoryOffset() { return directoryOffset; } public Zip64ExtensibleDataSector getExtraFields() { return extraFields; } public long getVersionToExtract() { return versionToExtract; } /** * Gets the byte representation of The Zip64 EOCD record. * * @return the bytes of the EOCD. */ public byte[] toBytes() { return byteSupplier.get(); } private byte[] computeByteRepresentation() { int size = size(); ByteBuffer out = ByteBuffer.allocate(size); try { F_SIGNATURE.write(out); F_EOCD_SIZE.write(out, size - F_EOCD_SIZE.endOffset()); F_MADE_BY.write(out, madeBy); F_VERSION_EXTRACT.write(out, versionToExtract); F_NUMBER_OF_DISK.write(out); F_DISK_CD_START.write(out); F_RECORDS_DISK.write(out, totalRecords); F_RECORDS_TOTAL.write(out, totalRecords); F_CD_SIZE.write(out, directorySize); F_CD_OFFSET.write(out, directoryOffset); // write version 2 fields if necessary. if (versionToExtract >= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION) { F_V2_CD_COMPRESSION_METHOD.write(out); F_V2_CD_COMPRESSED_SIZE.write(out, directorySize); F_V2_CD_UNCOMPRESSED_SIZE.write(out, directorySize); F_V2_CD_ENCRYPTION_ID.write(out); F_V2_CD_ENCRYPTION_KEY_LENGTH.write(out); F_V2_CD_ENCRYPTION_FLAGS.write(out); F_V2_HASH_ID.write(out); F_V2_HASH_LENGTH.write(out); } extraFields.write(out); return out.array(); } catch (IOException e) { throw new IOExceptionWrapper(e); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Zip64EocdLocator.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.utils.CachedSupplier; import com.android.tools.build.apkzlib.utils.IOExceptionWrapper; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.ByteBuffer; /** * Zip64 End of Central Directory Locator. Used to locate the Zip64 EOCD record in * the Zip64 format. This will be located right above the standard EOCD record, if it exists. */ class Zip64EocdLocator { /** Field in the record: the record signature, fixed at this value by the specification. */ private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x07064b50, "Zip64 EOCD Locator signature"); /** * Field in the record: the number of the disk where the Zip64 EOCD is located. This has to be * zero because multi-file archives are not supported. */ private static final ZipField.F4 F_NUMBER_OF_DISK = new ZipField.F4(F_SIGNATURE.endOffset(), 0, "Number of disk with Zip64 EOCD"); /** * Field in the record: the location of the zip64 EOCD record on the disk specified by * {@link #F_NUMBER_OF_DISK}. */ private static final ZipField.F8 F_Z64_EOCD_OFFSET = new ZipField.F8( F_NUMBER_OF_DISK.endOffset(), "Offset of Zip64 EOCD", new ZipFieldInvariantNonNegative()); /** * Field in the record: the total number of disks in the archive. This has to be zero because * multi-file archives are not supported. */ private static final ZipField.F4 F_TOTAL_NUMBER_OF_DISKS = new ZipField.F4( F_Z64_EOCD_OFFSET.endOffset(), 0,"Total number of disks"); public static final int LOCATOR_SIZE = F_TOTAL_NUMBER_OF_DISKS.endOffset(); /** * Offset from the beginning of the archive to where the Zip64 End of Central Directory record * is located. */ private final long z64EocdOffset; /** Supplier of the byte representation of the zip64 Eocd Locator. */ private final CachedSupplier byteSupplier; /** * Creates a new Zip64 EOCD Locator, reading it from a byte source. This method will parse the * byte source and obtain the EOCD Locator. It will check that the byte source starts with the * EOCD Locator signature. * * @param bytes the byte buffer with the Locator data; when this method finishes, the byte buffer * will have its position moved to the end of the Locator (the beginning of the standard EOCD) * @throws IOException failed to read information or the EOCD data is corrupt or invalid. */ Zip64EocdLocator(ByteBuffer bytes) throws IOException { F_SIGNATURE.verify(bytes); F_NUMBER_OF_DISK.verify(bytes); long z64EocdOffset = F_Z64_EOCD_OFFSET.read(bytes); F_TOTAL_NUMBER_OF_DISKS.verify(bytes); Verify.verify(z64EocdOffset >= 0); this.z64EocdOffset = z64EocdOffset; byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); } /** * Creates a new Zip64 EOCD Locator. This is used when generating an EOCD Locator for a * Zip64 EOCD that has been generated. * * @param z64EocdOffset offset position of the Zip64 EOCD from the beginning of the archive */ Zip64EocdLocator(long z64EocdOffset) { Preconditions.checkArgument(z64EocdOffset >= 0, "z64EocdOffset < 0"); this.z64EocdOffset = z64EocdOffset; byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); } /** * Obtains the offset from the beginning of the archive to where the Zip64 EOCD is located. * * @return the Zip64 EOCD offset. */ long getZ64EocdOffset() { return z64EocdOffset; } /** * Obtains the size of the Zip64 EOCD Locator * * @return the size, in bytes, of the EOCD Locator. i.e. 20. */ long getSize() { return F_TOTAL_NUMBER_OF_DISKS.endOffset(); } /** * Generates the EOCD Locator data. * * @return a byte representation of the EOCD Locator that has exactly {@link #getSize()} bytes * @throws IOException failed to generate the EOCD data. */ byte[] toBytes() throws IOException { return byteSupplier.get(); } /** * Computes the byte representation of the EOCD Locator. * * @return a byte representation of the Zip64 EOCD Locator that has exactly {@link #getSize()} * bytes * @throws UncheckedIOException failed to generate the EOCD Locator data */ private byte[] computeByteRepresentation() { ByteBuffer out = ByteBuffer.allocate(F_TOTAL_NUMBER_OF_DISKS.endOffset()); try { F_SIGNATURE.write(out); F_NUMBER_OF_DISK.write(out); F_Z64_EOCD_OFFSET.write(out, z64EocdOffset); F_TOTAL_NUMBER_OF_DISKS.write(out); return out.array(); } catch (IOException e) { throw new IOExceptionWrapper(e); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Zip64ExtensibleDataSector.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; /** * Contains the special purpose data for the Zip64 EOCD record. * *

According to the zip specification, the Zip64 EOCD is composed of a sequence of zero or more * Special Purpose Data fields. This class provides a way to access, parse, and modify that * information. * *

Each Special Purpose Data is represented by an instance of {@link Z64SpecialPurposeData} and * contains a header ID and data. */ public class Zip64ExtensibleDataSector { /** * The extensible data sector's raw data, if it is known. Either this variable or {@link #fields} * must be non-{@code null}. */ @Nullable private final byte[] rawData; /** * The list of fields in the data sector. Will be populated if the data sector is created based on * a list of special purpose data; will also be populated after parsing if the Data Sector is * created based on the raw bytes. */ @Nullable private ImmutableList fields; /** * Creates a Zip64 Extensible Data Sector based on existing raw data. * * @param rawData the raw data; will only be parsed if needed. */ public Zip64ExtensibleDataSector(byte[] rawData) { this.rawData = rawData; fields = null; } /** * Creates an Extensible Data Sector with no special purpose data. */ public Zip64ExtensibleDataSector() { rawData = null; fields = ImmutableList.of(); } /** * Creates a Zip64 Extensible Data with the given Special purpose data. * * @param fields all special purpose data. */ public Zip64ExtensibleDataSector(ImmutableList fields) { rawData = null; this.fields = fields; } int size() { if (rawData != null) { return rawData.length; } else { Preconditions.checkNotNull(fields); int sumSizes = 0; for (Z64SpecialPurposeData data : fields){ sumSizes += data.size(); } return sumSizes; } } void write(ByteBuffer out) throws IOException { if (rawData != null) { out.put(rawData); } else { Preconditions.checkNotNull(fields); for (Z64SpecialPurposeData data : fields) { data.write(out); } } } public ImmutableList getFields() throws IOException { if (fields == null) { parseData(); } Preconditions.checkNotNull(fields); return fields; } private void parseData() throws IOException { Preconditions.checkNotNull(rawData); Preconditions.checkState(fields == null); List fields = new ArrayList<>(); ByteBuffer buffer = ByteBuffer.wrap(rawData); while (buffer.remaining() > 0) { int headerId = LittleEndianUtils.readUnsigned2Le(buffer); long dataSize = LittleEndianUtils.readUnsigned4Le(buffer); byte[] data = new byte[Ints.checkedCast(dataSize)]; if (dataSize < 0) { throw new IOException( "Invalid data size for special purpose data with header ID " + headerId + ": " + dataSize); } buffer.get(data); SpecialPurposeDataFactory factory = RawSpecialPurposeData::new; Z64SpecialPurposeData spd = factory.make(headerId, data); fields.add(spd); } this.fields = ImmutableList.copyOf(fields); } public interface Z64SpecialPurposeData { /** Length of header id and the size length fields that comes before the data */ int PREFIX_LENGTH = 6; /** * Obtains the Special purpose data's header id. * * @return the data's header id. */ int getHeaderId(); /** * Obtains the size of the data in this special purpose data. * * @return the number of bytes needed to write the data. */ int size(); /** * Writes the special purpose data to the buffer. * * @param out the buffer where to write the data to; exactly {@link #size()} bytes will be * written. * @throws IOException failed to write special purpose data. */ void write(ByteBuffer out) throws IOException; } public interface SpecialPurposeDataFactory { /** * Creates a new special purpose data. * * @param headerId the header ID * @param data the data in the special purpose data * @return the created special purpose data. * @throws IOException failed to create the special purpose data from the data given. */ Z64SpecialPurposeData make(int headerId, byte[] data) throws IOException; } /** * Special Purpose Data containing raw data: this class represents a general "special purpose * data" containing an array of bytes as data. */ public static class RawSpecialPurposeData implements Z64SpecialPurposeData { /** Header ID. */ private final int headerId; /** Data in the segment */ private final byte[] data; RawSpecialPurposeData(int headerId, byte[] data) { this.headerId = headerId; this.data = data; } @Override public int getHeaderId() { return headerId; } @Override public int size() { return PREFIX_LENGTH + data.length; } @Override public void write(ByteBuffer out) throws IOException { LittleEndianUtils.writeUnsigned2Le(out, headerId); LittleEndianUtils.writeUnsigned4Le(out, data.length); out.put(data); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipField.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Set; import javax.annotation.Nullable; /** * The ZipField class represents a field in a record in a zip file. Zip files are made with records * that have fields. This class makes it easy to read, write and verify field values. * *

There are three main types of fields: 2-byte, 4-byte, and 8-byte fields. We represent each * one as a subclass of {@code ZipField}, {@code F2} for the 2-byte field, {@code F4} for the 4-byte * field and {@code F8} for the 8-byte field. Because Java's {@code int} data type is guaranteed to * be 4-byte, all methods use Java's native {@link int} as data type. * *

The {@code F8} subclass is to support the 8-byte fields in the Zip64 specification. Because * Java's 8-byte {@code long} does not support unsigned types, which reduces the support to 8-byte * numbers of the form 2^63-1 or less. As {@code F8} fields refer to file sizes, this should be * sufficient. * *

For each field we can either read, write or verify. Verification is used for fields whose * value we know. Some fields, e.g. signature fields, have fixed value. Other fields have * variable values, but in some situations we know which value they have. For example, the last * modification time of a file's local header will have to match the value of the file's * modification time as stored in the central directory. * *

Because records are compact, i.e. fields are stored sequentially with no empty * spaces, fields are generally created in the sequence they exist and the end offset of a field is * used as the offset of the next one. The end of a field can be obtained by invoking {@link * #endOffset()}. This allows creating fields in sequence without doing offset computation: * *

 * ZipField.F2 firstField = new ZipField.F2(0, "First field");
 * ZipField.F4 secondField = new ZipField(firstField.endOffset(), "Second field");
 * ZipField.F8 thirdField = new ZipField(secondField.endOffset(), "Third field");
 * 
*/ abstract class ZipField { /** Field name. Used for providing (more) useful error messages. */ private final String name; /** Offset of the file in the record. */ protected final int offset; /** Size of the field. Only 2, 4, or 8 allowed. */ private final int size; /** If a fixed value exists for the field, then this attribute will contain that value. */ @Nullable private final Long expected; /** All invariants that this field must verify. */ private final Set invariants; /** * Creates a new field that does not contain a fixed value. * * @param offset the field's offset in the record * @param size the field size * @param name the field's name * @param invariants the invariants that must be verified by the field */ ZipField(int offset, int size, String name, ZipFieldInvariant... invariants) { Preconditions.checkArgument(offset >= 0, "offset >= 0"); Preconditions.checkArgument( size == 2 || size == 4 || size == 8, "size != 2 && size != 4 && size != 8"); this.name = name; this.offset = offset; this.size = size; expected = null; this.invariants = Sets.newHashSet(invariants); } /** * Creates a new field that contains a fixed value. * * @param offset the field's offset in the record * @param size the field size * @param expected the expected field value * @param name the field's name */ ZipField(int offset, int size, long expected, String name) { Preconditions.checkArgument(offset >= 0, "offset >= 0"); Preconditions.checkArgument( size == 2 || size == 4 || size == 8, "size != 2 && size != 4 && size != 8"); this.name = name; this.offset = offset; this.size = size; this.expected = expected; invariants = Sets.newHashSet(); } /** * Checks whether a value verifies the field's invariants. Nothing happens if the value verifies * the invariants. * * @param value the value * @throws IOException the invariants are not verified */ private void checkVerifiesInvariants(long value) throws IOException { for (ZipFieldInvariant invariant : invariants) { if (!invariant.isValid(value)) { throw new IOException( "Value " + value + " of field " + name + " is invalid " + "(fails '" + invariant.getName() + "')."); } } } /** * Advances the position in the provided byte buffer by the size of this field. * * @param bytes the byte buffer; at the end of the method its position will be greater by the size * of this field * @throws IOException failed to advance the buffer */ void skip(ByteBuffer bytes) throws IOException { if (bytes.remaining() < size) { throw new IOException( "Cannot skip field " + name + " because only " + bytes.remaining() + " remain in the buffer."); } bytes.position(bytes.position() + size); } /** * Reads a field value. * * @param bytes the byte buffer with the record data; after this method finishes, the buffer will * be positioned at the first byte after the field * @return the value of the field * @throws IOException failed to read the field */ long read(ByteBuffer bytes) throws IOException { if (bytes.remaining() < size) { throw new IOException( "Cannot skip field " + name + " because only " + bytes.remaining() + " remain in the buffer."); } bytes.order(ByteOrder.LITTLE_ENDIAN); long r; if (size == 2) { r = LittleEndianUtils.readUnsigned2Le(bytes); } else if (size == 4) { r = LittleEndianUtils.readUnsigned4Le(bytes); } else { r = LittleEndianUtils.readUnsigned8Le(bytes); } checkVerifiesInvariants(r); return r; } /** * Verifies that the field at the current buffer position has the expected value. The field must * have been created with the constructor that defines the expected value. * * @param bytes the byte buffer with the record data; after this method finishes, the buffer will * be positioned at the first byte after the field * @throws IOException failed to read the field or the field does not have the expected value */ void verify(ByteBuffer bytes) throws IOException { verify(bytes, null); } /** * Verifies that the field at the current buffer position has the expected value. The field must * have been created with the constructor that defines the expected value. * * @param bytes the byte buffer with the record data; after this method finishes, the buffer will * be positioned at the first byte after the field * @param verifyLog if non-{@code null}, will log the verification error * @throws IOException failed to read the data or the field does not have the expected value; only * thrown if {@code verifyLog} is {@code null} */ void verify(ByteBuffer bytes, @Nullable VerifyLog verifyLog) throws IOException { Preconditions.checkState(expected != null, "expected == null"); verify(bytes, expected, verifyLog); } /** * Verifies that the field has an expected value. * * @param bytes the byte buffer with the record data; after this method finishes, the buffer will * be positioned at the first byte after the field * @param expected the value we expect the field to have; if this field has invariants, the value * must verify them * @throws IOException failed to read the data or the field does not have the expected value */ void verify(ByteBuffer bytes, long expected) throws IOException { verify(bytes, expected, null); } /** * Verifies that the field has an expected value. * * @param bytes the byte buffer with the record data; after this method finishes, the buffer will * be positioned at the first byte after the field * @param expected the value we expect the field to have; if this field has invariants, the value * must verify them * @param verifyLog if non-{@code null}, will log the verification error * @throws IOException failed to read the data or the field does not have the expected value; only * thrown if {@code verifyLog} is {@code null} */ void verify(ByteBuffer bytes, long expected, @Nullable VerifyLog verifyLog) throws IOException { checkVerifiesInvariants(expected); long r = read(bytes); if (r != expected) { String error = String.format( "Incorrect value for field '%s': value is %s but %s expected.", name, r, expected); if (verifyLog == null) { throw new IOException(error); } else { verifyLog.log(error); } } } /** * Writes the value of the field. * * @param output where to write the field; the field will be written at the current position of * the buffer * @param value the value to write * @throws IOException failed to write the value in the stream */ void write(ByteBuffer output, long value) throws IOException { checkVerifiesInvariants(value); Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); if (size == 2) { Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value); LittleEndianUtils.writeUnsigned2Le(output, Ints.checkedCast(value)); } else if (size == 4) { Preconditions.checkArgument( value <= 0x00000000ffffffffL, "value (%s) > 0x00000000ffffffffL", value); LittleEndianUtils.writeUnsigned4Le(output, value); } else { Verify.verify(size == 8); LittleEndianUtils.writeUnsigned8Le(output, value); } } /** * Writes the value of the field. The field must have an expected value set in the constructor. * * @param output where to write the field; the field will be written at the current position of * the buffer * @throws IOException failed to write the value in the stream */ void write(ByteBuffer output) throws IOException { Preconditions.checkState(expected != null, "expected == null"); write(output, expected); } /** * Obtains the offset at which the field starts. * * @return the start offset */ int offset() { return offset; } /** * Obtains the offset at which the field ends. This is the exact offset at which the next field * starts. * * @return the end offset */ int endOffset() { return offset + size; } /** Concrete implementation of {@link ZipField} that represents a 2-byte field. */ static class F2 extends ZipField { /** * Creates a new field. * * @param offset the field's offset in the record * @param name the field's name * @param invariants the invariants that must be verified by the field */ F2(int offset, String name, ZipFieldInvariant... invariants) { super(offset, 2, name, invariants); } /** * Creates a new field that contains a fixed value. * * @param offset the field's offset in the record * @param expected the expected field value * @param name the field's name */ F2(int offset, long expected, String name) { super(offset, 2, expected, name); } } /** Concrete implementation of {@link ZipField} that represents a 4-byte field. */ static class F4 extends ZipField { /** * Creates a new field. * * @param offset the field's offset in the record * @param name the field's name * @param invariants the invariants that must be verified by the field */ F4(int offset, String name, ZipFieldInvariant... invariants) { super(offset, 4, name, invariants); } /** * Creates a new field that contains a fixed value. * * @param offset the field's offset in the record * @param expected the expected field value * @param name the field's name */ F4(int offset, long expected, String name) { super(offset, 4, expected, name); } } /** Concrete implementation of {@link ZipField} that represents a 8-byte field. */ static class F8 extends ZipField { /** * Creates a new field * * @param offset offset the field's offset in the record * @param name the field's name * @param invariants the invariants that must be verified by the field */ F8(int offset, String name, ZipFieldInvariant... invariants) { super(offset, 8, name, invariants); } /** * Creates a new field that contains a fixed value. * * @param offset the field's offset in the record * @param expected the expected field value * @param name the field's name */ F8(int offset, long expected, String name) { super(offset, 8, expected, name); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariant.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** * A field rule defines an invariant (i.e., a constraint) that has to be verified by a * field value. */ interface ZipFieldInvariant { /** * Evalutes the invariant against a value. * * @param value the value to check the invariant * @return is the invariant valid? */ boolean isValid(long value); /** * Obtains the name of the invariant. Used for information purposes. * * @return the name of the invariant */ String getName(); } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantMaxValue.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** Invariant checking a zip field does not exceed a threshold. */ class ZipFieldInvariantMaxValue implements ZipFieldInvariant { /** The maximum value allowed. */ private final long max; /** * Creates a new invariant. * * @param max the maximum value allowed for the field */ ZipFieldInvariantMaxValue(long max) { this.max = max; } @Override public boolean isValid(long value) { return value <= max; } @Override public String getName() { return "Maximum value " + max; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantMinValue.java ================================================ /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** Invariant checking a zip field doesn't go below a given value.*/ class ZipFieldInvariantMinValue implements ZipFieldInvariant { /** The minimum value allowed. */ private final long min; /** * Creates a new invariant. * * @param min the minimum value allowed for the field */ ZipFieldInvariantMinValue(long min) { this.min = min; } @Override public boolean isValid(long value) { return value >= min; } @Override public String getName() { return "Min value " + min; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantNonNegative.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** Invariant that verifies a field's value is not negative. */ class ZipFieldInvariantNonNegative implements ZipFieldInvariant { @Override public boolean isValid(long value) { return value >= 0; } @Override public String getName() { return "Is positive"; } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFileState.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip; /** The {@code ZipFileState} enumeration holds the state of a {@link ZFile}. */ enum ZipFileState { /** Zip file is closed. */ CLOSED, /** File file is open in read-only mode. */ OPEN_RO, /** File file is open in read-write mode. */ OPEN_RW } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.compress; import com.android.tools.build.apkzlib.bytestorage.ByteStorage; import com.android.tools.build.apkzlib.zip.CompressionResult; import com.android.tools.build.apkzlib.zip.utils.ByteTracker; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.base.Preconditions; import java.util.concurrent.Executor; import java.util.zip.Deflater; /** * Compressor that tries both the best and default compression algorithms and picks the default * unless the best is at least a given percentage smaller. */ public class BestAndDefaultDeflateExecutorCompressor extends ExecutorCompressor { /** Deflater using the default compression level. */ private final DeflateExecutionCompressor defaultDeflater; /** Deflater using the best compression level. */ private final DeflateExecutionCompressor bestDeflater; /** * Minimum best compression size / default compression size ratio needed to pick the default * compression size. */ private final double minRatio; /** * Creates a new compressor. * * @param executor the executor used to perform compression activities. * @param minRatio the minimum best compression size / default compression size needed to pick the * default compression size; if {@code 0.0} then the default compression is always picked, if * {@code 1.0} then the best compression is always picked unless it produces the exact same * size as the default compression. */ public BestAndDefaultDeflateExecutorCompressor(Executor executor, double minRatio) { super(executor); Preconditions.checkArgument(minRatio >= 0.0, "minRatio < 0.0"); Preconditions.checkArgument(minRatio <= 1.0, "minRatio > 1.0"); defaultDeflater = new DeflateExecutionCompressor(executor, Deflater.DEFAULT_COMPRESSION); bestDeflater = new DeflateExecutionCompressor(executor, Deflater.BEST_COMPRESSION); this.minRatio = minRatio; } @Deprecated public BestAndDefaultDeflateExecutorCompressor( Executor executor, ByteTracker tracker, double minRatio) { this(executor, minRatio); } @Override protected CompressionResult immediateCompress(CloseableByteSource source, ByteStorage storage) throws Exception { CompressionResult defaultResult = defaultDeflater.immediateCompress(source, storage); CompressionResult bestResult = bestDeflater.immediateCompress(source, storage); double sizeRatio = bestResult.getSize() / (double) defaultResult.getSize(); if (sizeRatio >= minRatio) { return defaultResult; } else { return bestResult; } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/DeflateExecutionCompressor.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.compress; import com.android.tools.build.apkzlib.bytestorage.ByteStorage; import com.android.tools.build.apkzlib.bytestorage.CloseableByteSourceFromOutputStreamBuilder; import com.android.tools.build.apkzlib.zip.CompressionMethod; import com.android.tools.build.apkzlib.zip.CompressionResult; import com.android.tools.build.apkzlib.zip.utils.ByteTracker; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.io.ByteStreams; import java.io.InputStream; import java.util.concurrent.Executor; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; /** Compressor that uses deflate with an executor. */ public class DeflateExecutionCompressor extends ExecutorCompressor { /** Deflate compression level. */ private final int level; /** * Creates a new compressor. * * @param executor the executor to run deflation tasks * @param level the compression level */ public DeflateExecutionCompressor(Executor executor, int level) { super(executor); this.level = level; } @Deprecated public DeflateExecutionCompressor(Executor executor, ByteTracker tracker, int level) { this(executor, level); } @Override protected CompressionResult immediateCompress(CloseableByteSource source, ByteStorage storage) throws Exception { Deflater deflater = new Deflater(level, true); CloseableByteSourceFromOutputStreamBuilder resultBuilder = storage.makeBuilder(); try (InputStream inputStream = source.openBufferedStream(); DeflaterOutputStream dos = new DeflaterOutputStream(resultBuilder, deflater)) { ByteStreams.copy(inputStream, dos); } CloseableByteSource result = resultBuilder.build(); if (result.size() >= source.size()) { result.close(); return new CompressionResult(source, CompressionMethod.STORE, source.size()); } else { return new CompressionResult(result, CompressionMethod.DEFLATE, result.size()); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/ExecutorCompressor.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.compress; import com.android.tools.build.apkzlib.bytestorage.ByteStorage; import com.android.tools.build.apkzlib.zip.CompressionResult; import com.android.tools.build.apkzlib.zip.Compressor; import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.util.concurrent.Executor; /** * A synchronous compressor is a compressor that computes the result of compression immediately and * never returns an uncomputed future object. */ public abstract class ExecutorCompressor implements Compressor { /** The executor that does the work. */ private final Executor executor; /** * Compressor that delegates execution into the given executor. * * @param executor the executor that will do the compress */ public ExecutorCompressor(Executor executor) { this.executor = executor; } @Override public ListenableFuture compress( CloseableByteSource source, ByteStorage storage) { final SettableFuture future = SettableFuture.create(); executor.execute( () -> { try { future.set(immediateCompress(source, storage)); } catch (Throwable e) { future.setException(e); } }); return future; } /** * Immediately compresses a source. * * @param source the source to compress * @param storage a byte storage where the compressor can obtain data sources from * @return the result of compression * @throws Exception failed to compress */ protected abstract CompressionResult immediateCompress( CloseableByteSource source, ByteStorage storage) throws Exception; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/Zip64NotSupportedException.java ================================================ /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.compress; import java.io.IOException; /** Exception raised by ZFile when encountering unsupported Zip64 format jar files. */ public class Zip64NotSupportedException extends IOException { public Zip64NotSupportedException(String message) { super(message); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/package-info.java ================================================ /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** Compressors to use with the {@code zip} package. */ package com.android.tools.build.apkzlib.zip.compress; ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/ByteTracker.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.utils; /** * Keeps track of used bytes allowing gauging memory usage. * * @deprecated will be removed shortly. */ @Deprecated public class ByteTracker { } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableByteSource.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.utils; import com.google.common.io.ByteSource; import java.io.Closeable; import java.io.IOException; /** * Byte source that can be closed. Closing a byte source allows releasing any resources associated * with it. This should not be confused with closing streams. For example, {@link ByteTracker} uses * {@code CloseableByteSources} to know when the data associated with the byte source can be * released. */ public abstract class CloseableByteSource extends ByteSource implements Closeable { /** Has the source been closed? */ private boolean closed; /** Creates a new byte source. */ public CloseableByteSource() { closed = false; } @Override public final synchronized void close() throws IOException { if (closed) { return; } try { innerClose(); } finally { closed = true; } } /** * Closes the by source. This method is only invoked once, even if {@link #close()} is called * multiple times. * * @throws IOException failed to close */ protected abstract void innerClose() throws IOException; } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableDelegateByteSource.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.utils; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.io.ByteProcessor; import com.google.common.io.ByteSink; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; import javax.annotation.Nullable; /** Closeable byte source that delegates to another byte source. */ public class CloseableDelegateByteSource extends CloseableByteSource { /** The byte source we delegate all operations to. {@code null} if disposed. */ @Nullable private ByteSource inner; /** * Size of the byte source. This is the same as {@code inner.size()} (when {@code inner} is not * {@code null}), but we keep it separate to avoid calling {@code inner.size()} because it might * throw {@code IOException}. */ private final long mSize; /** * Creates a new byte source. * * @param inner the inner byte source * @param size the size of the source */ public CloseableDelegateByteSource(ByteSource inner, long size) { this.inner = inner; mSize = size; } /** * Obtains the inner byte source. Will throw an exception if the inner by byte source has been * disposed of. * * @return the inner byte source */ private synchronized ByteSource get() { if (inner == null) { throw new ByteSourceDisposedException(); } return inner; } /** Mark the byte source as disposed. */ @Override protected synchronized void innerClose() throws IOException { if (inner == null) { return; } inner = null; } /** * Obtains the size of this byte source. Equivalent to {@link #size()} but not throwing {@code * IOException}. * * @return the size of the byte source */ public long sizeNoException() { return mSize; } @Override public CharSource asCharSource(Charset charset) { return get().asCharSource(charset); } @Override public InputStream openBufferedStream() throws IOException { return get().openBufferedStream(); } @Override public ByteSource slice(long offset, long length) { return get().slice(offset, length); } @Override public boolean isEmpty() throws IOException { return get().isEmpty(); } @Override public long size() throws IOException { return get().size(); } @Override public long copyTo(OutputStream output) throws IOException { return get().copyTo(output); } @Override public long copyTo(ByteSink sink) throws IOException { return get().copyTo(sink); } @Override public byte[] read() throws IOException { return get().read(); } @Override public T read(ByteProcessor processor) throws IOException { return get().read(processor); } @Override public HashCode hash(HashFunction hashFunction) throws IOException { return get().hash(hashFunction); } @Override public boolean contentEquals(ByteSource other) throws IOException { return get().contentEquals(other); } @Override public InputStream openStream() throws IOException { return get().openStream(); } /** Exception thrown when trying to use a byte source that has been disposed. */ private static class ByteSourceDisposedException extends RuntimeException { /** Creates a new exception. */ private ByteSourceDisposedException() { super( "Byte source was created by a ByteTracker and is now disposed. If you see " + "this message, then there is a bug."); } } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtils.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.utils; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * Utilities to read and write 16, 32, and 64 bit integers with support for little-endian encoding, * as used in zip files. Zip files actually use unsigned data types. We use Java's native (signed) * data types but will use long (64 bit) to ensure we can fit the whole range for the 16 and 32 * bit fields. */ public class LittleEndianUtils { /** Utility class, no constructor. */ private LittleEndianUtils() {} /** * Reads 8 bytes in little-endian format and converts them into a 64-bit value. * * @param bytes from where should the bytes be read; the first 8 bytes of the source will be read. * @return the 64-bit value * @throws IOException failed to read the value. */ public static long readUnsigned8Le(ByteBuffer bytes) throws IOException { Preconditions.checkNotNull(bytes, "bytes == null"); if (bytes.remaining() < 8) { throw new EOFException( "Not enough data: 8 bytes expected, " + bytes.remaining() + " available."); } ByteOrder order = bytes.order(); bytes.order(ByteOrder.LITTLE_ENDIAN); long r = bytes.getLong(); bytes.order(order); return r; } /** * Reads 4 bytes in little-endian format and converts them into a 32-bit value. * * @param bytes from where should the bytes be read; the first 4 bytes of the source will be read * @return the 32-bit value * @throws IOException failed to read the value */ public static long readUnsigned4Le(ByteBuffer bytes) throws IOException { Preconditions.checkNotNull(bytes, "bytes == null"); if (bytes.remaining() < 4) { throw new EOFException( "Not enough data: 4 bytes expected, " + bytes.remaining() + " available."); } byte b0 = bytes.get(); byte b1 = bytes.get(); byte b2 = bytes.get(); byte b3 = bytes.get(); long r = (b0 & 0xff) | ((b1 & 0xff) << 8) | ((b2 & 0xff) << 16) | ((b3 & 0xffL) << 24); Verify.verify(r >= 0); Verify.verify(r <= 0x00000000ffffffffL); return r; } /** * Reads 2 bytes in little-endian format and converts them into a 16-bit value. * * @param bytes from where should the bytes be read; the first 2 bytes of the source will be read * @return the 16-bit value * @throws IOException failed to read the value */ public static int readUnsigned2Le(ByteBuffer bytes) throws IOException { Preconditions.checkNotNull(bytes, "bytes == null"); if (bytes.remaining() < 2) { throw new EOFException( "Not enough data: 2 bytes expected, " + bytes.remaining() + " available."); } byte b0 = bytes.get(); byte b1 = bytes.get(); int r = (b0 & 0xff) | ((b1 & 0xff) << 8); Verify.verify(r >= 0); Verify.verify(r <= 0x0000ffff); return r; } /** * Writes 8 bytes in little-endian format, converting them from a signed 64-bit value. * * @param output the output stream where the bytes will be written. * @param value the 64-bit value to convert. * @throws IOException failed to write the value data. */ public static void writeUnsigned8Le(ByteBuffer output, long value) throws IOException { Preconditions.checkNotNull(output, "output == null"); ByteOrder order = output.order(); output.order(ByteOrder.LITTLE_ENDIAN); output.putLong(value); output.order(order); } /** * Writes 4 bytes in little-endian format, converting them from a 32-bit value. * * @param output the output stream where the bytes will be written * @param value the 32-bit value to convert * @throws IOException failed to write the value data */ public static void writeUnsigned4Le(ByteBuffer output, long value) throws IOException { Preconditions.checkNotNull(output, "output == null"); Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); Preconditions.checkArgument( value <= 0x00000000ffffffffL, "value (%s) > 0x00000000ffffffffL", value); output.put((byte) (value & 0xff)); output.put((byte) ((value >> 8) & 0xff)); output.put((byte) ((value >> 16) & 0xff)); output.put((byte) ((value >> 24) & 0xff)); } /** * Writes 2 bytes in little-endian format, converting them from a 16-bit value. * * @param output the output stream where the bytes will be written * @param value the 16-bit value to convert * @throws IOException failed to write the value data */ public static void writeUnsigned2Le(ByteBuffer output, int value) throws IOException { Preconditions.checkNotNull(output, "output == null"); Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value); output.put((byte) (value & 0xff)); output.put((byte) ((value >> 8) & 0xff)); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtils.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.utils; import com.google.common.base.Verify; import java.util.Calendar; import java.util.Date; /** Yes. This actually refers to MS-DOS in 2015. That's all I have to say about legacy stuff. */ public class MsDosDateTimeUtils { /** Utility class: no constructor. */ private MsDosDateTimeUtils() {} /** * Packs java time value into an MS-DOS time value. * * @param time the time value * @return the MS-DOS packed time */ public static int packTime(long time) { Calendar c = Calendar.getInstance(); c.setTime(new Date(time)); int seconds = c.get(Calendar.SECOND); int minutes = c.get(Calendar.MINUTE); int hours = c.get(Calendar.HOUR_OF_DAY); /* * Here is how MS-DOS packs a time value: * 0-4: seconds (divided by 2 because we only have 5 bits = 32 different numbers) * 5-10: minutes (6 bits = 64 possible values) * 11-15: hours (5 bits = 32 possible values) * * source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx */ return (hours << 11) | (minutes << 5) | (seconds / 2); } /** * Packs the current time value into an MS-DOS time value. * * @return the MS-DOS packed time */ public static int packCurrentTime() { return packTime(new Date().getTime()); } /** * Packs java time value into an MS-DOS date value. * * @param time the time value * @return the MS-DOS packed date */ public static int packDate(long time) { Calendar c = Calendar.getInstance(); c.setTime(new Date(time)); /* * Even MS-DOS used 1 for January. Someone wasn't really thinking when they decided on Java * it would start at 0... */ int day = c.get(Calendar.DAY_OF_MONTH); int month = c.get(Calendar.MONTH) + 1; /* * MS-DOS counts years starting from 1980. Since its launch date was in 81, it was obviously * not necessary to talk about dates earlier than that. */ int year = c.get(Calendar.YEAR) - 1980; Verify.verify(year >= 0 && year < 128); /* * Here is how MS-DOS packs a date value: * 0-4: day (5 bits = 32 values) * 5-8: month (4 bits = 16 values) * 9-15: year (7 bits = 128 values) * * source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx */ return (year << 9) | (month << 5) | day; } /** * Packs the current time value into an MS-DOS date value. * * @return the MS-DOS packed date */ public static int packCurrentDate() { return packDate(new Date().getTime()); } } ================================================ FILE: apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/RandomAccessFileUtils.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.build.apkzlib.zip.utils; import java.io.IOException; import java.io.RandomAccessFile; /** Utility class with utility methods for random access files. */ public final class RandomAccessFileUtils { private RandomAccessFileUtils() {} /** * Reads from an random access file until the provided array is filled. Data is read from the * current position in the file. * * @param raf the file to read data from * @param data the array that will receive the data * @throws IOException failed to read the data */ public static void fullyRead(RandomAccessFile raf, byte[] data) throws IOException { int r; int p = 0; while ((r = raf.read(data, p, data.length - p)) > 0) { p += r; if (p == data.length) { break; } } if (p < data.length) { throw new IOException( "Failed to read " + data.length + " bytes from file. Only " + p + " bytes could be read."); } } } ================================================ FILE: build.gradle.kts ================================================ import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.gradle.BaseExtension import org.eclipse.jgit.api.Git import org.eclipse.jgit.internal.storage.file.FileRepository import org.eclipse.jgit.storage.file.FileRepositoryBuilder plugins { alias(libs.plugins.agp.lib) apply false alias(libs.plugins.agp.app) apply false alias(lspatch.plugins.kotlin.android) apply false } buildscript { repositories { google() mavenCentral() } dependencies { classpath("org.eclipse.jgit:org.eclipse.jgit:6.3.0.202209071007-r") } } val commitCount = run { val repo = FileRepository(rootProject.file(".git")) val refId = repo.refDatabase.exactRef("refs/remotes/origin/master").objectId!! Git(repo).log().add(refId).call().count() } val (coreCommitCount, coreLatestTag) = FileRepositoryBuilder().setGitDir(rootProject.file(".git/modules/core")) .runCatching { build().use { repo -> val git = Git(repo) val coreCommitCount = git.log() .add(repo.refDatabase.exactRef("HEAD").objectId) .call().count() + 4200 val ver = git.describe() .setTags(true) .setAbbrev(0).call().removePrefix("v") coreCommitCount to ver } }.getOrNull() ?: (1 to "1.0") // sync from https://github.com/LSPosed/LSPosed/blob/master/build.gradle.kts val defaultManagerPackageName by extra("org.lsposed.lspatch") val apiCode by extra(93) val verCode by extra(commitCount) val verName by extra("0.6") val coreVerCode by extra(coreCommitCount) val coreVerName by extra(coreLatestTag) val androidMinSdkVersion by extra(28) val androidTargetSdkVersion by extra(34) val androidCompileSdkVersion by extra(34) val androidCompileNdkVersion by extra("25.2.9519653") val androidBuildToolsVersion by extra("34.0.0") val androidSourceCompatibility by extra(JavaVersion.VERSION_17) val androidTargetCompatibility by extra(JavaVersion.VERSION_17) tasks.register("clean") { delete(rootProject.buildDir) } listOf("Debug", "Release").forEach { variant -> tasks.register("build$variant") { description = "Build LSPatch with $variant" dependsOn(projects.jar.dependencyProject.tasks["build$variant"]) dependsOn(projects.manager.dependencyProject.tasks["build$variant"]) } } tasks.register("buildAll") { dependsOn("buildDebug", "buildRelease") } fun Project.configureBaseExtension() { extensions.findByType(BaseExtension::class)?.run { compileSdkVersion(androidCompileSdkVersion) ndkVersion = androidCompileNdkVersion buildToolsVersion = androidBuildToolsVersion externalNativeBuild.cmake { version = "3.22.1+" } defaultConfig { minSdk = androidMinSdkVersion targetSdk = androidTargetSdkVersion versionCode = verCode versionName = verName signingConfigs.create("config") { val androidStoreFile = project.findProperty("androidStoreFile") as String? if (!androidStoreFile.isNullOrEmpty()) { storeFile = rootProject.file(androidStoreFile) storePassword = project.property("androidStorePassword") as String keyAlias = project.property("androidKeyAlias") as String keyPassword = project.property("androidKeyPassword") as String } } externalNativeBuild { cmake { arguments += "-DEXTERNAL_ROOT=${File(rootDir.absolutePath, "core/external")}" arguments += "-DCORE_ROOT=${File(rootDir.absolutePath, "core/core/src/main/jni")}" abiFilters("arm64-v8a", "armeabi-v7a", "x86", "x86_64") val flags = arrayOf( "-Wall", "-Qunused-arguments", "-Wno-gnu-string-literal-operator-template", "-fno-rtti", "-fvisibility=hidden", "-fvisibility-inlines-hidden", "-fno-exceptions", "-fno-stack-protector", "-fomit-frame-pointer", "-Wno-builtin-macro-redefined", "-Wno-unused-value", "-D__FILE__=__FILE_NAME__", ) cppFlags("-std=c++20", *flags) cFlags("-std=c18", *flags) arguments( "-DANDROID_STL=none", "-DVERSION_CODE=$verCode", "-DVERSION_NAME=$verName", ) } } } compileOptions { targetCompatibility(androidTargetCompatibility) sourceCompatibility(androidSourceCompatibility) } buildTypes { all { signingConfig = if (signingConfigs["config"].storeFile != null) signingConfigs["config"] else signingConfigs["debug"] } named("debug") { externalNativeBuild { cmake { arguments.addAll( arrayOf( "-DCMAKE_CXX_FLAGS_DEBUG=-Og", "-DCMAKE_C_FLAGS_DEBUG=-Og", ) ) } } } named("release") { externalNativeBuild { cmake { val flags = arrayOf( "-Wl,--exclude-libs,ALL", "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", "-fno-unwind-tables", "-fno-asynchronous-unwind-tables", "-flto=thin", "-Wl,--thinlto-cache-policy,cache_size_bytes=300m", "-Wl,--thinlto-cache-dir=${buildDir.absolutePath}/.lto-cache", ) cppFlags.addAll(flags) cFlags.addAll(flags) val configFlags = arrayOf( "-Oz", "-DNDEBUG" ).joinToString(" ") arguments.addAll( arrayOf( "-DCMAKE_CXX_FLAGS_RELEASE=$configFlags", "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=$configFlags", "-DCMAKE_C_FLAGS_RELEASE=$configFlags", "-DCMAKE_C_FLAGS_RELWITHDEBINFO=$configFlags", "-DDEBUG_SYMBOLS_PATH=${buildDir.absolutePath}/symbols", ) ) } } } } } extensions.findByType(ApplicationExtension::class)?.lint { abortOnError = true checkReleaseBuilds = false } extensions.findByType(ApplicationAndroidComponentsExtension::class)?.let { androidComponents -> val optimizeReleaseRes = task("optimizeReleaseRes").doLast { val aapt2 = File( androidComponents.sdkComponents.sdkDirectory.get().asFile, "build-tools/${androidBuildToolsVersion}/aapt2" ) val zip = java.nio.file.Paths.get( project.buildDir.path, "intermediates", "optimized_processed_res", "release", "resources-release-optimize.ap_" ) val optimized = File("${zip}.opt") val cmd = exec { commandLine( aapt2, "optimize", "--collapse-resource-names", "--enable-sparse-encoding", "-o", optimized, zip ) isIgnoreExitValue = false } if (cmd.exitValue == 0) { delete(zip) optimized.renameTo(zip.toFile()) } } tasks.configureEach { if (name == "optimizeReleaseResources") { finalizedBy(optimizeReleaseRes) } } } } subprojects { plugins.withId("com.android.application") { configureBaseExtension() } plugins.withId("com.android.library") { configureBaseExtension() } } ================================================ FILE: crowdin.yml ================================================ # # Your Crowdin credentials # "project_id_env": "CROWDIN_PROJECT_ID" "api_token_env": "CROWDIN_API_TOKEN" "base_path": "." "base_url": "https://lsposed.crowdin.com/api/v2" "pull_request_title": "[translation] Update translation from Crowdin" # # Choose file structure in Crowdin # e.g. true or false # "preserve_hierarchy": true # # Files configuration # files: [ { "source": "/manager/src/main/res/values/strings.xml", "translation": "/manager/src/main/res/values-%two_letters_code%/%original_file_name%", "type": "android", "dest": "/manager/strings.xml", "skip_untranslated_strings": true } ] ================================================ FILE: gradle/lspatch.versions.toml ================================================ [versions] room = "2.5.2" accompanist = "0.27.0" compose-destinations = "1.9.42-beta" shizuku = "13.1.2" hiddenapi-refine = "4.3.0" hiddenapi-stub = "4.2.0" compose-bom = "2023.06.01" kotlin = "1.8.21" ksp = "1.8.21-1.0.11" commons-io = "2.13.0" beust-jcommander = "1.82" google-gson = "2.10.1" [plugins] google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } rikka-tools-refine = { id = "dev.rikka.tools.refine", version.ref = "hiddenapi-refine" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } [libraries] androidx-customview = "androidx.customview:customview:1.2.0-alpha02" androidx-customview-poolingcontainer = "androidx.customview:customview-poolingcontainer:1.0.0" androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-navigation-compose = "androidx.navigation:navigation-compose:2.6.0" androidx-lifecycle-viewmodel-compose = "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" androidx-activity-compose = "androidx.activity:activity-compose:1.7.2" androidx-core-ktx = "androidx.core:core-ktx:1.10.1" androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } google-accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" } google-accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } google-accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" } rikka-shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } rikka-shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } rikka-refine = { module = "dev.rikka.tools.refine:runtime", version.ref = "hiddenapi-refine" } rikka-hidden-stub = { module = "dev.rikka.hidden:stub", version.ref = "hiddenapi-stub" } raamcosta-compose-destinations = { module = "io.github.raamcosta.compose-destinations:core", version.ref = "compose-destinations" } raamcosta-compose-destinations-ksp = { module = "io.github.raamcosta.compose-destinations:ksp", version.ref = "compose-destinations" } commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } beust-jcommander = { module = "com.beust:jcommander", version.ref = "beust-jcommander" } google-gson = { module = "com.google.code.gson:gson", version.ref = "google-gson" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ android.experimental.enableNewResourceShrinker.preciseShrinking=true android.enableAppCompileTimeRClass=true android.useAndroidX=true ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # 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 # # https://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. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: jar/.gitignore ================================================ /build ================================================ FILE: jar/build.gradle.kts ================================================ val verCode: Int by rootProject.extra val verName: String by rootProject.extra val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra plugins { id("java-library") } java { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility } dependencies { implementation(projects.patch) } fun Jar.configure(variant: String) { archiveBaseName.set("jar-v$verName-$verCode-$variant") destinationDirectory.set(file("${rootProject.projectDir}/out/$variant")) manifest { attributes("Main-Class" to "org.lsposed.patch.LSPatch") } dependsOn(configurations.runtimeClasspath) from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) into("assets") { from("src/main/assets") from("${rootProject.projectDir}/out/assets/$variant") } exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "META-INF/*.MF", "META-INF/*.txt", "META-INF/versions/**") } tasks.register("buildDebug") { dependsOn(":meta-loader:copyDebug") dependsOn(":patch-loader:copyDebug") configure("debug") } tasks.register("buildRelease") { dependsOn(":meta-loader:copyRelease") dependsOn(":patch-loader:copyRelease") configure("release") } ================================================ FILE: manager/.gitignore ================================================ /build ================================================ FILE: manager/build.gradle.kts ================================================ import java.util.Locale val defaultManagerPackageName: String by rootProject.extra val apiCode: Int by rootProject.extra val verCode: Int by rootProject.extra val verName: String by rootProject.extra val coreVerCode: Int by rootProject.extra val coreVerName: String by rootProject.extra plugins { alias(libs.plugins.agp.app) alias(lspatch.plugins.google.devtools.ksp) alias(lspatch.plugins.rikka.tools.refine) alias(lspatch.plugins.kotlin.android) id("kotlin-parcelize") } android { defaultConfig { applicationId = defaultManagerPackageName } androidResources { noCompress.add(".so") } buildTypes { debug { isMinifyEnabled = true proguardFiles("proguard-rules-debug.pro") } release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } all { sourceSets[name].assets.srcDirs(rootProject.projectDir.resolve("out/assets/$name")) } } kotlinOptions { jvmTarget = "17" } kotlin { jvmToolchain(17) } buildFeatures { compose = true buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = "1.4.7" } namespace = "org.lsposed.lspatch" applicationVariants.all { kotlin.sourceSets { getByName(name) { kotlin.srcDir("build/generated/ksp/$name/kotlin") } } } } afterEvaluate { android.applicationVariants.forEach { variant -> val variantLowered = variant.name.lowercase() val variantCapped = variant.name.replaceFirstChar { it.uppercase() } task("copy${variantCapped}Assets") { dependsOn(":meta-loader:copy$variantCapped") dependsOn(":patch-loader:copy$variantCapped") tasks["merge${variantCapped}Assets"].dependsOn(this) into("$buildDir/intermediates/assets/$variantLowered/merge${variantCapped}Assets") from("${rootProject.projectDir}/out/assets/${variant.name}") } task("build$variantCapped") { dependsOn(tasks["assemble$variantCapped"]) from(variant.outputs.map { it.outputFile }) into("${rootProject.projectDir}/out/$variantLowered") rename(".*.apk", "manager-v$verName-$verCode-$variantLowered.apk") } } } dependencies { implementation(projects.patch) implementation(projects.services.daemonService) implementation(projects.share.android) implementation(projects.share.java) implementation(platform(lspatch.androidx.compose.bom)) annotationProcessor(lspatch.androidx.room.compiler) compileOnly(lspatch.rikka.hidden.stub) debugImplementation(lspatch.androidx.compose.ui.tooling) debugImplementation(lspatch.androidx.customview) debugImplementation(lspatch.androidx.customview.poolingcontainer) implementation(lspatch.androidx.activity.compose) implementation(lspatch.androidx.compose.material.icons.extended) implementation(lspatch.androidx.compose.material3) implementation(lspatch.androidx.compose.ui) implementation(lspatch.androidx.compose.ui.tooling.preview) implementation(lspatch.androidx.core.ktx) implementation(lspatch.androidx.lifecycle.viewmodel.compose) implementation(lspatch.androidx.navigation.compose) implementation(libs.androidx.preference) implementation(lspatch.androidx.room.ktx) implementation(lspatch.androidx.room.runtime) implementation(lspatch.google.accompanist.navigation.animation) implementation(lspatch.google.accompanist.pager) implementation(lspatch.google.accompanist.swiperefresh) implementation(libs.material) implementation(libs.gson) implementation(lspatch.rikka.shizuku.api) implementation(lspatch.rikka.shizuku.provider) implementation(lspatch.rikka.refine) implementation(lspatch.raamcosta.compose.destinations) implementation(libs.appiconloader) implementation(libs.hiddenapibypass) ksp(lspatch.androidx.room.compiler) ksp(lspatch.raamcosta.compose.destinations.ksp) } ================================================ FILE: manager/proguard-rules-debug.pro ================================================ -dontobfuscate -keep class com.beust.jcommander.** { *; } -keep class org.lsposed.lspatch.Patcher$Options { *; } -keep class org.lsposed.lspatch.share.LSPConfig { *; } -keep class org.lsposed.lspatch.share.PatchConfig { *; } -keepclassmembers class org.lsposed.patch.LSPatch { private ; } -dontwarn com.google.auto.value.AutoValue$Builder -dontwarn com.google.auto.value.AutoValue ================================================ FILE: manager/proguard-rules.pro ================================================ -assumenosideeffects class kotlin.jvm.internal.Intrinsics { public static void check*(...); public static void throw*(...); } -assumenosideeffects class java.util.Objects { public static ** requireNonNull(...); } -assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt { private static ** getDebugMetadataAnnotation(...) return null; } -keep class com.beust.jcommander.** { *; } -keep class org.lsposed.lspatch.Patcher$Options { *; } -keep class org.lsposed.lspatch.share.LSPConfig { *; } -keep class org.lsposed.lspatch.share.PatchConfig { *; } -keepclassmembers class org.lsposed.patch.LSPatch { private ; } -dontwarn com.google.auto.value.AutoValue$Builder -dontwarn com.google.auto.value.AutoValue ================================================ FILE: manager/src/main/AndroidManifest.xml ================================================ ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt ================================================ package org.lsposed.lspatch import android.app.Application import android.content.Context import android.content.SharedPreferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.lspatch.manager.AppBroadcastReceiver import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.ShizukuApi import java.io.File lateinit var lspApp: LSPApplication class LSPApplication : Application() { lateinit var prefs: SharedPreferences lateinit var tmpApkDir: File val globalScope = CoroutineScope(Dispatchers.Default) override fun onCreate() { super.onCreate() HiddenApiBypass.addHiddenApiExemptions("") lspApp = this filesDir.mkdir() tmpApkDir = cacheDir.resolve("apk").also { it.mkdir() } prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE) ShizukuApi.init() AppBroadcastReceiver.register(this) globalScope.launch { LSPPackageManager.fetchAppList() } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/Patcher.kt ================================================ package org.lsposed.lspatch import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.lsposed.lspatch.config.Configs import org.lsposed.lspatch.config.MyKeyStore import org.lsposed.lspatch.share.Constants import org.lsposed.lspatch.share.PatchConfig import org.lsposed.patch.LSPatch import org.lsposed.patch.util.Logger import java.io.IOException object Patcher { class Options( private val config: PatchConfig, private val apkPaths: List, private val embeddedModules: List? ) { fun toStringArray(): Array { return buildList { addAll(apkPaths) add("-o"); add(lspApp.tmpApkDir.absolutePath) if (config.debuggable) add("-d") add("-l"); add(config.sigBypassLevel.toString()) if (config.useManager) add("--manager") if (config.overrideVersionCode) add("-r") if (Configs.detailPatchLogs) add("-v") embeddedModules?.forEach { add("-m"); add(it) } if (!MyKeyStore.useDefault) { addAll(arrayOf("-k", MyKeyStore.file.path, Configs.keyStorePassword, Configs.keyStoreAlias, Configs.keyStoreAliasPassword)) } }.toTypedArray() } } suspend fun patch(logger: Logger, options: Options) { withContext(Dispatchers.IO) { LSPatch(logger, *options.toStringArray()).doCommandLine() val uri = Configs.storageDirectory?.toUri() ?: throw IOException("Uri is null") val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException("DocumentFile is null") root.listFiles().forEach { if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) it.delete() } lspApp.tmpApkDir.walk() .filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } .forEach { apk -> val file = root.createFile("application/vnd.android.package-archive", apk.name) ?: throw IOException("Failed to create output file") val output = lspApp.contentResolver.openOutputStream(file.uri) ?: throw IOException("Failed to open output stream") output.use { apk.inputStream().use { input -> input.copyTo(output) } } } logger.i("Patched files are saved to ${root.uri.lastPathSegment}") } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/config/ConfigManager.kt ================================================ package org.lsposed.lspatch.config import android.content.pm.PackageManager import android.util.Log import androidx.room.Room import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import org.lsposed.lspatch.database.LSPDatabase import org.lsposed.lspatch.database.entity.Module import org.lsposed.lspatch.database.entity.Scope import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.util.ModuleLoader import java.io.File object ConfigManager { private const val TAG = "ConfigManager" @OptIn(ExperimentalCoroutinesApi::class) private val dispatcher = Dispatchers.Default.limitedParallelism(1) private val db: LSPDatabase = Room.databaseBuilder( lspApp, LSPDatabase::class.java, "modules_config.db" ).build() private val moduleDao = db.moduleDao() private val scopeDao = db.scopeDao() private val loadedModules = mutableMapOf() suspend fun updateModules(newModules: Map) = withContext(dispatcher) { for (module in moduleDao.getAll()) { val apkPath = newModules[module.pkgName] if (apkPath == null) { moduleDao.delete(module) loadedModules.remove(module) } else if (module.apkPath != apkPath) { module.apkPath = apkPath loadedModules.remove(module) } } for ((pkgName, apkPath) in newModules) { moduleDao.insert(Module(pkgName, apkPath)) } } suspend fun activateModule(pkgName: String, module: Module) = withContext(dispatcher) { scopeDao.insert(Scope(appPkgName = pkgName, modulePkgName = module.pkgName)) } suspend fun deactivateModule(pkgName: String, module: Module) = withContext(dispatcher) { scopeDao.delete(Scope(appPkgName = pkgName, modulePkgName = module.pkgName)) } suspend fun getModulesForApp(pkgName: String): List = withContext(dispatcher) { return@withContext scopeDao.getModulesForApp(pkgName) } suspend fun getModuleFilesForApp(pkgName: String): List = withContext(dispatcher) { val modules = scopeDao.getModulesForApp(pkgName) return@withContext modules.mapNotNull { if (!File(it.apkPath).exists()) { loadedModules.remove(it) try { it.apkPath = lspApp.packageManager.getApplicationInfo(it.pkgName, 0).sourceDir } catch (e: PackageManager.NameNotFoundException) { moduleDao.delete(moduleDao.getModule(it.pkgName)) Log.w(TAG, "Module may be uninstalled: ${it.pkgName}") return@mapNotNull null } Log.i(TAG, "Module apk path updated: ${it.pkgName}") } loadedModules.getOrPut(it) { org.lsposed.lspd.models.Module().apply { packageName = it.pkgName apkPath = it.apkPath file = ModuleLoader.loadModule(it.apkPath) } } } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/config/Configs.kt ================================================ package org.lsposed.lspatch.config import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.ui.util.delegateStateOf import org.lsposed.lspatch.ui.util.getValue import org.lsposed.lspatch.ui.util.setValue object Configs { private const val PREFS_KEYSTORE_PASSWORD = "keystore_password" private const val PREFS_KEYSTORE_ALIAS = "keystore_alias" private const val PREFS_KEYSTORE_ALIAS_PASSWORD = "keystore_alias_password" private const val PREFS_STORAGE_DIRECTORY = "storage_directory" private const val PREFS_DETAIL_PATCH_LOGS = "detail_patch_logs" var keyStorePassword by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_PASSWORD, "123456")!!) { lspApp.prefs.edit().putString(PREFS_KEYSTORE_PASSWORD, it).apply() } var keyStoreAlias by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_ALIAS, "key0")!!) { lspApp.prefs.edit().putString(PREFS_KEYSTORE_ALIAS, it).apply() } var keyStoreAliasPassword by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_ALIAS_PASSWORD, "123456")!!) { lspApp.prefs.edit().putString(PREFS_KEYSTORE_ALIAS_PASSWORD, it).apply() } var storageDirectory by delegateStateOf(lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)) { lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, it).apply() } var detailPatchLogs by delegateStateOf(lspApp.prefs.getBoolean(PREFS_DETAIL_PATCH_LOGS, true)) { lspApp.prefs.edit().putBoolean(PREFS_DETAIL_PATCH_LOGS, it).apply() } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt ================================================ package org.lsposed.lspatch.config import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.lsposed.lspatch.lspApp import java.io.File object MyKeyStore { val file = File("${lspApp.filesDir}/keystore.bks") val tmpFile = File("${lspApp.filesDir}/keystore.bks.tmp") var useDefault by mutableStateOf(!file.exists()) private set suspend fun reset() { withContext(Dispatchers.IO) { file.delete() Configs.keyStorePassword = "123456" Configs.keyStoreAlias = "key0" Configs.keyStoreAliasPassword = "123456" useDefault = true } } suspend fun setCustom(password: String, alias: String, aliasPassword: String) { withContext(Dispatchers.IO) { tmpFile.renameTo(file) Configs.keyStorePassword = password Configs.keyStoreAlias = alias Configs.keyStoreAliasPassword = aliasPassword useDefault = false } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/database/LSPDatabase.kt ================================================ package org.lsposed.lspatch.database import androidx.room.Database import androidx.room.RoomDatabase import org.lsposed.lspatch.database.dao.ModuleDao import org.lsposed.lspatch.database.dao.ScopeDao import org.lsposed.lspatch.database.entity.Module import org.lsposed.lspatch.database.entity.Scope @Database(entities = [Module::class, Scope::class], version = 1) abstract class LSPDatabase : RoomDatabase() { abstract fun moduleDao(): ModuleDao abstract fun scopeDao(): ScopeDao } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/database/dao/ModuleDao.kt ================================================ package org.lsposed.lspatch.database.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import org.lsposed.lspatch.database.entity.Module @Dao interface ModuleDao { @Query("SELECT * FROM module WHERE pkgName = :pkgName") suspend fun getModule(pkgName: String): Module @Query("SELECT * FROM module") suspend fun getAll(): List @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insert(module: Module) @Delete suspend fun delete(module: Module) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/database/dao/ScopeDao.kt ================================================ package org.lsposed.lspatch.database.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import org.lsposed.lspatch.database.entity.Module import org.lsposed.lspatch.database.entity.Scope @Dao interface ScopeDao { @Query("SELECT * FROM module INNER JOIN scope ON module.pkgName = scope.modulePkgName WHERE scope.appPkgName = :appPkgName") suspend fun getModulesForApp(appPkgName: String): List @Insert suspend fun insert(scope: Scope) @Delete suspend fun delete(scope: Scope) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/database/entity/Module.kt ================================================ package org.lsposed.lspatch.database.entity import androidx.room.Entity import androidx.room.PrimaryKey @Entity data class Module( @PrimaryKey val pkgName: String, var apkPath: String ) ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/database/entity/Scope.kt ================================================ package org.lsposed.lspatch.database.entity import androidx.room.Entity import androidx.room.ForeignKey @Entity( primaryKeys = ["appPkgName", "modulePkgName"], foreignKeys = [ForeignKey(entity = Module::class, parentColumns = ["pkgName"], childColumns = ["modulePkgName"], onDelete = ForeignKey.CASCADE)] ) data class Scope( val appPkgName: String, val modulePkgName: String ) ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/manager/AppBroadcastReceiver.kt ================================================ package org.lsposed.lspatch.manager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import kotlinx.coroutines.launch import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.util.LSPPackageManager class AppBroadcastReceiver : BroadcastReceiver() { companion object { private const val TAG = "AppBroadcastReceiver" private val actions = setOf( Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_REPLACED ) fun register(context: Context) { val filter = IntentFilter().apply { actions.forEach(::addAction) addDataScheme("package") } context.registerReceiver(AppBroadcastReceiver(), filter) } } override fun onReceive(context: Context, intent: Intent) { if (intent.action in actions) { lspApp.globalScope.launch { Log.i(TAG, "Received intent: $intent") LSPPackageManager.fetchAppList() } } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/manager/ManagerService.kt ================================================ package org.lsposed.lspatch.manager import android.os.Binder import android.os.Bundle import android.os.IBinder import android.os.ParcelFileDescriptor import android.util.Log import kotlinx.coroutines.runBlocking import org.lsposed.lspatch.config.ConfigManager import org.lsposed.lspatch.lspApp import org.lsposed.lspd.models.Module import org.lsposed.lspd.service.ILSPApplicationService object ManagerService : ILSPApplicationService.Stub() { private const val TAG = "ManagerService" override fun getLegacyModulesList(): List { val app = lspApp.packageManager.getNameForUid(Binder.getCallingUid()) val list = app?.let { runBlocking { ConfigManager.getModuleFilesForApp(it) } }.orEmpty() Log.d(TAG, "$app calls getLegacyModulesList: $list") return list } override fun getModulesList(): List { return emptyList() } override fun getPrefsPath(packageName: String): String { TODO("Not yet implemented") } override fun requestInjectedManagerBinder(binder: List?): ParcelFileDescriptor? { return null } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/manager/ModuleService.kt ================================================ package org.lsposed.lspatch.manager import android.app.Service import android.content.Intent import android.os.IBinder import android.util.Log class ModuleService : Service() { companion object { private const val TAG = "ModuleService" } override fun onBind(intent: Intent): IBinder? { val packageName = intent.getStringExtra("packageName") ?: return null // TODO: Authentication Log.i(TAG, "$packageName requests binder") return ManagerService.asBinder() } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt ================================================ package org.lsposed.lspatch.ui.activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.ramcosta.composedestinations.DestinationsNavHost import org.lsposed.lspatch.ui.page.BottomBarDestination import org.lsposed.lspatch.ui.page.NavGraphs import org.lsposed.lspatch.ui.page.appCurrentDestinationAsState import org.lsposed.lspatch.ui.page.destinations.Destination import org.lsposed.lspatch.ui.page.startAppDestination import org.lsposed.lspatch.ui.theme.LSPTheme import org.lsposed.lspatch.ui.util.LocalSnackbarHost class MainActivity : ComponentActivity() { @OptIn(ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val navController = rememberAnimatedNavController() LSPTheme { val snackbarHostState = remember { SnackbarHostState() } CompositionLocalProvider(LocalSnackbarHost provides snackbarHostState) { Scaffold( bottomBar = { BottomBar(navController) }, snackbarHost = { SnackbarHost(snackbarHostState) } ) { innerPadding -> DestinationsNavHost( modifier = Modifier.padding(innerPadding), navGraph = NavGraphs.root, navController = navController ) } } } } } } @Composable private fun BottomBar(navController: NavHostController) { val currentDestination: Destination = navController.appCurrentDestinationAsState().value ?: NavGraphs.root.startAppDestination var topDestination by rememberSaveable { mutableStateOf(currentDestination.route) } LaunchedEffect(currentDestination) { val queue = navController.currentBackStack.value if (queue.size == 2) topDestination = queue[1].destination.route!! else if (queue.size > 2) topDestination = queue[2].destination.route!! } NavigationBar(tonalElevation = 8.dp) { BottomBarDestination.values().forEach { destination -> NavigationBarItem( selected = topDestination == destination.direction.route, onClick = { navController.navigate(destination.direction.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } }, icon = { if (topDestination == destination.direction.route) Icon(destination.iconSelected, stringResource(destination.label)) else Icon(destination.iconNotSelected, stringResource(destination.label)) }, label = { Text(stringResource(destination.label)) }, alwaysShowLabel = false ) } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/AnywhereDropdown.kt ================================================ package org.lsposed.lspatch.ui.component import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material3.DropdownMenu import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.DpOffset @OptIn(ExperimentalFoundationApi::class) @Composable fun AnywhereDropdown( modifier: Modifier = Modifier, enabled: Boolean = true, expanded: Boolean, onDismissRequest: () -> Unit, onClick: () -> Unit, onLongClick: (() -> Unit)? = null, surface: @Composable () -> Unit, content: @Composable ColumnScope.() -> Unit ) { val indication = LocalIndication.current val interactionSource = remember { MutableInteractionSource() } val state by interactionSource.interactions.collectAsState(null) var offset by remember { mutableStateOf(Offset.Zero) } val dpOffset = with(LocalDensity.current) { DpOffset(offset.x.toDp(), offset.y.toDp()) } LaunchedEffect(state) { if (state is PressInteraction.Press) { val i = state as PressInteraction.Press offset = i.pressPosition } if (state is PressInteraction.Release) { val i = state as PressInteraction.Release offset = i.press.pressPosition } } Box( modifier = modifier .combinedClickable( interactionSource = interactionSource, indication = indication, enabled = enabled, onClick = onClick, onLongClick = onLongClick ) ) { surface() Box { DropdownMenu( expanded = expanded, onDismissRequest = onDismissRequest, offset = dpOffset, content = content ) } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt ================================================ package org.lsposed.lspatch.ui.component import android.graphics.drawable.GradientDrawable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForwardIos import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import org.lsposed.lspatch.ui.theme.LSPTheme @Composable fun AppItem( modifier: Modifier = Modifier, icon: ImageBitmap, label: String, packageName: String, checked: Boolean? = null, rightIcon: (@Composable () -> Unit)? = null, additionalContent: (@Composable ColumnScope.() -> Unit)? = null, ) { if (checked != null && rightIcon != null) throw IllegalArgumentException("`checked` and `rightIcon` should not be both set") Column( modifier = modifier .fillMaxWidth() .padding(20.dp) ) { Row( horizontalArrangement = Arrangement.spacedBy(20.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( bitmap = icon, contentDescription = label, tint = Color.Unspecified ) Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp) ) { Text(label) Text( text = packageName, fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.bodySmall ) additionalContent?.invoke(this) } if (checked != null) { Checkbox( checked = checked, onCheckedChange = null, modifier = Modifier.padding(start = 20.dp) ) } if (rightIcon != null) { rightIcon() } } } } @Preview @Composable private fun AppItemPreview() { LSPTheme { val shape = GradientDrawable() shape.shape = GradientDrawable.RECTANGLE shape.setColor(MaterialTheme.colorScheme.primary.toArgb()) AppItem( icon = shape.toBitmap().asImageBitmap(), label = "Sample App", packageName = "org.lsposed.sample", rightIcon = { Icon(Icons.Filled.ArrowForwardIos, null) } ) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/CenterTopBar.kt ================================================ package org.lsposed.lspatch.ui.component import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import org.lsposed.lspatch.ui.util.SampleStringProvider @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable fun CenterTopBar(@PreviewParameter(SampleStringProvider::class, 1) text: String) { CenterAlignedTopAppBar( title = { Text( text = text, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.titleMedium ) } ) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/LoadingDialog.kt ================================================ package org.lsposed.lspatch.ui.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @Preview @Composable fun LoadingDialog() { Dialog( onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) ) { Box( modifier = Modifier .size(100.dp) .background(Color.White, shape = RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center, content = { CircularProgressIndicator() } ) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/SearchBar.kt ================================================ package org.lsposed.lspatch.ui.component import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp private const val TAG = "SearchBar" @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @Composable fun SearchAppBar( title: @Composable () -> Unit, searchText: String, onSearchTextChange: (String) -> Unit, onClearClick: () -> Unit, onBackClick: () -> Unit, onConfirm: (() -> Unit)? = null ) { val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } var onSearch by remember { mutableStateOf(false) } if (onSearch) { LaunchedEffect(Unit) { focusRequester.requestFocus() } } DisposableEffect(Unit) { onDispose { keyboardController?.hide() } } TopAppBar( title = { Box { AnimatedVisibility( modifier = Modifier.align(Alignment.CenterStart), visible = !onSearch, enter = fadeIn(), exit = fadeOut(), content = { title() } ) AnimatedVisibility( visible = onSearch, enter = fadeIn(), exit = fadeOut() ) { OutlinedTextField( modifier = Modifier .fillMaxWidth() .padding(vertical = 2.dp) .focusRequester(focusRequester) .onFocusChanged { focusState -> if (focusState.isFocused) onSearch = true Log.d(TAG, "onFocusChanged: $focusState") }, value = searchText, onValueChange = onSearchTextChange, trailingIcon = { IconButton( onClick = { onSearch = false keyboardController?.hide() onClearClick() }, content = { Icon(Icons.Filled.Close, null) } ) }, maxLines = 1, singleLine = true, keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() onConfirm?.invoke() }) ) } } }, navigationIcon = { IconButton( onClick = onBackClick, content = { Icon(Icons.Outlined.ArrowBack, null) } ) }, actions = { AnimatedVisibility( visible = !onSearch ) { IconButton( onClick = { onSearch = true }, content = { Icon(Icons.Filled.Search, null) } ) } } ) } @Preview @Composable private fun SearchAppBarPreview() { var searchText by remember { mutableStateOf("") } SearchAppBar( title = { Text("Search text") }, searchText = searchText, onSearchTextChange = { searchText = it }, onClearClick = { searchText = "" }, onBackClick = {} ) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/SelectionColumn.kt ================================================ package org.lsposed.lspatch.ui.component import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp object SelectionColumnScope { @Composable fun SelectionItem( modifier: Modifier = Modifier, selected: Boolean, onClick: () -> Unit, icon: ImageVector, title: String, desc: String? = null, extraContent: (@Composable ColumnScope.() -> Unit)? = null ) { Row( modifier = modifier .fillMaxWidth() .heightIn(min = 64.dp) .clip(RoundedCornerShape(4.dp)) .background( animateColorAsState( if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.inverseOnSurface ).value ) .clickable { onClick() } .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( imageVector = icon, contentDescription = null, modifier = Modifier.size(24.dp) ) Column { Text( text = title, style = MaterialTheme.typography.titleMedium ) if (desc != null || extraContent != null) { AnimatedVisibility( visible = selected, enter = fadeIn() + expandVertically(expandFrom = Alignment.Top), exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom) ) { Column { if (desc != null) { Text( text = desc, modifier = Modifier.padding(top = 8.dp), style = MaterialTheme.typography.bodyMedium ) } extraContent?.invoke(this) } } } } } } } @Composable fun SelectionColumn( modifier: Modifier = Modifier, content: @Composable() (SelectionColumnScope.() -> Unit) ) { Column( modifier = modifier.clip(RoundedCornerShape(32.dp)), verticalArrangement = Arrangement.spacedBy(2.dp), content = { SelectionColumnScope.content() } ) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/Shimmer.kt ================================================ package org.lsposed.lspatch.ui.component import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp private val ShimmerColorShades @Composable get() = listOf( MaterialTheme.colorScheme.secondaryContainer.copy(0.9f), MaterialTheme.colorScheme.secondaryContainer.copy(0.2f), MaterialTheme.colorScheme.secondaryContainer.copy(0.9f) ) class ShimmerScope(val brush: Brush) @Composable fun ShimmerAnimation( modifier: Modifier = Modifier, enabled: Boolean = true, content: @Composable ShimmerScope.() -> Unit ) { val transition = rememberInfiniteTransition() val translateAnim by transition.animateFloat( initialValue = 0f, targetValue = 1000f, animationSpec = infiniteRepeatable( tween(durationMillis = 1200, easing = FastOutSlowInEasing), RepeatMode.Reverse ) ) val brush = Brush.linearGradient( colors = if (enabled) ShimmerColorShades else List(3) { ShimmerColorShades[0] }, start = Offset(10f, 10f), end = Offset(translateAnim, translateAnim) ) Surface(modifier.background(brush)) { content(ShimmerScope(brush)) } } @Preview @Composable private fun ShimmerPreview() { ShimmerAnimation { Column(modifier = Modifier.padding(16.dp)) { Spacer( modifier = Modifier .fillMaxWidth() .size(250.dp) .background(brush = brush) ) Spacer( modifier = Modifier .fillMaxWidth() .height(30.dp) .padding(vertical = 8.dp) .background(brush = brush) ) } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt ================================================ package org.lsposed.lspatch.ui.component.settings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Api import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsCheckBox( modifier: Modifier = Modifier, checked: Boolean, enabled: Boolean = true, icon: ImageVector? = null, title: String, desc: String? = null, extraContent: (@Composable ColumnScope.() -> Unit)? = null ) { SettingsSlot(modifier, enabled, icon, title, desc, extraContent) { Checkbox(checked = checked, onCheckedChange = null) } } @Preview @Composable private fun SettingsCheckBoxPreview() { var checked1 by remember { mutableStateOf(false) } var checked2 by remember { mutableStateOf(false) } Column { SettingsCheckBox( modifier = Modifier.clickable { checked1 = !checked1 }, checked = checked1, title = "Title", desc = "Description" ) SettingsCheckBox( modifier = Modifier.clickable { checked2 = !checked2 }, checked = checked2, icon = Icons.Outlined.Api, title = "Title", desc = "Description" ) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt ================================================ package org.lsposed.lspatch.ui.component.settings import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp @Composable fun SettingsSlot( modifier: Modifier, enabled: Boolean, icon: ImageVector? = null, title: String, desc: String?, extraContent: (@Composable ColumnScope.() -> Unit)? = null, action: (@Composable RowScope.() -> Unit)?, ) { Row( modifier = modifier .fillMaxWidth() .alpha(if (enabled) 1f else 0.5f) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = modifier.size(24.dp), contentAlignment = Alignment.Center, ) { if (icon != null) { Icon( imageVector = icon, contentDescription = null, modifier = Modifier.fillMaxSize() ) } } Column(Modifier.weight(1f).padding(vertical = 6.dp)) { Text(text = title, style = MaterialTheme.typography.titleMedium) Column { if (desc != null) { Text( text = desc, style = MaterialTheme.typography.bodyMedium, modifier = Modifier .alpha(0.75f) .padding(top = 4.dp) ) } extraContent?.invoke(this) } } action?.invoke(this) } } @Composable fun SettingsItem( modifier: Modifier = Modifier, enabled: Boolean = true, icon: ImageVector? = null, title: String, desc: String? = null, extraContent: (@Composable ColumnScope.() -> Unit)? = null ) = SettingsSlot(modifier, enabled, icon, title, desc, extraContent, null) ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Switch.kt ================================================ package org.lsposed.lspatch.ui.component.settings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Api import androidx.compose.material3.Switch import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview @Composable fun SettingsSwitch( modifier: Modifier = Modifier, checked: Boolean, enabled: Boolean = true, icon: ImageVector? = null, title: String, desc: String? = null, extraContent: (@Composable ColumnScope.() -> Unit)? = null ) { SettingsSlot(modifier, enabled, icon, title, desc, extraContent) { Switch(checked = checked, onCheckedChange = null) } } @Preview @Composable private fun SettingsCheckBoxPreview() { var checked1 by remember { mutableStateOf(false) } var checked2 by remember { mutableStateOf(false) } Column { SettingsSwitch( modifier = Modifier.clickable { checked1 = !checked1 }, checked = checked1, title = "Title", desc = "Description" ) SettingsSwitch( modifier = Modifier.clickable { checked2 = !checked2 }, checked = checked2, icon = Icons.Outlined.Api, title = "Title", desc = "Description" ) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/BottomBarDestination.kt ================================================ package org.lsposed.lspatch.ui.page import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.ui.graphics.vector.ImageVector import com.ramcosta.composedestinations.spec.DirectionDestinationSpec import org.lsposed.lspatch.R import org.lsposed.lspatch.ui.page.destinations.* enum class BottomBarDestination( val direction: DirectionDestinationSpec, @StringRes val label: Int, val iconSelected: ImageVector, val iconNotSelected: ImageVector ) { Repo(RepoScreenDestination, R.string.screen_repo, Icons.Filled.GetApp, Icons.Outlined.GetApp), Manage(ManageScreenDestination, R.string.screen_manage, Icons.Filled.Dashboard, Icons.Outlined.Dashboard), Home(HomeScreenDestination, R.string.app_name, Icons.Filled.Home, Icons.Outlined.Home), Logs(LogsScreenDestination, R.string.screen_logs, Icons.Filled.Assignment, Icons.Outlined.Assignment), Settings(SettingsScreenDestination, R.string.screen_settings, Icons.Filled.Settings, Icons.Outlined.Settings); } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/HomeScreen.kt ================================================ package org.lsposed.lspatch.ui.page import android.app.Activity import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch import org.lsposed.lspatch.R import org.lsposed.lspatch.share.LSPConfig import org.lsposed.lspatch.ui.component.CenterTopBar import org.lsposed.lspatch.ui.page.destinations.ManageScreenDestination import org.lsposed.lspatch.ui.page.destinations.NewPatchScreenDestination import org.lsposed.lspatch.ui.util.HtmlText import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.util.ShizukuApi import rikka.shizuku.Shizuku @OptIn(ExperimentalMaterial3Api::class) @RootNavGraph(start = true) @Destination @Composable fun HomeScreen(navigator: DestinationsNavigator) { // Install from intent var isIntentLaunched by rememberSaveable { mutableStateOf(false) } val activity = LocalContext.current as Activity val intent = activity.intent LaunchedEffect(Unit) { if (!isIntentLaunched && intent.action == Intent.ACTION_VIEW && intent.hasCategory(Intent.CATEGORY_DEFAULT) && intent.type == "application/vnd.android.package-archive") { isIntentLaunched = true val uri = intent.data if (uri != null) { navigator.navigate(ManageScreenDestination) navigator.navigate( NewPatchScreenDestination( id = ACTION_INTENT_INSTALL, data = uri ) ) } } } Scaffold( topBar = { CenterTopBar(stringResource(R.string.app_name)) } ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { ShizukuCard() InfoCard() SupportCard() Spacer(Modifier) } } } private val listener: (Int, Int) -> Unit = { _, grantResult -> ShizukuApi.isPermissionGranted = grantResult == PackageManager.PERMISSION_GRANTED } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ShizukuCard() { LaunchedEffect(Unit) { Shizuku.addRequestPermissionResultListener(listener) } DisposableEffect(Unit) { onDispose { Shizuku.removeRequestPermissionResultListener(listener) } } ElevatedCard( colors = CardDefaults.elevatedCardColors(containerColor = run { if (ShizukuApi.isPermissionGranted) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer }) ) { Row( modifier = Modifier .fillMaxWidth() .clickable { if (ShizukuApi.isBinderAvailable && !ShizukuApi.isPermissionGranted) { Shizuku.requestPermission(114514) } } .padding(24.dp), verticalAlignment = Alignment.CenterVertically ) { if (ShizukuApi.isPermissionGranted) { Icon(Icons.Outlined.CheckCircle, stringResource(R.string.shizuku_available)) Column(Modifier.padding(start = 20.dp)) { Text( text = stringResource(R.string.shizuku_available), fontFamily = FontFamily.Serif, style = MaterialTheme.typography.titleMedium ) Spacer(Modifier.height(4.dp)) Text( text = "API " + Shizuku.getVersion(), style = MaterialTheme.typography.bodyMedium ) } } else { Icon(Icons.Outlined.Warning, stringResource(R.string.shizuku_unavailable)) Column(Modifier.padding(start = 20.dp)) { Text( text = stringResource(R.string.shizuku_unavailable), fontFamily = FontFamily.Serif, style = MaterialTheme.typography.titleMedium ) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.home_shizuku_warning), style = MaterialTheme.typography.bodyMedium ) } } } } } private val apiVersion = if (Build.VERSION.PREVIEW_SDK_INT != 0) { "${Build.VERSION.CODENAME} Preview (API ${Build.VERSION.PREVIEW_SDK_INT})" } else { "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})" } private val device = buildString { append(Build.MANUFACTURER[0].uppercaseChar().toString() + Build.MANUFACTURER.substring(1)) if (Build.BRAND != Build.MANUFACTURER) { append(" " + Build.BRAND[0].uppercaseChar() + Build.BRAND.substring(1)) } append(" " + Build.MODEL + " ") } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun InfoCard() { val context = LocalContext.current val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() ElevatedCard { Column( modifier = Modifier .fillMaxWidth() .padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp) ) { val contents = StringBuilder() val infoCardContent: @Composable (Pair) -> Unit = { texts -> contents.appendLine(texts.first).appendLine(texts.second).appendLine() Text(text = texts.first, style = MaterialTheme.typography.bodyLarge) Text(text = texts.second, style = MaterialTheme.typography.bodyMedium) } infoCardContent(stringResource(R.string.home_api_version) to "${LSPConfig.instance.API_CODE}") Spacer(Modifier.height(24.dp)) infoCardContent(stringResource(R.string.home_lspatch_version) to LSPConfig.instance.VERSION_NAME + " (${LSPConfig.instance.VERSION_CODE})") Spacer(Modifier.height(24.dp)) infoCardContent(stringResource(R.string.home_framework_version) to LSPConfig.instance.CORE_VERSION_NAME + " (${LSPConfig.instance.CORE_VERSION_CODE})") Spacer(Modifier.height(24.dp)) infoCardContent(stringResource(R.string.home_system_version) to apiVersion) Spacer(Modifier.height(24.dp)) infoCardContent(stringResource(R.string.home_device) to device) Spacer(Modifier.height(24.dp)) infoCardContent(stringResource(R.string.home_system_abi) to Build.SUPPORTED_ABIS[0]) val copiedMessage = stringResource(R.string.home_info_copied) TextButton( modifier = Modifier.align(Alignment.End), onClick = { val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", contents.toString())) scope.launch { snackbarHost.showSnackbar(copiedMessage) } }, content = { Text(stringResource(android.R.string.copy)) } ) } } } @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable private fun SupportCard() { ElevatedCard { Column( modifier = Modifier .fillMaxWidth() .padding(24.dp) ) { Text( text = stringResource(R.string.home_support), fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.titleMedium ) Text( modifier = Modifier.padding(vertical = 8.dp), text = stringResource(R.string.home_description), style = MaterialTheme.typography.bodyMedium ) HtmlText( stringResource( R.string.home_view_source_code, "GitHub", "Telegram" ) ) } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/LogsScreen.kt ================================================ package org.lsposed.lspatch.ui.page import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.ramcosta.composedestinations.annotation.Destination import org.lsposed.lspatch.ui.component.CenterTopBar @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable fun LogsScreen() { Scaffold( topBar = { CenterTopBar(stringResource(BottomBarDestination.Logs.label)) } ) { innerPadding -> Text( modifier = Modifier .padding(innerPadding) .fillMaxSize(), text = "This page is not yet implemented", textAlign = TextAlign.Center ) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/ManageScreen.kt ================================================ package org.lsposed.lspatch.ui.page import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.ResultRecipient import kotlinx.coroutines.launch import org.lsposed.lspatch.R import org.lsposed.lspatch.ui.component.CenterTopBar import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination import org.lsposed.lspatch.ui.page.manage.AppManageBody import org.lsposed.lspatch.ui.page.manage.AppManageFab import org.lsposed.lspatch.ui.page.manage.ModuleManageBody @OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class) @Destination @Composable fun ManageScreen( navigator: DestinationsNavigator, resultRecipient: ResultRecipient ) { val scope = rememberCoroutineScope() val pagerState = rememberPagerState() Scaffold( topBar = { CenterTopBar(stringResource(BottomBarDestination.Manage.label)) }, floatingActionButton = { if (pagerState.currentPage == 0) AppManageFab(navigator) } ) { innerPadding -> Box(Modifier.padding(innerPadding)) { Column { TabRow( contentColor = MaterialTheme.colorScheme.secondary, selectedTabIndex = pagerState.currentPage ) { Tab( selected = pagerState.currentPage == 0, onClick = { scope.launch { pagerState.animateScrollToPage(0) } } ) { Text( modifier = Modifier.padding(vertical = 16.dp), text = stringResource(R.string.apps) ) } Tab( selected = pagerState.currentPage == 1, onClick = { scope.launch { pagerState.animateScrollToPage(1) } } ) { Text( modifier = Modifier.padding(vertical = 16.dp), text = stringResource(R.string.modules) ) } } HorizontalPager(count = 2, state = pagerState) { page -> when (page) { 0 -> AppManageBody(navigator, resultRecipient) 1 -> ModuleManageBody() } } } } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt ================================================ package org.lsposed.lspatch.ui.page import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.pm.PackageInstaller import android.net.Uri import android.util.Log import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.lsposed.lspatch.R import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.SelectionColumn import org.lsposed.lspatch.ui.component.ShimmerAnimation import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.ui.util.isScrolledToEnd import org.lsposed.lspatch.ui.util.lastItemIndex import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager.AppInfo import org.lsposed.lspatch.util.ShizukuApi private const val TAG = "NewPatchPage" const val ACTION_STORAGE = 0 const val ACTION_APPLIST = 1 const val ACTION_INTENT_INSTALL = 2 @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable fun NewPatchScreen( navigator: DestinationsNavigator, resultRecipient: ResultRecipient, id: Int, data: Uri? = null ) { val viewModel = viewModel() val snackbarHost = LocalSnackbarHost.current val errorUnknown = stringResource(R.string.error_unknown) val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> if (apks.isEmpty()) { navigator.navigateUp() return@rememberLauncherForActivityResult } runBlocking { LSPPackageManager.getAppInfoFromApks(apks) .onSuccess { viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) } .onFailure { lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: errorUnknown) } navigator.navigateUp() } } } var showSelectModuleDialog by remember { mutableStateOf(false) } val noXposedModules = stringResource(R.string.patch_no_xposed_module) val storageModuleLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> if (apks.isEmpty()) { return@rememberLauncherForActivityResult } runBlocking { LSPPackageManager.getAppInfoFromApks(apks).onSuccess { it -> viewModel.embeddedModules = it.filter { it.isXposedModule }.ifEmpty { lspApp.globalScope.launch { snackbarHost.showSnackbar(noXposedModules) } return@onSuccess } }.onFailure { lspApp.globalScope.launch { snackbarHost.showSnackbar( it.message ?: errorUnknown ) } } } } Log.d(TAG, "PatchState: ${viewModel.patchState}") when (viewModel.patchState) { PatchState.INIT -> { LaunchedEffect(Unit) { LSPPackageManager.cleanTmpApkDir() when (id) { ACTION_STORAGE -> { storageLauncher.launch(arrayOf("application/vnd.android.package-archive")) viewModel.dispatch(ViewAction.DoneInit) } ACTION_APPLIST -> { navigator.navigate(SelectAppsScreenDestination(false)) viewModel.dispatch(ViewAction.DoneInit) } ACTION_INTENT_INSTALL -> { runBlocking { data?.let { uri -> LSPPackageManager.getAppInfoFromApks(listOf(uri)).onSuccess { viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) }.onFailure { lspApp.globalScope.launch { snackbarHost.showSnackbar( it.message ?: errorUnknown ) } navigator.navigateUp() } } } } } } } PatchState.SELECTING -> { resultRecipient.onNavResult { Log.d(TAG, "onNavResult: $it") when (it) { is NavResult.Canceled -> navigator.navigateUp() is NavResult.Value -> { val result = it.value as SelectAppsResult.SingleApp viewModel.dispatch(ViewAction.ConfigurePatch(result.selected)) } } } } else -> { Scaffold( topBar = { when (viewModel.patchState) { PatchState.CONFIGURING -> ConfiguringTopBar { navigator.navigateUp() } PatchState.PATCHING, PatchState.FINISHED, PatchState.ERROR -> CenterAlignedTopAppBar(title = { Text(viewModel.patchApp.app.packageName) }) else -> Unit } }, floatingActionButton = { if (viewModel.patchState == PatchState.CONFIGURING) { ConfiguringFab() } } ) { innerPadding -> if (viewModel.patchState == PatchState.CONFIGURING) { PatchOptionsBody(Modifier.padding(innerPadding)) { showSelectModuleDialog = true } resultRecipient.onNavResult { if (it is NavResult.Value) { val result = it.value as SelectAppsResult.MultipleApps viewModel.embeddedModules = result.selected } } } else { DoPatchBody(Modifier.padding(innerPadding), navigator) } } if (showSelectModuleDialog) { AlertDialog(onDismissRequest = { showSelectModuleDialog = false }, confirmButton = {}, dismissButton = { TextButton(content = { Text(stringResource(android.R.string.cancel)) }, onClick = { showSelectModuleDialog = false }) }, title = { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.patch_embed_modules), textAlign = TextAlign.Center ) }, text = { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { TextButton(modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), onClick = { storageModuleLauncher.launch(arrayOf("application/vnd.android.package-archive")) showSelectModuleDialog = false }) { Text( modifier = Modifier.padding(vertical = 8.dp), text = stringResource(R.string.patch_from_storage), style = MaterialTheme.typography.bodyLarge ) } TextButton(modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), onClick = { navigator.navigate( SelectAppsScreenDestination(true, viewModel.embeddedModules.mapTo(ArrayList()) { it.app.packageName }) ) showSelectModuleDialog = false }) { Text( modifier = Modifier.padding(vertical = 8.dp), text = stringResource(R.string.patch_from_applist), style = MaterialTheme.typography.bodyLarge ) } } }) } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ConfiguringTopBar(onBackClick: () -> Unit) { TopAppBar( title = { Text(stringResource(R.string.screen_new_patch)) }, navigationIcon = { IconButton( onClick = onBackClick, content = { Icon(Icons.Outlined.ArrowBack, null) } ) } ) } @Composable private fun ConfiguringFab() { val viewModel = viewModel() ExtendedFloatingActionButton( text = { Text(stringResource(R.string.patch_start)) }, icon = { Icon(Icons.Outlined.AutoFixHigh, null) }, onClick = { viewModel.dispatch(ViewAction.SubmitPatch) } ) } @Composable private fun sigBypassLvStr(level: Int) = when (level) { 0 -> stringResource(R.string.patch_sigbypasslv0) 1 -> stringResource(R.string.patch_sigbypasslv1) 2 -> stringResource(R.string.patch_sigbypasslv2) else -> throw IllegalArgumentException("Invalid sigBypassLv: $level") } @Composable private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { val viewModel = viewModel() Column(modifier.verticalScroll(rememberScrollState())) { Text( text = viewModel.patchApp.label, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(horizontal = 24.dp) ) Text( text = viewModel.patchApp.app.packageName, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(horizontal = 24.dp) ) Text( text = stringResource(R.string.patch_mode), style = MaterialTheme.typography.titleLarge, modifier = Modifier .align(Alignment.CenterHorizontally) .padding(top = 24.dp, bottom = 12.dp) ) SelectionColumn(Modifier.padding(horizontal = 24.dp)) { SelectionItem( selected = viewModel.useManager, onClick = { viewModel.useManager = true }, icon = Icons.Outlined.Api, title = stringResource(R.string.patch_local), desc = stringResource(R.string.patch_local_desc) ) SelectionItem( selected = !viewModel.useManager, onClick = { viewModel.useManager = false }, icon = Icons.Outlined.WorkOutline, title = stringResource(R.string.patch_integrated), desc = stringResource(R.string.patch_integrated_desc), extraContent = { TextButton( onClick = onAddEmbed, content = { Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) } ) } ) } SettingsCheckBox( modifier = Modifier .padding(top = 6.dp) .clickable { viewModel.debuggable = !viewModel.debuggable }, checked = viewModel.debuggable, icon = Icons.Outlined.BugReport, title = stringResource(R.string.patch_debuggable) ) SettingsCheckBox( modifier = Modifier.clickable { viewModel.overrideVersionCode = !viewModel.overrideVersionCode }, checked = viewModel.overrideVersionCode, icon = Icons.Outlined.Layers, title = stringResource(R.string.patch_override_version_code), desc = stringResource(R.string.patch_override_version_code_desc) ) var bypassExpanded by remember { mutableStateOf(false) } AnywhereDropdown( expanded = bypassExpanded, onDismissRequest = { bypassExpanded = false }, onClick = { bypassExpanded = true }, surface = { SettingsItem( icon = Icons.Outlined.RemoveModerator, title = stringResource(R.string.patch_sigbypass), desc = sigBypassLvStr(viewModel.sigBypassLevel) ) } ) { repeat(3) { DropdownMenuItem( text = { Row(verticalAlignment = Alignment.CenterVertically) { RadioButton(selected = viewModel.sigBypassLevel == it, onClick = { viewModel.sigBypassLevel = it }) Text(sigBypassLvStr(it)) } }, onClick = { viewModel.sigBypassLevel = it bypassExpanded = false } ) } } } } @Composable private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { val viewModel = viewModel() val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() LaunchedEffect(Unit) { if (viewModel.logs.isEmpty()) { viewModel.dispatch(ViewAction.LaunchPatch) } } BoxWithConstraints(modifier.padding(start = 24.dp, end = 24.dp, bottom = 24.dp)) { val shellBoxMaxHeight = if (viewModel.patchState == PatchState.PATCHING) maxHeight else maxHeight - ButtonDefaults.MinHeight - 12.dp Column( Modifier .fillMaxSize() .wrapContentHeight() .animateContentSize(spring(stiffness = Spring.StiffnessLow)) ) { ShimmerAnimation(enabled = viewModel.patchState == PatchState.PATCHING) { ProvideTextStyle(MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)) { val scrollState = rememberLazyListState() LazyColumn( state = scrollState, modifier = Modifier .fillMaxWidth() .heightIn(max = shellBoxMaxHeight) .clip(RoundedCornerShape(32.dp)) .background(brush) .padding(horizontal = 24.dp, vertical = 18.dp) ) { items(viewModel.logs) { when (it.first) { Log.DEBUG -> Text(text = it.second) Log.INFO -> Text(text = it.second) Log.ERROR -> Text(text = it.second, color = MaterialTheme.colorScheme.error) } } } LaunchedEffect(scrollState.lastItemIndex) { if (!scrollState.isScrolledToEnd) { scrollState.animateScrollToItem(scrollState.lastItemIndex!!) } } } } when (viewModel.patchState) { PatchState.PATCHING -> BackHandler {} PatchState.FINISHED -> { val shizukuUnavailable = stringResource(R.string.shizuku_unavailable) val installSuccessfully = stringResource(R.string.patch_install_successfully) val installFailed = stringResource(R.string.patch_install_failed) val copyError = stringResource(R.string.copy_error) var installing by remember { mutableStateOf(false) } if (installing) InstallDialog(viewModel.patchApp) { status, message -> scope.launch { installing = false if (status == PackageInstaller.STATUS_SUCCESS) { lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } navigator.navigateUp() } else if (status != LSPPackageManager.STATUS_USER_CANCELLED) { val result = snackbarHost.showSnackbar(installFailed, copyError) if (result == SnackbarResult.ActionPerformed) { val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", message)) } } } } Row(Modifier.padding(top = 12.dp)) { Button( modifier = Modifier.weight(1f), onClick = { navigator.navigateUp() }, content = { Text(stringResource(R.string.patch_return)) } ) Spacer(Modifier.weight(0.2f)) Button( modifier = Modifier.weight(1f), onClick = { if (!ShizukuApi.isPermissionGranted) { scope.launch { snackbarHost.showSnackbar(shizukuUnavailable) } } else { installing = true } }, content = { Text(stringResource(R.string.install)) } ) } } PatchState.ERROR -> { Row(Modifier.padding(top = 12.dp)) { Button( modifier = Modifier.weight(1f), onClick = { navigator.navigateUp() }, content = { Text(stringResource(R.string.patch_return)) } ) Spacer(Modifier.weight(0.2f)) Button( modifier = Modifier.weight(1f), onClick = { val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", viewModel.logs.joinToString { it.second + "\n" })) }, content = { Text(stringResource(R.string.copy_error)) } ) } } else -> Unit } } } } @Composable private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { val scope = rememberCoroutineScope() var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) } var installing by remember { mutableStateOf(0) } suspend fun doInstall() { Log.i(TAG, "Installing app ${patchApp.app.packageName}") installing = 1 val (status, message) = LSPPackageManager.install() installing = 0 Log.i(TAG, "Installation end: $status, $message") onFinish(status, message) } LaunchedEffect(Unit) { if (!uninstallFirst) { doInstall() } } if (uninstallFirst) { AlertDialog( onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, confirmButton = { TextButton( onClick = { scope.launch { Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") uninstallFirst = false installing = 2 val (status, message) = LSPPackageManager.uninstall(patchApp.app.packageName) installing = 0 Log.i(TAG, "Uninstallation end: $status, $message") if (status == PackageInstaller.STATUS_SUCCESS) { doInstall() } else { onFinish(status, message) } } }, content = { Text(stringResource(android.R.string.ok)) } ) }, dismissButton = { TextButton( onClick = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, content = { Text(stringResource(android.R.string.cancel)) } ) }, title = { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.uninstall), textAlign = TextAlign.Center ) }, text = { Text(stringResource(R.string.patch_uninstall_text)) } ) } if (installing != 0) { AlertDialog( onDismissRequest = {}, confirmButton = {}, title = { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(if (installing == 1) R.string.installing else R.string.uninstalling), fontFamily = FontFamily.Serif, textAlign = TextAlign.Center ) } ) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/RepoScreen.kt ================================================ package org.lsposed.lspatch.ui.page import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.ramcosta.composedestinations.annotation.Destination import org.lsposed.lspatch.ui.component.CenterTopBar @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable fun RepoScreen() { Scaffold( topBar = { CenterTopBar(stringResource(BottomBarDestination.Repo.label)) } ) { innerPadding -> Text( modifier = Modifier .padding(innerPadding) .fillMaxSize(), text = "This page is not yet implemented", textAlign = TextAlign.Center ) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsScreen.kt ================================================ package org.lsposed.lspatch.ui.page import android.content.pm.ApplicationInfo import android.os.Parcelable import androidx.activity.compose.BackHandler import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Done import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toLowerCase import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.result.ResultBackNavigator import kotlinx.parcelize.Parcelize import org.lsposed.lspatch.R import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.component.SearchAppBar import org.lsposed.lspatch.ui.viewmodel.SelectAppsViewModel import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager.AppInfo @Parcelize sealed class SelectAppsResult : Parcelable { data class SingleApp(val selected: AppInfo) : SelectAppsResult() data class MultipleApps(val selected: List) : SelectAppsResult() } @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable fun SelectAppsScreen( navigator: ResultBackNavigator, multiSelect: Boolean, initialSelected: ArrayList? = null ) { val viewModel = viewModel() var searchPackage by remember { mutableStateOf("") } val filter: (AppInfo) -> Boolean = { val packageLowerCase = searchPackage.toLowerCase(Locale.current) val contains = it.label.toLowerCase(Locale.current).contains(packageLowerCase) || it.app.packageName.contains(packageLowerCase) if (multiSelect) contains && it.isXposedModule else contains && it.app.flags and ApplicationInfo.FLAG_SYSTEM == 0 } LaunchedEffect(Unit) { viewModel.filterAppList(false, filter) initialSelected?.let { val tmp = initialSelected.toSet() viewModel.multiSelected.addAll(LSPPackageManager.appList.filter { tmp.contains(it.app.packageName) }) } } BackHandler { navigator.navigateBack() } Scaffold( topBar = { SearchAppBar( title = { Text(stringResource(R.string.screen_select_apps)) }, searchText = searchPackage, onSearchTextChange = { searchPackage = it viewModel.filterAppList(false, filter) }, onClearClick = { searchPackage = "" viewModel.filterAppList(false, filter) }, onBackClick = { navigator.navigateBack() } ) }, floatingActionButton = { if (multiSelect) MultiSelectFab { navigator.navigateBack(SelectAppsResult.MultipleApps(viewModel.multiSelected)) } } ) { innerPadding -> SwipeRefresh( state = rememberSwipeRefreshState(viewModel.isRefreshing), onRefresh = { viewModel.filterAppList(true, filter) }, modifier = Modifier .padding(innerPadding) .fillMaxSize() ) { if (multiSelect) MultiSelect() else SingleSelect { navigator.navigateBack(SelectAppsResult.SingleApp(it)) } } } } @Composable private fun MultiSelectFab(onClick: () -> Unit) { FloatingActionButton( onClick = onClick, content = { Icon(Icons.Outlined.Done, stringResource(R.string.add)) } ) } @OptIn(ExperimentalFoundationApi::class) @Composable private fun SingleSelect(onSelect: (AppInfo) -> Unit) { val viewModel = viewModel() LazyColumn { items( items = viewModel.filteredList, key = { it.app.packageName } ) { AppItem( modifier = Modifier .animateItemPlacement(spring(stiffness = Spring.StiffnessLow)) .clickable { onSelect(it) }, icon = LSPPackageManager.getIcon(it), label = it.label, packageName = it.app.packageName ) } } } @OptIn(ExperimentalFoundationApi::class) @Composable private fun MultiSelect() { val viewModel = viewModel() LazyColumn { items( items = viewModel.filteredList, key = { it.app.packageName } ) { val checked = viewModel.multiSelected.contains(it) AppItem( modifier = Modifier .animateItemPlacement(spring(stiffness = Spring.StiffnessLow)) .clickable { if (checked) viewModel.multiSelected.remove(it) else viewModel.multiSelected.add(it) }, icon = LSPPackageManager.getIcon(it), label = it.label, packageName = it.app.packageName, checked = checked ) } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsScreen.kt ================================================ package org.lsposed.lspatch.ui.page import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Ballot import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.annotation.Destination import kotlinx.coroutines.launch import org.lsposed.lspatch.R import org.lsposed.lspatch.config.Configs import org.lsposed.lspatch.config.MyKeyStore import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.CenterTopBar import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.component.settings.SettingsSwitch import java.io.IOException import java.security.GeneralSecurityException import java.security.KeyStore @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable fun SettingsScreen() { Scaffold( topBar = { CenterTopBar(stringResource(BottomBarDestination.Settings.label)) } ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .verticalScroll(rememberScrollState()) ) { KeyStore() DetailPatchLogs() } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun KeyStore() { val context = LocalContext.current val scope = rememberCoroutineScope() var expanded by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) } AnywhereDropdown( expanded = expanded, onDismissRequest = { expanded = false }, onClick = { expanded = true }, surface = { SettingsItem( icon = Icons.Outlined.Ballot, title = stringResource(R.string.settings_keystore), desc = stringResource(if (MyKeyStore.useDefault) R.string.settings_keystore_default else R.string.settings_keystore_custom) ) } ) { DropdownMenuItem( text = { Text(stringResource(R.string.settings_keystore_default)) }, onClick = { scope.launch { MyKeyStore.reset() } expanded = false } ) DropdownMenuItem( text = { Text(stringResource(R.string.settings_keystore_custom)) }, onClick = { expanded = false showDialog = true } ) } if (showDialog) { var wrongKeystore by rememberSaveable { mutableStateOf(false) } var wrongPassword by rememberSaveable { mutableStateOf(false) } var wrongAliasName by rememberSaveable { mutableStateOf(false) } var wrongAliasPassword by rememberSaveable { mutableStateOf(false) } var path by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } var alias by rememberSaveable { mutableStateOf("") } var aliasPassword by rememberSaveable { mutableStateOf("") } val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) return@rememberLauncherForActivityResult context.contentResolver.openInputStream(uri).use { input -> MyKeyStore.tmpFile.outputStream().use { output -> input?.copyTo(output) } } path = uri.path ?: "" } AlertDialog( onDismissRequest = { expanded = false; showDialog = false }, confirmButton = { TextButton( content = { Text(stringResource(android.R.string.ok)) }, onClick = { wrongKeystore = false wrongPassword = false wrongAliasName = false wrongAliasPassword = false if (path.isEmpty()) { wrongKeystore = true return@TextButton } val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) try { MyKeyStore.tmpFile.inputStream().use { input -> keyStore.load(input, password.toCharArray()) } } catch (e: IOException) { wrongKeystore = true if (e.message == "KeyStore integrity check failed.") { wrongPassword = true } return@TextButton } if (!keyStore.containsAlias(alias)) { wrongAliasName = true return@TextButton } try { keyStore.getKey(alias, aliasPassword.toCharArray()) } catch (e: GeneralSecurityException) { wrongAliasPassword = true return@TextButton } scope.launch { MyKeyStore.setCustom(password, alias, aliasPassword) } expanded = false showDialog = false }) }, dismissButton = { TextButton( content = { Text(stringResource(android.R.string.cancel)) }, onClick = { expanded = false; showDialog = false } ) }, title = { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.settings_keystore_dialog_title), textAlign = TextAlign.Center ) }, text = { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { val interactionSource = remember { MutableInteractionSource() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> if (interaction is PressInteraction.Release) { launcher.launch("*/*") } } } val wrongText = when { wrongAliasPassword -> stringResource(R.string.settings_keystore_wrong_alias_password) wrongAliasName -> stringResource(R.string.settings_keystore_wrong_alias) wrongPassword -> stringResource(R.string.settings_keystore_wrong_password) wrongKeystore -> stringResource(R.string.settings_keystore_wrong_keystore) else -> null } Text( modifier = Modifier.padding(bottom = 8.dp), text = wrongText ?: stringResource(R.string.settings_keystore_desc), color = if (wrongText != null) MaterialTheme.colorScheme.error else Color.Unspecified ) OutlinedTextField( value = path, onValueChange = { path = it }, readOnly = true, label = { Text(stringResource(R.string.settings_keystore_file)) }, placeholder = { Text(stringResource(R.string.settings_keystore_file)) }, singleLine = true, isError = wrongKeystore, interactionSource = interactionSource ) OutlinedTextField( value = password, onValueChange = { password = it }, label = { Text(stringResource(R.string.settings_keystore_password)) }, singleLine = true, isError = wrongPassword ) OutlinedTextField( value = alias, onValueChange = { alias = it }, label = { Text(stringResource(R.string.settings_keystore_alias)) }, singleLine = true, isError = wrongAliasName ) OutlinedTextField( value = aliasPassword, onValueChange = { aliasPassword = it }, label = { Text(stringResource(R.string.settings_keystore_alias_password)) }, singleLine = true, isError = wrongAliasPassword ) } } ) } } @Composable private fun DetailPatchLogs() { SettingsSwitch( modifier = Modifier.clickable { Configs.detailPatchLogs = !Configs.detailPatchLogs }, checked = Configs.detailPatchLogs, icon = Icons.Outlined.BugReport, title = stringResource(R.string.settings_detail_patch_logs) ) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt ================================================ package org.lsposed.lspatch.ui.page.manage import android.app.Activity import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.KeyboardCapslock import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.viewmodel.compose.viewModel import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import kotlinx.coroutines.launch import org.lsposed.lspatch.BuildConfig import org.lsposed.lspatch.R import org.lsposed.lspatch.config.ConfigManager import org.lsposed.lspatch.config.Configs import org.lsposed.lspatch.database.entity.Module import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.share.Constants import org.lsposed.lspatch.share.LSPConfig import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.component.LoadingDialog import org.lsposed.lspatch.ui.page.ACTION_APPLIST import org.lsposed.lspatch.ui.page.ACTION_STORAGE import org.lsposed.lspatch.ui.page.SelectAppsResult import org.lsposed.lspatch.ui.page.destinations.NewPatchScreenDestination import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.ui.viewmodel.manage.AppManageViewModel import org.lsposed.lspatch.ui.viewstate.ProcessingState import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.ShizukuApi import java.io.IOException private const val TAG = "AppManagePage" @Composable fun AppManageBody( navigator: DestinationsNavigator, resultRecipient: ResultRecipient ) { val viewModel = viewModel() val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() if (viewModel.appList.isEmpty()) { Box(Modifier.fillMaxSize()) { Text( modifier = Modifier.align(Alignment.Center), text = run { if (LSPPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading) else stringResource(R.string.manage_no_apps) }, fontFamily = FontFamily.Serif, style = MaterialTheme.typography.headlineSmall ) } } else { var scopeApp by rememberSaveable { mutableStateOf("") } resultRecipient.onNavResult { if (it is NavResult.Value) { scope.launch { val result = it.value as SelectAppsResult.MultipleApps ConfigManager.getModulesForApp(scopeApp).forEach { ConfigManager.deactivateModule(scopeApp, it) } result.selected.forEach { Log.d(TAG, "Activate ${it.app.packageName} for $scopeApp") ConfigManager.activateModule(scopeApp, Module(it.app.packageName, it.app.sourceDir)) } } } } when (viewModel.updateLoaderState) { is ProcessingState.Idle -> Unit is ProcessingState.Processing -> LoadingDialog() is ProcessingState.Done -> { val it = viewModel.updateLoaderState as ProcessingState.Done val updateSuccessfully = stringResource(R.string.manage_update_loader_successfully) val updateFailed = stringResource(R.string.manage_update_loader_failed) val copyError = stringResource(R.string.copy_error) LaunchedEffect(Unit) { it.result.onSuccess { snackbarHost.showSnackbar(updateSuccessfully) }.onFailure { val result = snackbarHost.showSnackbar(updateFailed, copyError) if (result == SnackbarResult.ActionPerformed) { val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", it.toString())) } } viewModel.dispatch(AppManageViewModel.ViewAction.ClearUpdateLoaderResult) } } } when (viewModel.optimizeState) { is ProcessingState.Idle -> Unit is ProcessingState.Processing -> LoadingDialog() is ProcessingState.Done -> { val it = viewModel.optimizeState as ProcessingState.Done val optimizeSucceed = stringResource(R.string.manage_optimize_successfully) val optimizeFailed = stringResource(R.string.manage_optimize_failed) LaunchedEffect(Unit) { snackbarHost.showSnackbar(if (it.result) optimizeSucceed else optimizeFailed) viewModel.dispatch(AppManageViewModel.ViewAction.ClearOptimizeResult) } } } LazyColumn(Modifier.fillMaxHeight()) { items( items = viewModel.appList, key = { it.first.app.packageName } ) { val isRolling = it.second.useManager && it.second.lspConfig.VERSION_CODE >= Constants.MIN_ROLLING_VERSION_CODE val canUpdateLoader = !isRolling && it.second.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE var expanded by remember { mutableStateOf(false) } AnywhereDropdown( expanded = expanded, onDismissRequest = { expanded = false }, onClick = { expanded = true }, onLongClick = { expanded = true }, surface = { AppItem( icon = LSPPackageManager.getIcon(it.first), label = it.first.label, packageName = it.first.app.packageName, additionalContent = { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = buildAnnotatedString { val (text, color) = if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary else stringResource(R.string.patch_integrated) to MaterialTheme.colorScheme.tertiary append(AnnotatedString(text, SpanStyle(color = color))) append(" ") if (isRolling) append(stringResource(R.string.manage_rolling)) else append(it.second.lspConfig.VERSION_CODE.toString()) }, fontWeight = FontWeight.SemiBold, fontFamily = FontFamily.Serif, style = MaterialTheme.typography.bodySmall ) if (canUpdateLoader) { with(LocalDensity.current) { val size = MaterialTheme.typography.bodySmall.fontSize * 1.2 Icon(Icons.Filled.KeyboardCapslock, null, Modifier.size(size.toDp())) } } } } ) } ) { DropdownMenuItem( text = { Text(text = it.first.label, color = MaterialTheme.colorScheme.primary) }, onClick = {}, enabled = false ) val shizukuUnavailable = stringResource(R.string.shizuku_unavailable) if (canUpdateLoader || BuildConfig.DEBUG) { DropdownMenuItem( text = { Text(stringResource(R.string.manage_update_loader)) }, onClick = { expanded = false scope.launch { if (!ShizukuApi.isPermissionGranted) { snackbarHost.showSnackbar(shizukuUnavailable) } else { viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second)) } } } ) } if (it.second.useManager) { DropdownMenuItem( text = { Text(stringResource(R.string.manage_module_scope)) }, onClick = { expanded = false scope.launch { scopeApp = it.first.app.packageName val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet() val initialSelected = LSPPackageManager.appList.mapNotNullTo(ArrayList()) { if (activated.contains(it.app.packageName)) it.app.packageName else null } navigator.navigate(SelectAppsScreenDestination(true, initialSelected)) } } ) } DropdownMenuItem( text = { Text(stringResource(R.string.manage_optimize)) }, onClick = { expanded = false scope.launch { if (!ShizukuApi.isPermissionGranted) { snackbarHost.showSnackbar(shizukuUnavailable) } else { viewModel.dispatch(AppManageViewModel.ViewAction.PerformOptimize(it.first)) } } } ) val uninstallSuccessfully = stringResource(R.string.manage_uninstall_successfully) val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { scope.launch { snackbarHost.showSnackbar(uninstallSuccessfully) } } } DropdownMenuItem( text = { Text(stringResource(R.string.uninstall)) }, onClick = { expanded = false val intent = Intent(Intent.ACTION_DELETE).apply { data = Uri.parse("package:${it.first.app.packageName}") putExtra(Intent.EXTRA_RETURN_RESULT, true) } launcher.launch(intent) } ) } } } } } @Composable fun AppManageFab(navigator: DestinationsNavigator) { val context = LocalContext.current val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() var shouldSelectDirectory by remember { mutableStateOf(false) } var showNewPatchDialog by remember { mutableStateOf(false) } val errorText = stringResource(R.string.patch_select_dir_error) val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { try { if (it.resultCode == Activity.RESULT_CANCELED) return@rememberLauncherForActivityResult val uri = it.data?.data ?: throw IOException("No data") val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.takePersistableUriPermission(uri, takeFlags) Configs.storageDirectory = uri.toString() Log.i(TAG, "Storage directory: ${uri.path}") showNewPatchDialog = true } catch (e: Exception) { Log.e(TAG, "Error when requesting saving directory", e) scope.launch { snackbarHost.showSnackbar(errorText) } } } if (shouldSelectDirectory) { AlertDialog( onDismissRequest = { shouldSelectDirectory = false }, confirmButton = { TextButton( content = { Text(stringResource(android.R.string.ok)) }, onClick = { launcher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) shouldSelectDirectory = false } ) }, dismissButton = { TextButton( content = { Text(stringResource(android.R.string.cancel)) }, onClick = { shouldSelectDirectory = false } ) }, title = { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.patch_select_dir_title), textAlign = TextAlign.Center ) }, text = { Text(stringResource(R.string.patch_select_dir_text)) } ) } if (showNewPatchDialog) { AlertDialog( onDismissRequest = { showNewPatchDialog = false }, confirmButton = {}, dismissButton = { TextButton( content = { Text(stringResource(android.R.string.cancel)) }, onClick = { showNewPatchDialog = false } ) }, title = { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.screen_new_patch), textAlign = TextAlign.Center ) }, text = { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { TextButton( modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), onClick = { navigator.navigate(NewPatchScreenDestination(id = ACTION_STORAGE)) showNewPatchDialog = false } ) { Text( modifier = Modifier.padding(vertical = 8.dp), text = stringResource(R.string.patch_from_storage), style = MaterialTheme.typography.bodyLarge ) } TextButton( modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), onClick = { navigator.navigate(NewPatchScreenDestination(id = ACTION_APPLIST)) showNewPatchDialog = false } ) { Text( modifier = Modifier.padding(vertical = 8.dp), text = stringResource(R.string.patch_from_applist), style = MaterialTheme.typography.bodyLarge ) } } } ) } FloatingActionButton( content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) }, onClick = { val uri = Configs.storageDirectory?.toUri() if (uri == null) { shouldSelectDirectory = true } else { runCatching { val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.takePersistableUriPermission(uri, takeFlags) if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException("Storage directory was deleted") }.onSuccess { showNewPatchDialog = true }.onFailure { Log.w(TAG, "Failed to take persistable permission for saved uri", it) Configs.storageDirectory = null shouldSelectDirectory = true } } } ) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/page/manage/ModuleManagePage.kt ================================================ package org.lsposed.lspatch.ui.page.manage import android.content.Intent import android.net.Uri import android.provider.Settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.lifecycle.viewmodel.compose.viewModel import org.lsposed.lspatch.R import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.viewmodel.manage.ModuleManageViewModel import org.lsposed.lspatch.util.LSPPackageManager @Composable fun ModuleManageBody() { val context = LocalContext.current val viewModel = viewModel() if (viewModel.appList.isEmpty()) { Box(Modifier.fillMaxSize()) { Text( modifier = Modifier.align(Alignment.Center), text = run { if (LSPPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading) else stringResource(R.string.manage_no_modules) }, fontFamily = FontFamily.Serif, style = MaterialTheme.typography.headlineSmall ) } } else { LazyColumn(Modifier.fillMaxHeight()) { items( items = viewModel.appList, key = { it.first.app.packageName } ) { var expanded by remember { mutableStateOf(false) } val settingsIntent = remember { LSPPackageManager.getSettingsIntent(it.first.app.packageName) } AnywhereDropdown( expanded = expanded, onDismissRequest = { expanded = false }, onClick = { settingsIntent?.let { context.startActivity(it) } }, onLongClick = { expanded = true }, surface = { AppItem( icon = LSPPackageManager.getIcon(it.first), label = it.first.label, packageName = it.first.app.packageName, additionalContent = { Text( text = it.second.description, style = MaterialTheme.typography.bodySmall ) Text( text = buildAnnotatedString { append(AnnotatedString("API", SpanStyle(color = MaterialTheme.colorScheme.secondary))) append(" ") append(it.second.api.toString()) }, fontWeight = FontWeight.SemiBold, fontFamily = FontFamily.Serif, style = MaterialTheme.typography.bodySmall ) } ) } ) { DropdownMenuItem( text = { Text(text = it.first.label, color = MaterialTheme.colorScheme.primary) }, onClick = {}, enabled = false ) if (settingsIntent != null) { DropdownMenuItem( text = { Text(stringResource(R.string.manage_module_settings)) }, onClick = { context.startActivity(settingsIntent) } ) } DropdownMenuItem( text = { Text(stringResource(R.string.manage_app_info)) }, onClick = { val intent = Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", it.first.app.packageName, null) ) context.startActivity(intent) } ) } } } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/theme/Theme.kt ================================================ package org.lsposed.lspatch.ui.theme import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.ViewCompat @Composable fun LSPTheme( isDarkTheme: Boolean = isSystemInDarkTheme(), enableDynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { enableDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (isDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } isDarkTheme -> darkColorScheme() else -> lightColorScheme() } val view = LocalView.current if (!view.isInEditMode) { SideEffect { (view.context as Activity).window.statusBarColor = colorScheme.background.toArgb() ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = !isDarkTheme } } MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/theme/Type.kt ================================================ package org.lsposed.lspatch.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp val Typography = Typography( bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp ) ) ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/util/CompositionProvider.kt ================================================ package org.lsposed.lspatch.ui.util import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.compositionLocalOf val LocalSnackbarHost = compositionLocalOf { error("CompositionLocal LocalSnackbarController not present") } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/util/HtmlText.kt ================================================ package org.lsposed.lspatch.ui.util import android.text.method.LinkMovementMethod import android.widget.TextView import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import androidx.core.text.HtmlCompat @Composable fun HtmlText(html: String, modifier: Modifier = Modifier) { AndroidView( modifier = modifier, factory = { context -> TextView(context) }, update = { it.movementMethod = LinkMovementMethod.getInstance() it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) } ) } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/util/MultiDelegateState.kt ================================================ package org.lsposed.lspatch.ui.util import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import kotlin.reflect.KProperty class DelegateState(initial: T, private val sideEffectSetter: (T) -> Unit) { private var snapshot by mutableStateOf(initial) var value: T get() = snapshot set(value) { snapshot = value sideEffectSetter(snapshot) } operator fun component1(): T = value operator fun component2(): (T) -> Unit = { value = it } } fun delegateStateOf(initial: T, sideEffectSetter: (T) -> Unit) = DelegateState(initial, sideEffectSetter) @Suppress("NOTHING_TO_INLINE") inline operator fun DelegateState.getValue(thisObj: Any?, property: KProperty<*>): T = value @Suppress("NOTHING_TO_INLINE") inline operator fun DelegateState.setValue(thisObj: Any?, property: KProperty<*>, value: T) { this.value = value } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/util/Preview.kt ================================================ package org.lsposed.lspatch.ui.util import androidx.compose.ui.tooling.preview.PreviewParameterProvider class SampleStringProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf("Hello", "World") } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt ================================================ package org.lsposed.lspatch.ui.util import androidx.compose.foundation.lazy.LazyListState val LazyListState.lastVisibleItemIndex get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index val LazyListState.lastItemIndex get() = layoutInfo.totalItemsCount.let { if (it == 0) null else it } val LazyListState.isScrolledToEnd get() = lastVisibleItemIndex == lastItemIndex ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt ================================================ package org.lsposed.lspatch.ui.viewmodel import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.share.PatchConfig import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager.AppInfo import org.lsposed.patch.util.Logger class NewPatchViewModel : ViewModel() { companion object { private const val TAG = "NewPatchViewModel" } enum class PatchState { INIT, SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR } sealed class ViewAction { object DoneInit : ViewAction() data class ConfigurePatch(val app: AppInfo) : ViewAction() object SubmitPatch : ViewAction() object LaunchPatch : ViewAction() } var patchState by mutableStateOf(PatchState.INIT) private set var useManager by mutableStateOf(true) var debuggable by mutableStateOf(false) var overrideVersionCode by mutableStateOf(false) var sigBypassLevel by mutableStateOf(2) var embeddedModules = emptyList() lateinit var patchApp: AppInfo private set lateinit var patchOptions: Patcher.Options private set val logs = mutableStateListOf>() private val logger = object : Logger() { override fun d(msg: String) { if (verbose) { Log.d(TAG, msg) logs += Log.DEBUG to msg } } override fun i(msg: String) { Log.i(TAG, msg) logs += Log.INFO to msg } override fun e(msg: String) { Log.e(TAG, msg) logs += Log.ERROR to msg } } fun dispatch(action: ViewAction) { viewModelScope.launch { when (action) { is ViewAction.DoneInit -> doneInit() is ViewAction.ConfigurePatch -> configurePatch(action.app) is ViewAction.SubmitPatch -> submitPatch() is ViewAction.LaunchPatch -> launchPatch() } } } private fun doneInit() { patchState = PatchState.SELECTING } private fun configurePatch(app: AppInfo) { Log.d(TAG, "Configuring patch for ${app.app.packageName}") patchApp = app patchState = PatchState.CONFIGURING } private fun submitPatch() { Log.d(TAG, "Submit patch") if (useManager) embeddedModules = emptyList() patchOptions = Patcher.Options( config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null), apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()), embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) } ) patchState = PatchState.PATCHING } private suspend fun launchPatch() { logger.i("Launch patch") patchState = try { Patcher.patch(logger, patchOptions) PatchState.FINISHED } catch (t: Throwable) { logger.e(t.message.orEmpty()) logger.e(t.stackTraceToString()) PatchState.ERROR } finally { LSPPackageManager.cleanTmpApkDir() } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppsViewModel.kt ================================================ package org.lsposed.lspatch.ui.viewmodel import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager.AppInfo class SelectAppsViewModel : ViewModel() { companion object { private const val TAG = "SelectAppViewModel" } init { Log.d(TAG, "SelectAppsViewModel ${toString().substringAfterLast('@')} construct") } var isRefreshing by mutableStateOf(false) private set var filteredList by mutableStateOf(listOf()) private set val multiSelected = mutableStateListOf() fun filterAppList(refresh: Boolean, filter: (AppInfo) -> Boolean) { viewModelScope.launch { if (LSPPackageManager.appList.isEmpty() || refresh) { isRefreshing = true LSPPackageManager.fetchAppList() isRefreshing = false } filteredList = LSPPackageManager.appList.filter(filter) Log.d(TAG, "Filtered ${filteredList.size} apps") } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt ================================================ package org.lsposed.lspatch.ui.viewmodel.manage import android.content.pm.PackageInstaller import android.util.Base64 import android.util.Log import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.share.Constants import org.lsposed.lspatch.share.PatchConfig import org.lsposed.lspatch.ui.viewstate.ProcessingState import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager.AppInfo import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.patch.util.Logger import java.io.FileNotFoundException import java.util.zip.ZipFile class AppManageViewModel : ViewModel() { companion object { private const val TAG = "ManageViewModel" } sealed class ViewAction { data class UpdateLoader(val appInfo: AppInfo, val config: PatchConfig) : ViewAction() object ClearUpdateLoaderResult : ViewAction() data class PerformOptimize(val appInfo: AppInfo) : ViewAction() object ClearOptimizeResult : ViewAction() } val appList: List> by derivedStateOf { LSPPackageManager.appList.mapNotNull { appInfo -> appInfo.app.metaData?.getString("lspatch")?.let { val json = Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) Log.d(TAG, "Read patched config: $json") appInfo to Gson().fromJson(json, PatchConfig::class.java) } }.also { Log.d(TAG, "Loaded ${it.size} patched apps") } } var updateLoaderState: ProcessingState> by mutableStateOf(ProcessingState.Idle) private set var optimizeState: ProcessingState by mutableStateOf(ProcessingState.Idle) private set private val logger = object : Logger() { override fun d(msg: String) { if (verbose) Log.d(TAG, msg) } override fun i(msg: String) { Log.i(TAG, msg) } override fun e(msg: String) { Log.e(TAG, msg) } } fun dispatch(action: ViewAction) { viewModelScope.launch { when (action) { is ViewAction.UpdateLoader -> updateLoader(action.appInfo, action.config) is ViewAction.ClearUpdateLoaderResult -> updateLoaderState = ProcessingState.Idle is ViewAction.PerformOptimize -> performOptimize(action.appInfo) is ViewAction.ClearOptimizeResult -> optimizeState = ProcessingState.Idle } } } private suspend fun updateLoader(appInfo: AppInfo, config: PatchConfig) { Log.i(TAG, "Update loader for ${appInfo.app.packageName}") updateLoaderState = ProcessingState.Processing val result = runCatching { withContext(Dispatchers.IO) { LSPPackageManager.cleanTmpApkDir() val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray()) val patchPaths = mutableListOf() val embeddedModulePaths = mutableListOf() for (apk in apkPaths) { ZipFile(apk).use { zip -> var entry = zip.getEntry(Constants.ORIGINAL_APK_ASSET_PATH) if (entry == null) entry = zip.getEntry("assets/lspatch/origin_apk.bin") if (entry == null) throw FileNotFoundException("Original apk entry not found for $apk") zip.getInputStream(entry).use { input -> val dst = lspApp.tmpApkDir.resolve(apk.substringAfterLast('/')) patchPaths.add(dst.absolutePath) dst.outputStream().use { output -> input.copyTo(output) } } } } ZipFile(appInfo.app.sourceDir).use { zip -> zip.entries().iterator().forEach { entry -> if (entry.name.startsWith(Constants.EMBEDDED_MODULES_ASSET_PATH)) { val dst = lspApp.tmpApkDir.resolve(entry.name.substringAfterLast('/')) embeddedModulePaths.add(dst.absolutePath) zip.getInputStream(entry).use { input -> dst.outputStream().use { output -> input.copyTo(output) } } } } } Patcher.patch(logger, Patcher.Options(config, patchPaths, embeddedModulePaths)) val (status, message) = LSPPackageManager.install() if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message) } } updateLoaderState = ProcessingState.Done(result) } private suspend fun performOptimize(appInfo: AppInfo) { Log.i(TAG, "Perform optimize for ${appInfo.app.packageName}") optimizeState = ProcessingState.Processing val result = withContext(Dispatchers.IO) { ShizukuApi.performDexOptMode(appInfo.app.packageName) } optimizeState = ProcessingState.Done(result) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/ModuleManageViewModel.kt ================================================ package org.lsposed.lspatch.ui.viewmodel.manage import android.util.Log import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModel import org.lsposed.lspatch.util.LSPPackageManager class ModuleManageViewModel : ViewModel() { companion object { private const val TAG = "ModuleManageViewModel" } class XposedInfo( val api: Int, val description: String, val scope: List ) val appList: List> by derivedStateOf { LSPPackageManager.appList.mapNotNull { appInfo -> val metaData = appInfo.app.metaData ?: return@mapNotNull null appInfo to XposedInfo( metaData.getInt("xposedminversion", -1).also { if (it == -1) return@mapNotNull null }, metaData.getString("xposeddescription") ?: "", emptyList() // TODO: scope ) }.also { Log.d(TAG, "Loaded ${it.size} Xposed modules") } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/ui/viewstate/ProcessingState.kt ================================================ package org.lsposed.lspatch.ui.viewstate sealed class ProcessingState { object Idle : ProcessingState() object Processing : ProcessingState() data class Done(val result: T) : ProcessingState() } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/util/IntentSenderHelper.kt ================================================ package org.lsposed.lspatch.util import android.content.IIntentReceiver import android.content.IIntentSender import android.content.Intent import android.content.IntentSender import android.os.Bundle import android.os.IBinder object IntentSenderHelper { fun newIntentSender(binder: IIntentSender): IntentSender { return IntentSender::class.java.getConstructor(IIntentSender::class.java).newInstance(binder) } class IIntentSenderAdaptor(private val listener: (Intent) -> Unit) : IIntentSender.Stub() { override fun send( code: Int, intent: Intent, resolvedType: String?, finishedReceiver: IIntentReceiver?, requiredPermission: String?, options: Bundle? ): Int { listener(intent) return 0 } override fun send( code: Int, intent: Intent, resolvedType: String?, whitelistToken: IBinder?, finishedReceiver: IIntentReceiver?, requiredPermission: String?, options: Bundle? ) { listener(intent) } } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt ================================================ package org.lsposed.lspatch.util import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageInstaller import android.content.pm.PackageInstallerHidden.SessionParamsHidden import android.content.pm.PackageManager import android.content.pm.PackageManagerHidden import android.net.Uri import android.os.Parcelable import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import dev.rikka.tools.refine.Refine import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import me.zhanghai.android.appiconloader.AppIconLoader import org.lsposed.lspatch.config.ConfigManager import org.lsposed.lspatch.config.Configs import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.share.Constants import java.io.File import java.io.IOException import java.text.Collator import java.util.* import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine object LSPPackageManager { private const val TAG = "LSPPackageManager" private const val SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS" const val STATUS_USER_CANCELLED = -2 @Parcelize class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable { val isXposedModule: Boolean get() = app.metaData?.get("xposedminversion") != null } var appList by mutableStateOf(listOf()) private set @SuppressLint("StaticFieldLeak") private val iconLoader = AppIconLoader(lspApp.resources.getDimensionPixelSize(android.R.dimen.app_icon_size), false, lspApp) private val appIcon = mutableMapOf() suspend fun fetchAppList() { withContext(Dispatchers.IO) { val pm = lspApp.packageManager val collection = mutableListOf() pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach { val label = pm.getApplicationLabel(it) collection.add(AppInfo(it, label.toString())) appIcon[it.packageName] = iconLoader.loadIcon(it).asImageBitmap() } collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) val modules = buildMap { collection.forEach { if (it.isXposedModule) put(it.app.packageName, it.app.sourceDir) } } ConfigManager.updateModules(modules) appList = collection } } fun getIcon(appInfo: AppInfo) = appIcon[appInfo.app.packageName]!! suspend fun cleanTmpApkDir() { withContext(Dispatchers.IO) { lspApp.tmpApkDir.listFiles()?.forEach(File::delete) } } suspend fun install(): Pair { Log.i(TAG, "Perform install patched apks") var status = PackageInstaller.STATUS_FAILURE var message: String? = null withContext(Dispatchers.IO) { runCatching { val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) var flags = Refine.unsafeCast(params).installFlags flags = flags or PackageManagerHidden.INSTALL_ALLOW_TEST or PackageManagerHidden.INSTALL_REPLACE_EXISTING Refine.unsafeCast(params).installFlags = flags ShizukuApi.createPackageInstallerSession(params).use { session -> val uri = Configs.storageDirectory?.toUri() ?: throw IOException("Uri is null") val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException("DocumentFile is null") root.listFiles().forEach { file -> if (file.name?.endsWith(Constants.PATCH_FILE_SUFFIX) != true) return@forEach Log.d(TAG, "Add ${file.name}") val input = lspApp.contentResolver.openInputStream(file.uri) ?: throw IOException("Cannot open input stream") input.use { session.openWrite(file.name!!, 0, input.available().toLong()).use { output -> input.copyTo(output) session.fsync(output) } } } var result: Intent? = null suspendCoroutine { cont -> val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent -> result = intent cont.resume(Unit) } val intentSender = IntentSenderHelper.newIntentSender(adapter) session.commit(intentSender) } result?.let { status = it.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) message = it.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) } ?: throw IOException("Intent is null") } }.onFailure { status = PackageInstaller.STATUS_FAILURE message = it.message + "\n" + it.stackTraceToString() } } return Pair(status, message) } suspend fun uninstall(packageName: String): Pair { var status = PackageInstaller.STATUS_FAILURE var message: String? = null withContext(Dispatchers.IO) { runCatching { var result: Intent? = null suspendCoroutine { cont -> val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent -> result = intent cont.resume(Unit) } val intentSender = IntentSenderHelper.newIntentSender(adapter) ShizukuApi.uninstallPackage(packageName, intentSender) } result?.let { status = it.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) message = it.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) } ?: throw IOException("Intent is null") }.onFailure { status = PackageInstaller.STATUS_FAILURE message = "Exception happened\n$it" } } return Pair(status, message) } suspend fun getAppInfoFromApks(apks: List): Result> { return withContext(Dispatchers.IO) { runCatching { var primary: ApplicationInfo? = null val splits = mutableListOf() val appInfos = apks.mapNotNull { uri -> val src = DocumentFile.fromSingleUri(lspApp, uri) ?: throw IOException("DocumentFile is null") val dst = lspApp.tmpApkDir.resolve(src.name!!) val input = lspApp.contentResolver.openInputStream(uri) ?: throw IOException("InputStream is null") input.use { dst.outputStream().use { output -> input.copyTo(output) } } val appInfo = lspApp.packageManager.getPackageArchiveInfo( dst.absolutePath, PackageManager.GET_META_DATA )?.applicationInfo appInfo?.sourceDir = dst.absolutePath if (appInfo == null) { splits.add(dst.absolutePath) return@mapNotNull null } if (primary == null) { primary = appInfo } val label = lspApp.packageManager.getApplicationLabel(appInfo).toString() AppInfo(appInfo, label) } // TODO: Check selected apks are from the same app primary?.splitSourceDirs = splits.toTypedArray() if (appInfos.isEmpty()) throw IOException("No apks") appInfos }.recoverCatching { t -> cleanTmpApkDir() Log.e(TAG, "Failed to load apks", t) throw t } } } fun getLaunchIntentForPackage(packageName: String): Intent? { val intentToResolve = Intent(Intent.ACTION_MAIN) intentToResolve.addCategory(Intent.CATEGORY_INFO) intentToResolve.setPackage(packageName) var ris = lspApp.packageManager.queryIntentActivities(intentToResolve, 0) if (ris.size <= 0) { intentToResolve.removeCategory(Intent.CATEGORY_INFO) intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER) intentToResolve.setPackage(packageName) ris = lspApp.packageManager.queryIntentActivities(intentToResolve, 0) } if (ris.size <= 0) return null return Intent(intentToResolve) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .setClassName( ris[0].activityInfo.packageName, ris[0].activityInfo.name ) } fun getSettingsIntent(packageName: String): Intent? { val intentToResolve = Intent(Intent.ACTION_MAIN) intentToResolve.addCategory(SETTINGS_CATEGORY) intentToResolve.setPackage(packageName) val ris = lspApp.packageManager.queryIntentActivities(intentToResolve, 0) if (ris.size <= 0) return getLaunchIntentForPackage(packageName) return Intent(intentToResolve) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .setClassName( ris[0].activityInfo.packageName, ris[0].activityInfo.name ) } } ================================================ FILE: manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt ================================================ package org.lsposed.lspatch.util import android.content.IntentSender import android.content.pm.* import android.os.Build import android.os.IBinder import android.os.IInterface import android.os.Process import android.os.SystemProperties import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import dev.rikka.tools.refine.Refine import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper object ShizukuApi { private fun IBinder.wrap() = ShizukuBinderWrapper(this) private fun IInterface.asShizukuBinder() = this.asBinder().wrap() private val iPackageManager: IPackageManager by lazy { IPackageManager.Stub.asInterface(SystemServiceHelper.getSystemService("package").wrap()) } private val iPackageInstaller: IPackageInstaller by lazy { IPackageInstaller.Stub.asInterface(iPackageManager.packageInstaller.asShizukuBinder()) } private val packageInstaller: PackageInstaller by lazy { val userId = Process.myUserHandle().hashCode() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { Refine.unsafeCast(PackageInstallerHidden(iPackageInstaller, "com.android.shell", null, userId)) } else { Refine.unsafeCast(PackageInstallerHidden(iPackageInstaller, "com.android.shell", userId)) } } var isBinderAvailable = false var isPermissionGranted by mutableStateOf(false) fun init() { Shizuku.addBinderReceivedListenerSticky { isBinderAvailable = true isPermissionGranted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED } Shizuku.addBinderDeadListener { isBinderAvailable = false isPermissionGranted = false } } fun createPackageInstallerSession(params: PackageInstaller.SessionParams): PackageInstaller.Session { val sessionId = packageInstaller.createSession(params) val iSession = IPackageInstallerSession.Stub.asInterface(iPackageInstaller.openSession(sessionId).asShizukuBinder()) return Refine.unsafeCast(PackageInstallerHidden.SessionHidden(iSession)) } fun isPackageInstalledWithoutPatch(packageName: String): Boolean { val userId = Process.myUserHandle().hashCode() val app = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { iPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA.toLong(), userId) } else { iPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA, userId) } return (app != null) && (app.metaData?.containsKey("lspatch") != true) } fun uninstallPackage(packageName: String, intentSender: IntentSender) { packageInstaller.uninstall(packageName, intentSender) } fun performDexOptMode(packageName: String): Boolean { return iPackageManager.performDexOptMode( packageName, SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false), "verify", true, true, null ) } } ================================================ FILE: manager/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: manager/src/main/res/drawable-zh-rCN/ic_launcher_background.xml ================================================ ================================================ FILE: manager/src/main/res/drawable-zh-rTW/ic_launcher_background.xml ================================================ ================================================ FILE: manager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: manager/src/main/res/values/strings.xml ================================================ Add Install Installing Uninstall Uninstalling Copy error Apps Modules Shizuku service available Shizuku service not connected Repo Logs Off Unknown error Some functions unavailable API Version LSPatch Version Framework Version System Version Device System ABI Copied to clipboard Support LSPatch is a free non-root Xposed framework based on LSPosed core. Join our %2$s channel]]> Manage Loading No patched apps yet Rolling Update loader Update successfully Update failed Module scope Optimize Optimize successfully Optimize failed Uninstall successfully No modules yet Module settings App info New Patch Select storage directory Select a directory to store the patched apks Error when setting storage directory Select apk(s) from storage Select an installed app Patch Mode Local Patch an app without modules embedded.\nXposed scope can be changed dynamically without re-patch.\nLocal patched apps can only run on the local device. Integrated Patch an app with modules embedded.\nThe patched app can run without the manager, but cannot be managed dynamically.\nIntegrated patched apps can be used on devices that do not have LSPatch Manager installed. Embed modules Debuggable Signature bypass lv0: Off lv1: Bypass PM lv2: Bypass PM + openat (libc) Override version code Override the patched app\'s version code to 1\nThis allows downgrade installation in the future, and generally this will not affect the version code actually perceived by the application Start Patch Return Due to different signatures, you need to uninstall the original app before installing the patched one.\nMake sure you have backed up personal data. Install successfully Install failed No Xposed module(s) were found Select Apps Settings Signature keystore Built-in Custom Custom keystore Keystore file Password Alias Alias password Set keystore (BKS) Wrong type of keystore Wrong keystore password Wrong alias name Wrong alias password Detail patch logs ================================================ FILE: manager/src/main/res/values/strings_untranslatable.xml ================================================ LSPatch ================================================ FILE: manager/src/main/res/values-af/strings.xml ================================================ Voeg by Installeer Installeer tans Deïnstalleer Deïnstalleer tans Copy error برنامه ها Modules Shizuku diens beskikbaar Shizuku-diens nie gekoppel nie Repo Logs Af Onbekende fout Sommige funksies nie beskikbaar nie API weergawe LSPatch weergawe Raamwerk weergawe Stelsel weergawe Toestel Stelsel ABI Gekopieer na knipbord Ondersteuning LSPatch is \'n gratis nie-wortel Xposed-raamwerk gebaseer op LSPosed-kern. Sluit aan by ons %2$s -kanaal]]> Bestuur Laai tans Nog geen reggemaakte programme nie Rol Dateer laaier op Dateer suksesvol op Opdatering misluk Module omvang Optimaliseer Optimaliseer suksesvol Kon nie optimaliseer nie Deïnstalleer suksesvol Nog geen modules nie Module instellings App inligting Nuwe Patch Kies bergingsgids Kies \'n gids om die gelapte APK te stoor Fout met die opstel van berginggids Kies APK(s) uit die stoor Kies \'n geïnstalleerde toepassing Patch Mode Plaaslik Maak \'n toepassing sonder modules ingebed.\nXposed omvang kan dinamies verander word sonder her-patch.\nPlaaslike gelapte programme kan slegs op die plaaslike toestel loop. Geïntegreerde Patcher une application avec des modules intégrés.\nL\'application corrigée peut fonctionner sans le gestionnaire mais ne peut pas être gérée dynamiquement.\nLes applications patchées intégrées peuvent être utilisées sur des appareils sur lesquels LSPatch Manager n\'est pas installé. Sluit modules in Ontfoutbaar Handtekening omseil lv0: af lv1: Omseil PM lv2: Omseil PM + openat (libc) Ignoreer weergawekode Ignoreer die gelapte toepassing se weergawekode na 1\nDit laat afgradering installasie in die toekoms toe, en oor die algemeen sal dit nie die weergawekode beïnvloed wat werklik deur die toepassing waargeneem word nie Begin Patch Keer terug As gevolg van verskillende handtekeninge, moet u die oorspronklike toepassing deïnstalleer voordat u die gelapte een installeer.\nMaak seker dat jy persoonlike data gerugsteun het. Installeer suksesvol Installering het misluk Geen Xposed-module(s) is gevind nie Kies Toepassings Instellings Armazenamento de assinatura Ingeboude Pasgemaak Pasgemaakte sleutelstoor Sleutelstoor lêer Wagwoord Alias Alias wagwoord Stel sleutelstoor (BKS) Verkeerde tipe sleutelstoor Verkeerde sleutelstoor wagwoord Verkeerde alias naam Verkeerde alias wagwoord Detail pleister logs ================================================ FILE: manager/src/main/res/values-ar/strings.xml ================================================ إضافة تثبيت جارٍ التثبيت إلغاء التثبيت جارٍ إلغاء التثبيت خطأ في النسخ التطبيقات الوحدات خدمة Shizuku متاحة خدمة Shizuku غير متصلة المستودع السجلات متوقف Unknown error بعض الوظائف غير متاحة إصدار API إصدار LSPatch إصدار Framework إصدار النظام الجهاز نظام ABI تم النسخ إلى الحافظة الدعـم LSPatch هو إطار عمل Non-Root Xposed مجاني يعتمد على LSPosed core. انضم إلى قناتنا %2$s]]> إدارة جارٍ التحميل لا توجد تطبيقات معدلة حتى الآن المتداول تحديث المحمل تم التحديث بنجاح فشل التحديث نطاق الوحدة تحسين تم التحسين بنجاح فشل التحسين تم إلغاء التثبيت بنجاح لا توجد وحدات بعد إعدادات الوحدة معلومات التطبيق تعديل جديد حدد مجلد التخزين حدد مجلد لتخزين التطبيقات المعدلة خطأ عند تعيين مجلد التخزين تحديد APK(s) من التخزين تحديد تطبيقًا مثبتًا وضع التعديل محلي قم بتصحيح تطبيق بدون وحدات مدمجة.\nيمكن تغيير نطاق Xpose ديناميكيًا دون إعادة التصحيح.\nلا يمكن تشغيل التطبيقات المحلية المصححة إلا على الجهاز المحلي. متكامل تعديل تطبيق بوحدات مدمجة.\nيمكن تشغيل التطبيق المعدل بدون المدير، لكن لا يمكن إدارته ديناميكيًا.\nيمكن استخدام التطبيقات المعدلة المدمجة على الأجهزة التي لم يتم تثبيت LSPatch Manager عليها. الوحدات المُضْمَنة قابل للتصحيح تجاوز التوقيع مستوى 0: مغلق مستوى 1: تجاوز PM مستوى 2: تجاوز PM + openat (libc) تجاوز رمز الإصدار تجاوز رمز إصدار التطبيق المعدل إلى 1\nوهذا يسمح بالرجوع إلى إصدار أقدم في المستقبل، وعمومًا لن يؤثر هذا على رمز الإصدار الذي يتصوره التطبيق بالفعل بدء التعديل رجوع نظرًا لاختلاف التوقيعات، يلزمك إلغاء تثبيت التطبيق الأصلي قبل تثبيت التطبيق المعدل.\nتأكد من عمل نسخة احتياطية من البيانات الشخصية. تم التثبيت بنجاح فشل التثبيت تحديد التطبيقات الإعدادات مخزن مفاتيح التوقيع مدمج مخصص تخزين المفاتيح المخصص ملف Keystore كلمه السر الاسم المستعار كلمة مرور الاسم المستعار تعيين مخزن المفاتيح (BKS) نوع خاطئ من ملف تخزين المفاتيح كلمة مرور تخزين المفاتيح خاطئة اسم مستعار خاطئ كلمة مرور الاسم المستعار خاطئة تفاصيل سجلات التعديل ================================================ FILE: manager/src/main/res/values-bg/strings.xml ================================================ Добавяне на Инсталиране на Инсталиране на Деинсталиране на Деинсталиране на Грешка при копиране Приложения Модули Налична услуга Shizuku Услугата Shizuku не е свързана Репо Дневници Изключено Неизвестна грешка Някои функции са недостъпни Версия на API Версия на LSPatch Версия на рамката Версия на системата Устройство ABI на системата Копиране в клипборда Подкрепа LSPatch е безплатна некоренова рамка Xposed, базирана на ядрото LSPosed. Присъединете се към нашия %2$s канал]]> Управление на Зареждане на Все още няма поправени приложения Rolling Актуализиране на товарача Успешно актуализиране Обновяването е неуспешно Обхват на модула Оптимизиране на Оптимизирайте успешно Оптимизиране на неуспешни Успешно деинсталиране Все още няма модули Настройки на модула Информация за приложението Нова кръпка Изберете директория за съхранение Изберете директория, в която да съхранявате поправените apks Грешка при задаване на директория за съхранение Изберете apk(s) от хранилището Изберете инсталирано приложение Режим Patch Местни Поправка на приложение без вградени модули.\nОбхватът на Xposed може да бъде променян динамично, без да се налага повторна кръпка.\nЛокално патчираните приложения могат да работят само на локалното устройство. Интегриран Пач на приложение с вградени модули.\nПриложението с корекция може да работи без мениджъра, но не може да се управлява динамично.\nИнтегрираните пакетирани приложения могат да се използват на устройства, които нямат инсталиран LSPatch Manager. Вграждане на модули Отстраняване на грешки Подписване на байпас lv0: Изключено 1 лев: Заобикаляне на PM 2 лв: Заобикаляне на PM openat (libc) Код на версията за пренастройване Презаписване на кода на версията на коригираното приложение на 1\nТова позволява понижаване на инсталацията в бъдеще и като цяло няма да се отрази на кода на версията, възприеман от приложението. Начална кръпка Връщане на Поради различните сигнатури е необходимо да деинсталирате оригиналното приложение, преди да инсталирате коригираното.\nУверете се, че сте направили резервно копие на личните си данни. Инсталирайте успешно Инсталацията е неуспешна Не са открити модул(и) Xposed Изберете приложения Настройки Хранилище на ключове за подписване Вграден Потребителски Потребителско хранилище на ключове Файл на хранилището за ключове Парола Псевдоним Псевдоним за парола Задаване на хранилище на ключове (BKS) Неправилен тип хранилище на ключове Грешна парола на хранилището на ключове Неправилно име на псевдоним Грешна парола за псевдоним Подробни дневници на кръпките ================================================ FILE: manager/src/main/res/values-bn/strings.xml ================================================ যোগ করুন ইনস্টল করুন ইনস্টল করা হচ্ছে আনইনস্টল করুন আনইনস্টল হচ্ছে কপি ত্রুটি অ্যাপস মডিউল শিজুকু পরিষেবা উপলব্ধ Shizuku পরিষেবা সংযুক্ত নেই৷ রেপো লগ বন্ধ অজানা ত্রুটি কিছু ফাংশন অনুপলব্ধ API সংস্করণ এলএসপ্যাচ সংস্করণ ফ্রেমওয়ার্ক সংস্করণ সিস্টেম সংস্করণ যন্ত্র সিস্টেম ABI ক্লিপবোর্ডে কপি করা হয়েছে সমর্থন LSPatch হল LSPosed কোরের উপর ভিত্তি করে একটি বিনামূল্যের নন-রুট Xposed ফ্রেমওয়ার্ক। এ সোর্স কোড দেখুন আমাদের %2$s চ্যানেলে যোগ দিন]]> পরিচালনা করুন লোড হচ্ছে এখনো কোনো প্যাচড অ্যাপস ঘূর্ণায়মান লোডার আপডেট করুন সফলভাবে আপডেট হালনাগাদ ব্যর্থ হয়েছে মডিউল সুযোগ অপ্টিমাইজ করুন সফলভাবে অপ্টিমাইজ করুন অপ্টিমাইজ ব্যর্থ হয়েছে৷ সফলভাবে আনইনস্টল এখনো কোনো মডিউল নেই মডিউল সেটিংস অ্যাপের তথ্য নতুন প্যাচ স্টোরেজ ডিরেক্টরি নির্বাচন করুন প্যাচ করা apks সংরক্ষণ করতে একটি ডিরেক্টরি নির্বাচন করুন স্টোরেজ ডিরেক্টরি সেট করার সময় ত্রুটি স্টোরেজ থেকে apk(গুলি) নির্বাচন করুন একটি ইনস্টল করা অ্যাপ্লিকেশন নির্বাচন করুন প্যাচ মোড স্থানীয় এম্বেড করা মডিউল ছাড়াই একটি অ্যাপ প্যাচ করুন।\nএক্সপোজড স্কোপ রি-প্যাচ ছাড়াই গতিশীলভাবে পরিবর্তন করা যেতে পারে।\nস্থানীয় প্যাচ করা অ্যাপ শুধুমাত্র স্থানীয় ডিভাইসে চলতে পারে। কই তুমি কি নাই. করছি ডিবাগযোগ্য স্বাক্ষর বাইপাস lv0: বন্ধ lv1: PM বাইপাস করুন lv2: বাইপাস PM + openat (libc) সংস্করণ কোড ওভাররাইড করুন প্যাচ করা অ্যাপের সংস্করণ কোডটিকে 1\nএ ওভাররাইড করুন এটি ভবিষ্যতে ইনস্টলেশন ডাউনগ্রেড করার অনুমতি দেয় এবং সাধারণত এটি অ্যাপ্লিকেশন দ্বারা অনুভূত সংস্করণ কোডকে প্রভাবিত করবে না প্যাচ শুরু করুন প্রত্যাবর্তন বিভিন্ন স্বাক্ষরের কারণে, প্যাচ করা একটি ইনস্টল করার আগে আপনাকে আসল অ্যাপটি আনইনস্টল করতে হবে।\nআপনি ব্যক্তিগত ডেটা ব্যাক আপ করেছেন তা নিশ্চিত করুন৷ সফলভাবে ইনস্টল করুন ইনস্টল ব্যর্থ হয়েছে No Xposed module(s) were found অ্যাপস নির্বাচন করুন সেটিংস স্বাক্ষর কীস্টোর অন্তর্নির্মিত কাস্টম কাস্টম কীস্টোর কীস্টোর ফাইল পাসওয়ার্ড উপনাম উপনাম পাসওয়ার্ড সেট কীস্টোর (বিকেএস) কীস্টোরের ভুল ধরন ভুল কীস্টোর পাসওয়ার্ড ভুল উপনাম নাম ভুল ওরফে পাসওয়ার্ড বিস্তারিত প্যাচ লগ ================================================ FILE: manager/src/main/res/values-ca/strings.xml ================================================ Afegeix Instal·lar Instal·lació Desinstal·la S\'està desinstal·lant Error de còpia Aplicacions Mòduls Servei Shizuku disponible El servei Shizuku no està connectat Repo Registres Apagat Error desconegut Algunes funcions no estan disponibles Versió de l\'API Versió LSPatch Versió del marc Versió del sistema Dispositiu Sistema ABI S\'ha copiat al porta-retalls Suport LSPatch és un marc Xposed gratuït no root basat en el nucli LSPosed. Uneix-te al nostre canal %2$s]]> Gestionar Carregant Encara no hi ha cap aplicació pegada Rodant Actualitza el carregador Actualitza correctament No s\'ha pogut actualitzar Àmbit del mòdul Optimitzar Optimitzar amb èxit L\'optimització ha fallat Desinstal·la correctament Encara no hi ha mòduls Configuració del mòdul Informació de l\'aplicació Nou Pegat Seleccioneu el directori d\'emmagatzematge Seleccioneu un directori per emmagatzemar els apks pegats Error en configurar el directori d\'emmagatzematge Seleccioneu apk(s) de l\'emmagatzematge Seleccioneu una aplicació instal·lada Mode de pegat Local Apliqueu una aplicació sense mòduls incrustats.\nL\'àmbit Xposed es pot canviar dinàmicament sense tornar a aplicar el pedaç.\nLes aplicacions localitzades només es poden executar al dispositiu local. Integrat Apliqueu una aplicació amb mòduls incrustats.\nL\'aplicació pegada es pot executar sense el gestor, però no es pot gestionar de manera dinàmica.\nLes aplicacions amb pedaços integrades es poden utilitzar en dispositius que no tinguin LSPatch Manager instal·lat. Incrustar mòduls Depurable Bypass de signatura lv0: apagat lv1: anul·lar PM lv2: ometre PM + openat (libc) Anul·la el codi de versió Substituïu el codi de versió de l\'aplicació aplicada a 1\nAixò permetrà la instal·lació de baixada en el futur i, en general, això no afectarà el codi de versió realment percebut per l\'aplicació. Inicia el pegat Tornar A causa de les diferents signatures, heu de desinstal·lar l\'aplicació original abans d\'instal·lar la pegada.\nAssegureu-vos que heu fet una còpia de seguretat de les dades personals. Instal·la correctament La instal·lació ha fallat No s\'han trobat mòduls Xposed Seleccioneu Aplicacions Configuració Botiga de claus de signatures Integrat Personalitzat Magatzem de claus personalitzat Fitxer de magatzem de claus Contrasenya Àlies Contrasenya d\'àlies Estableix el magatzem de claus (BKS) Tipus de magatzem de claus incorrecte La contrasenya del magatzem de claus és incorrecta Nom d\'àlies incorrecte Contrasenya d\'àlies incorrecta Registres de pedaços detallats ================================================ FILE: manager/src/main/res/values-cs/strings.xml ================================================ Přidat Instalace Instalace stránek Odinstalujte stránku Odinstalování stránky Chyba kopírování Aplikace Moduly K dispozici služba Shizuku Služba Shizuku není připojena Repo Protokoly Vypnuto Neznámá chyba Některé funkce nejsou k dispozici Verze API Verze LSPatch Verze rámce Verze systému přístroj Systém ABI Zkopírováno do schránky Podpěra, podpora LSPatch je bezplatný non-root Xposed framework založený na jádru LSPosed. Připojte se k našemu %2$s kanálu]]> Správa načítání Zatím žádné opravené aplikace Rolling Aktualizace zavaděče Úspěšná aktualizace Aktualizace se nezdařila Rozsah modulu Optimalizace Úspěšná optimalizace Optimalizace selhala Úspěšná odinstalace Zatím žádné moduly Nastavení modulu Informace o aplikaci Nová záplata Vyberte adresář úložiště Vyberte adresář pro uložení opravených souborů APK Chyba při nastavování adresáře úložiště Vyberte apk z úložiště Vyberte nainstalovanou aplikaci Režim opravy Místní Oprava aplikace bez vložených modulů.\nRozsah Xposed lze dynamicky měnit bez opětovného záplatování.\nMístní záplatované aplikace lze spustit pouze na místním zařízení. Integrovaný Opravte aplikaci s vestavěnými moduly.\nOpravená aplikace může běžet bez správce, ale nelze ji spravovat dynamicky.\nIntegrované opravené aplikace lze používat na zařízeních, která nemají nainstalovaný LSPatch Manager. Vložit moduly Laditelné Přemostění podpisu lv0: Vypnuto lv1: Obejít PM lv2: Bypass PM + openat (libc) Přepsat kód verze Přepsat kód verze opravené aplikace na 1\nTo umožní v budoucnu downgradovat (snížit verzi) a obecně to neovlivní kód verze, který aplikace skutečně vnímá Spusťte opravu Vrátit se Kvůli různým podpisům musíte před instalací opravené odinstalovat původní aplikaci.\nUjistěte se, že máte zálohovaná osobní data. Nainstalujte úspěšně Instalace se nezdařila Nebyly nalezeny žádné moduly Xposed Vybrané aplikace Nastavení Úložiště klíčů s podpisem Vestavěný Vlastní Vlastní úložiště klíčů Soubor úložiště klíčů Heslo Alias Alias heslo Nastavení úložiště klíčů (BKS) Nesprávný typ úložiště klíčů Špatné heslo úložiště klíčů Špatný název aliasu Špatné heslo aliasu Podrobné protokoly oprav ================================================ FILE: manager/src/main/res/values-da/strings.xml ================================================ Tilføje Installer Installation af Afinstaller Afinstallation af Kopieringsfejl Apps Moduler Shizuku service tilgængelig Shizuku-tjenesten er ikke tilsluttet Repo Logfiler Off Ukendt fejl Nogle funktioner er ikke tilgængelige API-version LSPatch version Rammeversion Systemversion Enhed System ABI Kopieret til udklipsholder Support LSPatch er en gratis ikke-root Xposed-ramme baseret på LSPosed-kerne. Deltag i vores %2$s kanal]]> Administrer Indlæser Ingen lappede apps endnu Rullende Opdatering af loader Opdatering lykkedes Opdatering mislykkedes Modulets anvendelsesområde Optimering af Optimering med succes Optimering af mislykkedes Afinstaller med succes Ingen moduler endnu Modulindstillinger App-info Ny patch Vælg lagerbibliotek Vælg en mappe til at gemme de patchede apks Fejl ved indstilling af lagerbibliotek Vælg apk(er) fra lageret Vælg en installeret app Patch-tilstand Lokal Patch en app uden indlejrede moduler.\nXposed scope kan ændres dynamisk uden re-patch.\nLokalt patchede apps kan kun køre på den lokale enhed. Integreret Patch en app med indlejrede moduler.\nDen patchede app kan køre uden manageren, men kan ikke administreres dynamisk.\nIntegrerede patchede apps kan bruges på enheder, der ikke har LSPatch Manager installeret. Indlejre moduler Debuggable Signatur bypass lv0: Slukket lv1: Omgå PM lv2: Bypass PM + openat (libc) Tilsidesæt versionskode Overskrive den patchede apps versionskode til 1\nDette giver mulighed for at nedgradere installationen i fremtiden, og generelt vil det ikke påvirke den versionskode, som programmet faktisk opfatter. Start patch Vend tilbage På grund af forskellige signaturer skal du afinstallere den originale app, før du installerer den patchede.\nSørg for, at du har sikkerhedskopieret personlige data. Installer med succes Installationen mislykkedes Ingen Xposed-modul(er) blev fundet Vælg apps Indstillinger Signatur-nøgleopbevaring Indbygget Tilpasset Brugerdefineret keystore Keystore-fil Adgangskode Alias Alias-adgangskode Indstil nøgleopbevaring (BKS) Forkert type nøgleopbevaring Forkert adgangskode til keystore Forkert aliasnavn Forkert adgangskode til alias Detaljerede patch-logfiler ================================================ FILE: manager/src/main/res/values-de/strings.xml ================================================ Hinzufügen Installieren Installieren Deinstallieren Deinstallieren Fehler kopieren Apps Module Shizuku-Dienst verfügbar Shizuku-Dienst nicht verbunden Repo Protokolle Aus Unbekannter Fehler Einige Funktionen nicht verfügbar API-Version LSPatch-Version Framework-Version Systemversion Gerät System-ABI In Zwischenablage kopiert Unterstützung LSPatch ist ein kostenloses Xposed-Framework ohne Rootberechtigung, das auf den LSPosed-Kern basiert. Trete unserem %2$s -Kanal bei]]> Verwalten Laden Noch keine gepatchten Apps Rollen Lader aktualisieren Aktualisierung erfolgreich Aktualisierung fehlgeschlagen Modulumfang Optimierung Optimierung erfolgreich Optimierung fehlgeschlagen Deinstallation erfolgreich Noch keine Module Modul-Einstellungen App-Info Neuer Patch Speicherverzeichnis auswählen Verzeichnis zum Speichern der gepatchten Apks auswählen Fehler beim Festlegen des Speicherverzeichnisses Wählen Sie apk(s) aus dem Speicher aus Wählen Sie eine installierte App aus Patch-Modus Lokal Patchen Sie eine App ohne eingebettete Module.\nDer Xposed-Bereich kann dynamisch geändert werden, ohne dass ein neuer Patch erforderlich ist.\nLokal gepatchte Apps können nur auf dem lokalen Gerät ausgeführt werden. Integriert Patche eine App mit eingebetteten Modulen.\nDie gepatchte App kann ohne den Manager ausgeführt, aber nicht dynamisch verwaltet werden.\nIntegrierte gepatchte Apps können auf Geräten verwendet werden, auf denen der LSPatch Manager nicht installiert ist. Module einbetten Debuggingfähig Signaturumgehung lv0: Aus lv1: PM umgehen lv2: PM umgehen + openat (libc) Versionscode überschreiben Überschreiben Sie den Versionscode der gepatchten Anwendung auf 1\nDies ermöglicht eine Downgrade-Installation in der Zukunft und hat im Allgemeinen keinen Einfluss auf den Versionscode, der von der Anwendung tatsächlich wahrgenommen wird Patch starten Zurückkehren Aufgrund unterschiedlicher Signaturen müssen Sie die Original-App deinstallieren, bevor Sie die gepatchte App installieren.\nStellen Sie sicher, dass Sie Ihre persönlichen Daten gesichert haben. Installation erfolgreich Installation fehlgeschlagen Es wurde(n) kein(e) Xposed-Modul(e) gefunden Apps auswählen Einstellungen Signatur-Schlüsselspeicher Eingebaut Benutzerdefiniert Benutzerdefinierter Schlüsselspeicher Schlüsselspeicher-Datei Passwort Alias Alias-Passwort Schlüsselspeicher festlegen (BKS) Falscher Typ des Schlüsselspeichers Falsches Schlüsselspeicher-Passwort Falscher Alias-Name Falsches Alias-Passwort Detaillierte Patch-Protokolle ================================================ FILE: manager/src/main/res/values-el/strings.xml ================================================ Προσθήκη Εγκαταστήστε το Εγκατάσταση του Απεγκατάσταση του Απεγκατάσταση του Σφάλμα αντιγραφής Εφαρμογές Ενότητες Διατίθεται υπηρεσία Shizuku Η υπηρεσία Shizuku δεν είναι συνδεδεμένη Repo Ημερολόγια Off Άγνωστο σφάλμα Ορισμένες λειτουργίες δεν είναι διαθέσιμες Έκδοση API Έκδοση LSPatch Έκδοση πλαισίου Έκδοση συστήματος Συσκευή Σύστημα ABI Αντιγράφηκε στο πρόχειρο Υποστήριξη Το LSPatch είναι ένα δωρεάν πλαίσιο Xposed χωρίς ρίζα που βασίζεται στον πυρήνα LSPosed. Εγγραφείτε στο %2$s κανάλι μας]]> Διαχείριση Φόρτωση Δεν υπάρχουν ακόμα επιδιορθωμένες εφαρμογές Rolling Ενημέρωση φορτωτή Ενημέρωση με επιτυχία Η ενημέρωση απέτυχε Πεδίο εφαρμογής της ενότητας Βελτιστοποίηση Βελτιστοποιήστε με επιτυχία Βελτιστοποίηση αποτυχημένων Απεγκατάσταση με επιτυχία Δεν υπάρχουν ακόμη ενότητες Ρυθμίσεις ενότητας Πληροφορίες εφαρμογής Νέο patch Επιλέξτε κατάλογο αποθήκευσης Επιλέξτε έναν κατάλογο για να αποθηκεύσετε τα επιδιορθωμένα apk Σφάλμα κατά τη ρύθμιση του καταλόγου αποθήκευσης Επιλέξτε apk από την αποθήκευση Επιλέξτε μια εγκατεστημένη εφαρμογή Λειτουργία ενημέρωσης κώδικα Τοπικός Επιδιόρθωση μιας εφαρμογής χωρίς ενσωματωμένες ενότητες.\nΤο πεδίο εφαρμογής του Xposed μπορεί να αλλάξει δυναμικά χωρίς επαναπατρισμό.\nΟι τοπικές επιδιορθωμένες εφαρμογές μπορούν να εκτελούνται μόνο στην τοπική συσκευή. Ολοκληρωμένο Επιδιορθώστε μια εφαρμογή με ενσωματωμένες λειτουργικές μονάδες.\nΗ επιδιορθωμένη εφαρμογή μπορεί να εκτελεστεί χωρίς τον διαχειριστή, αλλά δεν είναι δυνατή η δυναμική διαχείριση της.\nΟι ενσωματωμένες ενημερωμένες εφαρμογές μπορούν να χρησιμοποιηθούν σε συσκευές που δεν έχουν εγκατεστημένο το LSPatch Manager. Ενσωμάτωση λειτουργικών μονάδων Δυνατότητα εντοπισμού σφαλμάτων Παράκαμψη υπογραφής lv0: Ανενεργό lv1: Παράκαμψη PM lv2: Παράκαμψη PM + openat (libc) Παράκαμψη κωδικού έκδοσης Αντικαταστήστε τον κωδικό έκδοσης της επιδιορθωμένης εφαρμογής σε 1\nΑυτό επιτρέπει την εγκατάσταση υποβάθμισης στο μέλλον και γενικά αυτό δεν θα επηρεάσει τον κωδικό έκδοσης που αντιλαμβάνεται πραγματικά η εφαρμογή. Ξεκινήστε το Patch ΕΠΙΣΤΡΟΦΗ Λόγω διαφορετικών υπογραφών, πρέπει να απεγκαταστήσετε την αρχική εφαρμογή πριν εγκαταστήσετε την επιδιορθωμένη.\nΒεβαιωθείτε ότι έχετε δημιουργήσει αντίγραφα ασφαλείας προσωπικών δεδομένων. Εγκατάσταση με επιτυχία Η εγκατάσταση απέτυχε Δεν βρέθηκαν μονάδες Xposed Επιλέξτε εφαρμογές Ρυθμίσεις Υπογραφή keystore Ενσωματωμένο Προσαρμοσμένο Προσαρμοσμένο keystore Αρχείο κλειδοθήκης Κωδικός πρόσβασης Ψευδώνυμο Κωδικός πρόσβασης Alias Ορισμός κλειδοθήκης (BKS) Λάθος τύπος κλειδοθήκης Λάθος κωδικός πρόσβασης keystore Λάθος όνομα ψευδώνυμου Λάθος κωδικός πρόσβασης ψευδώνυμου Λεπτομερή αρχεία καταγραφής επιδιορθώσεων ================================================ FILE: manager/src/main/res/values-es/strings.xml ================================================ Agregar Instalar Instalación de Desinstalar Desinstalación de Error de copia Aplicaciones Módulos Servicio Shizuku disponible Servicio Shizuku no conectado Repo Registros Fuera de Error desconocido Algunas funciones no disponibles Versión API Versión de LSPatch Versión del marco Versión del sistema Dispositivo Sistema ABI Copiado al portapapeles Apoyo LSPatch es un marco Xposed no root gratuito basado en el núcleo LSPosed. Únete a nuestro canal %2$s]]> Gestionar Cargando Aún no hay aplicaciones parcheadas Rodando Actualizar el cargador Actualizar con éxito Actualización fallida Alcance del módulo Optimizar Optimizar con éxito Optimización fallida Desinstalar con éxito Todavía no hay módulos Ajustes del módulo Información de la aplicación Nuevo parche Seleccionar directorio de almacenamiento Seleccione un directorio para almacenar las aplicaciones parcheadas Error al configurar el directorio de almacenamiento Seleccionar apk(s) del almacenamiento Seleccione una aplicación instalada Modo de parche Local Parche de una aplicación sin módulos incrustados.\nEl ámbito de Xposed puede cambiarse dinámicamente sin necesidad de volver a parchear.\nLas aplicaciones locales parcheadas solo pueden ejecutarse en el dispositivo local. Integrado Parchea una app con módulos incrustados.\nLa aplicación parcheada puede ejecutarse sin el gestor, pero no puede gestionarse dinámicamente.\nLas apps parcheadas integradas pueden utilizarse en dispositivos que no tengan instalado LSPatch Manager. Módulos incrustados depurable Omisión de firma lv0: Desactivado lv1: Pasar PM lv2: Omitir PM + abrir en (libc) Anular código de versión Anular el código de versión de la app parcheada a 1\nEsto permite la instalación de downgrade en el futuro, y generalmente esto no afectará al código de versión realmente percibido por la aplicación Parche de inicio Regreso Debido a las diferentes firmas, debe desinstalar la aplicación original antes de instalar la parcheada.\nAsegúrese de haber realizado una copia de seguridad de los datos personales. Instalar con éxito Instalación fallida No se han encontrado módulos Xposed Seleccionar aplicaciones Ajustes Almacén de claves de firma Construido en Personalizado Almacén de claves personalizado Archivo Keystore Contraseña Alias Contraseña del alias Establecer el almacén de claves (BKS) Tipo de almacén de claves incorrecto Contraseña incorrecta del almacén de claves Nombre de alias incorrecto Contraseña de alias incorrecta Registros de parches detallados ================================================ FILE: manager/src/main/res/values-et/strings.xml ================================================ Lisama Installige Paigaldamine Desinstallige Desinstallimine Kopeerimise viga Rakendused Moodulid Shizuku teenus saadaval Shizuku teenus pole ühendatud Repo Palgid Väljas Teadmata viga Mõned funktsioonid pole saadaval API versioon LSPatchi versioon Raamversioon Süsteemi versioon Seade Süsteemi ABI Kopeeriti lõikelauale Toetus LSPatch on tasuta mittejuur-Xposedi raamistik, mis põhineb LSPosedi tuumal. Liituge meie %2$s kanaliga]]> Halda Laadimine Paigutatud rakendusi pole veel Veeremine Värskenda laadurit Värskendamine õnnestus Uuendus ebaõnnestus Mooduli ulatus Optimeerige Optimeerimine õnnestus Optimeerimine ebaõnnestus Desinstallimine õnnestus Mooduleid veel pole Mooduli seaded Rakenduse teave Uus plaaster Valige salvestuskataloog Valige kataloog, kuhu paigatud APK-d salvestada Viga salvestuskataloogi seadistamisel Valige mälust apk(id). Valige installitud rakendus Paigutuse režiim Kohalik Paigaldage rakendus ilma mooduliteta sisseehitatud.\nXposed ulatust saab dünaamiliselt muuta ilma uuesti paranduseta.\nKohalikud parandatud rakendused saavad töötada ainult kohalikus seadmes. Integreeritud Paigutage manustatud moodulitega rakendust.\nPaigutatud rakendus võib töötada ilma haldurita, kuid seda ei saa dünaamiliselt hallata.\nIntegreeritud paigatud rakendusi saab kasutada seadmetes, kuhu pole installitud LSPatch Manager. Manustage moodulid Silutav Allkirjast möödasõit lv0: väljas lv1: PM ümbersõit lv2: PM + openat (libc) ümbersõit Alista versioonikood Paigutatud rakenduse versioonikoodi alistamine väärtusele 1\nSee võimaldab edaspidi installida madalamale versioonile ja üldiselt ei mõjuta see rakenduse poolt tegelikult tajutavat versioonikoodi Käivitage patch Tagasi Erinevate allkirjade tõttu peate enne paigatud rakenduse installimist algse rakenduse desinstallima.\nVeenduge, et olete isikuandmed varundanud. Installimine õnnestus Install ebaõnnestus Xposed moodul(id) ei leitud Valige Rakendused Seaded Allkirja võtmehoidla Sisseehitatud Kohandatud Kohandatud võtmehoidla Võtmehoidla fail Parool Teise nimega Alias parool Määra võtmehoidja (BKS) Vale võtmehoidja tüüp Vale võtmehoidla parool Vale pseudonüümi nimi Vale aliase parool Üksikasjade paikade logid ================================================ FILE: manager/src/main/res/values-fa/strings.xml ================================================ افزودن نصب در حال نصب ... حذف نصب در حال حذف نصب ... خطا کپی شود برنامه ها ماژول ها سرویس Shizuku در دسترس‌ است سرویس Shizuku متصل نیست مخزن گزارش ها خاموش برخی از عملکردها در دسترس نیستند نسخه API نسخه LSPatch نسخه فریمورک نسخه اندروید مدل دستگاه نوع پردازنده در کلیپ بورد کپی شده حمایت LSPatch یک فریمورک Xposed بدون نیاز به روت رایگان بر اساس هسته LSPosed است و به کانال %2$s ما بپیوندید]]> مدیریت در حال بارگذاری ... هنوز هیچ برنامه ای پچ نشده متحرک به روز رسانی بارگذاری کننده به روز رسانی انجام شده به روز رسانی انجام نشده محدوده ماژول بهینه سازی بهینه سازی انجام شده بهینه سازی انجام نشده حذف نصب شده هنوز ماژولی وجود ندارد تنظیمات ماژول اطلاعات برنامه پچ جدید پوشه ذخیره سازی را انتخاب کنید یک پوشه را برای ذخیره apk های پچ شده انتخاب کنید خطا در هنگام تنظیم پوشه ذخیره سازی انتخاب فایل (های) apk از حافظه انتخاب یک برنامه نصب شده حالت پچ محلی یک برنامه بدون ماژول های تعبیه شده را وصله کنید.\nمحدوده Xposed را می توان به صورت پویا بدون وصله مجدد تغییر داد.\nبرنامه های وصله شده محلی فقط می توانند در دستگاه محلی اجرا شوند. یکپارچه یک برنامه را با ماژول های تعبیه شده پچ کنید برنامه پچ شده می‌تواند بدون نیاز مدیر اجرا شود اما نمی توان آن را به‌ صورت پویا مدیریت کرد برنامه‌های پچ شده را می‌توان در دستگاه‌ هایی که مدیر LSPatch در آن نصب نشده استفاده کرد جاسازی ماژول ها قابل اشکال زدایی دور زدن امضا سطح 0 : خاموش سطح ۱ : دور زدن PM سطح ۲ : دور زدن PM + openat (libc) لغو کد نسخه کد نسخه برنامه پچ شده را به ۱ تغییر می دهد این کار امکان بازگردانی نسخه قبلی را فراهم می کند به طور کلی این کار روی کد نسخه ای که توسط برنامه درک می شود تاثیر نمی گذارد شروع پچ بازیابی به دلیل انجام امضاهای مختلف باید قبل از نصب برنامه پچ شده برنامه اصلی را حذف نصب کنید لطفا قبل از انجام این کار از اطلاعات خود پشتیبان گیری کنید نصب شده نصب نشده هیچ ماژول(های) Xposed یافت نشد برنامه ها را انتخاب کنید تنظیمات فروشگاه کلید امضا ساخته شده در سفارشی فروشگاه کلید سفارشی فایل فروشگاه کلید رمز نام مستعار رمز مستعار تنظیم فروشگاه کلید (BKS) نوع اشتباه ذخیره کلید رمز فروشگاه کلید اشتباه است نام مستعار اشتباه است رمز یا نام مستعار اشتباه است گزارش های اطلاعات پچ ================================================ FILE: manager/src/main/res/values-fi/strings.xml ================================================ Lisätä Asenna Asennus Poista Asennuksen poistaminen Kopiointivirhe Sovellukset Moduulit Shizuku-palvelu saatavilla Shizuku-palvelua ei ole yhdistetty Repo Lokit Off Tuntematon virhe Jotkut toiminnot eivät ole käytettävissä API-versio LSPatch-versio Framework-versio Järjestelmän versio Laite Järjestelmä ABI Kopioitu leikepöydälle Tuki LSPatch on ilmainen ei-root Xposed -kehys, joka perustuu LSPosed-ytimeen. Liity %2$s kanavaamme]]> Hallitse Ladataan Ei vielä korjattuja sovelluksia Rolling Päivitä lataaja Päivitys onnistuneesti Päivitys epäonnistui Moduulin laajuus Optimoi Optimoi onnistuneesti Optimoi epäonnistunut Poista asennus onnistuneesti Ei vielä moduuleja Moduulien asetukset Sovelluksen tiedot Uusi laastari Valitse tallennushakemisto Valitse hakemisto, johon tallennetaan korjatut APK:t Virhe asetettaessa tallennushakemistoa Valitse apk:t tallennustilasta Valitse asennettu sovellus Patch-tila Paikallinen Korjaa sovellus ilman upotettuja moduuleja.\nXposedin laajuutta voidaan muuttaa dynaamisesti ilman uudelleenpatchausta.\nPaikalliset paikallistetut sovellukset voivat toimia vain paikallisella laitteella. Integroitu Korjaa sovellus, jossa on upotettuja moduuleja.\nKorjattu sovellus voi toimia ilman hallintaa, mutta sitä ei voida hallita dynaamisesti.\nIntegroituja korjattuja sovelluksia voidaan käyttää laitteissa, joihin ei ole asennettu LSPatch Manageria. Upota moduulit Virheenkorjaus Allekirjoituksen ohitus lv0: Pois lv1: Ohita PM lv2: Ohita PM + openat (libc) Ohita versiokoodi Korjatun sovelluksen versiokoodi korvataan arvolla 1\nTämä mahdollistaa downgrade-asennuksen tulevaisuudessa, eikä tämä yleensä vaikuta sovelluksen havaitsemaan versiokoodiin. Aloita korjaustiedosto Palata Erilaisten allekirjoitusten vuoksi sinun on poistettava alkuperäinen sovellus ennen korjatun sovelluksen asentamista.\nVarmista, että olet varmuuskopioinut henkilökohtaiset tiedot. Asennus onnistui Asennus epäonnistui Xposed-moduulia (-moduulia) ei löytynyt Valitse sovellukset Asetukset Allekirjoituksen avainsäilö Sisäänrakennettu Custom Mukautettu avainsäilö Avainsäilytystiedosto Salasana Alias Alias-salasana Avainsäilön asettaminen (BKS) Väärän tyyppinen avainsäilö Väärä avainsäilön salasana Väärä alias-nimi Väärä alias-salasana Yksityiskohtaiset korjauslokit ================================================ FILE: manager/src/main/res/values-fr/strings.xml ================================================ Ajouter Install Installation de Désinstaller Désinstallation de Erreur de copie Applications Modules Service Shizuku disponible Service Shizuku non connecté Dépôt Journaux Arrêt Erreur inconnue Certaines fonctionnalités seront indisponibles Version de l\'API Version de LSPatch Version du Framework Version du système Appareil Architecture du système Copié dans le presse-papier Support LSPatch est un framework Xposed gratuit non root basé sur le noyau de LSPosed. Rejoignez notre %2$s canal]]> Gérer Chargement Aucune appli patchée pour le moment Rouler Mettre à jour le chargeur Mise à jour réussie Échec de la mise à jour Portée du module Optimiser Optimiser avec succès Échec de l\'optimisation Désinstallation réussie Aucun module pour le moment Réglages du module Infos sur l’application Nouveau Patch Sélectionner le répertoire de stockage Sélectionnez un répertoire pour stocker les apks modifiés Érreur lors de la définition du répertoire de stockage Selctionner un ou plusieurs apks depuis le stockage Sélectionner une appli installée Mode de patch Local Patch d\'une application sans modules intégrés.\nLe champ d\'application de Xposed peut être modifié dynamiquement sans qu\'il soit nécessaire d\'appliquer un nouveau correctif.\nLes applications patchées localement ne peuvent fonctionner que sur l\'appareil local. Intégré Patcher une application avec des modules intégrés.\nL\'application corrigée peut fonctionner sans le gestionnaire mais ne peut pas être gérée dynamiquement.\nLes applications patchées intégrées peuvent être utilisées sur des appareils sur lesquels LSPatch Manager n\'est pas installé. Intégrer des modules Débogable Contournement des signatures lv0 : Désactivé lv1 : Contourner PM lv2 : Contourner PM + openat (libc) Remplacer le code de version Remplacer le code de version de l\'application corrigée par 1\nCela permet de rétrograder l\'installation à l\'avenir, et généralement cela n\'affectera pas le code de version réellement perçu par l\'application. Démarrer le correctif Retour En raison de signatures différentes, vous devez désinstaller l\'appli originale avant d\'installer celle modifiée.\nAssurez-vous d\'avoir sauvegardé vos données personnelles. Installation réussie Échec de l\'installation Aucun module Xposed n\'a été trouvé Sélectionner Applications Réglages Magasin de clés de signature Intégré Personnalisé Magasin de clés personnalisé Fichier Keystore Mot de passe Alias Mot de passe alias Définir le keystore (BKS) Mauvais type de keystore Mot de passe erroné pour le keystore Nom d\'alias erroné Mot de passe d\'alias erroné Détails des journaux du patch ================================================ FILE: manager/src/main/res/values-hi/strings.xml ================================================ जोड़ना इंस्टॉल इंस्टॉल हो रहा है अनइंस्टॉल अनइंस्टॉल हो रहा है कॉपी मैं त्रुटि है ऐप्स मॉड्यूलस शिज़ुकु सेवा उपलब्ध शिज़ुकु सेवा कनेक्ट नहीं है रेपो लॉग्स बंद कुछ फ़ंक्शन अनुपलब्ध एपीआई संस्करण एलएसपैच संस्करण फ्रेमवर्क संस्करण सिस्टम संस्करण उपकरण सिस्टम एबीआई क्लिपबोर्ड पर नकल सहायता LSPatch LSPosed कोर पर आधारित एक निःशुल्क गैर-रूट Xposed ढांचा है। पर सोर्स कोड देखें हमारे %2$s चैनल से जुड़ें]]> प्रबंधित करना लोड हो रहा है अभी तक कोई पैच किया गया ऐप्स नहीं रोलिंग लोडर अपडेट करें सफलतापूर्वक अपडेट करें अपडेट विफल हुआ मॉड्यूल स्कोप अनुकूलन सफलतापूर्वक अनुकूलित करें अनुकूलन विफल सफलतापूर्वक अनइंस्टॉल करें अभी तक कोई मॉड्यूल नहीं मॉड्यूल सेटिंग्स अनुप्रयोग की जानकारी नया पैच भंडारण निर्देशिका का चयन करें पैच किए गए एपीके को स्टोर करने के लिए एक निर्देशिका का चयन करें संग्रहण निर्देशिका सेट करते समय त्रुटि स्टोरेज से एपीके चुनें एक इंस्टॉल किए गए ऐप का चयन करें पैच मोड स्थानीय बिना मॉड्यूल एम्बेडेड किसी ऐप को पैच करें।\nएक्सपोज़ड स्कोप को री-पैच के बिना गतिशील रूप से बदला जा सकता है।\nस्थानीय पैच किए गए ऐप्स केवल स्थानीय डिवाइस पर चल सकते हैं। एकीकृत एम्बेडेड मॉड्यूल के साथ ऐप को पैच करें।\nपैच किए गए ऐप प्रबंधक के बिना चल सकते हैं, लेकिन गतिशील रूप से प्रबंधित नहीं किए जा सकते।\nएकीकृत पैच किए गए ऐप्स का उपयोग उन उपकरणों पर किया जा सकता है जिनमें LSPatch Manager स्थापित नहीं है। मॉड्यूल एम्बेड करें डीबग करने योग्य सिग्नेचर बायपास lv0: बंद lv1: बाईपास पीएम lv2: बाईपास PM + openat (libc) संस्करण कोड ओवरराइड करें पैच किए गए ऐप के संस्करण कोड को 1\nपर ओवरराइड करें यह भविष्य में डाउनग्रेड इंस्टॉलेशन की अनुमति देता है, और आम तौर पर यह एप्लिकेशन द्वारा वास्तव में देखे जाने वाले संस्करण कोड को प्रभावित नहीं करेगा। पैच शुरू करें वापस करना अलग-अलग हस्ताक्षरों के कारण, आपको पैच किए गए ऐप को इंस्टॉल करने से पहले मूल ऐप को अनइंस्टॉल करना होगा।\nसुनिश्चित करें कि आपने व्यक्तिगत डेटा का बैकअप लिया है। सफलतापूर्वक स्थापित करें स्थापित करना विफल ऐप्स चुनें समायोजन सिग्नेचर कीस्टोर में निर्मित रिवाज़ कस्टम कीस्टोर कीस्टोर फ़ाइल पासवर्ड उपनाम उपनाम पासवर्ड कीस्टोर सेट करें (बीकेएस) गलत प्रकार का कीस्टोर गलत कीस्टोर पासवर्ड गलत उपनाम गलत उपनाम पासवर्ड विस्तार पैच लॉग ================================================ FILE: manager/src/main/res/values-hr/strings.xml ================================================ Dodaj Instaliraj Instaliranje Deinstaliraj Deinstaliranje Greška kopiranja Aplikacije Moduli Shizuku usluga dostupna Usluga Shizuku nije povezana Repo Dnevnici Isključeno Nepoznata pogreška Neke funkcije nisu dostupne API verzija LSPatch verzija Framework verzija Verzija sustava Uređaj Sustav ABI Kopirano u međuspremnik Podrška LSPatch je besplatni Xposed framework bez root-a temeljen na LSPosed jezgri. Pridružite se našem %2$s kanalu]]> Upravljanje Učitavanje Još nema zakrpanih aplikacija Kotrljanje Ažuriraj učitavač Uspješno ažurirano Ažuriranje nije uspjelo Opseg modula Optimiziraj Optimiranje uspješno Optimizacija nije uspjela Deinstaliranje uspješno Još nema modula Postavke modula Informacije o aplikaciji Nova zakrpa Odaberite direktorij za pohranu Odaberite direktorij za pohranu zakrpanih apk-ova Pogreška prilikom postavljanja direktorija za pohranu Odaberite apk(ove) iz pohrane Odaberite instaliranu aplikaciju Način zakrpe Lokalni Zakrpite aplikaciju bez ugrađenih modula.\nXposed opseg može se mijenjati dinamički bez ponovnog zakrpa.\nLokalno zakrpane aplikacije mogu se izvoditi samo na lokalnom uređaju. Integriran Zakrpite aplikaciju s ugrađenim modulima.\nZakrpana aplikacija može raditi bez upravitelja, ali se njome ne može upravljati dinamički.\nIntegrirane zakrpane aplikacije mogu se koristiti na uređajima koji nemaju instaliran LSPatch Manager. Ugradite module Mogućnost otklanjanja pogrešaka Premosnica potpisa lv0: isključeno lv1: Zaobići PM lv2: Zaobići PM + openat (libc) Nadjačaj kod verzije Nadjačajte kod verzije zakrpane aplikacije na 1\nTo omogućuje instalaciju na stariju verziju u budućnosti i općenito to neće utjecati na kod verzije koji aplikacija stvarno percipira Pokreni zakrpu Povratak Zbog različitih potpisa morate deinstalirati izvornu aplikaciju prije instaliranja zakrpane.\nProvjerite jeste li sigurnosno kopirali osobne podatke. Instalirajte uspješno Instalacija nije uspjela Nisu pronađeni Xposed moduli Odaberite Aplikacije postavke Spremište ključeva potpisa Ugrađeni Prilagođen Prilagođeno spremište ključeva Datoteka spremišta ključeva Lozinka Alias Alias lozinka Postavi spremište ključeva (BKS) Pogrešna vrsta spremišta ključeva Pogrešna lozinka spremišta ključeva Pogrešno pseudonim Pogrešna lozinka za alias Dnevnici zakrpa detalja ================================================ FILE: manager/src/main/res/values-hu/strings.xml ================================================ Hozzáadás Telepítés Telepítés folyamatban Eltávolítás Eltávolítás folyamatban Másolási hiba Alkalmazások Modulok A Shizuku szolgáltatás elérhető A Shizuku szolgáltatás nincs csatlakoztatva Repo Naplók Off Ismeretlen hiba Néhány funkció nem elérhető API verzió LSPatch verzió Keretrendszer verziója Rendszer verzió Eszköz Rendszer ABI Vágólapra másolva Támogatás Az LSPatch egy ingyenes, nem root Xposed keretrendszer, amely az LSPosed magon alapul. Csatlakozzon %2$s csatornánkhoz]]> Kezelés Betöltés Még nincsenek patchelt alkalmazások Gördülő Betöltő frissítése A frissítés sikeres A frissítés sikertelen A modul hatóköre Optimalizálás Az optimalizálás sikeres Az optimalizálás nem sikerült Az eltávolítás sikeres Még nincsenek modulok Modul beállítások App info Új Patch Válassza ki a tárolási könyvtárat Válasszon ki egy könyvtárat a patchelt apk-k tárolására Hiba a tárolási könyvtár beállításakor Válassza ki az apk-t a tárhelyről Válasszon ki egy telepített alkalmazást Patch mód Helyi Beágyazott modulok nélküli alkalmazás foltozása.\nAz Xposed hatókör dinamikusan megváltoztatható újrapatchelés nélkül.\nA helyi javított alkalmazások csak a helyi eszközön futhatnak. Integrált Patcheljen egy alkalmazást beágyazott modulokkal.\nA patchelt alkalmazás futhat a Manager nélkül, de nem kezelhető dinamikusan.\nAz integrált\nhordozható, patchelt alkalmazások olyan eszközökön használhatók, amelyeken nincs telepítve az LSPatch Manager. Modulok beágyazása Hibakeresés Aláírás megkerülése lv0: Ki lv1: Bypass PM lv2: PM megkerülése + openat (libc) Verziókód felülírása A javított alkalmazás verziószámának felülírása 1-re\nEz lehetővé teszi a jövőbeni downgrade telepítést, és általában ez nem befolyásolja az alkalmazás által ténylegesen érzékelt verziószámot. Indítsa el a Patch-et Visszatérés Az eltérő aláírások miatt a javított alkalmazás telepítése előtt el kell távolítania az eredeti alkalmazást.\nGyőződjön meg arról, hogy biztonsági másolatot készített a személyes adatokról. Sikeres telepítés A telepítés sikertelen Nem találtak Xposed modul(ok)at Alkalmazások kiválasztása Beállítások Aláírás kulcstároló Beépített Egyedi Egyéni kulcstár Kulcstároló fájl Jelszó Alias Alias jelszó Kulcstároló beállítása (BKS) Rossz típusú kulcstároló Rossz kulcstároló jelszó Rossz alias név Rossz alias jelszó Részletes patch naplók ================================================ FILE: manager/src/main/res/values-in/strings.xml ================================================ Tambah Install Menginstal Copot pemasangan Mencopot pemasangan Kesalahan penyalinan Aplikasi Modul Layanan Shizuku tersedia Layanan Shizuku tidak terhubung Repo Log Mati Beberapa fungsi tidak tersedia Versi API Versi LSPatch Versi Kerangka Versi Sistem Perangkat Sistem ABI Disalin ke papan klip Mendukung LSPatch adalah kerangka kerja Xposed non-root gratis berdasarkan inti LSPosed. Bergabunglah dengan %2$s saluran kami]]> Kelola Memuat Belum ada aplikasi yang ditambal Bergulir Perbarui pemuat Pembaruan berhasil Pembaruan gagal Lingkup modul Optimalkan Optimalkan berhasil Pengoptimalan gagal Copot pemasangan dengan sukses Belum ada modul Pengaturan modul Info aplikasi Tambalan Baru Pilih direktori penyimpanan Pilih direktori untuk menyimpan apk yang ditambal Kesalahan saat mengatur direktori penyimpanan Pilih apk dari penyimpanan Pilih aplikasi yang diinstal Modus Patch Lokal Menambal aplikasi tanpa modul yang tertanam.\nCakupan Xposed dapat diubah secara dinamis tanpa menambal ulang.\nAplikasi yang ditambal lokal hanya dapat berjalan di perangkat lokal. Terintegrasi Patch aplikasi dengan modul yang disematkan.\nAplikasi yang di-patch dapat berjalan tanpa manajer, tetapi tidak dapat dikelola secara dinamis.\nAplikasi yang di-patch terintegrasi dapat digunakan pada perangkat yang tidak memasang LSPatch Manager. Sematkan modul Dapat di-debug Bypass tanda tangan lv0: Mati lv1: Bypass PM lv2: Bypass PM + openat (libc) Ganti kode versi Ganti kode versi aplikasi yang ditambal ke 1\nIni memungkinkan penginstalan downgrade di masa mendatang, dan umumnya ini tidak akan memengaruhi kode versi yang sebenarnya dirasakan oleh aplikasi Mulai Patch Kembali Dikarenakan adanya perbedaan tanda tangan, Anda perlu menghapus aplikasi asli sebelum menginstal aplikasi yang telah di-patch.\nPastikan Anda telah mencadangkan data personal. Instal berhasil Instal gagal Pilih Aplikasi Pengaturan Toko kunci tanda tangan Built-in Kebiasaan Toko kunci khusus File penyimpanan kunci Kata sandi Alias Kata sandi alias Setel penyimpanan kunci (BKS) Jenis keystore yang salah Kata sandi keystore salah Nama alias salah Kata sandi alias salah Detail log tambalan ================================================ FILE: manager/src/main/res/values-it/strings.xml ================================================ Aggiungi Installa Installazione Disinstalla Disinstallazione Errore nella copia Apps Moduli Servizio Shizuku disponibile Servizio Shizuku non connesso Repo Log Off Errore sconosciuto Alcune funzioni non sono disponibili Versione API Versione LSPatch Versione del framework Versione del sistema Dispositivo ABI del sistema Copiato negli appunti Supporto LSPatch è un framework Xposed non-root gratuito basato sul core LSPosed. Unisciti al nostro canale %2$s]]> Gestire Caricamento in corso Ancora nessuna app patchata Rotolamento Aggiornamento del caricatore Aggiornamento riuscito Aggiornamento fallito Ambito del modulo Ottimizzare Ottimizzare con successo Ottimizzare il fallimento Disinstallazione riuscita Non ci sono ancora moduli Impostazioni del modulo Info sull\'app Nuova patch Seleziona la directory di archiviazione Seleziona una directory in cui archiviare gli apk patchati Errore durante l\'impostazione della directory di archiviazione Seleziona gli apk dalla memoria Seleziona un\'app installata Modalità patch Locale Patchare un\'applicazione senza moduli incorporati.\nL\'ambito di Xposed può essere modificato dinamicamente senza dover rifare la patch.\nLe applicazioni locali patchate possono essere eseguite solo sul dispositivo locale. Integrato Patcha un\'app con moduli incorporati.\nL\'app patchata può essere eseguita senza il manager, ma non può essere gestita dinamicamente.\nLe app patchate integrate possono essere utilizzate su dispositivi su cui non è installato LSPatch Manager. Moduli incorporati Debuggabile Bypass della firma lv0: Off lv1: Bypass PM lv2: Bypass PM + openat (libc) Sostituisci il codice versione Sostituisci il codice versione dell\'applicazione patchata con 1\nQuesto permette il downgrade dell\'installazione in futuro, e generalmente questo non influenzerà il codice versione effettivamente percepito dall\'applicazione Avvia Patch Ritorna A causa delle diverse firme, è necessario disinstallare l\'app originale prima di installare quella patchata.\nAssicurati di aver eseguito il backup dei dati personali. Installato con successo Installazione non riuscita Nessun modulo Xposed trovato Seleziona le app Impostazioni Firma del keystore Integrato Personalizzato Keystore personalizzato File keystore Password Alias Password alias Imposta il keystore (BKS) Tipo del keystore errato Password keystore errata Nome alias errato Password alias errata Registri dettagliati delle patch ================================================ FILE: manager/src/main/res/values-iw/strings.xml ================================================ לְהוֹסִיף להתקין מתקין הסר את ההתקנה מסיר התקנה שגיאת העתקה אפליקציות מודולים שירות שיזוקו זמין שירות Shizuku לא מחובר ריפו יומנים כבוי שגיאה לא ידועה חלק מהפונקציות אינן זמינות גרסת API גרסת LSPatch גרסת מסגרת גרסת מערכת התקן מערכת ABI הועתק ללוח תמיכה LSPatch היא מסגרת חינמית ללא שורש Xposed המבוססת על ליבת LSPosed. הצטרף לערוץ %2$s שלנו]]> לנהל טוען עדיין אין אפליקציות מתוקנות גִלגוּל עדכון מטעין עדכן בהצלחה עדכון נכשל היקף מודול בצע אופטימיזציה בצע אופטימיזציה בהצלחה האופטימיזציה נכשלה הסר את ההתקנה בהצלחה עדיין אין מודולים הגדרות מודול מידע על האפליקציה תיקון חדש בחר ספריית אחסון בחר ספרייה לאחסון ה-apks המתוקן שגיאה בעת הגדרת ספריית אחסון בחר apk(ים) מהאחסון בחר אפליקציה מותקנת מצב תיקון מְקוֹמִי תקן אפליקציה ללא מודולים מוטבעים.\nניתן לשנות את היקף Xposed באופן דינמי ללא תיקון מחדש.\nאפליקציות מקומיות מתוקנות יכולות לפעול רק במכשיר המקומי. מְשׁוּלָב תקן אפליקציה עם מודולים משובצים.\nהאפליקציה המתוקנת יכולה לפעול ללא המנהל, אך לא ניתן לנהל אותה באופן דינמי.\nניתן להשתמש באפליקציות מתוקנות משולבות במכשירים שלא מותקן בהם LSPatch Manager. הטמע מודולים ניתן לניפוי באגים עוקף חתימה lv0: כבוי lv1: עוקף את PM lv2: עוקף PM + openat (libc) עוקף את קוד הגרסה עוקף את קוד הגרסה של האפליקציה המתוקנת ל-1\nזה מאפשר שדרוג לאחור של התקנה בעתיד, ובדרך כלל זה לא ישפיע על קוד הגרסה הנתפס בפועל על ידי האפליקציה התחל תיקון לַחֲזוֹר עקב חתימות שונות, עליך להסיר את ההתקנה של האפליקציה המקורית לפני התקנת האפליקציה המתוקנת.\nודא שגיבית את הנתונים האישיים. התקן בהצלחה ההתקנה נכשלה לא נמצאו מודולי Xposed בחר אפליקציות הגדרות מאגר מפתחות חתימה מובנה המותאם אישית מאגר מפתחות מותאם אישית קובץ מאגר מפתחות סיסמה כינוי סיסמת כינוי הגדר מאגר מפתחות (BKS) סוג שגוי של מאגר מפתחות סיסמת מאגר מפתחות שגויה שם כינוי שגוי סיסמת כינוי שגויה פירוט יומני תיקון ================================================ FILE: manager/src/main/res/values-ja/strings.xml ================================================ 追加 インストール インストール中 アンインストール アンインストール中 エラーをコピー アプリ モジュール Shizukuが有効です Shizukuが有効化されていません リポジトリ ログ オフ 不明なエラー 一部の機能が使用できません APIのバージョン LSPatchのバージョン フレームワークのバージョン システムのバージョン デバイス システムABI クリップボードにコピーしました サポート LSPatchは、LSPosedコアに基づく無料の非root環境向けXposedフレームワークです。 %2$sチャンネルに参加する]]> 管理 読み込み中 パッチが適用されたアプリはありません ロール ローダーを更新 アップデートが正常に完了しました アップデートに失敗しました モジュールのスコープ 最適化 正常に最適化されました 最適化に失敗しました 正常にアンインストールされました モジュールはまだありません モジュールの設定 アプリの情報 新しいパッチ ディレクトリの選択 パッチを適用したapkを保存するディレクトリを選択してください ディレクトリの設定中にエラーが発生しました ストレージからapkを選択 インストールされているアプリを選択 パッチモード ローカル モジュールを埋め込んでいないアプリにパッチを当てる。\nXposedのスコープは、再パッチなしで動的に変更できます。\nローカルでパッチを適用したアプリは、ローカルデバイスでのみ実行できます。 統合 モジュールが埋め込まれたアプリにパッチを適用します。\nパッチ適用されたアプリはマネージャーなしで実行できますが、動的に管理することはできません。\n統合されたパッチ適用済みアプリは、LSPatch Manager がインストールされていないデバイスでも使用できます。 モジュールを埋め込む デバッグを有効化 署名のバイパス lv0: オフ lv1: PMをバイパス lv2: PMをバイパス + openat (libc) バージョンコードを上書き パッチを適用するアプリのバージョンコードを1に上書きします。\nこれを行う事で将来的にダウングレードのインストールが可能になります。アプリが通常時に認識するバージョンコードには影響は与えません。 パッチを開始 戻る 署名が異なるため、パッチを適用したアプリをインストールする前に、元のアプリをアンインストールする必要があります。\nデータは必要に応じてバックアップしてください。 正常にインストールされました インストールに失敗しました Xposedモジュールが見つかりませんでした アプリを選択 設定 署名のキーストア ビルトイン カスタム カスタムキーストア キーストアファイル パスワード エイリアス エイリアスのパスワード キーストア (BKS) の設定 キーストアの種類が間違っています キーストアのパスワードが違います エイリアス名が間違っています エイリアスのパスワードが違います 詳細なパッチログ ================================================ FILE: manager/src/main/res/values-ko/strings.xml ================================================ 추가 설치 설치 중 제거 제거 중 복사 오류 모듈 시즈쿠 서비스 가능 시즈쿠 서비스가 연결되지 않았습니다 레포 로그 끄다 알 수 없는 오류 일부 기능을 사용할 수 없음 API 버전 LSPatch 버전 프레임워크 버전 시스템 버전 장치 시스템 ABI 클립보드에 복사됨 지원 LSPatch는 LSPosed 코어를 기반으로 하는 무료 비루트 Xposed 프레임워크입니다. 에서 소스 코드 보기 %2$s 채널 가입]]> 관리하다 로딩 중 아직 패치된 앱이 없습니다. 구르는 로더 업데이트 업데이트 성공 업데이트가 실패 모듈 범위 최적화 최적화 성공 최적화 실패 성공적으로 제거함 아직 모듈이 없습니다. 모듈 설정 앱 정보 새로운 패치 저장 디렉토리 선택 패치된 APK를 저장할 디렉토리를 선택하세요. 저장 디렉토리를 설정하는 동안 오류가 발생했습니다. 저장소에서 APK 선택 설치된 앱 선택 패치 모드 로컬 모듈이 임베드되지 않은 앱을 패치합니다.\n재패치 없이 동적으로 노출 범위를 변경할 수 있습니다.\n로컬로 패치된 앱은 로컬 디바이스에서만 실행할 수 있습니다. 통합 모듈이 포함된 앱을 패치합니다.\n패치된 앱은 관리자 없이 실행할 수 있지만 동적으로 관리할 수는 없습니다.\n통합 패치 앱은 LSPatch Manager가 설치되지 않은 장치에서 사용할 수 있습니다. 모듈 포함 디버깅 가능 서명 우회 lv0: 꺼짐 lv1: PM 우회 lv2: PM + openat(libc) 우회 버전 코드 덮어씌우기 패치된 앱의 버전 코드를 1\n으로 덮어씌웁니다. 이렇게 하면 향후 다운그레이드 설치를 할 수 있으며 일반적으로 애플리케이션에서 실제로 인식하는 버전 코드에는 영향을 미치지 않습니다. 패치 시작 되돌리기 서명이 다르기 때문에 패치된 앱을 설치하기 전에 기존 앱을 제거해야 합니다.\n개인 데이터를 백업했는지 확인하십시오. 성공적으로 설치함 설치 실패함 X포지드 모듈을 찾을 수 없습니다. 앱 선택 설정 서명 키 저장소 내장 사용자 정의 사용자 정의 키 저장소 키 저장소 파일 비밀번호 별칭 별칭 비밀번호 키 저장소 설정(BKS) 잘못된 유형의 키 저장소 잘못된 키 저장소 비밀번호 잘못된 별칭 이름 잘못된 별칭 암호 세부 패치 로그 ================================================ FILE: manager/src/main/res/values-ku/strings.xml ================================================ Lêzêdekirin Lêkirin Sazkirin Rakirin Rakirin Çewtiya kopîkirinê Apps Modules Karûbarê Shizuku heye Karûbarê Shizuku ne girêdayî ye Repo Logs Ji Çewtiya nenas Hin fonksiyon ne berdest in Guhertoya API Guhertoya LSPatch Guhertoya Çarçoveyê Versiyon ji System Sazî Pergala ABI Li clipboardê hate kopî kirin Alîkarî LSPatch çarçoveyek Xposed-a ne-root-a belaş e ku li ser bingeha LSPosed-ê ye. bibînin Tevlî %2$s kanala me bibin]]> Rêvebirin Barkirin Hê sepanên patched tune Rolling Barkerê nûve bikin Rojanekirin bi serkeftî Nûvekirin têk çû Qada modulê Optimize bikin Optimize bi serkeftî Optimîzekirin têk çû Rakirina bi serkeftî Hîn modul tune Mîhengên Modulê Agahdariya app Patch Nû Peldanka hilanînê hilbijêrin Ji bo hilanîna apkên patched pelrêçek hilbijêrin Di sazkirina pelrêça hilanînê de çewtî Ji hilanînê apk(ên) hilbijêrin Serlêdanek sazkirî hilbijêrin Mode Patch Herêmî Serlêdanek bêyî modulên pêvekirî paqij bikin.\nQada Xposed dikare bi dînamîk bêyî nûvekirin were guheztin.\nSerlêdanên paçkirî yên herêmî tenê dikarin li ser cîhaza herêmî bixebitin. Integrated Serlêdanek bi modulên pêvekirî veqetînin.\nSerlêdana paçkirî dikare bêyî rêveberê bixebite, lê bi dînamîk nayê rêvebirin.\nSerlêdanên pejirandî yên yekbûyî dikarin li ser cîhazên ku Rêvebirê LSPatch-ê sazkirî ne têne bikar anîn. Modulên tevde bikin Debuggable Îmzeya bipass lv0: Off lv1: PM derbas bike lv2: PM + vekirî (libc) derbas bike Koda guhertoyê bişopîne Koda guhertoya sepana pejirandî biguhezîne 1\nEv rê dide sazkirina dakêşanê di pêşerojê de, û bi gelemperî ev ê bandorê li koda guhertoya ku bi rastî ji hêla serîlêdanê ve tê fêm kirin neke. Patchê dest pê bikin Vegerr Ji ber îmzeyên cihêreng, hûn hewce ne ku berî ku ya patched saz bikin sepana orîjînal rakin.\nPiştrast bike ku we daneya kesane piştguh kiriye. Bi serkeftî saz bike Sazkirin têk çû Modul(ên) Xposed nehatin dîtin Serlêdan hilbijêrin Settings keystore îmza Avakirin Hûnbunî keystore Custom Pelê keystore Şîfre Navê dizî Nasnav şîfreya Set keystore (BKS) Cûreyek çewt a keystore Şîfreya keystore çewt Navê nasnavê xelet Şîfreya nasnavê çewt Detail patch têketin ================================================ FILE: manager/src/main/res/values-lt/strings.xml ================================================ Papildyti Įdiekite diegimas Pašalinti pašalinimas Kopijavimo klaida Programėlės Moduliai Galimas Shizuku servisas „Shizuku“ paslauga neprijungta Repo Žurnalai Išjungta Nežinoma klaida Kai kurios funkcijos nepasiekiamos API versija LSPatch versija Framework versija Sistemos versija Įrenginys Sistema ABI Nukopijuota į mainų sritį Palaikymas LSPatch yra nemokama ne šakninė Xposed sistema, pagrįsta LSPosed branduoliu. Prisijunkite prie mūsų %2$s kanalo]]> Tvarkykite Įkeliama Dar nėra pataisytų programų Rolling Atnaujinti pakrovėją Sėkmingai atnaujinta Atnaujinti nepavyko Modulio taikymo sritis Optimizuokite Sėkmingai optimizuoti Optimizuoti nepavyko Sėkmingai pašalinti Modulių dar nėra Modulio nustatymai Programėlės informacija Naujas pataisymas Pasirinkite saugojimo katalogą Pasirinkite katalogą, kuriame bus saugomi pataisyti APK Klaida nustatant saugojimo katalogą Pasirinkite apk (-us) iš saugyklos Pasirinkite įdiegtą programą Patch režimas Vietinis Pataisykite programą be įterptųjų modulių.\n\"Xposed\" apimtį galima dinamiškai keisti be pakartotinio pataisymo.\nVietinės pataisytos programos gali veikti tik vietiniame įrenginyje. Integruota Pataisykite programą su įterptais moduliais.\nPataisyta programa gali veikti be tvarkyklės, bet negali būti valdoma dinamiškai.\nIntegruotas pataisytas programas galima naudoti įrenginiuose, kuriuose nėra įdiegta LSPatch Manager. Įterpti modulius Derinamas Parašo apėjimas lv0: Išjungta lv1: apeiti PM lv2: apeiti PM + openat (libc) Nepaisyti versijos kodo Pakeiskite pataisytos programos versijos kodą į 1\nTai leidžia ateityje sumažinti įdiegimo lygį, ir paprastai tai neturės įtakos programos faktiškai suvokiamam versijos kodui. Pradėti pataisą Grįžti Dėl skirtingų parašų prieš įdiegdami pataisytą programą turite pašalinti originalią programą.\nĮsitikinkite, kad turite atsargines asmens duomenų kopijas. Įdiegti sėkmingai Diegimas nepavyko Nerastas (-i) nė vienas (-i) \"Xposed\" modulis (-iai) Pasirinkite programas Nustatymai Parašo raktų saugykla Įmontuotas Pasirinktinis Pasirinktinė raktų saugykla Raktų saugyklos failas Slaptažodis Pseudonimas Pseudonimas slaptažodis Nustatyti raktų saugyklą (BKS) Neteisingas raktų saugyklos tipas Neteisingas raktų saugyklos slaptažodis Neteisingas pseudonimas Neteisingas slapyvardžio slaptažodis Išsamūs pataisų žurnalai ================================================ FILE: manager/src/main/res/values-nl/strings.xml ================================================ Toevoegen Installeer Installatie van Verwijder De-installeren van Kopieerfout Apps Modules Shizuku-service beschikbaar Shizuku-service niet verbonden Repo Logs Uit Onbekende fout Sommige functies niet beschikbaar API-versie LSPatch-versie Framework-versie Systeemversie Apparaat Systeem ABI Gekopieerd naar het klembord Steun LSPatch is een gratis niet-root Xposed-framework op basis van LSPosed-kern. Word lid van ons %2$s -kanaal]]> Beheer Bezig met laden Nog geen gepatchte apps Rolling Update lader Update met succes Update mislukt Toepassingsgebied van de module Optimaliseren Succesvol optimaliseren Optimaliseren mislukt Installatie ongedaan maken Nog geen modules Module-instellingen App info Nieuwe patch Selecteer opslagmap Selecteer een map om de gepatchte apks op te slaan Fout bij het instellen van de opslagmap Selecteer apk(s) uit opslag Selecteer een geïnstalleerde app Patch-modus lokaal Een app patchen zonder geïntegreerde modules.\nXposed scope kan dynamisch worden gewijzigd zonder opnieuw te patchen.\nLokaal gepatchte apps kunnen alleen draaien op het lokale apparaat. Geïntegreerd Patch een app met ingesloten modules.\nDe gepatchte app kan zonder de manager worden uitgevoerd, maar kan niet dynamisch worden beheerd.\nGeïntegreerde gepatchte apps kunnen worden gebruikt op apparaten waarop LSPatch Manager niet is geïnstalleerd. Modules insluiten Foutopsporing Handtekening bypass lv0: Uit lv1: PM overslaan lv2: Bypass PM + openat (libc) Versiecode overschrijven Overschrijf de versiecode van de gepatchte app naar 1\nDit maakt downgrade installatie in de toekomst mogelijk, en over het algemeen zal dit geen invloed hebben op de versiecode die daadwerkelijk wordt waargenomen door de applicatie Patch starten Opbrengst Vanwege verschillende handtekeningen moet u de originele app verwijderen voordat u de gepatchte app installeert.\nZorg ervoor dat u een back-up hebt gemaakt van persoonlijke gegevens. Succesvol installeren Installatie mislukt Geen Xposed-module(s) gevonden Selecteer toepassingen Instellingen Handtekening sleutelbewaarplaats Ingebouwd Custom Aangepaste sleutelbewaarplaats Keystore bestand Wachtwoord Alias Alias wachtwoord Sleutelbewaarplaats instellen (BKS) Verkeerde type sleutelbewaarplaats Verkeerd wachtwoord voor sleutelbewaarplaats Verkeerde aliasnaam Verkeerd alias wachtwoord Detail patch logs ================================================ FILE: manager/src/main/res/values-no/strings.xml ================================================ Legge til Installere Installerer Avinstaller Avinstallerer Kopifeil Apper Moduler Shizuku-tjeneste tilgjengelig Shizuku-tjenesten er ikke tilkoblet Repo Tømmerstokker Av Ukjent feil Noen funksjoner er utilgjengelige API-versjon LSPatch-versjon Rammeversjon Systemversjon Enhet System ABI Kopiert til utklippstavlen Brukerstøtte LSPatch er et gratis ikke-root Xposed-rammeverk basert på LSPosed-kjerne. Bli med i vår %2$s -kanal]]> Få til Laster Ingen lappede apper ennå Rullende Oppdater loader Oppdatering vellykket Oppdatering mislyktes Modulomfang Optimaliser Optimaliser vellykket Optimaliseringen mislyktes Avinstaller vellykket Ingen moduler ennå Modulinnstillinger App info Ny oppdatering Velg lagringskatalog Velg en katalog for å lagre de lappede APK-ene Feil ved innstilling av lagringskatalog Velg apk(er) fra lagring Velg en installert app Patch-modus Lokalt Patch en app uten innebygde moduler.\nXposed scope kan endres dynamisk uten re-patch.\nLokale lappede apper kan bare kjøres på den lokale enheten. Integrert Patch en app med innebygde moduler.\nDen lappede appen kan kjøres uten administratoren, men kan ikke administreres dynamisk.\nIntegrerte lappede apper kan brukes på enheter som ikke har LSPatch Manager installert. Bygg inn moduler Kan feilsøkes Signaturomkjøring lv0: Av lv1: Omgå PM lv2: Bypass PM + openat (libc) Overstyr versjonskoden Overstyr den lappede appens versjonskode til 1\nDette tillater nedgraderingsinstallasjon i fremtiden, og generelt vil dette ikke påvirke versjonskoden som faktisk oppfattes av applikasjonen Start patch Komme tilbake På grunn av forskjellige signaturer, må du avinstallere den originale appen før du installerer den lappede.\nSørg for at du har sikkerhetskopiert personlige data. Installer vellykket Installasjonen mislyktes Ingen Xposed-modul(er) ble funnet Velg Apper Innstillinger Signatur nøkkellager Innebygd Tilpasset Egendefinert nøkkellager Keystore-fil Passord Alias Alias passord Angi nøkkellager (BKS) Feil type nøkkellager Feil nøkkellagerpassord Feil aliasnavn Feil alias passord Detalj patchlogger ================================================ FILE: manager/src/main/res/values-pl/strings.xml ================================================ Dodaj Zainstaluj Instalowanie Odinstaluj Odinstalowywanie Błąd kopiowania Aplikacje Moduły Dostępna usługa Shizuku Usługa Shizuku nie jest połączona Repozytorium Logi Wyłącz Nieznany błąd Niektóre funkcje niedostępne Wersja API Wersja LSPatch Wersja Frameworka Wersja systemu Urządzenie ABI systemu Skopiowane do schowka Wsparcie LSPatch jest darmowym odłamem non-root Xposed framework bazowanym na rdzeniu LSPosed. Dołącz do naszego kanału %2$s]]> Zarządzaj Ładowanie Brak spatchowanych aplikacji Rolling Zaktualizuj program ładujący Zaktualizowano pomyślnie Aktualizacja nie powiodła się Zakres modułu Optymalizuj Zoptymalizowano pomyślnie Optymalizacja nie powiodła się Odinstalowywanie pomyślne Brak modułów Ustawienia modułu Informacje o aplikacji Nowy Patch Wybierz katalog przechowywania Wybierz katalog, w którym chcesz przechowywać spatchowane pliki apk Błąd podczas ustawiania katalogu przechowywania Wybierz plik(i) apk z pamięci Wybierz zainstalowaną aplikację Tryb Patchu Lokalny Patchowanie aplikacji bez osadzonych modułów.\nZakres Xposed może być dynamicznie zmieniany bez konieczności ponownego łatania.\nLokalnie załatane aplikacje mogą działać tylko na lokalnym urządzeniu. Zintegrowane Patchuj aplikację z osadzonymi modułami.\nSpatchowana aplikacja może działać bez menedżera, ale nie mogą być one dynamicznie zarządzane.\nZintegrowane spatchowane aplikacje mogą być użyte na urządzeniach, które nie mają zainstalowanego menedżera LSPosed. Osadź moduły Możliwość debugowania Ominięcie podpisu poziom 0: wyłączony lv1: Pomiń PM lv2: Pomiń PM + openat (libc) Zastąp kod wersji Zastąp kod wersji załatanej aplikacji na 1\nUmożliwi to instalację w przyszłości, a generalnie nie będzie miało wpływu na kod wersji faktycznie odbierany przez aplikację Rozpocznij łatkę Powrót Ze względu na różne sygnatury musisz odinstalować oryginalną aplikację przed zainstalowaniem poprawionej.\nUpewnij się, że wykonałeś kopię zapasową danych osobowych. Zainstaluj pomyślnie Instalacja nie powiodła się Nie znaleziono modułów Xposed Wybierz aplikacje Ustawienia Podpisz bazę kluczy Wbudowana strona Własna strona Własny magazyn kluczy Plik Keystore Hasło Alias Alias password Ustawienie magazynu kluczy (BKS) Nieprawidłowy typ magazynu kluczy Nieprawidłowe hasło do magazynu kluczy Nieprawidłowa nazwa aliasu Nieprawidłowe hasło aliasu Szczegóły logów poprawek ================================================ FILE: manager/src/main/res/values-pt/strings.xml ================================================ Adicionar Instalar Instalando Desinstalaroi Desinstalarou Erro de cópia Apps Módulos Serviço Shizuku disponível Serviço Shizuku não conectado Repo Registos Desligado Erro desconhecido Algumas funções indisponíveis Versão da API Versão LSPatch Versão da estrutura Versão do sistema Dispositivo Sistema ABI Copiado para a área de transferência Apoiar LSPatch é uma estrutura Xposed não raiz gratuita baseada no núcleo LSPosed. Junte-se ao nosso %2$s canais]]> Gerir Carregando Nenhum aplicativo corrigido ainda Rolagem Carregador de actualização Actualização com sucesso Actualização falhada Âmbito do módulo Optimizar Optimizar com sucesso Optimizar falhou Desinstalar com sucesso Ainda sem módulos Configurações do módulo Informação da aplicação Novo Patch Selecione o diretório de armazenamento Selecione um diretório para armazenar os apks corrigidos Erro ao definir o diretório de armazenamento Selecione apk(s) do armazenamento Selecione um aplicativo instalado Modo Patch Local Corrigir uma aplicação sem módulos incorporados.\nO escopo do Xposed pode ser alterado dinamicamente sem a necessidade de re-patch.\nOs aplicativos locais corrigidos só podem ser executados no dispositivo local. Integrado Corrija um aplicativo com módulos incorporados.\nO aplicativo corrigido pode ser executado sem o gerenciador, mas não pode ser gerenciado dinamicamente.\nAplicativos corrigidos integrados podem ser usados em dispositivos que não possuem o LSPatch Manager instalado. Incorporar módulos Depurável Bypass de assinatura lv0: Desligado lv1: Ignorar PM lv2: Ignorar PM + openat (libc) Substituir o código da versão Substitua o código de versão do aplicativo corrigido para 1\nIsso permite a instalação de downgrade no futuro, e geralmente isso não afetará o código de versão realmente percebido pela aplicação Iniciar patch Retornar Devido a assinaturas diferentes, você precisa desinstalar o aplicativo original antes de instalar o corrigido.\nCertifique-se de ter feito backup dos dados pessoais. Instalar com sucesso Falha na instalação Nenhum módulo Xposed foi encontrado Seleccionar aplicações Definições Chaveiro de Assinatura Incorporado em Personalizado Chaveiro personalizado Arquivo da Keystore Senha Alias Palavra-passe Set keystore (BKS) Tipo errado de chaveiro Palavra-chave de loja errada Nome falso Palavra-passe de outrora incorrecta Registos de remendos detalhados ================================================ FILE: manager/src/main/res/values-pt-rBR/strings.xml ================================================ Adicionar Instalar Instalando Desinstalar Desinstalando Erro de cópia Apps Módulos Serviço Shizuku disponível Serviço Shizuku não conectado Repositório Registos Desligado Erro desconhecido Algumas funções indisponíveis Versão da API Versão do LSPatch Versão do Framework Versão do sistema Dispositivo Sistema ABI Copiado para a área de transferência Apoiar LSPatch é uma estrutura Xposed gratuita e não root baseada no núcleo LSPosed. Junte-se ao nosso canal %2$s]]> Gerenciar Carregando Nenhum app corrigido ainda Rolando Atualizar carregador Atualizado com sucesso Atualização falhou Escopo do módulo Otimizar Otimizado com sucesso Otimização falhou Desinstalado com sucesso Nenhum módulo ainda Configurações do módulo Informações do app Novo Patch Selecione o diretório de armazenamento Selecione um diretório para armazenar os apks corrigidos Erro ao configurar o diretório de armazenamento Selecione apk(s) do armazenamento Selecione um app instalado Modo Patch Local Faça o patch de um aplicativo sem módulos incorporados.\nO escopo do Xposed pode ser alterado dinamicamente sem nova correção.\nAplicativos com patches locais só podem ser executados no dispositivo local. Integrado Corrija um app com módulos incorporados.\nO app corrigido pode ser executado sem o gerenciador, mas não pode ser gerenciado dinamicamente.\nApps corrigidos integrados podem ser usados ​​em dispositivos que não têm o LSPatch Manager instalado. Incorporar módulos Depurável Ignorar assinatura lv0: Desligado lv1: Ignorar PM lv2: Ignorar PM + openat (libc) Substituir código da versão Substitua o código da versão do app corrigido para 1.\nIsso permite a instalação de downgrade no futuro e, geralmente, isso não afetará o código da versão realmente percebido pelo app. Iniciar patch Retornar Devido às diferentes assinaturas, você precisa desinstalar o app original antes de instalar o corrigido.\nCertifique-se de ter feito backup dos dados pessoais. Instalado com sucesso Falha na instalação Nenhum módulo Xposed foi encontrado Selecionar apps Configurações Armazenamento de chaves de assinatura Construídas em Personalizado Armazenamento de chaves personalizado Arquivo de armazenamento de chaves Senha Apelido Senha do apelido Definir armazenamento de chaves (BKS) Tipo errado de armazenamento de chaves Senha de armazenamento de chaves incorreta Nome do apelido incorreto Senha do apelido incorreta Registros de patch detalhados ================================================ FILE: manager/src/main/res/values-ro/strings.xml ================================================ Adăuga Instalați Instalarea Dezinstalați Dezinstalarea Eroare de copiere Aplicații Module Serviciu Shizuku disponibil Serviciul Shizuku nu este conectat Repo Bușteni Off Eroare necunoscută Unele funcții nu sunt disponibile Versiunea API Versiunea LSPatch Versiunea cadru Versiunea de sistem Dispozitiv Sistem ABI Copiat în clipboard A sustine LSPatch este un cadru Xposed gratuit non-root, bazat pe nucleul LSPosed. Alăturați-vă canalului nostru %2$s]]> Gestionați Se încarcă Nicio aplicație corecţionată încă Rostogolire Actualizarea încărcătorului Actualizare cu succes Actualizarea a eșuat Domeniul de aplicare al modulului Optimizați Optimizați cu succes Optimizarea a eșuat Dezinstalare cu succes Nu există încă module Setări ale modulelor Informații despre aplicație Patch nou Selectați directorul de stocare Selectați un director pentru a stoca apk-urile corectate Eroare la setarea directorului de stocare Selectați apk-uri din stocare Selectați o aplicație instalată Modul Patch Local Remediați o aplicație fără module încorporate.\nDomeniul de aplicare Xposed poate fi modificat în mod dinamic fără a fi nevoie de re-patch.\nAplicațiile patch-uite local pot rula numai pe dispozitivul local. Integrat Corectați o aplicație cu module încorporate.\nAplicația corectată poate rula fără manager, dar nu poate fi gestionată dinamic.\nAplicațiile integrate cu corecții pot fi utilizate pe dispozitivele care nu au LSPatch Manager instalat. Încorporați module Depanabil Ocolire semnătură lv0: oprit lv1: Ocoli PM lv2: Bypass PM + openat (libc) Ignorați codul versiunii Suprascrieți codul de versiune al aplicației corectate la 1\nAcest lucru permite o instalare retrogradată în viitor și, în general, acest lucru nu va afecta codul de versiune perceput efectiv de aplicație. Începeți Patch-ul Întoarcere Din cauza semnăturilor diferite, trebuie să dezinstalați aplicația originală înainte de a o instala pe cea corectată.\nAsigurați-vă că ați făcut o copie de rezervă a datelor personale. Instalați cu succes Instalarea a eșuat Nu au fost găsite module Xposed Selectați aplicații Setări Keystore de semnături Built-in Personalizat Keystore personalizat Fișier Keystore Parola Alias Alias parola Setați keystore (BKS) Tip greșit de depozit de chei Parolă greșită pentru keystore Nume alias greșit Parolă alias greșită Detaliile jurnalelor de patch-uri ================================================ FILE: manager/src/main/res/values-ru/strings.xml ================================================ Добавить Установите Установка Удалить Удаление Ошибка копирования Приложения Модули Служба Shizuku доступна Служба Shizuku не подключена Репозиторий Логи Выкл. Неизвестная ошибка Некоторые функции недоступны Версия API Версия LSPatch Версия фреймворк Версия Android Устройство Разрядность системы (ABI) Скопировано в буфер обмена О приложении LSPatch — это бесплатный фреймворк Xposed без полномочий root, основанный на ядре LSPosed Присоединяйтесь к нашему %2$s каналу]]> Управлять Загрузка Пропатченных приложений пока нет Прокат Обновление загрузчика Успешное обновление Обновление не удалось Область применения модуля Оптимизировать Оптимизация завершена! Ошибка оптимизации Удаление завершено! Модулей пока нет Настройки модуля Информация о приложении Новый патч Выберите каталог хранения Выберите каталог для хранения пропатченных Apk Ошибка при настройке каталога хранения Выберите apk из хранилища Выберите установленное приложение Режим патча Локальный Патч приложения без встроенных модулей.\nОбласть действия Xposed может быть изменена динамически без повторного исправления.\nЛокально пропатченные приложения могут работать только на локальном устройстве. Интегрированный Патч приложения со встроенными модулями.\nПатченное приложение может работать без менеджера, но не может управляться динамически.\nПортативные патченные приложения могут быть использованы на устройствах, у которых не установлен LSPatch менеджер Встроить модули Отладка Обход подписи lv0: Выкл. lv1: Обход PM lv2: Обход PM + openat (libc) Переопределить код версии Переопределить код версии исправленного приложения на 1\nЭто позволит понижать версию установки, и это не повлияет на код версии, фактически воспринимаемый приложением Запустить патч Вернуться Из-за разных сигнатур вам необходимо удалить оригинальное приложение перед установкой исправленного.\nУбедитесь, что у вас есть резервная копия личных данных. Установка завершена Установка не удалась Не найдено ни одного модуля Xposed Выберите приложения Настройки Хранилище ключей подписи Встроенное Пользовательское Пользовательское хранилище ключей Файл хранилища ключей Пароль Псевдоним Псевдоним пароля Установите хранилище ключей (BKS) Неправильный тип хранилища ключей Неправильный пароль хранилища ключей Неправильное имя псевдонима Неправильный пароль псевдонима Подробные логи патчей ================================================ FILE: manager/src/main/res/values-si/strings.xml ================================================ එකතු කරන්න ස්ථාපනය කරන්න ස්ථාපනය කිරීම අස්ථාපනය කරන්න අස්ථාපනය කිරීම පිටපත් කිරීමේ දෝෂයකි යෙදුම් මොඩියුල Shizku සේවාව ඇත Shizku සේවාව සම්බන්ධ නොවේ රෙපෝ සටහන් අක්රියයි නොදන්නා දෝෂයකි සමහර කාර්යයන් නොමැත API අනුවාදය LSPatch අනුවාදය රාමු අනුවාදය පද්ධති අනුවාදය උපාංගය පද්ධතිය ABI පසුරු පුවරුවට පිටපත් කර ඇත සහාය LSPatch යනු LSPosed core මත පදනම් වූ නොමිලේ Root නොවන Xposed රාමුවකි. ට බලන්න අපගේ %2$s නාලිකාවට සම්බන්ධ වන්න]]> කළමනාකරණය කරන්න පැටවීම තවම පැච් කළ යෙදුම් නැත පෙරළීම යාවත්කාලීන ලෝඩරය සාර්ථකව යාවත්කාලීන කරන්න යාවත්කාලීන කිරීම අසාර්ථක විය මොඩියුලය විෂය පථය ප්‍රශස්ත කරන්න සාර්ථකව ප්‍රශස්ත කරන්න ප්‍රශස්ත කිරීම අසාර්ථක විය සාර්ථකව අස්ථාපනය කරන්න තවම මොඩියුල නැත මොඩියුල සැකසුම් යෙදුම් තොරතුරු නව පැච් ගබඩා නාමාවලිය තෝරන්න පැච් කරන ලද apks ගබඩා කිරීමට නාමාවලියක් තෝරන්න ගබඩා නාමාවලිය සැකසීමේදී දෝෂයකි ගබඩාවෙන් apk(s) තෝරන්න ස්ථාපිත යෙදුමක් තෝරන්න පැච් මාදිලිය දේශීය කාවැද්දූ මොඩියුල නොමැති යෙදුමක් පැච් කරන්න.\nXposed විෂය පථය නැවත පැච් කිරීමකින් තොරව ගතිකව වෙනස් කළ හැක.\nදේශීය පැච් කළ යෙදුම් ධාවනය කළ හැක්කේ දේශීය උපාංගයේ පමණි. ඒකාබද්ධ කාවැද්දූ මොඩියුල සහිත යෙදුමක් පැච් කරන්න.\nපැච් කළ යෙදුම කළමනාකරු නොමැතිව ධාවනය කළ හැකි නමුත් ගතිකව කළමනාකරණය කළ නොහැක.\nLSPatch Manager ස්ථාපනය කර නොමැති උපාංග මත ඒකාබද්ධ පැච් යෙදුම් භාවිතා කළ හැක. මොඩියුල තැන්පත් කරන්න නිදොස් කළ හැකි අත්සන බයිපාස් lv0: අක්‍රියයි lv1: PM බයිපාස් කරන්න lv2: බයිපාස් PM + openat (libc) අනුවාද කේතය අභිබවා යන්න පැච් කරන ලද යෙදුමේ අනුවාද කේතය 1\nවෙත ප්‍රතික්‍ෂේප කරන්න මෙය අනාගතයේදී ස්ථාපනය පහත හෙලීමට ඉඩ සලසයි, සාමාන්‍යයෙන් මෙය යෙදුමට සත්‍ය වශයෙන්ම පෙනෙන අනුවාද කේතයට බලපාන්නේ නැත. පැච් ආරම්භ කරන්න ආපසු විවිධ අත්සන් හේතුවෙන්, පැච් කළ එක ස්ථාපනය කිරීමට පෙර ඔබට මුල් යෙදුම අස්ථාපනය කිරීමට අවශ්‍ය වේ.\nඔබ පුද්ගලික දත්ත උපස්ථ කර ඇති බවට වග බලා ගන්න. සාර්ථකව ස්ථාපනය කරන්න ස්ථාපනය අසාර්ථක විය Xposed මොඩියුල(ය) හමු නොවිණි යෙදුම් තෝරන්න සැකසුම් අත්සන යතුරු ගබඩාව බිල්ට්-ඉන් අභිරුචි අභිරුචි යතුරු ගබඩාව යතුරු ගබඩා ගොනුව මුරපදය අන්වර්ථ නාමය අන්වර්ථ මුරපදය යතුරු ගබඩාව සකසන්න (BKS) වැරදි ආකාරයේ යතුරු ගබඩාවක් වැරදි යතුරු ගබඩා මුරපදය වැරදි අන්වර්ථ නාමයක් වැරදි අන්වර්ථ මුරපදය විස්තර පැච් ලොග ================================================ FILE: manager/src/main/res/values-sk/strings.xml ================================================ Pridať Inštalácia Inštaluje sa Odinštalovať Prebieha odinštalovanie Chyba kopírovania Aplikácie Moduly K dispozícii je služba Shizuku Služba Shizuku nie je pripojená Repo Denníky Vypnuté Neznáma chyba Niektoré funkcie sú nedostupné Verzia API Verzia LSPatch Verzia rámca Verzia systému Zariadenie Systém ABI Skopírované do schránky podpora LSPatch je bezplatný rámec Xposed bez rootov založený na jadre LSPosed. Pripojte sa k nášmu %2$s kanálu]]> Spravovať Načítava Zatiaľ žiadne opravené aplikácie Rolovanie Aktualizujte nakladač Aktualizácia bola úspešná Aktualizacia neuspešná Rozsah modulu Optimalizovať Úspešná optimalizácia Optimalizácia zlyhala Odinštalovanie bolo úspešné Zatiaľ žiadne moduly Nastavenia modulu Informácie o aplikácii Nový Patch Vyberte priečinok úložiska Vyberte adresár, do ktorého chcete uložiť opravené súbory APK Chyba pri nastavovaní priečinka úložiska Vyberte apk z úložiska Vyberte nainštalovanú aplikáciu Režim opravy Miestne Oprava aplikácie bez vložených modulov.\nRozsah Xposed možno dynamicky meniť bez opätovného záplatovania.\nLokálne opravené aplikácie môžu byť spustené len na lokálnom zariadení. Integrovaný Opravte aplikáciu so vstavanými modulmi.\nOpravená aplikácia môže bežať bez správcu, ale nedá sa spravovať dynamicky.\nIntegrované opravené aplikácie možno použiť na zariadeniach, ktoré nemajú nainštalovaný LSPatch Manager. Vložiť moduly Laditeľné Obídenie podpisu lv0: vypnuté lv1: Obíďte PM lv2: Obísť PM + openat (libc) Prepísať kód verzie Prepísať kód verzie opravenej aplikácie na 1\nUmožňuje to v budúcnosti inštaláciu na nižšiu verziu a vo všeobecnosti to neovplyvní kód verzie skutočne vnímaný aplikáciou Spustite opravu Návrat Kvôli rôznym podpisom musíte pred inštaláciou opravenej aplikácie odinštalovať pôvodnú aplikáciu.\nUistite sa, že máte zálohované osobné údaje. Nainštalujte úspešne Inštalácia zlyhala Neboli nájdené žiadne moduly Xposed Vyberte položku Aplikácie nastavenie Úložisko podpisových kľúčov Vstavaný Vlastné Vlastné úložisko kľúčov Súbor úložiska kľúčov heslo Alias Heslo aliasu Nastaviť úložisko kľúčov (BKS) Nesprávny typ úložiska kľúčov Nesprávne heslo úložiska kľúčov Nesprávny alias Nesprávne heslo aliasu Podrobné protokoly o opravách ================================================ FILE: manager/src/main/res/values-sv/strings.xml ================================================ Lägg till Installera Installerar Avinstallera Avinstallera Kopieringsfel Appar Moduler Shizuku-tjänsten är tillgänglig Shizuku-tjänsten är inte ansluten Repo Loggar Av Okänt fel Vissa funktioner är inte tillgängliga API-version LSPatch-version Ramverksversion Systemversion Enhet System ABI Kopierat till urklipp Stöd LSPatch är ett gratis, icke-root Xposed-ramverk baserad på LSPosed-kärnan. Gå med i vår %2$s -kanal]]> Hantera Laddar Inga patchade appar ännu Rullande Uppdatera loader Uppdatering lyckades Uppdateringen misslyckades Modulens omfattning Optimera Optimeringen lyckades Optimeringen misslyckades Avinstalleringen lyckades Inga moduler ännu Modulinställningar Om appen Ny patch Välj lagringsmapp Välj en mapp för att lagra de patchade Apk-filerna Ett fel uppstod vid inställningen av lagringsmapp Välj Apk-fil(er) från lagringen Välj en installerad app Patch-läge Lokal Patcha en app utan inbäddade moduler.\nXposed scope kan ändras dynamiskt utan ny patchning.\nLokalt patchade appar kan endast köras på den lokala enheten. Integrerad Patcha en app med inbäddade moduler.\nDen patchade appen kan köras utan behovet av hantering, men kan då inte hanteras dynamiskt.\nIntegrerade patchade appar kan användas på enheter som inte har LSPatch Manager installerat. Inbäddade moduler Felsökningsbar Hoppa över signaturkontrollen lv0: Av lv1: Åtsidosätt pakethanteraren lv2: Åtsidosätt pakethanteraren + openat (libc) Åsidosätt versionskoden Åsidosätt den patchade appens versionskod till 1.\nDetta tillåter nedgraderad installation i framtiden och generellt påverkar detta inte den versionskod som faktiskt uppfattas av applikationen Påbörja patchning Återgå På grund av olika signaturer måste du avinstallera den ursprungliga appen innan du installerar den korrigerade.\nSe till att du har säkerhetskopierat personlig data. Installationen lyckades Installationen misslyckades Inga Xposed-modul(er) hittades Välj appar Inställningar Arkiv för signaturnycklar Inbyggt Anpassad Anpassat nyckelarkiv Nyckelarkivfil Lösenord Alias Alias-lösenord Ställ in nyckelarkiv (BKS) Fel typ av nyckelarkiv Fel lösenord till nyckelarkivet Fel aliasnamn Fel lösenord för alias Detaljerade patchloggar ================================================ FILE: manager/src/main/res/values-th/strings.xml ================================================ Addไทย ติดตั้ง กำลังติดตั้ง ถอนการติดตั้ง กำลังถอนการติดตั้ง ข้อผิดพลาดในการคัดลอก แอพ โมดูล ชิซึกุ มีบริการ ไม่ได้เชื่อมต่อบริการ Shizuku Repo บันทึก ปิด Unknown error ฟังก์ชั่นบางอย่างใช้งานไม่ได้ เวอร์ชัน API เวอร์ชัน LSPatch เวอร์ชันกรอบงาน เวอร์ชันของระบบ อุปกรณ์ ระบบ ABI คัดลอกไปยังคลิปบอร์ดแล้ว สนับสนุน LSPatch เป็นเฟรมเวิร์ก Xposed ที่ไม่ใช่รูทฟรีโดยอิงตามคอร์ LSPosed เข้าร่วม %2$s ช่องของเรา]]> จัดการ กำลังโหลด ยังไม่มีแอพแพตช์ กลิ้ง อัพเดทตัวโหลด อัพเดทเรียบร้อย การอัพเดทล้มเหลว ขอบเขตโมดูล เพิ่มประสิทธิภาพ เพิ่มประสิทธิภาพสำเร็จ การเพิ่มประสิทธิภาพล้มเหลว ถอนการติดตั้งสำเร็จ ยังไม่มีโมดูล การตั้งค่าโมดูล ข้อมูลแอพ แพทช์ใหม่ เลือกไดเร็กทอรีการจัดเก็บ เลือกไดเร็กทอรีเพื่อจัดเก็บ apks ที่แพตช์แล้ว เกิดข้อผิดพลาดเมื่อตั้งค่าไดเร็กทอรีการจัดเก็บ เลือก apk จากที่เก็บข้อมูล เลือกแอพที่ติดตั้ง โหมดแพทช์ ท้องถิ่น แพทช์แอปที่ไม่มีโมดูลฝังอยู่\nขอบเขต Xposed สามารถเปลี่ยนแปลงได้แบบไดนามิกโดยไม่ต้องแพตช์ใหม่\nแอพที่ได้รับการติดตั้งในเครื่องสามารถทำงานได้บนอุปกรณ์ในเครื่องเท่านั้น แบบบูรณาการ Patch an app with modules embedded.\nThe patched app can run without the manager, but cannot be managed dynamically.\nIntegrated patched apps can be used on devices that do not have LSPatch Manager installed. โมดูลฝังตัว แก้จุดบกพร่องได้ บายพาสลายเซ็น lv0: ปิด lv1: บายพาส PM lv2: บายพาส PM + openat (libc) แทนที่รหัสเวอร์ชัน แทนที่รหัสเวอร์ชันของแอปที่แพตช์เป็น\nซึ่งช่วยให้สามารถติดตั้งดาวน์เกรดได้ในอนาคต และโดยทั่วไปแล้วสิ่งนี้จะไม่ส่งผลต่อโค้ดเวอร์ชันที่แอปพลิเคชันรับรู้จริงๆ เริ่มโปรแกรมแก้ไข กลับ เนื่องจากลายเซ็นต่างกัน คุณต้องถอนการติดตั้งแอปเดิมก่อนที่จะติดตั้งแอปที่แพตช์\nตรวจสอบให้แน่ใจว่าคุณได้สำรองข้อมูลส่วนบุคคลแล้ว ติดตั้งสำเร็จ ติดตั้งไม่สำเร็จ เลือกแอพ การตั้งค่า ที่เก็บคีย์ลายเซ็น ในตัว กำหนดเอง ที่เก็บคีย์แบบกำหนดเอง ไฟล์คีย์สโตร์ รหัสผ่าน นามแฝง รหัสผ่านนามแฝง ตั้งค่าที่เก็บคีย์ (BKS) ประเภทของที่เก็บคีย์ผิด รหัสผ่านที่เก็บคีย์ไม่ถูกต้อง ชื่อนามแฝงไม่ถูกต้อง รหัสผ่านนามแฝงไม่ถูกต้อง บันทึกการแก้ไขรายละเอียด ================================================ FILE: manager/src/main/res/values-tr/strings.xml ================================================ Ekle Düzenlemek yükleme Kaldır Kaldırma Kopyalama hatası Uygulamalar Modüller Shizuku servisi mevcut Shizuku hizmeti bağlı değil Repo Günlükler Kapalı Bilinmeyen hata Bazı işlevler kullanılamıyor API Sürümü LSPatch Sürümü Çerçeve Sürümü Sistem Sürümü Cihaz Sistem ABI\'si Panoya kopyalandı Destek LSPatch, LSPosed çekirdeğine dayalı ücretsiz bir kök olmayan Xposed çerçevesidir. %2$s kanalımıza katılın]]> Yönetmek Yükleniyor Henüz yama uygulanmış uygulama yok Yuvarlanma Yükleyiciyi güncelle Başarıyla güncelleyin Güncelleme başarısız Modül kapsamı optimize et Başarıyla optimize et Optimize başarısız oldu başarıyla kaldır Henüz modül yok Modül ayarları Uygulama bilgisi Yeni Yama Depolama dizini seçin Yamalı apk\'leri saklamak için bir dizin seçin Depolama dizini ayarlanırken hata Depodan apk(ler) seçin Yüklü bir uygulama seçin Yama Modu Yerel Gömülü modüller olmadan bir uygulamayı yamalayın.\nXposed kapsamı yeniden yama yapılmadan dinamik olarak değiştirilebilir.\nYerel yamalı uygulamalar yalnızca yerel cihazda çalışabilir. Birleşik Gömülü modüller içeren bir uygulamaya yama yapın.\nYamalı uygulama, yönetici olmadan çalışabilir ancak dinamik olarak yönetilemez.\nTümleşik yamalı uygulamalar, LSPatch Yöneticisi yüklü olmayan cihazlarda kullanılabilir. Gömülü modüller Hata ayıklanabilir İmza atlama lv0: Kapalı lv1: PM\'yi atla lv2: PM + openat\'ı atla (libc) Sürüm kodunu geçersiz kıl Yama uygulanmış uygulamanın sürüm kodunu 1\nolarak geçersiz kıl Bu, gelecekte sürüm düşürme kurulumuna izin verir ve genellikle bu, uygulama tarafından gerçekte algılanan sürüm kodunu etkilemez Yama Başlat Dönüş Farklı imzalar nedeniyle, yamalı uygulamayı yüklemeden önce orijinal uygulamayı kaldırmanız gerekir.\nKişisel verilerinizi yedeklediğinizden emin olun. başarıyla yükle Yükleme başarısız Xposed modül(ler)i bulunamadı Uygulamaları Seçin Ayarlar imza anahtar deposu yerleşik Gelenek Özel anahtar deposu anahtar deposu dosyası Parola takma ad takma ad şifresi Anahtar deposunu ayarla (BKS) Yanlış türde anahtar deposu Yanlış anahtar deposu şifresi Yanlış takma ad Yanlış takma ad şifresi Ayrıntılı yama günlükleri ================================================ FILE: manager/src/main/res/values-uk/strings.xml ================================================ Додати Встановити Встановлення Видалити Видалення Помилка копіювання Додатки Модулі Доступна послуга Shizuku Служба Shizuku не підключена Репо Журнали Вимкнено Невідома помилка Деякі функції недоступні Версія API Версія LSPatch Версія Framework Версія системи Пристрій Система ABI Скопійовано в буфер обміну Підтримка LSPatch — це безкоштовний некорневий фреймворк Xposed, заснований на ядрі LSPosed. Приєднуйтесь до нашого %2$s каналу]]> Керування Завантаження Ще немає виправлених програм Прокатка Оновити завантажувач Оновлення завершено Оновлення не вдалося Область застосування модуля Оптимізувати Оптимізація успішно Не вдалося оптимізувати Успішно видалити Поки що відсутні модулі Налаштування модуля Інформація про додаток Новий патч Виберіть каталог зберігання Виберіть каталог для зберігання виправлених apks Помилка під час налаштування каталогу зберігання Виберіть apk(s) зі сховища Виберіть встановлену програму Режим виправлення Місцеві Виправлення програми без вбудованих модулів.\nОбласть застосування можна динамічно змінювати без перевстановлення патчу.\nЛокальні виправлені програми можна запускати лише на локальному пристрої. Інтегрований Виправте додаток із вбудованими модулями.\nВиправлена програма може працювати без диспетчера, але нею не можна керувати динамічно.\nІнтегровані виправлені програми можна використовувати на пристроях, на яких не встановлено LSPatch Manager. Вставляти модулі Налагоджується Обхід підпису lv0: Вимкнено lv1: Обійти PM lv2: обійти PM + openat (libc) Замінити код версії Замінити код версії виправленого додатка на 1\nЦе дозволить встановити попередню версію в майбутньому, і, як правило, це не вплине на код версії, який фактично сприймається програмою Запустити патч Повернення Через різні сигнатури вам потрібно видалити оригінальну програму, перш ніж встановлювати виправлену.\nПереконайтеся, що ви створили резервну копію особистих даних. Встановити успішно Помилка встановлення Не знайдено жодного модуля (модулів) Xposed Виберіть додатки Налаштування Сховище ключів підпису Вбудований На замовлення Спеціальне сховище ключів Файл сховища ключів Пароль Псевдонім Псевдонім пароль Встановити сховище ключів (BKS) Неправильний тип сховища ключів Неправильний пароль сховища ключів Неправильний псевдонім Неправильний пароль псевдоніма Детальні журнали патчів ================================================ FILE: manager/src/main/res/values-ur/strings.xml ================================================ شامل کریں۔ Install Installال ان انسٹال کریں۔ ان انسٹال کرنا کاپی کی غلطی ایپس ماڈیولز شیزوکو سروس دستیاب ہے۔ شیزوکو سروس منسلک نہیں ہے۔ ریپو نوشتہ جات بند نامعلوم خامی کچھ فنکشنز دستیاب نہیں ہیں۔ API ورژن ایل ایس پیچ ورژن فریم ورک ورژن سسٹم ورژن ڈیوائس سسٹم ABI کلپ بورڈ پر کاپی ہو گیا۔ حمایت LSPatch LSPosed کور پر مبنی ایک مفت نان روٹ Xposed فریم ورک ہے۔ پر سورس کوڈ دیکھیں ہمارے %2$s چینل میں شامل ہوں۔]]> انتظام کریں۔ لوڈ ہو رہا ہے۔ ابھی تک کوئی پیچ شدہ ایپس نہیں ہیں۔ رولنگ لوڈر کو اپ ڈیٹ کریں۔ کامیابی سے اپ ڈیٹ ہو گیا۔ اپ ڈیٹ ناکام ماڈیول دائرہ کار بہتر بنائیں کامیابی کے ساتھ اصلاح کریں۔ اصلاح ناکام ہوگئی کامیابی کے ساتھ اَن انسٹال کریں۔ ابھی تک کوئی ماڈیولز نہیں ہیں۔ ماڈیول کی ترتیبات ایپ کی معلومات نیا پیچ اسٹوریج ڈائرکٹری منتخب کریں۔ پیچ شدہ apks کو ذخیرہ کرنے کے لیے ایک ڈائریکٹری منتخب کریں۔ اسٹوریج ڈائرکٹری ترتیب دیتے وقت خرابی اسٹوریج سے apk(s) کو منتخب کریں۔ انسٹال کردہ ایپ منتخب کریں۔ پیچ موڈ مقامی ایمبیڈڈ ماڈیولز کے بغیر ایپ کو پیچ کریں۔\nایکس پوزڈ اسکوپ کو دوبارہ پیچ کے بغیر متحرک طور پر تبدیل کیا جا سکتا ہے۔\nمقامی پیچ والی ایپس صرف مقامی ڈیوائس پر چل سکتی ہیں۔ ضم ایمبیڈڈ ماڈیولز کے ساتھ ایک ایپ پیچ کریں۔\nپیچ شدہ ایپ مینیجر کے بغیر چل سکتی ہے، لیکن اسے متحرک طور پر منظم نہیں کیا جا سکتا۔\nانٹیگریٹڈ پیچ شدہ ایپس ان آلات پر استعمال کی جا سکتی ہیں جن میں LSPatch مینیجر انسٹال نہیں ہے۔ ایمبیڈ ماڈیولز ڈیبگ ایبل دستخط بائی پاس lv0: آف lv1: PM کو بائی پاس کریں۔ lv2: بائی پاس PM + openat (libc) ورژن کوڈ کو اوور رائڈ کریں۔ پیچ شدہ ایپ کے ورژن کوڈ کو 1\nپر اوور رائیڈ کریں یہ مستقبل میں انسٹالیشن کو ڈاؤن گریڈ کرنے کی اجازت دیتا ہے، اور عام طور پر اس سے ایپلیکیشن کے ذریعہ سمجھے گئے ورژن کوڈ پر کوئی اثر نہیں پڑے گا۔ پیچ شروع کریں۔ واپسی مختلف دستخطوں کی وجہ سے، آپ کو پیچ شدہ ایپ کو انسٹال کرنے سے پہلے اصل ایپ کو ان انسٹال کرنے کی ضرورت ہے۔\nیقینی بنائیں کہ آپ نے ذاتی ڈیٹا کا بیک اپ لیا ہے۔ کامیابی سے انسٹال کریں۔ انسٹال ناکام ہو گیا۔ کوئی ایکس پوز ماڈیول نہیں ملا ایپس کو منتخب کریں۔ ترتیبات دستخطی کی اسٹور بلٹ ان اپنی مرضی کے مطابق حسب ضرورت کی اسٹور کلیدی اسٹور فائل پاس ورڈ عرف عرفی پاس ورڈ سیٹ کی اسٹور (BKS) کی اسٹور کی غلط قسم کی اسٹور کا غلط پاس ورڈ غلط عرفی نام غلط عرفی پاس ورڈ تفصیلی پیچ لاگز ================================================ FILE: manager/src/main/res/values-vi/strings.xml ================================================ Thêm vào Cài đặt Đang cài đặt Gỡ cài đặt Gỡ cài đặt Sao chép lỗi Ứng dụng Mô-đun Có dịch vụ Shizuku Dịch vụ Shizuku không được kết nối Repo Nhật ký Tắt Lỗi không rõ Một số chức năng không khả dụng Phiên bản API LSPatch Version Phiên bản khung Phiên bản hệ thống Thiết bị Hệ thống ABI Sao chép vào clipboard Hỗ trợ LSPatch là một khung công tác Xposed không gốc miễn phí dựa trên lõi LSPosed. Tham gia kênh %2$s của chúng tôi]]> Quản lý Đang tải Chưa có ứng dụng được vá Lăn Cập nhật trình tải Cập nhật thành công Cập nhật không thành công Phạm vi mô-đun Tối ưu hóa Tối ưu hóa thành công Tối ưu hóa không thành công Gỡ cài đặt thành công Chưa có mô-đun nào Cài đặt mô-đun Thông tin ứng dụng Bản vá mới Chọn thư mục lưu trữ Chọn một thư mục để lưu trữ các bản vá lỗi Lỗi khi đặt thư mục lưu trữ Chọn (các) gói ứng dụng từ bộ nhớ Chọn một ứng dụng đã cài đặt Chế độ vá lỗi Địa phương Vá một ứng dụng không có mô-đun được nhúng.\nXpose có thể được thay đổi linh hoạt mà không cần vá lại.\nCác ứng dụng được vá cục bộ chỉ có thể chạy trên thiết bị cục bộ. tích hợp Vá một ứng dụng với các mô-đun được nhúng.\nỨng dụng đã vá có thể chạy mà không cần trình quản lý, nhưng không thể quản lý động.\nCác ứng dụng được vá tích hợp có thể được sử dụng trên các thiết bị chưa cài đặt LSPatch Manager. Nhúng mô-đun Có thể gỡ lỗi Bỏ qua chữ ký lv0: Tắt lv1: Bỏ qua PM lv2: Bỏ qua PM + openat (libc) Ghi đè mã phiên bản Ghi đè mã phiên bản của ứng dụng đã vá thành 1\nĐiều này cho phép hạ cấp cài đặt trong tương lai và nói chung điều này sẽ không ảnh hưởng đến mã phiên bản mà ứng dụng thực sự nhận thấy Bắt đầu bản vá Trở về Do các chữ ký khác nhau, bạn cần gỡ cài đặt ứng dụng gốc trước khi cài đặt bản vá.\nĐảm bảo rằng bạn đã sao lưu dữ liệu cá nhân. Cài đặt thành công Cài đặt không thành công Không tìm thấy (các) mô-đun Xpose nào Chọn ứng dụng Cài đặt Kho khóa chữ ký Được xây dựng trong Tập quán Kho khóa tùy chỉnh Tệp kho khóa Mật khẩu Bí danh Mật khẩu bí danh Đặt kho khóa (BKS) Kho khóa sai loại Mật khẩu kho khóa sai Tên bí danh sai Mật khẩu bí danh sai Nhật ký vá chi tiết ================================================ FILE: manager/src/main/res/values-zh-rCN/strings.xml ================================================ 添加 安装 安装中 卸载 卸载中 复制错误信息 应用 模块 Shizuku 服务可用 Shizuku 服务未连接 仓库 日志 关闭 未知错误 部分功能不可用 API 版本 LSPatch 版本 框架版本 系统版本 设备 系统架构 已复制到剪贴板 支持 LSPatch 是一款免费的基于 LSPosed 核心的免 Root Xposed 框架。 加入我们的 %2$s 频道]]> 管理 加载中 尚无已修补的应用 滚动 更新加载器 更新成功 更新失败 模块作用域 优化 优化成功 优化失败 卸载成功 尚无模块 模块设置 应用信息 新建修补 选择存储目录 选择一个目录来存储已修补的 apk 设置存储目录时出错 从存储目录中选择(多个)apk 选择已安装的应用程序 修补模式 本地模式 为未嵌入模块的应用程序打补丁。\nXposed 范围可动态更改,无需重新打补丁。\n打了本地补丁的应用程序只能在本地设备上运行。 集成模式 修补 App 并内置模块。\n经修补的应用可以在没有管理器的情况下运行,但不能动态管理配置。\n以集成模式修补的应用可在未安装 LSPatch 管理器的设备上运行。 嵌入模块 可调试 破解签名校验 lv0: 关闭 lv1: 绕过 PM lv2: 绕过 PM + openat (libc) 覆写版本号 将修补的 App 版本号重写为 1\n这将允许后续降级安装,并且通常来说这不会影响应用实际感知到的版本号 开始修补 返回 由于签名不同,安装修补的应用前需要先卸载原应用。\n确保您已备份好个人数据。 安装成功 安装失败 未找到 Xposed 模块 选择应用程序 设置 签名密钥库 内置 自定义 自定义密钥库 密钥库文件 密码 别名 别名密码 设置密钥库(BKS) 密钥库类型错误 密钥库密码错误 别名错误 别名密码错误 详细修补日志 ================================================ FILE: manager/src/main/res/values-zh-rHK/strings.xml ================================================ Add 安裝 安裝中 卸载 Shizuku 服務可用 Shizuku 服務未連接 日誌 ================================================ FILE: manager/src/main/res/values-zh-rTW/strings.xml ================================================ 新增 安裝 安裝中 解除安裝 解除安裝中 複製錯誤 應用程式 模組 Shizuku 服務可用 Shizuku 服務未連線 倉庫 日誌 關閉 未知錯誤 部分功能不可用 API 版本 LSPatch 版本 框架版本 系統版本 裝置 系統架構 已複製到剪貼簿 支援 LSPatch 是一款免費的基於 LSPosed 核心的免 Root Xposed 框架。 加入我們的 %2$s 頻道]]> 管理 正在加載 還沒有打包的應用程式 滾動 更新載入程式 更新成功 更新失敗 模組作用域 最佳化 最佳化成功 最佳化失敗 解除安裝成功 還沒有模組 模組設定 程式資訊 新增打包程式 選擇儲存資料夾 選擇一個資料夾儲存打包好的 apk 設定儲存資料夾出錯 從儲存空間中選取 apk 選取已安裝的應用程式 打包模式 本機 修補未嵌入模組的應用程式。\nXpose 範圍可以動態更改,無需重新修補。\n本地修補的應用程式只能在本機裝置上運作。 集成模式 打包內建模組的應用程式。\n打包後的程式,可以在沒有管理器的情況下執行,但無法動態管理啟用的模組。\n以集成模式打包的應用程式,可以在沒有安裝 LSPatch Manager 的裝置上使用。 內建模組 程式可偵錯 破解簽名驗證 lv0: 關閉 lv1: 繞過 PM lv2: 繞過 PM + openat (libc) 覆蓋版本編號 將打包應用程式的版本編號改成 1\n允許以後降級安裝,一般來說,這不會影響應用程式實際感知的版本編號。 開始打包 返回 由於簽名不同,安裝前需要先解除安裝原程式。\n確保您已備份好個人資料。 安裝成功 安裝失敗 未找到 Xpose 模組 選取應用程式 設定 簽名金鑰庫 內建 自訂 自訂金鑰庫 金鑰庫檔案 密碼 別名 別名密碼 設定金鑰庫 (BKS) 金鑰庫類型錯誤 金鑰庫密碼錯誤 別名錯誤 別名密碼錯誤 詳細打包日誌 ================================================ FILE: meta-loader/.gitignore ================================================ /build ================================================ FILE: meta-loader/build.gradle.kts ================================================ import java.util.Locale plugins { alias(libs.plugins.agp.app) } android { defaultConfig { multiDexEnabled = false } buildTypes { release { isMinifyEnabled = true proguardFiles("proguard-rules.pro") } } namespace = "org.lsposed.lspatch.metaloader" } androidComponents.onVariants { variant -> val variantCapped = variant.name.replaceFirstChar { it.uppercase() } val variantLowered = variant.name.lowercase() task("copyDex$variantCapped") { dependsOn("assemble$variantCapped") val dexOutPath = if (variant.buildType == "release") "$buildDir/intermediates/dex/$variantLowered/minify${variantCapped}WithR8" else "$buildDir/intermediates/dex/$variantLowered/mergeDex$variantCapped" from(dexOutPath) rename("classes.dex", "metaloader.dex") into("${rootProject.projectDir}/out/assets/${variant.name}/lspatch") } task("copy$variantCapped") { dependsOn("copyDex$variantCapped") doLast { println("Loader dex has been copied to ${rootProject.projectDir}${File.separator}out") } } } dependencies { compileOnly(projects.hiddenapi.stubs) implementation(projects.share.java) implementation(libs.hiddenapibypass) } ================================================ FILE: meta-loader/proguard-rules.pro ================================================ -keep class org.lsposed.lspatch.metaloader.LSPAppComponentFactoryStub { public static byte[] dex; (); } -dontwarn androidx.annotation.NonNull -dontwarn androidx.annotation.Nullable -dontwarn androidx.annotation.VisibleForTesting ================================================ FILE: meta-loader/src/main/AndroidManifest.xml ================================================ ================================================ FILE: meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java ================================================ package org.lsposed.lspatch.metaloader; import android.annotation.SuppressLint; import android.app.AppComponentFactory; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.os.Build; import android.os.Process; import android.os.ServiceManager; import android.util.JsonReader; import android.util.Log; import org.lsposed.hiddenapibypass.HiddenApiBypass; import org.lsposed.lspatch.share.Constants; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.zip.ZipFile; @SuppressLint("UnsafeDynamicallyLoadedCode") public class LSPAppComponentFactoryStub extends AppComponentFactory { private static final String TAG = "LSPatch-MetaLoader"; private static final Map archToLib = new HashMap(4); public static byte[] dex; static { try { archToLib.put("arm", "armeabi-v7a"); archToLib.put("arm64", "arm64-v8a"); archToLib.put("x86", "x86"); archToLib.put("x86_64", "x86_64"); var cl = Objects.requireNonNull(LSPAppComponentFactoryStub.class.getClassLoader()); Class VMRuntime = Class.forName("dalvik.system.VMRuntime"); Method getRuntime = VMRuntime.getDeclaredMethod("getRuntime"); getRuntime.setAccessible(true); Method vmInstructionSet = VMRuntime.getDeclaredMethod("vmInstructionSet"); vmInstructionSet.setAccessible(true); String arch = (String) vmInstructionSet.invoke(getRuntime.invoke(null)); String libName = archToLib.get(arch); boolean useManager = false; String soPath; try (var is = cl.getResourceAsStream(Constants.CONFIG_ASSET_PATH); var reader = new JsonReader(new InputStreamReader(is))) { reader.beginObject(); while (reader.hasNext()) { var name = reader.nextName(); if (name.equals("useManager")) { useManager = reader.nextBoolean(); break; } else { reader.skipValue(); } } } if (useManager) { Log.i(TAG, "Bootstrap loader from manager"); var ipm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); ApplicationInfo manager; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { manager = (ApplicationInfo) HiddenApiBypass.invoke(IPackageManager.class, ipm, "getApplicationInfo", Constants.MANAGER_PACKAGE_NAME, 0L, Process.myUid() / 100000); } else { manager = ipm.getApplicationInfo(Constants.MANAGER_PACKAGE_NAME, 0, Process.myUid() / 100000); } try (var zip = new ZipFile(new File(manager.sourceDir)); var is = zip.getInputStream(zip.getEntry(Constants.LOADER_DEX_ASSET_PATH)); var os = new ByteArrayOutputStream()) { transfer(is, os); dex = os.toByteArray(); } soPath = manager.sourceDir + "!/assets/lspatch/so/" + libName + "/liblspatch.so"; } else { Log.i(TAG, "Bootstrap loader from embedment"); try (var is = cl.getResourceAsStream(Constants.LOADER_DEX_ASSET_PATH); var os = new ByteArrayOutputStream()) { transfer(is, os); dex = os.toByteArray(); } soPath = cl.getResource("assets/lspatch/so/" + libName + "/liblspatch.so").getPath().substring(5); } System.load(soPath); } catch (Throwable e) { throw new ExceptionInInitializerError(e); } } private static void transfer(InputStream is, OutputStream os) throws IOException { byte[] buffer = new byte[8192]; int n; while (-1 != (n = is.read(buffer))) { os.write(buffer, 0, n); } } } ================================================ FILE: patch/.gitignore ================================================ /build ================================================ FILE: patch/build.gradle.kts ================================================ val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra plugins { id("java-library") } java { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility sourceSets { main { java.srcDirs("libs/manifest-editor/lib/src/main/java") resources.srcDirs("libs/manifest-editor/lib/src/main") } } } dependencies { implementation(projects.apkzlib) implementation(projects.share.java) implementation(lspatch.commons.io) implementation(lspatch.beust.jcommander) implementation(lspatch.google.gson) } ================================================ FILE: patch/src/main/java/org/lsposed/patch/LSPatch.java ================================================ package org.lsposed.patch; import static org.lsposed.lspatch.share.Constants.CONFIG_ASSET_PATH; import static org.lsposed.lspatch.share.Constants.EMBEDDED_MODULES_ASSET_PATH; import static org.lsposed.lspatch.share.Constants.LOADER_DEX_ASSET_PATH; import static org.lsposed.lspatch.share.Constants.ORIGINAL_APK_ASSET_PATH; import static org.lsposed.lspatch.share.Constants.PROXY_APP_COMPONENT_FACTORY; import com.android.tools.build.apkzlib.sign.SigningExtension; import com.android.tools.build.apkzlib.sign.SigningOptions; import com.android.tools.build.apkzlib.zip.AlignmentRules; import com.android.tools.build.apkzlib.zip.StoredEntry; import com.android.tools.build.apkzlib.zip.ZFile; import com.android.tools.build.apkzlib.zip.ZFileOptions; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.google.gson.Gson; import com.wind.meditor.core.ManifestEditor; import com.wind.meditor.property.AttributeItem; import com.wind.meditor.property.ModificationProperty; import com.wind.meditor.utils.NodeValue; import org.apache.commons.io.FilenameUtils; import org.lsposed.lspatch.share.Constants; import org.lsposed.lspatch.share.LSPConfig; import org.lsposed.lspatch.share.PatchConfig; import org.lsposed.patch.util.ApkSignatureHelper; import org.lsposed.patch.util.JavaLogger; import org.lsposed.patch.util.Logger; import org.lsposed.patch.util.ManifestParser; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; public class LSPatch { static class PatchError extends Error { public PatchError(String message, Throwable cause) { super(message, cause); } PatchError(String message) { super(message); } } @Parameter(description = "apks") private List apkPaths = new ArrayList<>(); @Parameter(names = {"-h", "--help"}, help = true, order = 0, description = "Print this message") private boolean help = false; @Parameter(names = {"-o", "--output"}, description = "Output directory") private String outputPath = "."; @Parameter(names = {"-f", "--force"}, description = "Force overwrite exists output file") private boolean forceOverwrite = false; @Parameter(names = {"-d", "--debuggable"}, description = "Set app to be debuggable") private boolean debuggableFlag = false; @Parameter(names = {"-l", "--sigbypasslv"}, description = "Signature bypass level. 0 (disable), 1 (pm), 2 (pm+openat). default 0") private int sigbypassLevel = 0; @Parameter(names = {"-k", "--keystore"}, arity = 4, description = "Set custom signature keystore. Followed by 4 arguments: keystore path, keystore password, keystore alias, keystore alias password") private List keystoreArgs = Arrays.asList(null, "123456", "key0", "123456"); @Parameter(names = {"--manager"}, description = "Use manager (Cannot work with embedding modules)") private boolean useManager = false; @Parameter(names = {"-r", "--allowdown"}, description = "Allow downgrade installation by overriding versionCode to 1 (In most cases, the app can still get the correct versionCode)") private boolean overrideVersionCode = false; @Parameter(names = {"-v", "--verbose"}, description = "Verbose output") private boolean verbose = false; @Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk") private List modules = new ArrayList<>(); private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml"; private static final HashSet ARCHES = new HashSet<>(Arrays.asList( "armeabi-v7a", "arm64-v8a", "x86", "x86_64" )); private static final ZFileOptions Z_FILE_OPTIONS = new ZFileOptions().setAlignmentRule(AlignmentRules.compose( AlignmentRules.constantForSuffix(".so", 4096), AlignmentRules.constantForSuffix(ORIGINAL_APK_ASSET_PATH, 4096) )); private final JCommander jCommander; private final Logger logger; public LSPatch(Logger logger, String... args) { jCommander = JCommander.newBuilder().addObject(this).build(); try { jCommander.parse(args); } catch (ParameterException e) { logger.e(e.getMessage() + "\n"); help = true; } if (apkPaths == null || apkPaths.isEmpty()) { logger.e("No apk specified\n"); help = true; } if (!modules.isEmpty() && useManager) { logger.e("Should not use --embed and --manager at the same time\n"); help = true; } this.logger = logger; logger.verbose = verbose; } public static void main(String... args) throws IOException { LSPatch lspatch = new LSPatch(new JavaLogger(), args); if (lspatch.help) { lspatch.jCommander.usage(); return; } try { lspatch.doCommandLine(); } catch (PatchError e) { e.printStackTrace(System.err); } } public void doCommandLine() throws PatchError, IOException { for (var apk : apkPaths) { File srcApkFile = new File(apk).getAbsoluteFile(); String apkFileName = srcApkFile.getName(); var outputDir = new File(outputPath); outputDir.mkdirs(); File outputFile = new File(outputDir, String.format( Locale.getDefault(), "%s-%d-lspatched.apk", FilenameUtils.getBaseName(apkFileName), LSPConfig.instance.VERSION_CODE) ).getAbsoluteFile(); if (outputFile.exists() && !forceOverwrite) throw new PatchError(outputPath + " exists. Use --force to overwrite"); logger.i("Processing " + srcApkFile + " -> " + outputFile); patch(srcApkFile, outputFile); } } public void patch(File srcApkFile, File outputFile) throws PatchError, IOException { if (!srcApkFile.exists()) throw new PatchError("The source apk file does not exit. Please provide a correct path."); outputFile.delete(); logger.d("apk path: " + srcApkFile); logger.i("Parsing original apk..."); try (var dstZFile = ZFile.openReadWrite(outputFile, Z_FILE_OPTIONS); var srcZFile = dstZFile.addNestedZip((ignore) -> ORIGINAL_APK_ASSET_PATH, srcApkFile, false)) { // sign apk try { var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); if (keystoreArgs.get(0) == null) { logger.i("Register apk signer with default keystore..."); try (var is = getClass().getClassLoader().getResourceAsStream("assets/keystore")) { keyStore.load(is, keystoreArgs.get(1).toCharArray()); } } else { logger.i("Register apk signer with custom keystore..."); try (var is = new FileInputStream(keystoreArgs.get(0))) { keyStore.load(is, keystoreArgs.get(1).toCharArray()); } } var entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(keystoreArgs.get(2), new KeyStore.PasswordProtection(keystoreArgs.get(3).toCharArray())); new SigningExtension(SigningOptions.builder() .setMinSdkVersion(28) .setV2SigningEnabled(true) .setCertificates((X509Certificate[]) entry.getCertificateChain()) .setKey(entry.getPrivateKey()) .build()).register(dstZFile); } catch (Exception e) { throw new PatchError("Failed to register signer", e); } String originalSignature = null; if (sigbypassLevel > 0) { originalSignature = ApkSignatureHelper.getApkSignInfo(srcApkFile.getAbsolutePath()); if (originalSignature == null || originalSignature.isEmpty()) { throw new PatchError("get original signature failed"); } logger.d("Original signature\n" + originalSignature); } // copy out manifest file from zlib var manifestEntry = srcZFile.get(ANDROID_MANIFEST_XML); if (manifestEntry == null) throw new PatchError("Provided file is not a valid apk"); // parse the app appComponentFactory full name from the manifest file final String appComponentFactory; int minSdkVersion; try (var is = manifestEntry.open()) { var pair = ManifestParser.parseManifestFile(is); if (pair == null) throw new PatchError("Failed to parse AndroidManifest.xml"); appComponentFactory = pair.appComponentFactory; minSdkVersion = pair.minSdkVersion; logger.d("original appComponentFactory class: " + appComponentFactory); logger.d("original minSdkVersion: " + minSdkVersion); } logger.i("Patching apk..."); // modify manifest final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory); final var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); final var metadata = Base64.getEncoder().encodeToString(configBytes); try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion))) { dstZFile.add(ANDROID_MANIFEST_XML, is); } catch (Throwable e) { throw new PatchError("Error when modifying manifest", e); } logger.i("Adding config..."); // save lspatch config to asset.. try (var is = new ByteArrayInputStream(configBytes)) { dstZFile.add(CONFIG_ASSET_PATH, is); } catch (Throwable e) { throw new PatchError("Error when saving config"); } logger.i("Adding metaloader dex..."); try (var is = getClass().getClassLoader().getResourceAsStream(Constants.META_LOADER_DEX_ASSET_PATH)) { dstZFile.add("classes.dex", is); } catch (Throwable e) { throw new PatchError("Error when adding dex", e); } if (!useManager) { logger.i("Adding loader dex..."); try (var is = getClass().getClassLoader().getResourceAsStream(LOADER_DEX_ASSET_PATH)) { dstZFile.add(LOADER_DEX_ASSET_PATH, is); } catch (Throwable e) { throw new PatchError("Error when adding assets", e); } logger.i("Adding native lib..."); // copy so and dex files into the unzipped apk // do not put liblspatch.so into apk!lib because x86 native bridge causes crash for (String arch : ARCHES) { String entryName = "assets/lspatch/so/" + arch + "/liblspatch.so"; try (var is = getClass().getClassLoader().getResourceAsStream(entryName)) { dstZFile.add(entryName, is, false); // no compress for so } catch (Throwable e) { // More exception info throw new PatchError("Error when adding native lib", e); } logger.d("added " + entryName); } logger.i("Embedding modules..."); embedModules(dstZFile); } // create zip link logger.d("Creating nested apk link..."); for (StoredEntry entry : srcZFile.entries()) { String name = entry.getCentralDirectoryHeader().getName(); if (name.startsWith("classes") && name.endsWith(".dex")) continue; if (dstZFile.get(name) != null) continue; if (name.equals("AndroidManifest.xml")) continue; if (name.startsWith("META-INF") && (name.endsWith(".SF") || name.endsWith(".MF") || name.endsWith(".RSA"))) continue; srcZFile.addFileLink(name, name); } dstZFile.realign(); logger.i("Writing apk..."); } logger.i("Done. Output APK: " + outputFile.getAbsolutePath()); } private void embedModules(ZFile zFile) { for (var module : modules) { File file = new File(module); try (var apk = ZFile.openReadOnly(new File(module)); var fileIs = new FileInputStream(file); var xmlIs = Objects.requireNonNull(apk.get(ANDROID_MANIFEST_XML)).open() ) { var manifest = Objects.requireNonNull(ManifestParser.parseManifestFile(xmlIs)); var packageName = manifest.packageName; logger.i(" - " + packageName); zFile.add(EMBEDDED_MODULES_ASSET_PATH + packageName + ".apk", fileIs); } catch (NullPointerException | IOException e) { logger.e(module + " does not exist or is not a valid apk file."); } } } private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVersion) throws IOException { ModificationProperty property = new ModificationProperty(); if (overrideVersionCode) property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, 1)); if (minSdkVersion < 28) property.addUsesSdkAttribute(new AttributeItem(NodeValue.UsesSDK.MIN_SDK_VERSION, "28")); property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag)); property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY)); property.addMetaData(new ModificationProperty.MetaData("lspatch", metadata)); // TODO: replace query_all with queries -> manager if (useManager) property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); var os = new ByteArrayOutputStream(); (new ManifestEditor(is, os, property)).processManifest(); is.close(); os.flush(); os.close(); return os.toByteArray(); } } ================================================ FILE: patch/src/main/java/org/lsposed/patch/util/ApkSignatureHelper.java ================================================ package org.lsposed.patch.util; import java.io.InputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.security.cert.Certificate; import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * Created by Wind */ public class ApkSignatureHelper { private static final byte[] APK_V2_MAGIC = {'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ', 'B', 'l', 'o', 'c', 'k', ' ', '4', '2'}; private static char[] toChars(byte[] mSignature) { byte[] sig = mSignature; final int N = sig.length; final int N2 = N * 2; char[] text = new char[N2]; for (int j = 0; j < N; j++) { byte v = sig[j]; int d = (v >> 4) & 0xf; text[j * 2] = (char) (d >= 10 ? ('a' + d - 10) : ('0' + d)); d = v & 0xf; text[j * 2 + 1] = (char) (d >= 10 ? ('a' + d - 10) : ('0' + d)); } return text; } private static Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer) { try { InputStream is = jarFile.getInputStream(je); while (is.read(readBuffer, 0, readBuffer.length) != -1) { } is.close(); return (Certificate[]) (je != null ? je.getCertificates() : null); } catch (Exception e) { } return null; } public static String getApkSignInfo(String apkFilePath) { try { return getApkSignV2(apkFilePath); } catch (Exception e) { return getApkSignV1(apkFilePath); } } public static String getApkSignV1(String apkFilePath) { byte[] readBuffer = new byte[8192]; Certificate[] certs = null; try { JarFile jarFile = new JarFile(apkFilePath); Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry je = (JarEntry) entries.nextElement(); if (je.isDirectory()) { continue; } if (je.getName().startsWith("META-INF/")) { continue; } Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer); if (certs == null) { certs = localCerts; } else { for (int i = 0; i < certs.length; i++) { boolean found = false; for (int j = 0; j < localCerts.length; j++) { if (certs[i] != null && certs[i].equals(localCerts[j])) { found = true; break; } } if (!found || certs.length != localCerts.length) { jarFile.close(); return null; } } } } jarFile.close(); return certs != null ? new String(toChars(certs[0].getEncoded())) : null; } catch (Throwable ignored) { } return null; } private static String getApkSignV2(String apkFilePath) throws IOException { try (RandomAccessFile apk = new RandomAccessFile(apkFilePath, "r")) { ByteBuffer buffer = ByteBuffer.allocate(0x10); buffer.order(ByteOrder.LITTLE_ENDIAN); apk.seek(apk.length() - 0x6); apk.readFully(buffer.array(), 0x0, 0x6); int offset = buffer.getInt(); if (buffer.getShort() != 0) { throw new UnsupportedEncodingException("no zip"); } apk.seek(offset - 0x10); apk.readFully(buffer.array(), 0x0, 0x10); if (!Arrays.equals(buffer.array(), APK_V2_MAGIC)) { throw new UnsupportedEncodingException("no apk v2"); } // Read and compare size fields apk.seek(offset - 0x18); apk.readFully(buffer.array(), 0x0, 0x8); buffer.rewind(); int size = (int) buffer.getLong(); ByteBuffer block = ByteBuffer.allocate(size + 0x8); block.order(ByteOrder.LITTLE_ENDIAN); apk.seek(offset - block.capacity()); apk.readFully(block.array(), 0x0, block.capacity()); if (size != block.getLong()) { throw new UnsupportedEncodingException("no apk v2"); } while (block.remaining() > 24) { size = (int) block.getLong(); if (block.getInt() == 0x7109871a) { // signer-sequence length, signer length, signed data length block.position(block.position() + 12); size = block.getInt(); // digests-sequence length // digests, certificates length block.position(block.position() + size + 0x4); size = block.getInt(); // certificate length break; } else { block.position(block.position() + size - 0x4); } } byte[] certificate = new byte[size]; block.get(certificate); return new String(toChars(certificate)); } } } ================================================ FILE: patch/src/main/java/org/lsposed/patch/util/JavaLogger.java ================================================ package org.lsposed.patch.util; public class JavaLogger extends Logger { @Override public void d(String msg) { if (verbose) System.out.println(msg); } @Override public void i(String msg) { System.out.println(msg); } @Override public void e(String msg) { System.err.println(msg); } } ================================================ FILE: patch/src/main/java/org/lsposed/patch/util/Logger.java ================================================ package org.lsposed.patch.util; public abstract class Logger { public boolean verbose = false; abstract public void d(String msg); abstract public void i(String msg); abstract public void e(String msg); } ================================================ FILE: patch/src/main/java/org/lsposed/patch/util/ManifestParser.java ================================================ package org.lsposed.patch.util; import com.wind.meditor.utils.Utils; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import pxb.android.axml.AxmlParser; /** * Created by Wind */ public class ManifestParser { public static Pair parseManifestFile(InputStream is) throws IOException { AxmlParser parser = new AxmlParser(Utils.getBytesFromInputStream(is)); String packageName = null; String appComponentFactory = null; int minSdkVersion = 0; try { while (true) { int type = parser.next(); if (type == AxmlParser.END_FILE) { break; } if (type == AxmlParser.START_TAG) { int attrCount = parser.getAttributeCount(); for (int i = 0; i < attrCount; i++) { String attrName = parser.getAttrName(i); int attrNameRes = parser.getAttrResId(i); String name = parser.getName(); if ("manifest".equals(name)) { if ("package".equals(attrName)) { packageName = parser.getAttrValue(i).toString(); } } if ("uses-sdk".equals(name)) { if ("minSdkVersion".equals(attrName)) { minSdkVersion = Integer.parseInt(parser.getAttrValue(i).toString()); } } if ("appComponentFactory".equals(attrName) || attrNameRes == 0x0101057a) { appComponentFactory = parser.getAttrValue(i).toString(); } if (packageName != null && packageName.length() > 0 && appComponentFactory != null && appComponentFactory.length() > 0 && minSdkVersion > 0 ) { return new Pair(packageName, appComponentFactory, minSdkVersion); } } } else if (type == AxmlParser.END_TAG) { // ignored } } } catch (Exception e) { return null; } return new Pair(packageName, appComponentFactory, minSdkVersion); } /** * Get the package name and the main application name from the manifest file */ public static Pair parseManifestFile(String filePath) throws IOException { File file = new File(filePath); try (var is = new FileInputStream(file)) { return parseManifestFile(is); } } public static class Pair { public String packageName; public String appComponentFactory; public int minSdkVersion; public Pair(String packageName, String appComponentFactory, int minSdkVersion) { this.packageName = packageName; this.appComponentFactory = appComponentFactory; this.minSdkVersion = minSdkVersion; } } } ================================================ FILE: patch-loader/.gitignore ================================================ /build /.cxx ================================================ FILE: patch-loader/build.gradle.kts ================================================ import java.util.Locale plugins { alias(libs.plugins.agp.app) } android { defaultConfig { multiDexEnabled = false } buildFeatures { buildConfig = true } buildTypes { release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } } namespace = "org.lsposed.lspatch.loader" } androidComponents.onVariants { variant -> val variantCapped = variant.name.replaceFirstChar { it.uppercase() } task("copyDex$variantCapped") { dependsOn("assemble$variantCapped") from("$buildDir/intermediates/dex/${variant.name}/mergeDex$variantCapped/classes.dex") rename("classes.dex", "loader.dex") into("${rootProject.projectDir}/out/assets/${variant.name}/lspatch") } task("copySo$variantCapped") { dependsOn("assemble$variantCapped") from( fileTree( "dir" to "$buildDir/intermediates/stripped_native_libs/${variant.name}/out/lib", "include" to listOf("**/liblspatch.so") ) ) into("${rootProject.projectDir}/out/assets/${variant.name}/lspatch/so") } task("copy$variantCapped") { dependsOn("copySo$variantCapped") dependsOn("copyDex$variantCapped") doLast { println("Dex and so files has been copied to ${rootProject.projectDir}${File.separator}out") } } } dependencies { compileOnly(projects.hiddenapi.stubs) implementation(projects.core) implementation(projects.hiddenapi.bridge) implementation(projects.services.daemonService) implementation(projects.share.android) implementation(projects.share.java) implementation(libs.gson) } ================================================ FILE: patch-loader/proguard-rules.pro ================================================ -keep class com.wind.xposed.entry.MMPEntry { public (); public void initAndLoadModules(); } -keep class com.wind.xpatch.proxy.**{*;} -keep class de.robv.android.xposed.**{*;} -keep class android.app.**{*;} -keep class android.content.**{*;} -keep class android.os.**{*;} -keep class android.view.**{*;} -keep class com.lody.whale.**{*;} -keep class com.android.internal.**{*;} -keep class xposed.dummy.**{*;} -keep class com.wind.xposed.entry.util.**{*;} -keep class com.swift.sandhook.**{*;} -keep class com.swift.sandhook.xposedcompat.**{*;} -dontwarn android.content.res.Resources -dontwarn android.content.res.Resources$Theme -dontwarn android.content.res.AssetManager -dontwarn android.content.res.TypedArray ================================================ FILE: patch-loader/src/main/AndroidManifest.xml ================================================ ================================================ FILE: patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java ================================================ package org.lsposed.lspatch.loader; import static org.lsposed.lspatch.share.Constants.CONFIG_ASSET_PATH; import static org.lsposed.lspatch.share.Constants.ORIGINAL_APK_ASSET_PATH; import android.app.ActivityThread; import android.app.LoadedApk; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.os.Build; import android.os.RemoteException; import android.system.Os; import android.util.Log; import com.google.gson.Gson; import org.lsposed.lspatch.loader.util.FileUtils; import org.lsposed.lspatch.loader.util.XLog; import org.lsposed.lspatch.service.LocalApplicationService; import org.lsposed.lspatch.service.RemoteApplicationService; import org.lsposed.lspatch.share.PatchConfig; import org.lsposed.lspd.core.Startup; import org.lsposed.lspd.service.ILSPApplicationService; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.function.BiConsumer; import java.util.zip.ZipFile; import de.robv.android.xposed.XposedHelpers; import hidden.HiddenApiBridge; /** * Created by Windysha */ @SuppressWarnings("unused") public class LSPApplication { private static final String TAG = "LSPatch"; private static final int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000; private static final int PER_USER_RANGE = 100000; private static ActivityThread activityThread; private static LoadedApk stubLoadedApk; private static LoadedApk appLoadedApk; private static PatchConfig config; public static boolean isIsolated() { return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID; } public static void onLoad() throws RemoteException, IOException { if (isIsolated()) { XLog.d(TAG, "Skip isolated process"); return; } activityThread = ActivityThread.currentActivityThread(); var context = createLoadedApkWithContext(); if (context == null) { XLog.e(TAG, "Error when creating context"); return; } Log.d(TAG, "Initialize service client"); ILSPApplicationService service; if (config.useManager) { service = new RemoteApplicationService(context); } else { service = new LocalApplicationService(context); } disableProfile(context); Startup.initXposed(false, ActivityThread.currentProcessName(), context.getApplicationInfo().dataDir, service); Startup.bootstrapXposed(); // WARN: Since it uses `XResource`, the following class should not be initialized // before forkPostCommon is invoke. Otherwise, you will get failure of XResources Log.i(TAG, "Load modules"); LSPLoader.initModules(appLoadedApk); Log.i(TAG, "Modules initialized"); switchAllClassLoader(); SigBypass.doSigBypass(context, config.sigBypassLevel); Log.i(TAG, "LSPatch bootstrap completed"); } private static Context createLoadedApkWithContext() { try { var mBoundApplication = XposedHelpers.getObjectField(activityThread, "mBoundApplication"); stubLoadedApk = (LoadedApk) XposedHelpers.getObjectField(mBoundApplication, "info"); var appInfo = (ApplicationInfo) XposedHelpers.getObjectField(mBoundApplication, "appInfo"); var compatInfo = (CompatibilityInfo) XposedHelpers.getObjectField(mBoundApplication, "compatInfo"); var baseClassLoader = stubLoadedApk.getClassLoader(); try (var is = baseClassLoader.getResourceAsStream(CONFIG_ASSET_PATH)) { BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); config = new Gson().fromJson(streamReader, PatchConfig.class); } catch (IOException e) { Log.e(TAG, "Failed to load config file"); return null; } Log.i(TAG, "Use manager: " + config.useManager); Log.i(TAG, "Signature bypass level: " + config.sigBypassLevel); Path originPath = Paths.get(appInfo.dataDir, "cache/lspatch/origin/"); Path cacheApkPath; try (ZipFile sourceFile = new ZipFile(appInfo.sourceDir)) { cacheApkPath = originPath.resolve(sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc() + ".apk"); } appInfo.sourceDir = cacheApkPath.toString(); appInfo.publicSourceDir = cacheApkPath.toString(); appInfo.appComponentFactory = config.appComponentFactory; if (!Files.exists(cacheApkPath)) { Log.i(TAG, "Extract original apk"); FileUtils.deleteFolderIfExists(originPath); Files.createDirectories(originPath); try (InputStream is = baseClassLoader.getResourceAsStream(ORIGINAL_APK_ASSET_PATH)) { Files.copy(is, cacheApkPath); } } cacheApkPath.toFile().setWritable(false); var mPackages = (Map) XposedHelpers.getObjectField(activityThread, "mPackages"); mPackages.remove(appInfo.packageName); appLoadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo); XposedHelpers.setObjectField(mBoundApplication, "info", appLoadedApk); var activityClientRecordClass = XposedHelpers.findClass("android.app.ActivityThread$ActivityClientRecord", ActivityThread.class.getClassLoader()); var fixActivityClientRecord = (BiConsumer) (k, v) -> { if (activityClientRecordClass.isInstance(v)) { var pkgInfo = XposedHelpers.getObjectField(v, "packageInfo"); if (pkgInfo == stubLoadedApk) { Log.d(TAG, "fix loadedapk from ActivityClientRecord"); XposedHelpers.setObjectField(v, "packageInfo", appLoadedApk); } } }; var mActivities = (Map) XposedHelpers.getObjectField(activityThread, "mActivities"); mActivities.forEach(fixActivityClientRecord); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { var mLaunchingActivities = (Map) XposedHelpers.getObjectField(activityThread, "mLaunchingActivities"); mLaunchingActivities.forEach(fixActivityClientRecord); } } catch (Throwable ignored) { } Log.i(TAG, "hooked app initialized: " + appLoadedApk); var context = (Context) XposedHelpers.callStaticMethod(Class.forName("android.app.ContextImpl"), "createAppContext", activityThread, stubLoadedApk); if (config.appComponentFactory != null) { try { context.getClassLoader().loadClass(config.appComponentFactory); } catch (ClassNotFoundException e) { // This will happen on some strange shells like 360 Log.w(TAG, "Original AppComponentFactory not found: " + config.appComponentFactory); appInfo.appComponentFactory = null; } } return context; } catch (Throwable e) { Log.e(TAG, "createLoadedApk", e); return null; } } public static void disableProfile(Context context) { final ArrayList codePaths = new ArrayList<>(); var appInfo = context.getApplicationInfo(); var pkgName = context.getPackageName(); if (appInfo == null) return; if ((appInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0) { codePaths.add(appInfo.sourceDir); } if (appInfo.splitSourceDirs != null) { Collections.addAll(codePaths, appInfo.splitSourceDirs); } if (codePaths.isEmpty()) { // If there are no code paths there's no need to setup a profile file and register with // the runtime, return; } var profileDir = HiddenApiBridge.Environment_getDataProfilesDePackageDirectory(appInfo.uid / PER_USER_RANGE, pkgName); var attrs = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("r--------")); for (int i = codePaths.size() - 1; i >= 0; i--) { String splitName = i == 0 ? null : appInfo.splitNames[i - 1]; File curProfileFile = new File(profileDir, splitName == null ? "primary.prof" : splitName + ".split.prof").getAbsoluteFile(); Log.d(TAG, "Processing " + curProfileFile.getAbsolutePath()); try { if (!curProfileFile.canWrite() && Files.size(curProfileFile.toPath()) == 0) { Log.d(TAG, "Skip profile " + curProfileFile.getAbsolutePath()); continue; } if (curProfileFile.exists() && !curProfileFile.delete()) { try (var writer = new FileOutputStream(curProfileFile)) { Log.d(TAG, "Failed to delete, try to clear content " + curProfileFile.getAbsolutePath()); } catch (Throwable e) { Log.e(TAG, "Failed to delete and clear profile file " + curProfileFile.getAbsolutePath(), e); } Os.chmod(curProfileFile.getAbsolutePath(), 00400); } else { Files.createFile(curProfileFile.toPath(), attrs); } } catch (Throwable e) { Log.e(TAG, "Failed to disable profile file " + curProfileFile.getAbsolutePath(), e); } } } private static void switchAllClassLoader() { var fields = LoadedApk.class.getDeclaredFields(); for (Field field : fields) { if (field.getType() == ClassLoader.class) { var obj = XposedHelpers.getObjectField(appLoadedApk, field.getName()); XposedHelpers.setObjectField(stubLoadedApk, field.getName(), obj); } } } } ================================================ FILE: patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPLoader.java ================================================ package org.lsposed.lspatch.loader; import android.app.ActivityThread; import android.app.LoadedApk; import android.content.res.XResources; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedInit; import de.robv.android.xposed.callbacks.XC_LoadPackage; public class LSPLoader { public static void initModules(LoadedApk loadedApk) { XposedInit.loadedPackagesInProcess.add(loadedApk.getPackageName()); XResources.setPackageNameForResDir(loadedApk.getPackageName(), loadedApk.getResDir()); XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam( XposedBridge.sLoadedPackageCallbacks); lpparam.packageName = loadedApk.getPackageName(); lpparam.processName = ActivityThread.currentProcessName(); lpparam.classLoader = loadedApk.getClassLoader(); lpparam.appInfo = loadedApk.getApplicationInfo(); lpparam.isFirstApplication = true; XC_LoadPackage.callAll(lpparam); } } ================================================ FILE: patch-loader/src/main/java/org/lsposed/lspatch/loader/SigBypass.java ================================================ package org.lsposed.lspatch.loader; import static org.lsposed.lspatch.share.Constants.ORIGINAL_APK_ASSET_PATH; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageParser; import android.content.pm.Signature; import android.os.Parcel; import android.os.Parcelable; import android.util.Base64; import android.util.Log; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import org.lsposed.lspatch.loader.util.XLog; import org.lsposed.lspatch.share.Constants; import org.lsposed.lspatch.share.PatchConfig; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.zip.ZipFile; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; public class SigBypass { private static final String TAG = "LSPatch-SigBypass"; private static final Map signatures = new HashMap<>(); private static void replaceSignature(Context context, PackageInfo packageInfo) { boolean hasSignature = (packageInfo.signatures != null && packageInfo.signatures.length != 0) || packageInfo.signingInfo != null; if (hasSignature) { String packageName = packageInfo.packageName; String replacement = signatures.get(packageName); if (replacement == null && !signatures.containsKey(packageName)) { try { var metaData = context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData; String encoded = null; if (metaData != null) encoded = metaData.getString("lspatch"); if (encoded != null) { var json = new String(Base64.decode(encoded, Base64.DEFAULT), StandardCharsets.UTF_8); var patchConfig = new Gson().fromJson(json, PatchConfig.class); replacement = patchConfig.originalSignature; } } catch (PackageManager.NameNotFoundException | JsonSyntaxException ignored) { } signatures.put(packageName, replacement); } if (replacement != null) { if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { XLog.d(TAG, "Replace signature info for `" + packageName + "` (method 1)"); packageInfo.signatures[0] = new Signature(replacement); } if (packageInfo.signingInfo != null) { XLog.d(TAG, "Replace signature info for `" + packageName + "` (method 2)"); Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); if (signaturesArray != null && signaturesArray.length > 0) { signaturesArray[0] = new Signature(replacement); } } } } } private static void hookPackageParser(Context context) { XposedBridge.hookAllMethods(PackageParser.class, "generatePackageInfo", new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) { var packageInfo = (PackageInfo) param.getResult(); if (packageInfo == null) return; replaceSignature(context, packageInfo); } }); } private static void proxyPackageInfoCreator(Context context) { Parcelable.Creator originalCreator = PackageInfo.CREATOR; Parcelable.Creator proxiedCreator = new Parcelable.Creator<>() { @Override public PackageInfo createFromParcel(Parcel source) { PackageInfo packageInfo = originalCreator.createFromParcel(source); replaceSignature(context, packageInfo); return packageInfo; } @Override public PackageInfo[] newArray(int size) { return originalCreator.newArray(size); } }; XposedHelpers.setStaticObjectField(PackageInfo.class, "CREATOR", proxiedCreator); try { Map mCreators = (Map) XposedHelpers.getStaticObjectField(Parcel.class, "mCreators"); mCreators.clear(); } catch (NoSuchFieldError ignore) { } catch (Throwable e) { Log.w(TAG, "fail to clear Parcel.mCreators", e); } try { Map sPairedCreators = (Map) XposedHelpers.getStaticObjectField(Parcel.class, "sPairedCreators"); sPairedCreators.clear(); } catch (NoSuchFieldError ignore) { } catch (Throwable e) { Log.w(TAG, "fail to clear Parcel.sPairedCreators", e); } } static void doSigBypass(Context context, int sigBypassLevel) throws IOException { if (sigBypassLevel >= Constants.SIGBYPASS_LV_PM) { hookPackageParser(context); proxyPackageInfoCreator(context); } if (sigBypassLevel >= Constants.SIGBYPASS_LV_PM_OPENAT) { String cacheApkPath; try (ZipFile sourceFile = new ZipFile(context.getPackageResourcePath())) { cacheApkPath = context.getCacheDir() + "/lspatch/origin/" + sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc() + ".apk"; } org.lsposed.lspd.nativebridge.SigBypass.enableOpenatHook(context.getPackageResourcePath(), cacheApkPath); } } } ================================================ FILE: patch-loader/src/main/java/org/lsposed/lspatch/loader/util/FileUtils.java ================================================ package org.lsposed.lspatch.loader.util; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; public class FileUtils { public static void deleteFolderIfExists(Path target) throws IOException { if (Files.notExists(target)) return; Files.walkFileTree(target, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { if (e == null) { Files.delete(dir); return FileVisitResult.CONTINUE; } else { throw e; } } }); } } ================================================ FILE: patch-loader/src/main/java/org/lsposed/lspatch/loader/util/XLog.java ================================================ package org.lsposed.lspatch.loader.util; import org.lsposed.lspatch.loader.BuildConfig; public class XLog { private static boolean enableLog = BuildConfig.DEBUG; public static void d(String tag, String msg) { if (enableLog) { android.util.Log.d(tag, msg); } } public static void v(String tag, String msg) { if (enableLog) { android.util.Log.v(tag, msg); } } public static void w(String tag, String msg) { if (enableLog) { android.util.Log.w(tag, msg); } } public static void i(String tag, String msg) { if (enableLog) { android.util.Log.i(tag, msg); } } public static void e(String tag, String msg) { if (enableLog) { android.util.Log.e(tag, msg); } } public static void e(String tag, String msg, Throwable tr) { if (enableLog) { android.util.Log.e(tag, msg, tr); } } } ================================================ FILE: patch-loader/src/main/java/org/lsposed/lspatch/service/LocalApplicationService.java ================================================ package org.lsposed.lspatch.service; import android.content.Context; import android.os.Environment; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import org.lsposed.lspatch.loader.util.FileUtils; import org.lsposed.lspatch.share.Constants; import org.lsposed.lspatch.util.ModuleLoader; import org.lsposed.lspd.models.Module; import org.lsposed.lspd.service.ILSPApplicationService; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipFile; public class LocalApplicationService extends ILSPApplicationService.Stub { private static final String TAG = "LSPatch"; private final List modules = new ArrayList<>(); public LocalApplicationService(Context context) { try { for (var name : context.getAssets().list("lspatch/modules")) { String packageName = name.substring(0, name.length() - 4); String modulePath = context.getCacheDir() + "/lspatch/" + packageName + "/"; String cacheApkPath; try (ZipFile sourceFile = new ZipFile(context.getPackageResourcePath())) { cacheApkPath = modulePath + sourceFile.getEntry(Constants.EMBEDDED_MODULES_ASSET_PATH + name).getCrc() + ".apk"; } if (!Files.exists(Paths.get(cacheApkPath))) { Log.i(TAG, "Extract module apk: " + packageName); FileUtils.deleteFolderIfExists(Paths.get(modulePath)); Files.createDirectories(Paths.get(modulePath)); try (var is = context.getAssets().open("lspatch/modules/" + name)) { Files.copy(is, Paths.get(cacheApkPath)); } } var module = new Module(); module.apkPath = cacheApkPath; module.packageName = packageName; module.file = ModuleLoader.loadModule(cacheApkPath); modules.add(module); } } catch (IOException e) { Log.e(TAG, "Error when initializing LocalApplicationServiceClient", e); } } @Override public List getLegacyModulesList() { return modules; } @Override public List getModulesList() { return new ArrayList<>(); } @Override public String getPrefsPath(String packageName) { return new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/").getAbsolutePath(); } @Override public ParcelFileDescriptor requestInjectedManagerBinder(List binder) { return null; } @Override public IBinder asBinder() { return this; } } ================================================ FILE: patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java ================================================ package org.lsposed.lspatch.service; import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import android.widget.Toast; import org.lsposed.lspatch.share.Constants; import org.lsposed.lspd.models.Module; import org.lsposed.lspd.service.ILSPApplicationService; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class RemoteApplicationService implements ILSPApplicationService { private static final String TAG = "LSPatch"; private static final String MODULE_SERVICE = Constants.MANAGER_PACKAGE_NAME + ".manager.ModuleService"; private volatile ILSPApplicationService service; @SuppressLint("DiscouragedPrivateApi") public RemoteApplicationService(Context context) throws RemoteException { try { var intent = new Intent() .setComponent(new ComponentName(Constants.MANAGER_PACKAGE_NAME, MODULE_SERVICE)) .putExtra("packageName", context.getPackageName()); // TODO: Authentication var latch = new CountDownLatch(1); var conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder binder) { Log.i(TAG, "Manager binder received"); service = Stub.asInterface(binder); latch.countDown(); } @Override public void onServiceDisconnected(ComponentName name) { Log.e(TAG, "Manager service died"); service = null; } }; Log.i(TAG, "Request manager binder"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { context.bindService(intent, Context.BIND_AUTO_CREATE, Executors.newSingleThreadExecutor(), conn); } else { var handlerThread = new HandlerThread("RemoteApplicationService"); handlerThread.start(); var handler = new Handler(handlerThread.getLooper()); var contextImplClass = context.getClass(); var getUserMethod = contextImplClass.getMethod("getUser"); var bindServiceAsUserMethod = contextImplClass.getDeclaredMethod( "bindServiceAsUser", Intent.class, ServiceConnection.class, int.class, Handler.class, UserHandle.class); var userHandle = (UserHandle) getUserMethod.invoke(context); bindServiceAsUserMethod.invoke(context, intent, conn, Context.BIND_AUTO_CREATE, handler, userHandle); } boolean success = latch.await(1, TimeUnit.SECONDS); if (!success) throw new TimeoutException("Bind service timeout"); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InterruptedException | TimeoutException e) { Toast.makeText(context, "Unable to connect to Manager", Toast.LENGTH_SHORT).show(); var r = new RemoteException("Failed to get manager binder"); r.initCause(e); throw r; } } @Override public List getLegacyModulesList() throws RemoteException { return service == null ? new ArrayList<>() : service.getLegacyModulesList(); } @Override public List getModulesList() throws RemoteException { return service == null ? new ArrayList<>() : service.getModulesList(); } @Override public String getPrefsPath(String packageName) { return new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/").getAbsolutePath(); } @Override public IBinder asBinder() { return service == null ? null : service.asBinder(); } @Override public ParcelFileDescriptor requestInjectedManagerBinder(List binder) { return null; } } ================================================ FILE: patch-loader/src/main/java/org/lsposed/lspd/nativebridge/SigBypass.java ================================================ package org.lsposed.lspd.nativebridge; public class SigBypass { public static native void enableOpenatHook(String origApkPath, String cacheApkPath); } ================================================ FILE: patch-loader/src/main/jni/CMakeLists.txt ================================================ project(lspatch) cmake_minimum_required(VERSION 3.4.1) add_subdirectory(${CORE_ROOT} core) aux_source_directory(src SRC_LIST) aux_source_directory(src/jni SRC_LIST) set(SRC_LIST ${SRC_LIST} api/patch_main.cpp) add_library(${PROJECT_NAME} SHARED ${SRC_LIST}) target_include_directories(${PROJECT_NAME} PUBLIC include) target_include_directories(${PROJECT_NAME} PRIVATE src) target_link_libraries(${PROJECT_NAME} core log) if (DEFINED DEBUG_SYMBOLS_PATH) set(DEBUG_SYMBOLS_PATH ${DEBUG_SYMBOLS_PATH}/${API}) message(STATUS "Debug symbols will be placed at ${DEBUG_SYMBOLS_PATH}") add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug COMMAND ${CMAKE_STRIP} --strip-all $ COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug $) endif() ================================================ FILE: patch-loader/src/main/jni/api/patch_main.cpp ================================================ /* * This file is part of LSPosed. * * LSPosed is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LSPosed is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LSPosed. If not, see . * * Copyright (C) 2022 LSPosed Contributors */ // // Created by Nullptr on 2022/3/17. // #include #include "config_impl.h" #include "patch_loader.h" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } lspd::PatchLoader::Init(); lspd::ConfigImpl::Init(); lspd::PatchLoader::GetInstance()->Load(env); return JNI_VERSION_1_6; } ================================================ FILE: patch-loader/src/main/jni/include/art/runtime/jit/profile_saver.h ================================================ // // Created by loves on 6/19/2021. // #ifndef LSPATCH_PROFILE_SAVER_H #define LSPATCH_PROFILE_SAVER_H #include "utils/hook_helper.hpp" using namespace lsplant; namespace art { CREATE_MEM_HOOK_STUB_ENTRY( "_ZN3art12ProfileSaver20ProcessProfilingInfoEbPt", bool, ProcessProfilingInfo, (void * thiz, bool, uint16_t *), { LOGD("skipped profile saving"); return true; }); CREATE_MEM_HOOK_STUB_ENTRY( "_ZN3art12ProfileSaver20ProcessProfilingInfoEbbPt", bool, ProcessProfilingInfoWithBool, (void * thiz, bool, bool, uint16_t *), { LOGD("skipped profile saving"); return true; }); CREATE_HOOK_STUB_ENTRY( "execve", int, execve, (const char *pathname, const char *argv[], char *const envp[]), { if (strstr(pathname, "dex2oat")) { size_t count = 0; while (argv[count++] != nullptr); std::unique_ptr new_args = std::make_unique( count + 1); for (size_t i = 0; i < count - 1; ++i) new_args[i] = argv[i]; new_args[count - 1] = "--inline-max-code-units=0"; new_args[count] = nullptr; LOGD("dex2oat by disable inline!"); int ret = backup(pathname, new_args.get(), envp); return ret; } int ret = backup(pathname, argv, envp); return ret; }); static void DisableInline(const HookHandler &handler) { HookSyms(handler, ProcessProfilingInfo, ProcessProfilingInfoWithBool); HookSymNoHandle(handler, reinterpret_cast(&::execve), execve); } } #endif //LSPATCH_PROFILE_SAVER_H ================================================ FILE: patch-loader/src/main/jni/include/art/runtime/oat_file_manager.h ================================================ /* * This file is part of LSPosed. * * LSPosed is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LSPosed is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LSPosed. If not, see . * * Copyright (C) 2021 - 2022 LSPosed Contributors */ #ifndef LSPATCH_OAT_FILE_MANAGER_H #define LSPATCH_OAT_FILE_MANAGER_H #include "context.h" #include "utils/hook_helper.hpp" using namespace lsplant; namespace art { CREATE_MEM_HOOK_STUB_ENTRY( "_ZN3art14OatFileManager25RunBackgroundVerificationERKNSt3__16vectorIPKNS_7DexFileENS1_9allocatorIS5_EEEEP8_jobjectPKc", void, RunBackgroundVerificationWithContext, (void * thiz, const std::vector &dex_files, jobject class_loader, const char *class_loader_context), { if (lspd::Context::GetInstance()->GetCurrentClassLoader() == nullptr) { LOGD("Disabled background verification"); return; } backup(thiz, dex_files, class_loader, class_loader_context); }); CREATE_MEM_HOOK_STUB_ENTRY( "_ZN3art14OatFileManager25RunBackgroundVerificationERKNSt3__16vectorIPKNS_7DexFileENS1_9allocatorIS5_EEEEP8_jobject", void, RunBackgroundVerification, (void * thiz, const std::vector &dex_files, jobject class_loader), { if (lspd::Context::GetInstance()->GetCurrentClassLoader() == nullptr) { LOGD("Disabled background verification"); return; } backup(thiz, dex_files, class_loader); }); static void DisableBackgroundVerification(const lsplant::HookHandler &handler) { const int api_level = lspd::GetAndroidApiLevel(); if (api_level >= __ANDROID_API_Q__) { HookSyms(handler, RunBackgroundVerificationWithContext, RunBackgroundVerification); } } } #endif //LSPATCH_OAT_FILE_MANAGER_H ================================================ FILE: patch-loader/src/main/jni/src/config_impl.h ================================================ /* * This file is part of LSPosed. * * LSPosed is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LSPosed is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LSPosed. If not, see . * * Copyright (C) 2022 LSPosed Contributors */ // // Created by Nullptr on 2022/5/11. // #pragma once #include #include "config_bridge.h" namespace lspd { class ConfigImpl : public ConfigBridge { public: inline static void Init() { instance_ = std::make_unique(); } virtual obfuscation_map_t& obfuscation_map() override { return obfuscation_map_; } virtual void obfuscation_map(obfuscation_map_t m) override { obfuscation_map_ = std::move(m); } private: inline static std::map obfuscation_map_ = { {"de.robv.android.xposed.", "de.robv.android.xposed."}, { "android.app.AndroidApp", "android.app.AndroidApp"}, { "android.content.res.XRes", "android.content.res.XRes"}, { "android.content.res.XModule", "android.content.res.XModule"}, { "org.lsposed.lspd.core.", "org.lsposed.lspd.core."}, { "org.lsposed.lspd.nativebridge.", "org.lsposed.lspd.nativebridge."}, { "org.lsposed.lspd.service.", "org.lsposed.lspd.service."}, }; }; } ================================================ FILE: patch-loader/src/main/jni/src/jni/bypass_sig.cpp ================================================ // // Created by VIP on 2021/4/25. // #include "bypass_sig.h" #include "elf_util.h" #include "logging.h" #include "native_util.h" #include "patch_loader.h" #include "utils/hook_helper.hpp" #include "utils/jni_helper.hpp" namespace lspd { std::string apkPath; std::string redirectPath; CREATE_HOOK_STUB_ENTRY( "__openat", int, __openat, (int fd, const char* pathname, int flag, int mode), { if (pathname == apkPath) { LOGD("redirect openat"); return backup(fd, redirectPath.c_str(), flag, mode); } return backup(fd, pathname, flag, mode); }); LSP_DEF_NATIVE_METHOD(void, SigBypass, enableOpenatHook, jstring origApkPath, jstring cacheApkPath) { auto sym_openat = SandHook::ElfImg("libc.so").getSymbAddress("__openat"); auto r = HookSymNoHandle(handler, sym_openat, __openat); if (!r) { LOGE("Hook __openat fail"); return; } lsplant::JUTFString str1(env, origApkPath); lsplant::JUTFString str2(env, cacheApkPath); apkPath = str1.get(); redirectPath = str2.get(); LOGD("apkPath %s", apkPath.c_str()); LOGD("redirectPath %s", redirectPath.c_str()); } static JNINativeMethod gMethods[] = { LSP_NATIVE_METHOD(SigBypass, enableOpenatHook, "(Ljava/lang/String;Ljava/lang/String;)V") }; void RegisterBypass(JNIEnv* env) { REGISTER_LSP_NATIVE_METHODS(SigBypass); } } ================================================ FILE: patch-loader/src/main/jni/src/jni/bypass_sig.h ================================================ #pragma once #include namespace lspd { void RegisterBypass(JNIEnv* env); } // namespace lspd ================================================ FILE: patch-loader/src/main/jni/src/patch_loader.cpp ================================================ /* * This file is part of LSPosed. * * LSPosed is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LSPosed is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LSPosed. If not, see . * * Copyright (C) 2022 LSPosed Contributors */ // // Created by Nullptr on 2022/3/17. // #include "art/runtime/oat_file_manager.h" #include "art/runtime/jit/profile_saver.h" #include "elf_util.h" #include "jni/bypass_sig.h" #include "native_util.h" #include "patch_loader.h" #include "symbol_cache.h" #include "utils/jni_helper.hpp" using namespace lsplant; namespace lspd { void PatchLoader::LoadDex(JNIEnv* env, Context::PreloadedDex&& dex) { auto class_activity_thread = JNI_FindClass(env, "android/app/ActivityThread"); auto class_activity_thread_app_bind_data = JNI_FindClass(env, "android/app/ActivityThread$AppBindData"); auto class_loaded_apk = JNI_FindClass(env, "android/app/LoadedApk"); auto mid_current_activity_thread = JNI_GetStaticMethodID(env, class_activity_thread, "currentActivityThread", "()Landroid/app/ActivityThread;"); auto mid_get_classloader = JNI_GetMethodID(env, class_loaded_apk, "getClassLoader", "()Ljava/lang/ClassLoader;"); auto fid_m_bound_application = JNI_GetFieldID(env, class_activity_thread, "mBoundApplication", "Landroid/app/ActivityThread$AppBindData;"); auto fid_info = JNI_GetFieldID(env, class_activity_thread_app_bind_data, "info", "Landroid/app/LoadedApk;"); auto activity_thread = JNI_CallStaticObjectMethod(env, class_activity_thread, mid_current_activity_thread); auto m_bound_application = JNI_GetObjectField(env, activity_thread, fid_m_bound_application); auto info = JNI_GetObjectField(env, m_bound_application, fid_info); auto stub_classloader = JNI_CallObjectMethod(env, info, mid_get_classloader); if (!stub_classloader) [[unlikely]] { LOGE("getStubClassLoader failed!!!"); return; } auto in_memory_classloader = JNI_FindClass(env, "dalvik/system/InMemoryDexClassLoader"); auto mid_init = JNI_GetMethodID(env, in_memory_classloader, "", "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); auto byte_buffer_class = JNI_FindClass(env, "java/nio/ByteBuffer"); auto dex_buffer = env->NewDirectByteBuffer(dex.data(), dex.size()); if (auto my_cl = JNI_NewObject(env, in_memory_classloader, mid_init, dex_buffer, stub_classloader)) { inject_class_loader_ = JNI_NewGlobalRef(env, my_cl); } else { LOGE("InMemoryDexClassLoader creation failed!!!"); return; } env->DeleteLocalRef(dex_buffer); } void PatchLoader::InitArtHooker(JNIEnv* env, const InitInfo& initInfo) { Context::InitArtHooker(env, initInfo); handler = initInfo; art::DisableInline(initInfo); art::DisableBackgroundVerification(initInfo); } void PatchLoader::InitHooks(JNIEnv* env) { Context::InitHooks(env); RegisterBypass(env); } void PatchLoader::SetupEntryClass(JNIEnv* env) { if (auto entry_class = FindClassFromLoader(env, GetCurrentClassLoader(), "org.lsposed.lspatch.loader.LSPApplication")) { entry_class_ = JNI_NewGlobalRef(env, entry_class); } } void PatchLoader::Load(JNIEnv* env) { InitSymbolCache(nullptr); lsplant::InitInfo initInfo { .inline_hooker = [](auto t, auto r) { void* bk = nullptr; return HookFunction(t, r, &bk) == RS_SUCCESS ? bk : nullptr; }, .inline_unhooker = [](auto t) { return UnhookFunction(t) == RT_SUCCESS; }, .art_symbol_resolver = [](auto symbol) { return GetArt()->getSymbAddress(symbol); }, .art_symbol_prefix_resolver = [](auto symbol) { return GetArt()->getSymbPrefixFirstAddress(symbol); }, }; auto stub = JNI_FindClass(env, "org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub"); auto dex_field = JNI_GetStaticFieldID(env, stub, "dex", "[B"); ScopedLocalRef array = JNI_GetStaticObjectField(env, stub, dex_field); auto dex = PreloadedDex {env->GetByteArrayElements(array.get(), nullptr), static_cast(JNI_GetArrayLength(env, array))}; InitArtHooker(env, initInfo); LoadDex(env, std::move(dex)); InitHooks(env); GetArt(true); SetupEntryClass(env); FindAndCall(env, "onLoad", "()V"); } } // namespace lspd ================================================ FILE: patch-loader/src/main/jni/src/patch_loader.h ================================================ /* * This file is part of LSPosed. * * LSPosed is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LSPosed is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LSPosed. If not, see . * * Copyright (C) 2022 LSPosed Contributors */ // // Created by Nullptr on 2022/3/17. // #pragma once #include "context.h" namespace lspd { inline lsplant::InitInfo handler; class PatchLoader : public Context { public: inline static void Init() { instance_ = std::make_unique(); } inline static PatchLoader* GetInstance() { return static_cast(instance_.get()); } void Load(JNIEnv* env); protected: void InitArtHooker(JNIEnv* env, const lsplant::InitInfo& initInfo) override; void InitHooks(JNIEnv* env) override; void LoadDex(JNIEnv* env, PreloadedDex&& dex) override; void SetupEntryClass(JNIEnv* env) override; }; } // namespace lspd ================================================ FILE: settings.gradle.kts ================================================ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() mavenLocal { content { includeGroup("io.github.libxposed") } } } versionCatalogs { create("libs") { from(files("core/gradle/libs.versions.toml")) } create("lspatch") { from(files("gradle/lspatch.versions.toml")) } } } rootProject.name = "LSPatch" include( ":apkzlib", ":core", ":hiddenapi:bridge", ":hiddenapi:stubs", ":jar", ":manager", ":meta-loader", ":patch", ":patch-loader", ":services:daemon-service", ":services:manager-service", ":services:xposed-service:interface", ":share:android", ":share:java", ) project(":core").projectDir = file("core/core") project(":hiddenapi:bridge").projectDir = file("core/hiddenapi/bridge") project(":hiddenapi:stubs").projectDir = file("core/hiddenapi/stubs") project(":services:daemon-service").projectDir = file("core/services/daemon-service") project(":services:manager-service").projectDir = file("core/services/manager-service") project(":services:xposed-service:interface").projectDir = file("core/services/xposed-service/interface") buildCache { local { removeUnusedEntriesAfterDays = 1 } } ================================================ FILE: share/android/.gitignore ================================================ /build ================================================ FILE: share/android/build.gradle.kts ================================================ plugins { alias(libs.plugins.agp.lib) } android { namespace = "org.lsposed.lspatch.share" buildFeatures { androidResources = false buildConfig = false } buildTypes { release { isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) } } } dependencies { implementation(projects.services.daemonService) } ================================================ FILE: share/android/src/main/java/org/lsposed/lspatch/util/ModuleLoader.java ================================================ package org.lsposed.lspatch.util; import android.os.SharedMemory; import android.system.ErrnoException; import android.system.OsConstants; import android.util.Log; import org.lsposed.lspd.models.PreLoadedApk; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.channels.Channels; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipFile; public class ModuleLoader { private static final String TAG = "LSPatch"; private static void readDexes(ZipFile apkFile, List preLoadedDexes) { int secondary = 2; for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null; dexFile = apkFile.getEntry("classes" + secondary + ".dex"), secondary++) { try (var in = apkFile.getInputStream(dexFile)) { var memory = SharedMemory.create(null, in.available()); var byteBuffer = memory.mapReadWrite(); Channels.newChannel(in).read(byteBuffer); SharedMemory.unmap(byteBuffer); memory.setProtect(OsConstants.PROT_READ); preLoadedDexes.add(memory); } catch (IOException | ErrnoException e) { Log.w(TAG, "Can not load " + dexFile + " in " + apkFile, e); } } } private static void readName(ZipFile apkFile, String initName, List names) { var initEntry = apkFile.getEntry(initName); if (initEntry == null) return; try (var in = apkFile.getInputStream(initEntry)) { var reader = new BufferedReader(new InputStreamReader(in)); String name; while ((name = reader.readLine()) != null) { name = name.trim(); if (name.isEmpty() || name.startsWith("#")) continue; names.add(name); } } catch (IOException e) { Log.e(TAG, "Can not open " + initEntry, e); } } public static PreLoadedApk loadModule(String path) { if (path == null) return null; var file = new PreLoadedApk(); var preLoadedDexes = new ArrayList(); var moduleClassNames = new ArrayList(1); var moduleLibraryNames = new ArrayList(1); try (var apkFile = new ZipFile(path)) { readDexes(apkFile, preLoadedDexes); readName(apkFile, "assets/xposed_init", moduleClassNames); readName(apkFile, "assets/native_init", moduleLibraryNames); } catch (IOException e) { Log.e(TAG, "Can not open " + path, e); return null; } if (preLoadedDexes.isEmpty()) return null; if (moduleClassNames.isEmpty()) return null; file.preLoadedDexes = preLoadedDexes; file.moduleClassNames = moduleClassNames; file.moduleLibraryNames = moduleLibraryNames; return file; } } ================================================ FILE: share/java/.gitignore ================================================ /build ================================================ FILE: share/java/build.gradle.kts ================================================ val apiCode: Int by rootProject.extra val verCode: Int by rootProject.extra val verName: String by rootProject.extra val coreVerCode: Int by rootProject.extra val coreVerName: String by rootProject.extra val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra plugins { id("java-library") } java { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility } val generateTask = task("generateJava") { val template = mapOf( "apiCode" to apiCode, "verCode" to verCode, "verName" to verName, "coreVerCode" to coreVerCode, "coreVerName" to coreVerName ) inputs.properties(template) from("src/template/java") into("$buildDir/generated/java") expand(template) } sourceSets["main"].java.srcDir("$buildDir/generated/java") tasks["compileJava"].dependsOn(generateTask) ================================================ FILE: share/java/src/main/java/org/lsposed/lspatch/share/Constants.java ================================================ package org.lsposed.lspatch.share; public class Constants { final static public String CONFIG_ASSET_PATH = "assets/lspatch/config.json"; final static public String LOADER_DEX_ASSET_PATH = "assets/lspatch/loader.dex"; final static public String META_LOADER_DEX_ASSET_PATH = "assets/lspatch/metaloader.dex"; final static public String ORIGINAL_APK_ASSET_PATH = "assets/lspatch/origin.apk"; final static public String EMBEDDED_MODULES_ASSET_PATH = "assets/lspatch/modules/"; final static public String PATCH_FILE_SUFFIX = "-lspatched.apk"; final static public String PROXY_APP_COMPONENT_FACTORY = "org.lsposed.lspatch.metaloader.LSPAppComponentFactoryStub"; final static public String MANAGER_PACKAGE_NAME = "org.lsposed.lspatch"; final static public int MIN_ROLLING_VERSION_CODE = 348; final static public int SIGBYPASS_LV_DISABLE = 0; final static public int SIGBYPASS_LV_PM = 1; final static public int SIGBYPASS_LV_PM_OPENAT = 2; final static public int SIGBYPASS_LV_MAX = 3; } ================================================ FILE: share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java ================================================ package org.lsposed.lspatch.share; public class PatchConfig { public final boolean useManager; public final boolean debuggable; public final boolean overrideVersionCode; public final int sigBypassLevel; public final String originalSignature; public final String appComponentFactory; public final LSPConfig lspConfig; public PatchConfig( boolean useManager, boolean debuggable, boolean overrideVersionCode, int sigBypassLevel, String originalSignature, String appComponentFactory ) { this.useManager = useManager; this.debuggable = debuggable; this.overrideVersionCode = overrideVersionCode; this.sigBypassLevel = sigBypassLevel; this.originalSignature = originalSignature; this.appComponentFactory = appComponentFactory; this.lspConfig = LSPConfig.instance; } } ================================================ FILE: share/java/src/template/java/org.lsposed.lspatch.share/LSPConfig.java ================================================ package org.lsposed.lspatch.share; public class LSPConfig { public static final LSPConfig instance; public int API_CODE; public int VERSION_CODE; public String VERSION_NAME; public int CORE_VERSION_CODE; public String CORE_VERSION_NAME; private LSPConfig() { } static { instance = new LSPConfig(); instance.API_CODE = ${apiCode}; instance.VERSION_CODE = ${verCode}; instance.VERSION_NAME = "${verName}"; instance.CORE_VERSION_CODE = ${coreVerCode}; instance.CORE_VERSION_NAME = "${coreVerName}"; } }