├── .gitignore ├── LICENSE.txt ├── app ├── .gitignore ├── android.keystore ├── build.gradle ├── custom.txt ├── gradle.properties ├── local.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── gen │ └── com │ │ └── mcxiaoke │ │ └── mpp │ │ └── sample │ │ ├── BuildConfig.java │ │ ├── Manifest.java │ │ └── R.java │ ├── java │ └── com │ │ └── mcxiaoke │ │ └── packer │ │ └── samples │ │ └── MainActivity.java │ └── res │ └── drawable-xxhdpi │ └── ic_launcher.png ├── build.gradle ├── channels ├── channels.txt ├── free.txt └── paid.txt ├── cli ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── java │ └── com │ │ └── mcxiaoke │ │ └── packer │ │ └── cli │ │ ├── Bridge.java │ │ ├── Helper.java │ │ ├── Main.java │ │ └── Options.java │ └── resources │ └── com │ └── mcxiaoke │ └── packer │ └── cli │ └── help.txt ├── common ├── build.gradle ├── gradle.properties └── src │ ├── main │ └── java │ │ └── com │ │ └── mcxiaoke │ │ └── packer │ │ ├── common │ │ └── PackerCommon.java │ │ └── support │ │ └── walle │ │ ├── ApkSigningBlock.java │ │ ├── ApkSigningPayload.java │ │ ├── ApkUtil.java │ │ ├── Pair.java │ │ ├── PayloadReader.java │ │ ├── PayloadWriter.java │ │ ├── Support.java │ │ ├── V2Const.java │ │ └── V2Utils.java │ └── test │ └── java │ └── com │ └── mcxiaoke │ └── packer │ └── common │ ├── PayloadTests.java │ └── TestUtils.java ├── compatibility.md ├── deploy-local.sh ├── deploy-remote.sh ├── docs ├── _config.yml └── index.md ├── gradle-mvn-push.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── helper ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mcxiaoke │ │ └── packer │ │ └── helper │ │ └── PackerNg.java │ └── resources │ └── META-INF │ └── MANIFEST.MF ├── huge_markets_test.py ├── markets.txt ├── plugin ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── groovy │ └── com │ │ └── mcxiaoke │ │ └── packer │ │ └── ng │ │ ├── CleanTask.groovy │ │ ├── Const.groovy │ │ ├── GradleExtension.groovy │ │ ├── GradlePlugin.groovy │ │ ├── GradleTask.groovy │ │ └── PluginException.groovy │ ├── java │ └── com │ │ └── mcxiaoke │ │ └── packer │ │ └── ng │ │ ├── HASH.java │ │ └── StringVersion.java │ └── resources │ └── META-INF │ └── gradle-plugins │ └── packer.properties ├── readme.md ├── settings.gradle ├── test-build.sh ├── test-market.sh └── tools ├── apkinfo.py ├── build.sh ├── packer-ng-2.0.1.jar ├── packer-ng-v2.py └── src ├── CMakeLists.txt ├── Makefile └── read.c /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | build/ 4 | apks/ 5 | repo/ 6 | dist/ 7 | tmp/ 8 | *.iml 9 | *.apk 10 | *.pyc 11 | *.d 12 | *.o 13 | *.class 14 | .DS_Store 15 | a.out 16 | .classpath 17 | .project 18 | .settings/ 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | packer.properties 3 | -------------------------------------------------------------------------------- /app/android.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/packer-ng-plugin/ffbe05a2d27406f3aea574d083cded27f0742160/app/android.keystore -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.packer_version = '2.0.1-SNAPSHOT' 3 | 4 | repositories { 5 | maven { url '/tmp/repo/' } 6 | mavenCentral() 7 | jcenter() 8 | google() 9 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } 10 | } 11 | 12 | dependencies { 13 | classpath "com.mcxiaoke.packer-ng:plugin:$packer_version" 14 | } 15 | } 16 | 17 | repositories { 18 | maven { url '/tmp/repo/' } 19 | mavenCentral() 20 | jcenter() 21 | google() 22 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } 23 | } 24 | 25 | apply plugin: 'com.android.application' 26 | apply plugin: 'packer' 27 | 28 | // https://code.google.com/p/android/issues/detail?id=171089 29 | dependencies { 30 | implementation "com.mcxiaoke.packer-ng:helper:$packer_version" 31 | } 32 | 33 | //packer-begin 34 | packer { 35 | archiveNameFormat = '${appPkg}-${buildType}-v${versionName}-${channel}' 36 | archiveOutput = new File(project.rootProject.buildDir, "apks") 37 | // channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', 38 | // 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] 39 | channelFile = project.rootProject.file("channels/channels.txt") 40 | // channelMap = [ 41 | // "free" : project.rootProject.file("channels/free.txt"), 42 | // "paid" : project.rootProject.file("channels/paid.txt"), 43 | // "other": project.rootProject.file("channels/channels.txt") 44 | // ] 45 | } 46 | //packer-end 47 | 48 | android { 49 | 50 | compileOptions { 51 | sourceCompatibility JavaVersion.VERSION_1_7 52 | targetCompatibility JavaVersion.VERSION_1_7 53 | encoding "UTF-8" 54 | } 55 | 56 | compileSdkVersion project.compileSdkVersion 57 | buildToolsVersion project.buildToolsVersion 58 | 59 | defaultConfig { 60 | versionName project.VERSION_NAME 61 | versionCode Integer.parseInt(project.VERSION_CODE) 62 | minSdkVersion project.minSdkVersion 63 | targetSdkVersion project.targetSdkVersion 64 | } 65 | 66 | signingConfigs { 67 | v2 { 68 | storeFile file("android.keystore") 69 | storePassword "android" 70 | keyAlias "android" 71 | keyPassword "android" 72 | v2SigningEnabled true 73 | } 74 | 75 | v1 { 76 | storeFile file("android.keystore") 77 | storePassword "android" 78 | keyAlias "android" 79 | keyPassword "android" 80 | v2SigningEnabled false 81 | } 82 | 83 | } 84 | 85 | buildTypes { 86 | release { 87 | signingConfig signingConfigs.v2 88 | minifyEnabled false 89 | } 90 | 91 | beta { 92 | signingConfig signingConfigs.v1 93 | minifyEnabled false 94 | } 95 | 96 | alpha { 97 | minifyEnabled false 98 | } 99 | 100 | } 101 | 102 | flavorDimensions "tier" 103 | 104 | productFlavors { 105 | free {} 106 | 107 | paid {} 108 | 109 | other {} 110 | } 111 | 112 | lintOptions { 113 | abortOnError false 114 | htmlReport true 115 | } 116 | 117 | packagingOptions { 118 | exclude 'LICENSE.txt' 119 | exclude 'META-INF/services/javax.annotation.processing.Processor' 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /app/custom.txt: -------------------------------------------------------------------------------- 1 | App_Market# market test 2 | # 3 | PlayStore# for google play 4 | 5 | PackerTest# just test 6 | # 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/local.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | #Thu Dec 11 11:24:17 CST 2014 11 | sdk.dir=/Users/mcxiaoke/develop/android-sdk-macosx 12 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 | package="com.mcxiaoke.packer.samples"> 4 | 5 | <uses-permission android:name="android.permission.INTERNET" /> 6 | <uses-permission android:name="android.permission.READ_PHONE_STATE" /> 7 | <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 8 | <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 9 | 10 | <application 11 | android:allowBackup="false" 12 | android:icon="@drawable/ic_launcher" 13 | android:label="PackerNg"> 14 | 15 | <activity 16 | android:name=".MainActivity" 17 | android:label="PackerNg"> 18 | <intent-filter> 19 | <action android:name="android.intent.action.MAIN" /> 20 | 21 | <category android:name="android.intent.category.LAUNCHER" /> 22 | </intent-filter> 23 | </activity> 24 | </application> 25 | 26 | </manifest> 27 | -------------------------------------------------------------------------------- /app/src/main/gen/com/mcxiaoke/mpp/sample/BuildConfig.java: -------------------------------------------------------------------------------- 1 | /*___Generated_by_IDEA___*/ 2 | 3 | package com.mcxiaoke.mpp.sample; 4 | 5 | /* This stub is only used by the IDE. It is NOT the BuildConfig class actually packed into the APK */ 6 | public final class BuildConfig { 7 | public final static boolean DEBUG = Boolean.parseBoolean(null); 8 | } -------------------------------------------------------------------------------- /app/src/main/gen/com/mcxiaoke/mpp/sample/Manifest.java: -------------------------------------------------------------------------------- 1 | /*___Generated_by_IDEA___*/ 2 | 3 | package com.mcxiaoke.mpp.sample; 4 | 5 | /* This stub is only used by the IDE. It is NOT the Manifest class actually packed into the APK */ 6 | public final class Manifest { 7 | } -------------------------------------------------------------------------------- /app/src/main/gen/com/mcxiaoke/mpp/sample/R.java: -------------------------------------------------------------------------------- 1 | /*___Generated_by_IDEA___*/ 2 | 3 | package com.mcxiaoke.mpp.sample; 4 | 5 | /* This stub is only used by the IDE. It is NOT the R class actually packed into the APK */ 6 | public final class R { 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.samples; 2 | 3 | import android.app.Activity; 4 | import android.content.pm.ApplicationInfo; 5 | import android.content.pm.PackageManager; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.util.TypedValue; 9 | import android.view.Gravity; 10 | import android.view.ViewGroup.LayoutParams; 11 | import android.widget.TextView; 12 | import com.mcxiaoke.packer.helper.PackerNg; 13 | 14 | import java.io.File; 15 | import java.util.List; 16 | 17 | 18 | public class MainActivity extends Activity { 19 | private static final String TAG = "PackerNg"; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | TextView v = new TextView(this); 25 | LayoutParams p = new LayoutParams(-1, -1); 26 | setContentView(v, p); 27 | v.setTextSize(TypedValue.COMPLEX_UNIT_SP, 40); 28 | v.setGravity(Gravity.CENTER); 29 | v.setPadding(40, 40, 40, 40); 30 | v.setText(PackerNg.getChannel(this)); 31 | 32 | PackageManager pm = getPackageManager(); 33 | List<ApplicationInfo> apps = pm.getInstalledApplications(PackageManager.GET_META_DATA); 34 | for (ApplicationInfo app : apps) { 35 | if (app.packageName.startsWith("com.douban.")) { 36 | Log.d("TAG", "app=" + app.packageName + ", channel=" 37 | + PackerNg.getChannel(new File(app.sourceDir))); 38 | } 39 | } 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/packer-ng-plugin/ffbe05a2d27406f3aea574d083cded27f0742160/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | compileSdkVersion = 27 3 | buildToolsVersion = "27.0.3" 4 | minSdkVersion = 14 5 | targetSdkVersion = 26 6 | } 7 | 8 | buildscript { 9 | repositories { 10 | mavenCentral() 11 | jcenter() 12 | google() 13 | } 14 | dependencies { 15 | classpath "com.android.tools.build:gradle:3.0.1" 16 | } 17 | } 18 | 19 | group = GROUP 20 | version = VERSION_NAME 21 | -------------------------------------------------------------------------------- /channels/channels.txt: -------------------------------------------------------------------------------- 1 | Google_Market#Google电子市场 2 | Hiapk_Market#安卓市场 3 | Yingyonghui_Market#应用汇市场 4 | ali_market#阿里云商店 5 | Xiaomi_Market#小米市场 6 | Yingyongbao_Market#腾讯应用宝市场 7 | Samsung_Market#三星市场 8 | OPPO_Market#OPPO市场 9 | Huawei_Market#华为市场 10 | amazon_market#亚马逊市场 11 | Meizu_Market#魅族市场 12 | 3G_market#3G安卓市场 13 | WanDouJia_Parter#豌豆荚 14 | Baidu_Market#百度应用中心 15 | 360_Market#360手机助手 16 | Taobao_Market#淘宝应用市场 17 | -------------------------------------------------------------------------------- /channels/free.txt: -------------------------------------------------------------------------------- 1 | Cat1#hello2 2 | cat2#哈哈哈 3 | BigCat#hello1 4 | 田园猫 5 | 橘Cat#gogogo 6 | GoodCat 7 | Special@Cat%001 # oooo 8 | -------------------------------------------------------------------------------- /channels/paid.txt: -------------------------------------------------------------------------------- 1 | Dog1 2 | Dog2#d1 3 | Dog3#d5 4 | 金毛# it is a dog -------------------------------------------------------------------------------- /cli/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | google() 6 | } 7 | 8 | apply plugin: 'java' 9 | //apply plugin: 'application' 10 | 11 | sourceCompatibility = 1.7 12 | targetCompatibility = 1.7 13 | 14 | dependencies { 15 | compile project(":common") 16 | compile 'com.android.tools.build:apksig:2.3.3' 17 | } 18 | 19 | //mainClassName = 'com.mcxiaoke.packer.cli.Main' 20 | 21 | task fatJar(type: Jar) { 22 | with jar 23 | from { 24 | configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } 25 | } 26 | manifest { 27 | attributes('Implementation-Title': 'PackerNg 2 Executable Jar', 28 | 'Implementation-Version': VERSION_NAME, 29 | 'Main-Class': 'com.mcxiaoke.packer.cli.Main', 30 | 'Description': 'This is PackerNg 2 executable Jar.', 31 | 'Owner': 'packer-ng-plugin@mcxiaoke.com', 32 | 'Project': 'https://github.com/mcxiaoke/packer-ng-plugin') 33 | } 34 | baseName = 'packer-ng' 35 | 36 | } 37 | 38 | task distJar(type: Copy, dependsOn: fatJar) { 39 | from fatJar.outputs.files 40 | into project.rootProject.file('tools') 41 | } 42 | 43 | // apply from: '../jar.gradle' 44 | apply from: '../gradle-mvn-push.gradle' 45 | -------------------------------------------------------------------------------- /cli/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=cli 2 | POM_PACKAGING=jar 3 | POM_NAME=Commandline Classes for Packer-Ng 4 | -------------------------------------------------------------------------------- /cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.cli; 2 | 3 | import com.android.apksig.ApkVerifier; 4 | import com.android.apksig.ApkVerifier.Builder; 5 | import com.android.apksig.ApkVerifier.Result; 6 | import com.android.apksig.apk.ApkFormatException; 7 | import com.mcxiaoke.packer.common.PackerCommon; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.security.NoSuchAlgorithmException; 12 | 13 | /** 14 | * User: mcxiaoke 15 | * Date: 2017/5/26 16 | * Time: 16:21 17 | */ 18 | public class Bridge { 19 | 20 | public static void writeChannel(File file, String channel) throws IOException { 21 | PackerCommon.writeChannel(file, channel); 22 | } 23 | 24 | public static String readChannel(File file) throws IOException { 25 | return PackerCommon.readChannel(file); 26 | } 27 | 28 | public static boolean verifyChannel(File file, String channel) throws IOException { 29 | return verifyApk(file) && (channel.equals(readChannel(file))); 30 | } 31 | 32 | public static boolean verifyApk(File file) throws IOException { 33 | ApkVerifier verifier = new Builder(file).build(); 34 | try { 35 | Result result = verifier.verify(); 36 | return result.isVerified() 37 | && result.isVerifiedUsingV1Scheme() 38 | && result.isVerifiedUsingV2Scheme(); 39 | } catch (ApkFormatException e) { 40 | throw new IOException(e); 41 | } catch (NoSuchAlgorithmException e) { 42 | throw new IOException(e); 43 | } 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.cli; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.FileOutputStream; 7 | import java.io.FileReader; 8 | import java.io.FilenameFilter; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.nio.channels.FileChannel; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.regex.Pattern; 19 | 20 | /** 21 | * User: mcxiaoke 22 | * Date: 2017/5/31 23 | * Time: 16:52 24 | */ 25 | 26 | public class Helper { 27 | 28 | public static Set<String> readChannels(String value) throws IOException { 29 | if (value.startsWith("@")) { 30 | return parseChannels(new File(value.substring(1))); 31 | } else { 32 | return parseChannels(value); 33 | } 34 | } 35 | 36 | public static Set<String> parseChannels(final File file) throws IOException { 37 | final List<String> channels = new ArrayList<>(); 38 | FileReader fr = new FileReader(file); 39 | BufferedReader br = new BufferedReader(fr); 40 | String line; 41 | while ((line = br.readLine()) != null) { 42 | String parts[] = line.split("#"); 43 | if (parts.length > 0) { 44 | final String ch = parts[0].trim(); 45 | if (ch.length() > 0) { 46 | channels.add(ch); 47 | } 48 | } 49 | } 50 | br.close(); 51 | fr.close(); 52 | return escape(channels); 53 | } 54 | 55 | public static Set<String> parseChannels(String text) { 56 | String[] lines = text.split(","); 57 | List<String> channels = new ArrayList<>(); 58 | for (String line : lines) { 59 | String ch = line.trim(); 60 | if (ch.length() > 0) { 61 | channels.add(ch); 62 | } 63 | } 64 | return escape(channels); 65 | } 66 | 67 | public static Set<String> escape(Collection<String> cs) { 68 | // filter invalid chars for filename 69 | Pattern p = Pattern.compile("[\\\\/:*?\"'<>|]"); 70 | Set<String> set = new HashSet<>(); 71 | for (String s : cs) { 72 | set.add(p.matcher(s).replaceAll("_")); 73 | } 74 | return set; 75 | } 76 | 77 | public static void copyFile(File src, File dest) throws IOException { 78 | if (!dest.exists()) { 79 | dest.createNewFile(); 80 | } 81 | FileChannel source = null; 82 | FileChannel destination = null; 83 | try { 84 | source = new FileInputStream(src).getChannel(); 85 | destination = new FileOutputStream(dest).getChannel(); 86 | destination.transferFrom(source, 0, source.size()); 87 | } finally { 88 | if (source != null) { 89 | source.close(); 90 | } 91 | if (destination != null) { 92 | destination.close(); 93 | } 94 | } 95 | } 96 | 97 | public static void deleteAPKs(File dir) { 98 | FilenameFilter filter = new FilenameFilter() { 99 | @Override 100 | public boolean accept(final File dir, final String name) { 101 | return name.toLowerCase().endsWith(".apk"); 102 | } 103 | }; 104 | File[] files = dir.listFiles(filter); 105 | if (files == null || files.length == 0) { 106 | return; 107 | } 108 | for (File file : files) { 109 | file.delete(); 110 | } 111 | } 112 | 113 | public static String getExtName(final String fileName) { 114 | int dot = fileName.lastIndexOf("."); 115 | if (dot > 0) { 116 | return fileName.substring(dot + 1); 117 | } else { 118 | return null; 119 | } 120 | } 121 | 122 | public static String getBaseName(final String fileName) { 123 | int dot = fileName.lastIndexOf("."); 124 | if (dot > 0) { 125 | return fileName.substring(0, dot); 126 | } else { 127 | return fileName; 128 | } 129 | } 130 | 131 | public static void printUsage() { 132 | try { 133 | BufferedReader in = new BufferedReader(new InputStreamReader( 134 | Main.class.getResourceAsStream("help.txt"), 135 | StandardCharsets.UTF_8)); 136 | String line; 137 | while ((line = in.readLine()) != null) { 138 | System.out.println(line); 139 | } 140 | } catch (IOException e) { 141 | throw new RuntimeException("Failed to read help resource"); 142 | } 143 | } 144 | 145 | 146 | } 147 | -------------------------------------------------------------------------------- /cli/src/main/java/com/mcxiaoke/packer/cli/Main.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.cli; 2 | 3 | import com.mcxiaoke.packer.cli.Options.OptionsException; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.Locale; 11 | 12 | /** 13 | * User: mcxiaoke 14 | * Date: 2017/5/26 15 | * Time: 15:56 16 | */ 17 | public class Main { 18 | 19 | public static final String OUTPUT = "output"; 20 | 21 | public static void main(String[] args) { 22 | if ((args.length == 0) 23 | || ("--help".equals(args[0])) 24 | || ("-h".equals(args[0])) 25 | || "-v".equals(args[0]) 26 | || "--version".equals(args[0])) { 27 | printUsage(); 28 | return; 29 | } 30 | final String cmd = args[0]; 31 | final String[] params = Arrays.copyOfRange(args, 1, args.length); 32 | try { 33 | if ("generate".equals(cmd)) { 34 | generate(params); 35 | } else if ("verify".equals(cmd)) { 36 | verify(params); 37 | } else if ("help".equals(cmd)) { 38 | printUsage(); 39 | } else if ("version".equals(cmd)) { 40 | printUsage(); 41 | } else { 42 | System.err.println( 43 | "Unsupported command: " + cmd); 44 | printUsage(); 45 | } 46 | } catch (Exception e) { 47 | System.err.println("Error: " + e.getMessage()); 48 | System.exit(1); 49 | } 50 | } 51 | 52 | public static void printUsage() { 53 | Helper.printUsage(); 54 | } 55 | 56 | private static void generate(String[] params) throws Exception { 57 | if (params.length == 0) { 58 | printUsage(); 59 | return; 60 | } 61 | System.out.println("========== APK Packer =========="); 62 | // --channels=a,b,c, -c (list mode) 63 | // --channels=@list.txt -c (file mode) 64 | Collection<String> channels = null; 65 | // --input, -i (input apk file) 66 | File apkFile = null; 67 | // --output, -o (output directory) 68 | File outputDir = null; 69 | Options optionsParser = new Options(params); 70 | String name; 71 | String form = null; 72 | while ((name = optionsParser.nextOption()) != null) { 73 | form = optionsParser.getOptionOriginalForm(); 74 | if (("help".equals(name)) || ("h".equals(name))) { 75 | printUsage(); 76 | return; 77 | } else if ("channels".equals(name) 78 | || "c".equals(name)) { 79 | String value = optionsParser.getRequiredValue("Channels file(@) or list(,)."); 80 | if (value.startsWith("@")) { 81 | channels = Helper.parseChannels(new File(value.substring(1))); 82 | } else { 83 | channels = Helper.parseChannels(value); 84 | } 85 | } else if ("input".equals(name) 86 | || "i".equals(name)) { 87 | String value = optionsParser.getRequiredValue("Input APK file"); 88 | apkFile = new File(value); 89 | } else if ("output".equals(name) 90 | || "o".equals(name)) { 91 | String value = optionsParser.getRequiredValue("Output Directory"); 92 | outputDir = new File(value); 93 | } else { 94 | System.err.println( 95 | "Unsupported option: " + form); 96 | printUsage(); 97 | } 98 | } 99 | params = optionsParser.getRemainingParams(); 100 | if (apkFile == null) { 101 | if (params.length < 1) { 102 | throw new OptionsException("Missing Input APK"); 103 | } 104 | apkFile = new File(params[0]); 105 | } 106 | if (outputDir == null) { 107 | outputDir = new File(OUTPUT); 108 | } 109 | doGenerate(apkFile, channels, outputDir); 110 | } 111 | 112 | private static void doGenerate(File apkFile, Collection<String> channels, File outputDir) 113 | throws IOException { 114 | if (apkFile == null 115 | || !apkFile.exists() 116 | || !apkFile.isFile()) { 117 | throw new IOException("Invalid Input APK: " + apkFile); 118 | } 119 | if (!Bridge.verifyApk(apkFile)) { 120 | throw new IOException("Invalid Signature: " + apkFile); 121 | } 122 | if (outputDir.exists()) { 123 | Helper.deleteAPKs(outputDir); 124 | } else { 125 | outputDir.mkdirs(); 126 | } 127 | System.out.println("Input: " + apkFile.getAbsolutePath()); 128 | System.out.println("Output:" + outputDir.getAbsolutePath()); 129 | System.out.println("Channels:" + Arrays.toString(channels.toArray())); 130 | final String fileName = apkFile.getName(); 131 | final String baseName = Helper.getBaseName(fileName); 132 | final String extName = Helper.getExtName(fileName); 133 | for (final String channel : channels) { 134 | final String apkName = String.format(Locale.US, 135 | "%s-%s.%s", baseName, channel, extName); 136 | File destFile = new File(outputDir, apkName); 137 | Helper.copyFile(apkFile, destFile); 138 | Bridge.writeChannel(destFile, channel); 139 | if (Bridge.verifyChannel(destFile, channel)) { 140 | System.out.println("Generating " + apkName); 141 | } else { 142 | destFile.delete(); 143 | throw new IOException("Failed to verify APK: " + apkName); 144 | } 145 | } 146 | } 147 | 148 | private static void verify(String[] params) throws Exception { 149 | if (params.length == 0) { 150 | printUsage(); 151 | return; 152 | } 153 | System.out.println("========== APK Verify =========="); 154 | if (params.length < 1) { 155 | throw new IllegalArgumentException("Missing Input APK"); 156 | } 157 | File apkFile = new File(params[0]); 158 | doVerify(apkFile); 159 | } 160 | 161 | private static void doVerify(File apkFile) throws IOException { 162 | if (apkFile == null 163 | || !apkFile.exists() 164 | || !apkFile.isFile()) { 165 | throw new IOException("Invalid Input APK: " + apkFile); 166 | } 167 | final boolean verified = Bridge.verifyApk(apkFile); 168 | final String channel = Bridge.readChannel(apkFile); 169 | System.out.println("File: " + apkFile.getName()); 170 | System.out.println("Signed: " + verified); 171 | System.out.println("Channel: " + channel); 172 | } 173 | 174 | 175 | } 176 | -------------------------------------------------------------------------------- /cli/src/main/java/com/mcxiaoke/packer/cli/Options.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.mcxiaoke.packer.cli; 18 | 19 | import java.util.Arrays; 20 | 21 | /** 22 | * Parser of command-line options/switches/flags. 23 | * <p> 24 | * <p>Supported option formats: 25 | * <ul> 26 | * <li>{@code --name value}</li> 27 | * <li>{@code --name=value}</li> 28 | * <li>{@code -name value}</li> 29 | * <li>{@code --name} (boolean options only)</li> 30 | * </ul> 31 | * <p> 32 | * <p>To use the parser, create an instance, providing it with the command-line parameters, then 33 | * iterate over options by invoking {@link #nextOption()} until it returns {@code null}. 34 | */ 35 | class Options { 36 | private final String[] params; 37 | private int index; 38 | private String lastOptionValue; 39 | private String lastOptionOriginalForm; 40 | 41 | /** 42 | * Constructs a new {@code OptionsParser} initialized with the provided command-line. 43 | */ 44 | public Options(String[] params) { 45 | this.params = params.clone(); 46 | } 47 | 48 | /** 49 | * Returns the name (without leading dashes) of the next option (starting with the very first 50 | * option) or {@code null} if there are no options left. 51 | * <p> 52 | * <p>The value of this option can be obtained via {@link #getRequiredValue(String)}, 53 | * {@link #getRequiredIntValue(String)}, and {@link #getOptionalBooleanValue(boolean)}. 54 | */ 55 | public String nextOption() { 56 | if (index >= params.length) { 57 | // No more parameters left 58 | return null; 59 | } 60 | String param = params[index]; 61 | if (!param.startsWith("-")) { 62 | // Not an option 63 | return null; 64 | } 65 | 66 | index++; 67 | lastOptionOriginalForm = param; 68 | lastOptionValue = null; 69 | if (param.startsWith("--")) { 70 | // FORMAT: --name value OR --name=value 71 | if ("--".equals(param)) { 72 | // End of options marker 73 | return null; 74 | } 75 | int valueDelimiterIndex = param.indexOf('='); 76 | if (valueDelimiterIndex != -1) { 77 | lastOptionValue = param.substring(valueDelimiterIndex + 1); 78 | lastOptionOriginalForm = param.substring(0, valueDelimiterIndex); 79 | return param.substring("--".length(), valueDelimiterIndex); 80 | } else { 81 | return param.substring("--".length()); 82 | } 83 | } else { 84 | // FORMAT: -name value 85 | return param.substring("-".length()); 86 | } 87 | } 88 | 89 | /** 90 | * Returns the original form of the current option. The original form includes the leading dash 91 | * or dashes. This is intended to be used for referencing the option in error messages. 92 | */ 93 | public String getOptionOriginalForm() { 94 | return lastOptionOriginalForm; 95 | } 96 | 97 | /** 98 | * Returns the value of the current option, throwing an exception if the value is missing. 99 | */ 100 | public String getRequiredValue(String valueDescription) throws OptionsException { 101 | if (lastOptionValue != null) { 102 | String result = lastOptionValue; 103 | lastOptionValue = null; 104 | return result; 105 | } 106 | if (index >= params.length) { 107 | // No more parameters left 108 | throw new OptionsException( 109 | valueDescription + " missing after " + lastOptionOriginalForm); 110 | } 111 | String param = params[index]; 112 | if ("--".equals(param)) { 113 | // End of options marker 114 | throw new OptionsException( 115 | valueDescription + " missing after " + lastOptionOriginalForm); 116 | } 117 | index++; 118 | return param; 119 | } 120 | 121 | /** 122 | * Returns the value of the current numeric option, throwing an exception if the value is 123 | * missing or is not numeric. 124 | */ 125 | public int getRequiredIntValue(String valueDescription) throws OptionsException { 126 | String value = getRequiredValue(valueDescription); 127 | try { 128 | return Integer.parseInt(value); 129 | } catch (NumberFormatException e) { 130 | throw new OptionsException( 131 | valueDescription + " (" + lastOptionOriginalForm 132 | + ") must be a decimal number: " + value); 133 | } 134 | } 135 | 136 | /** 137 | * Gets the value of the current boolean option. Boolean options are not required to have 138 | * explicitly specified values. 139 | */ 140 | public boolean getOptionalBooleanValue(boolean defaultValue) throws OptionsException { 141 | if (lastOptionValue != null) { 142 | // --option=value form 143 | String stringValue = lastOptionValue; 144 | lastOptionValue = null; 145 | if ("true".equals(stringValue)) { 146 | return true; 147 | } else if ("false".equals(stringValue)) { 148 | return false; 149 | } 150 | throw new OptionsException( 151 | "Unsupported value for " + lastOptionOriginalForm + ": " + stringValue 152 | + ". Only true or false supported."); 153 | } 154 | 155 | // --option (true|false) form OR just --option 156 | if (index >= params.length) { 157 | return defaultValue; 158 | } 159 | 160 | String stringValue = params[index]; 161 | if ("true".equals(stringValue)) { 162 | index++; 163 | return true; 164 | } else if ("false".equals(stringValue)) { 165 | index++; 166 | return false; 167 | } else { 168 | return defaultValue; 169 | } 170 | } 171 | 172 | /** 173 | * Returns the remaining command-line parameters. This is intended to be invoked once 174 | * {@link #nextOption()} returns {@code null}. 175 | */ 176 | public String[] getRemainingParams() { 177 | if (index >= params.length) { 178 | return new String[0]; 179 | } 180 | String param = params[index]; 181 | if ("--".equals(param)) { 182 | // Skip end of options marker 183 | return Arrays.copyOfRange(params, index + 1, params.length); 184 | } else { 185 | return Arrays.copyOfRange(params, index, params.length); 186 | } 187 | } 188 | 189 | /** 190 | * Indicates that an error was encountered while parsing command-line options. 191 | */ 192 | public static class OptionsException extends Exception { 193 | private static final long serialVersionUID = 1L; 194 | 195 | public OptionsException(String message) { 196 | super(message); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt: -------------------------------------------------------------------------------- 1 | 2 | INTRODUCTION 3 | 4 | PackerNg is a tool for add channel information to Android APK files. 5 | 6 | PROJECT 7 | 8 | URL: https://github.com/mcxiaoke/packer-ng-plugin 9 | Email: packer-ng-plugin@mcxiaoke.com 10 | 11 | USAGE 12 | 13 | packer-ng <command> [options] 14 | packer-ng --help [-h] 15 | packer-ng generate --channels --output apk 16 | packer-ng verify apk 17 | 18 | EXAMPLE 19 | 20 | generate Add channel info to the provided APK 21 | 22 | packer-ng generate --channels=ch1,ch2,ch3 --output=archives app.apk 23 | packer-ng generate --channels=@file.txt --output=archives app.apk 24 | 25 | --channels=@file.txt - using channels from the provided file. 26 | --channels=ch1,ch2,ch3 - using channels from the provided list. 27 | --output=archives - output directory for save final APK files. 28 | --input=file - base APK file for add channel information. 29 | 30 | verify Check whether signatures and channel of the provided APK is valid. 31 | 32 | packer-ng verify app.apk 33 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | google() 6 | } 7 | 8 | apply plugin: 'java' 9 | 10 | sourceCompatibility = 1.7 11 | targetCompatibility = 1.7 12 | 13 | dependencies { 14 | // JUnit and Mockito 15 | testCompile "junit:junit:4.12" 16 | testCompile "org.mockito:mockito-core:1.10.19" 17 | testCompile "commons-io:commons-io:2.5" 18 | testCompile 'com.android.tools.build:apksig:2.3.3' 19 | testCompile "com.mcxiaoke.next:core:1.5.0" 20 | } 21 | 22 | test { 23 | testLogging.showStandardStreams = true 24 | } 25 | 26 | javadoc { 27 | failOnError false 28 | } 29 | 30 | 31 | 32 | apply from: '../gradle-mvn-push.gradle' 33 | -------------------------------------------------------------------------------- /common/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=common 2 | POM_PACKAGING=jar 3 | POM_NAME=Common Classes for Packer-Ng Plugin 4 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.common; 2 | 3 | import com.mcxiaoke.packer.support.walle.Support; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.UnsupportedEncodingException; 8 | import java.nio.ByteBuffer; 9 | import java.nio.ByteOrder; 10 | import java.util.Arrays; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.Map.Entry; 14 | 15 | /** 16 | * User: mcxiaoke 17 | * Date: 2017/5/26 18 | * Time: 13:18 19 | */ 20 | public class PackerCommon { 21 | public static final String SEP_KV = "∘";//\u2218 22 | public static final String SEP_LINE = "∙";//\u2219 23 | // charset utf8 24 | public static final String UTF8 = "UTF-8"; 25 | // plugin block magic 26 | public static final String BLOCK_MAGIC = "Packer Ng Sig V2"; // magic 27 | 28 | // channel block id 29 | public static final int CHANNEL_BLOCK_ID = 0x7a786b21; // "zxk!" 30 | // channel info key 31 | public static final String CHANNEL_KEY = "CHANNEL"; 32 | 33 | public static String readChannel(File file) throws IOException { 34 | return readValue(file, CHANNEL_KEY, CHANNEL_BLOCK_ID); 35 | } 36 | 37 | public static void writeChannel(File file, String channel) 38 | throws IOException { 39 | writeValue(file, CHANNEL_KEY, channel, CHANNEL_BLOCK_ID); 40 | } 41 | 42 | // package visible for test 43 | static String readValue(File file, 44 | String key, 45 | int blockId) 46 | throws IOException { 47 | final Map<String, String> map = readValues(file, blockId); 48 | if (map == null || map.isEmpty()) { 49 | return null; 50 | } 51 | return map.get(key); 52 | } 53 | 54 | // package visible for test 55 | static void writeValue(File file, 56 | String key, 57 | String value, 58 | int blockId) 59 | throws IOException { 60 | final Map<String, String> values = new HashMap<>(); 61 | values.put(key, value); 62 | writeValues(file, values, blockId); 63 | } 64 | 65 | public static Map<String, String> readValues(File file, int blockId) 66 | throws IOException { 67 | final String content = readString(file, blockId); 68 | return mapFromString(content); 69 | } 70 | 71 | public static String readString(File file, int blockId) 72 | throws IOException { 73 | final byte[] bytes = readBytes(file, blockId); 74 | if (bytes == null || bytes.length == 0) { 75 | return null; 76 | } 77 | return new String(bytes, UTF8); 78 | } 79 | 80 | public static byte[] readBytes(File file, int blockId) 81 | throws IOException { 82 | return readPayloadImpl(file, blockId); 83 | } 84 | 85 | public static void writeValues(File file, 86 | Map<String, String> values, 87 | int blockId) 88 | throws IOException { 89 | if (values == null || values.isEmpty()) { 90 | return; 91 | } 92 | final Map<String, String> newValues = new HashMap<>(); 93 | final Map<String, String> oldValues = readValues(file, blockId); 94 | if (oldValues != null) { 95 | newValues.putAll(oldValues); 96 | } 97 | newValues.putAll(values); 98 | writeString(file, mapToString(newValues), blockId); 99 | } 100 | 101 | public static void writeString(File file, 102 | String content, 103 | int blockId) 104 | throws IOException { 105 | writeBytes(file, content.getBytes(UTF8), blockId); 106 | } 107 | 108 | public static void writeBytes(File file, 109 | byte[] payload, 110 | int blockId) 111 | throws IOException { 112 | writePayloadImpl(file, payload, blockId); 113 | } 114 | 115 | // package visible for test 116 | static void writePayloadImpl(File file, 117 | byte[] payload, 118 | int blockId) 119 | throws IOException { 120 | ByteBuffer buffer = wrapPayload(payload); 121 | Support.writeBlock(file, blockId, buffer); 122 | } 123 | 124 | // package visible for test 125 | static byte[] readPayloadImpl(File file, int blockId) 126 | throws IOException { 127 | ByteBuffer buffer = Support.readBlock(file, blockId); 128 | if (buffer == null) { 129 | return null; 130 | } 131 | byte[] magic = BLOCK_MAGIC.getBytes(UTF8); 132 | byte[] actual = new byte[magic.length]; 133 | buffer.get(actual); 134 | if (Arrays.equals(magic, actual)) { 135 | int payloadLength1 = buffer.getInt(); 136 | if (payloadLength1 > 0) { 137 | byte[] payload = new byte[payloadLength1]; 138 | buffer.get(payload); 139 | int payloadLength2 = buffer.getInt(); 140 | if (payloadLength2 == payloadLength1) { 141 | return payload; 142 | } 143 | } 144 | } 145 | return null; 146 | } 147 | 148 | // package visible for test 149 | static ByteBuffer wrapPayload(byte[] payload) 150 | throws UnsupportedEncodingException { 151 | /* 152 | PLUGIN BLOCK LAYOUT 153 | OFFSET DATA TYPE DESCRIPTION 154 | @+0 magic string magic string 16 bytes 155 | @+16 payload length payload length int 4 bytes 156 | @+20 payload payload data bytes 157 | @-4 payload length same as @+16 4 bytes 158 | */ 159 | byte[] magic = BLOCK_MAGIC.getBytes(UTF8); 160 | int magicLen = magic.length; 161 | int payloadLen = payload.length; 162 | int length = (magicLen + 4) * 2 + payloadLen; 163 | ByteBuffer buffer = ByteBuffer.allocate(length); 164 | buffer.order(ByteOrder.LITTLE_ENDIAN); 165 | buffer.put(magic); //16 166 | buffer.putInt(payloadLen); //4 payload length 167 | buffer.put(payload); // payload 168 | buffer.putInt(payloadLen); // 4 169 | buffer.flip(); 170 | return buffer; 171 | } 172 | 173 | public static String mapToString(Map<String, String> map) 174 | throws IOException { 175 | final StringBuilder builder = new StringBuilder(); 176 | for (Entry<String, String> entry : map.entrySet()) { 177 | builder.append(entry.getKey()).append(SEP_KV) 178 | .append(entry.getValue()).append(SEP_LINE); 179 | } 180 | return builder.toString(); 181 | } 182 | 183 | public static Map<String, String> mapFromString(final String string) { 184 | if (string == null || string.length() == 0) { 185 | return null; 186 | } 187 | final Map<String, String> map = new HashMap<>(); 188 | final String[] entries = string.split(SEP_LINE); 189 | for (String entry : entries) { 190 | final String[] kv = entry.split(SEP_KV); 191 | if (kv.length == 2) { 192 | map.put(kv[0], kv[1]); 193 | } 194 | } 195 | return map; 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningBlock.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.support.walle; 2 | 3 | import java.io.DataOutput; 4 | import java.io.IOException; 5 | import java.nio.ByteBuffer; 6 | import java.nio.ByteOrder; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * https://source.android.com/security/apksigning/v2.html 12 | * https://en.wikipedia.org/wiki/Zip_(file_format) 13 | */ 14 | class ApkSigningBlock { 15 | // The format of the APK Signing Block is as follows (all numeric fields are little-endian): 16 | 17 | // .size of block in bytes (excluding this field) (uint64) 18 | // .Sequence of uint64-length-prefixed ID-value pairs: 19 | // *ID (uint32) 20 | // *value (variable-length: length of the pair - 4 bytes) 21 | // .size of block in bytes—same as the very first field (uint64) 22 | // .magic “APK Sig Block 42” (16 bytes) 23 | 24 | // FORMAT: 25 | // OFFSET DATA TYPE DESCRIPTION 26 | // * @+0 bytes uint64: size in bytes (excluding this field) 27 | // * @+8 bytes payload 28 | // * @-24 bytes uint64: size in bytes (same as the one above) 29 | // * @-16 bytes uint128: magic 30 | 31 | // payload 有 8字节的大小,4字节的ID,还有payload的内容组成 32 | 33 | private final List<ApkSigningPayload> payloads; 34 | 35 | ApkSigningBlock() { 36 | super(); 37 | payloads = new ArrayList<ApkSigningPayload>(); 38 | } 39 | 40 | public final List<ApkSigningPayload> getPayloads() { 41 | return payloads; 42 | } 43 | 44 | public void addPayload(final ApkSigningPayload payload) { 45 | payloads.add(payload); 46 | } 47 | 48 | /** 49 | * @param dataOutput DataOutput 50 | * @return ApkSigningBlock length 51 | * @throws IOException IOException 52 | */ 53 | public long writeTo(final DataOutput dataOutput) throws IOException { 54 | long length = 24; // 24 = 8(size of block in bytes 55 | // same as the very first field (uint64)) + 16 (magic “APK Sig Block 42” (16 bytes)) 56 | for (int index = 0; index < payloads.size(); ++index) { 57 | final ApkSigningPayload payload = payloads.get(index); 58 | final byte[] bytes = payload.getByteBuffer(); 59 | length += 12 + bytes.length; // 12 = 8(uint64-length-prefixed) + 4 (ID (uint32)) 60 | } 61 | 62 | ByteBuffer byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 63 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 64 | byteBuffer.putLong(length); 65 | byteBuffer.flip(); 66 | dataOutput.write(byteBuffer.array()); 67 | 68 | for (int index = 0; index < payloads.size(); ++index) { 69 | final ApkSigningPayload payload = payloads.get(index); 70 | final byte[] bytes = payload.getByteBuffer(); 71 | 72 | byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 73 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 74 | byteBuffer.putLong(bytes.length + (8 - 4)); // Long.BYTES - Integer.BYTES 75 | byteBuffer.flip(); 76 | dataOutput.write(byteBuffer.array()); 77 | 78 | byteBuffer = ByteBuffer.allocate(4); // Integer.BYTES 79 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 80 | byteBuffer.putInt(payload.getId()); 81 | byteBuffer.flip(); 82 | dataOutput.write(byteBuffer.array()); 83 | 84 | dataOutput.write(bytes); 85 | } 86 | 87 | byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 88 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 89 | byteBuffer.putLong(length); 90 | byteBuffer.flip(); 91 | dataOutput.write(byteBuffer.array()); 92 | 93 | byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 94 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 95 | byteBuffer.putLong(V2Const.APK_SIG_BLOCK_MAGIC_LO); 96 | byteBuffer.flip(); 97 | dataOutput.write(byteBuffer.array()); 98 | 99 | byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 100 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 101 | byteBuffer.putLong(V2Const.APK_SIG_BLOCK_MAGIC_HI); 102 | byteBuffer.flip(); 103 | dataOutput.write(byteBuffer.array()); 104 | 105 | return length; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningPayload.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.support.walle; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.ByteOrder; 5 | import java.util.Arrays; 6 | 7 | class ApkSigningPayload { 8 | private final int id; 9 | private final ByteBuffer buffer; 10 | 11 | ApkSigningPayload(final int id, final ByteBuffer buffer) { 12 | super(); 13 | this.id = id; 14 | if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 15 | throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 16 | } 17 | this.buffer = buffer; 18 | } 19 | 20 | public int getId() { 21 | return id; 22 | } 23 | 24 | public byte[] getByteBuffer() { 25 | final byte[] array = buffer.array(); 26 | final int arrayOffset = buffer.arrayOffset(); 27 | return Arrays.copyOfRange(array, arrayOffset + buffer.position(), 28 | arrayOffset + buffer.limit()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/ApkUtil.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.support.walle; 2 | 3 | import java.io.IOException; 4 | import java.nio.BufferUnderflowException; 5 | import java.nio.ByteBuffer; 6 | import java.nio.ByteOrder; 7 | import java.nio.channels.FileChannel; 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | 11 | final class ApkUtil { 12 | private ApkUtil() { 13 | super(); 14 | } 15 | 16 | public static long findZipCommentLength(final FileChannel fileChannel) throws IOException { 17 | // End of central directory record (EOCD) 18 | // Offset Bytes Description[23] 19 | // 0 4 End of central directory signature = 0x06054b50 20 | // 4 2 Number of this disk 21 | // 6 2 Disk where central directory starts 22 | // 8 2 Number of central directory records on this disk 23 | // 10 2 Total number of central directory records 24 | // 12 4 Size of central directory (bytes) 25 | // 16 4 Offset of start of central directory, relative to start of archive 26 | // 20 2 Comment length (n) 27 | // 22 n Comment 28 | // For a zip with no archive comment, the 29 | // end-of-central-directory record will be 22 bytes long, so 30 | // we expect to find the EOCD marker 22 bytes from the end. 31 | 32 | 33 | final long archiveSize = fileChannel.size(); 34 | if (archiveSize < V2Const.ZIP_EOCD_REC_MIN_SIZE) { 35 | throw new IOException("APK too small for ZIP End of Central Directory (EOCD) record"); 36 | } 37 | // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 38 | // The record can be identified by its 4-byte signature/magic which is located at the very 39 | // beginning of the record. A complication is that the record is variable-length because of 40 | // the comment field. 41 | // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 42 | // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 43 | // the candidate record's comment length is such that the remainder of the record takes up 44 | // exactly the remaining bytes in the buffer. The search is bounded because the maximum 45 | // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 46 | final long maxCommentLength = Math.min(archiveSize - V2Const.ZIP_EOCD_REC_MIN_SIZE, V2Const.UINT16_MAX_VALUE); 47 | final long eocdWithEmptyCommentStartPosition = archiveSize - V2Const.ZIP_EOCD_REC_MIN_SIZE; 48 | for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; 49 | expectedCommentLength++) { 50 | final long eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 51 | 52 | final ByteBuffer byteBuffer = ByteBuffer.allocate(4); 53 | fileChannel.position(eocdStartPos); 54 | fileChannel.read(byteBuffer); 55 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 56 | 57 | if (byteBuffer.getInt(0) == V2Const.ZIP_EOCD_REC_SIG) { 58 | final ByteBuffer commentLengthByteBuffer = ByteBuffer.allocate(2); 59 | fileChannel.position(eocdStartPos + V2Const.ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 60 | fileChannel.read(commentLengthByteBuffer); 61 | commentLengthByteBuffer.order(ByteOrder.LITTLE_ENDIAN); 62 | 63 | final int actualCommentLength = commentLengthByteBuffer.getShort(0); 64 | if (actualCommentLength == expectedCommentLength) { 65 | return actualCommentLength; 66 | } 67 | } 68 | } 69 | throw new IOException("ZIP End of Central Directory (EOCD) record not found"); 70 | } 71 | 72 | public static long findCentralDirStartOffset(final FileChannel fileChannel) throws IOException { 73 | return findCentralDirStartOffset(fileChannel, findZipCommentLength(fileChannel)); 74 | } 75 | 76 | public static long findCentralDirStartOffset(final FileChannel fileChannel, final long commentLength) throws IOException { 77 | // End of central directory record (EOCD) 78 | // Offset Bytes Description[23] 79 | // 0 4 End of central directory signature = 0x06054b50 80 | // 4 2 Number of this disk 81 | // 6 2 Disk where central directory starts 82 | // 8 2 Number of central directory records on this disk 83 | // 10 2 Total number of central directory records 84 | // 12 4 Size of central directory (bytes) 85 | // 16 4 Offset of start of central directory, relative to start of archive 86 | // 20 2 Comment length (n) 87 | // 22 n Comment 88 | // For a zip with no archive comment, the 89 | // end-of-central-directory record will be 22 bytes long, so 90 | // we expect to find the EOCD marker 22 bytes from the end. 91 | 92 | final ByteBuffer zipCentralDirectoryStart = ByteBuffer.allocate(4); 93 | zipCentralDirectoryStart.order(ByteOrder.LITTLE_ENDIAN); 94 | fileChannel.position(fileChannel.size() - commentLength - 6); // 6 = 2 (Comment length) + 4 (Offset of start of central directory, relative to start of archive) 95 | fileChannel.read(zipCentralDirectoryStart); 96 | final long centralDirStartOffset = zipCentralDirectoryStart.getInt(0); 97 | return centralDirStartOffset; 98 | } 99 | 100 | public static Pair<ByteBuffer, Long> findApkSigningBlock( 101 | final FileChannel fileChannel) throws IOException { 102 | final long centralDirOffset = findCentralDirStartOffset(fileChannel); 103 | return findApkSigningBlock(fileChannel, centralDirOffset); 104 | } 105 | 106 | public static Pair<ByteBuffer, Long> findApkSigningBlock( 107 | final FileChannel fileChannel, final long centralDirOffset) throws IOException { 108 | 109 | // Find the APK Signing Block. The block immediately precedes the Central Directory. 110 | 111 | // FORMAT: 112 | // OFFSET DATA TYPE DESCRIPTION 113 | // * @+0 bytes uint64: size in bytes (excluding this field) 114 | // * @+8 bytes payload 115 | // * @-24 bytes uint64: size in bytes (same as the one above) 116 | // * @-16 bytes uint128: magic 117 | 118 | if (centralDirOffset < V2Const.APK_SIG_BLOCK_MIN_SIZE) { 119 | throw new IOException( 120 | "APK too small for APK Signing Block. ZIP Central Directory offset: " 121 | + centralDirOffset); 122 | } 123 | // Read the magic and offset in file from the footer section of the block: 124 | // * uint64: size of block 125 | // * 16 bytes: magic 126 | fileChannel.position(centralDirOffset - 24); 127 | final ByteBuffer footer = ByteBuffer.allocate(24); 128 | fileChannel.read(footer); 129 | footer.order(ByteOrder.LITTLE_ENDIAN); 130 | if ((footer.getLong(8) != V2Const.APK_SIG_BLOCK_MAGIC_LO) 131 | || (footer.getLong(16) != V2Const.APK_SIG_BLOCK_MAGIC_HI)) { 132 | throw new IOException( 133 | "No APK Signing Block before ZIP Central Directory"); 134 | } 135 | // Read and compare size fields 136 | final long apkSigBlockSizeInFooter = footer.getLong(0); 137 | if ((apkSigBlockSizeInFooter < footer.capacity()) 138 | || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { 139 | throw new IOException( 140 | "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); 141 | } 142 | final int totalSize = (int) (apkSigBlockSizeInFooter + 8); 143 | final long apkSigBlockOffset = centralDirOffset - totalSize; 144 | if (apkSigBlockOffset < 0) { 145 | throw new IOException( 146 | "APK Signing Block offset out of range: " + apkSigBlockOffset); 147 | } 148 | fileChannel.position(apkSigBlockOffset); 149 | final ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); 150 | fileChannel.read(apkSigBlock); 151 | apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); 152 | final long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); 153 | if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { 154 | throw new IOException( 155 | "APK Signing Block sizes in header and footer do not match: " 156 | + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); 157 | } 158 | return Pair.of(apkSigBlock, apkSigBlockOffset); 159 | } 160 | 161 | public static Map<Integer, ByteBuffer> findIdValues(final ByteBuffer apkSigningBlock) throws IOException { 162 | checkByteOrderLittleEndian(apkSigningBlock); 163 | // FORMAT: 164 | // OFFSET DATA TYPE DESCRIPTION 165 | // * @+0 bytes uint64: size in bytes (excluding this field) 166 | // * @+8 bytes pairs 167 | // * @-24 bytes uint64: size in bytes (same as the one above) 168 | // * @-16 bytes uint128: magic 169 | final ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); 170 | 171 | final Map<Integer, ByteBuffer> idValues = new LinkedHashMap<Integer, ByteBuffer>(); // keep order 172 | 173 | int entryCount = 0; 174 | while (pairs.hasRemaining()) { 175 | entryCount++; 176 | if (pairs.remaining() < 8) { 177 | throw new IOException( 178 | "Insufficient data to read size of APK Signing Block entry #" + entryCount); 179 | } 180 | final long lenLong = pairs.getLong(); 181 | if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { 182 | throw new IOException( 183 | "APK Signing Block entry #" + entryCount 184 | + " size out of range: " + lenLong); 185 | } 186 | final int len = (int) lenLong; 187 | final int nextEntryPos = pairs.position() + len; 188 | if (len > pairs.remaining()) { 189 | throw new IOException( 190 | "APK Signing Block entry #" + entryCount + " size out of range: " + len 191 | + ", available: " + pairs.remaining()); 192 | } 193 | final int id = pairs.getInt(); 194 | idValues.put(id, getByteBuffer(pairs, len - 4)); 195 | 196 | pairs.position(nextEntryPos); 197 | } 198 | 199 | return idValues; 200 | } 201 | 202 | /** 203 | * Returns new byte buffer whose content is a shared subsequence of this buffer's content 204 | * between the specified start (inclusive) and end (exclusive) positions. As opposed to 205 | * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source 206 | * buffer's byte order. 207 | */ 208 | private static ByteBuffer sliceFromTo(final ByteBuffer source, final int start, final int end) { 209 | if (start < 0) { 210 | throw new IllegalArgumentException("start: " + start); 211 | } 212 | if (end < start) { 213 | throw new IllegalArgumentException("end < start: " + end + " < " + start); 214 | } 215 | final int capacity = source.capacity(); 216 | if (end > source.capacity()) { 217 | throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); 218 | } 219 | final int originalLimit = source.limit(); 220 | final int originalPosition = source.position(); 221 | try { 222 | source.position(0); 223 | source.limit(end); 224 | source.position(start); 225 | final ByteBuffer result = source.slice(); 226 | result.order(source.order()); 227 | return result; 228 | } finally { 229 | source.position(0); 230 | source.limit(originalLimit); 231 | source.position(originalPosition); 232 | } 233 | } 234 | 235 | /** 236 | * Relative <em>readBlock</em> method for reading {@code size} number of bytes from the current 237 | * position of this buffer. 238 | * <p> 239 | * <p>This method reads the next {@code size} bytes at this buffer's current position, 240 | * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to 241 | * {@code size}, byte order set to this buffer's byte order; and then increments the position by 242 | * {@code size}. 243 | */ 244 | private static ByteBuffer getByteBuffer(final ByteBuffer source, final int size) 245 | throws BufferUnderflowException { 246 | if (size < 0) { 247 | throw new IllegalArgumentException("size: " + size); 248 | } 249 | final int originalLimit = source.limit(); 250 | final int position = source.position(); 251 | final int limit = position + size; 252 | if ((limit < position) || (limit > originalLimit)) { 253 | throw new BufferUnderflowException(); 254 | } 255 | source.limit(limit); 256 | try { 257 | final ByteBuffer result = source.slice(); 258 | result.order(source.order()); 259 | source.position(limit); 260 | return result; 261 | } finally { 262 | source.limit(originalLimit); 263 | } 264 | } 265 | 266 | private static void checkByteOrderLittleEndian(final ByteBuffer buffer) { 267 | if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 268 | throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 269 | } 270 | } 271 | 272 | 273 | } 274 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/Pair.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.mcxiaoke.packer.support.walle; 18 | 19 | /** 20 | * Pair of two elements. 21 | */ 22 | final class Pair<A, B> { 23 | private final A f; 24 | private final B s; 25 | 26 | private Pair(final A first, final B second) { 27 | f = first; 28 | s = second; 29 | } 30 | 31 | public static <A, B> Pair<A, B> of(final A first, final B second) { 32 | return new Pair<A, B>(first, second); 33 | } 34 | 35 | public A getFirst() { 36 | return f; 37 | } 38 | 39 | public B getSecond() { 40 | return s; 41 | } 42 | 43 | @Override 44 | public boolean equals(final Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | 48 | final Pair<?, ?> pair = (Pair<?, ?>) o; 49 | 50 | if (f != null ? !f.equals(pair.f) : pair.f != null) return false; 51 | return s != null ? s.equals(pair.s) : pair.s == null; 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | int result = f != null ? f.hashCode() : 0; 57 | result = 31 * result + (s != null ? s.hashCode() : 0); 58 | return result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.support.walle; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.ByteBuffer; 7 | import java.nio.channels.FileChannel; 8 | import java.util.Map; 9 | 10 | final class PayloadReader { 11 | private PayloadReader() { 12 | super(); 13 | } 14 | 15 | public static byte[] readBytes(final File apkFile, final int id) 16 | throws IOException { 17 | final ByteBuffer buf = readBlock(apkFile, id); 18 | return buf == null ? null : V2Utils.getBytes(buf); 19 | } 20 | 21 | public static ByteBuffer readBlock(final File apkFile, final int id) 22 | throws IOException { 23 | final Map<Integer, ByteBuffer> blocks = readAllBlocks(apkFile); 24 | if (blocks == null) { 25 | return null; 26 | } 27 | return blocks.get(id); 28 | } 29 | 30 | private static Map<Integer, ByteBuffer> readAllBlocks(final File apkFile) 31 | throws IOException { 32 | Map<Integer, ByteBuffer> blocks = null; 33 | 34 | RandomAccessFile raf = null; 35 | FileChannel fc = null; 36 | try { 37 | raf = new RandomAccessFile(apkFile, "r"); 38 | fc = raf.getChannel(); 39 | final ByteBuffer apkSigningBlock = ApkUtil.findApkSigningBlock(fc).getFirst(); 40 | blocks = ApkUtil.findIdValues(apkSigningBlock); 41 | } finally { 42 | V2Utils.close(fc); 43 | V2Utils.close(raf); 44 | } 45 | return blocks; 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.support.walle; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.ByteBuffer; 7 | import java.nio.ByteOrder; 8 | import java.nio.channels.FileChannel; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | 14 | final class PayloadWriter { 15 | private PayloadWriter() { 16 | super(); 17 | } 18 | 19 | public static void writeBlock(File apkFile, final int id, 20 | final byte[] bytes) throws IOException { 21 | final ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); 22 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 23 | byteBuffer.put(bytes, 0, bytes.length); 24 | byteBuffer.flip(); 25 | writeBlock(apkFile, id, byteBuffer); 26 | } 27 | 28 | public static void writeBlock(final File apkFile, final int id, 29 | final ByteBuffer buffer) throws IOException { 30 | final Map<Integer, ByteBuffer> idValues = new HashMap<>(); 31 | idValues.put(id, buffer); 32 | writeValues(apkFile, idValues); 33 | } 34 | 35 | /** 36 | * writeBlock new idValues into apk, update if id exists 37 | * NOTE: use unknown IDs. DO NOT use ID that have already been used. See <a href='https://source.android.com/security/apksigning/v2.html'>APK Signature Scheme v2</a> 38 | */ 39 | private static void writeValues(final File apkFile, final Map<Integer, ByteBuffer> idValues) throws IOException { 40 | final ApkSigningBlockHandler handler = new ApkSigningBlockHandler() { 41 | @Override 42 | public ApkSigningBlock handle(final Map<Integer, ByteBuffer> originIdValues) { 43 | if (idValues != null && !idValues.isEmpty()) { 44 | originIdValues.putAll(idValues); 45 | } 46 | final ApkSigningBlock apkSigningBlock = new ApkSigningBlock(); 47 | final Set<Map.Entry<Integer, ByteBuffer>> entrySet = originIdValues.entrySet(); 48 | for (Map.Entry<Integer, ByteBuffer> entry : entrySet) { 49 | final ApkSigningPayload payload = new ApkSigningPayload(entry.getKey(), entry.getValue()); 50 | apkSigningBlock.addPayload(payload); 51 | } 52 | return apkSigningBlock; 53 | } 54 | }; 55 | writeApkSigningBlock(apkFile, handler); 56 | } 57 | 58 | static void writeApkSigningBlock(final File apkFile, final ApkSigningBlockHandler handler) throws IOException { 59 | RandomAccessFile raf = null; 60 | FileChannel fc = null; 61 | try { 62 | raf = new RandomAccessFile(apkFile, "rw"); 63 | fc = raf.getChannel(); 64 | final long commentLength = ApkUtil.findZipCommentLength(fc); 65 | final long centralDirStartOffset = ApkUtil.findCentralDirStartOffset(fc, commentLength); 66 | // Find the APK Signing Block. The block immediately precedes the Central Directory. 67 | final Pair<ByteBuffer, Long> apkSigningBlockAndOffset = ApkUtil.findApkSigningBlock(fc, centralDirStartOffset); 68 | final ByteBuffer apkSigningBlock2 = apkSigningBlockAndOffset.getFirst(); 69 | final long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); 70 | 71 | if (centralDirStartOffset == 0 || apkSigningBlockOffset == 0) { 72 | throw new IOException( 73 | "No APK Signature Scheme v2 block in APK Signing Block"); 74 | } 75 | final Map<Integer, ByteBuffer> originIdValues = ApkUtil.findIdValues(apkSigningBlock2); 76 | // Find the APK Signature Scheme v2 Block inside the APK Signing Block. 77 | final ByteBuffer apkSignatureSchemeV2Block = originIdValues.get(V2Const.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); 78 | 79 | if (apkSignatureSchemeV2Block == null) { 80 | throw new IOException( 81 | "No APK Signature Scheme v2 block in APK Signing Block"); 82 | } 83 | final ApkSigningBlock apkSigningBlock = handler.handle(originIdValues); 84 | // read CentralDir 85 | raf.seek(centralDirStartOffset); 86 | final byte[] centralDirBytes = new byte[(int) (fc.size() - centralDirStartOffset)]; 87 | raf.read(centralDirBytes); 88 | 89 | fc.position(apkSigningBlockOffset); 90 | 91 | final long length = apkSigningBlock.writeTo(raf); 92 | 93 | // store CentralDir 94 | raf.write(centralDirBytes); 95 | // update length 96 | raf.setLength(raf.getFilePointer()); 97 | 98 | // update CentralDir Offset 99 | // End of central directory record (EOCD) 100 | // Offset Bytes Description[23] 101 | // 0 4 End of central directory signature = 0x06054b50 102 | // 4 2 Number of this disk 103 | // 6 2 Disk where central directory starts 104 | // 8 2 Number of central directory records on this disk 105 | // 10 2 Total number of central directory records 106 | // 12 4 Size of central directory (bytes) 107 | // 16 4 Offset of start of central directory, relative to start of archive 108 | // 20 2 Comment length (n) 109 | // 22 n Comment 110 | 111 | raf.seek(fc.size() - commentLength - 6); 112 | // 6 = 2(Comment length) + 4 113 | // (Offset of start of central directory, relative to start of archive) 114 | final ByteBuffer temp = ByteBuffer.allocate(4); 115 | temp.order(ByteOrder.LITTLE_ENDIAN); 116 | temp.putInt((int) (centralDirStartOffset + length + 8 - (centralDirStartOffset - apkSigningBlockOffset))); 117 | // 8 = size of block in bytes (excluding this field) (uint64) 118 | temp.flip(); 119 | raf.write(temp.array()); 120 | 121 | } finally { 122 | V2Utils.close(fc); 123 | V2Utils.close(raf); 124 | } 125 | } 126 | 127 | interface ApkSigningBlockHandler { 128 | ApkSigningBlock handle(Map<Integer, ByteBuffer> originIdValues); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/Support.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.support.walle; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.ByteBuffer; 6 | 7 | /** 8 | * bridge class between support and common 9 | * User: mcxiaoke 10 | * Date: 2017/6/13 11 | * Time: 14:06 12 | */ 13 | 14 | public class Support { 15 | 16 | public static ByteBuffer readBlock(final File apkFile, final int id) 17 | throws IOException { 18 | return PayloadReader.readBlock(apkFile, id); 19 | } 20 | 21 | public static byte[] readBytes(final File apkFile, final int id) 22 | throws IOException { 23 | return PayloadReader.readBytes(apkFile, id); 24 | } 25 | 26 | public static void writeBlock(final File apkFile, final int id, 27 | final ByteBuffer buffer) throws IOException { 28 | PayloadWriter.writeBlock(apkFile, id, buffer); 29 | } 30 | 31 | public static void writeBlock(final File apkFile, final int id, 32 | final byte[] bytes) throws IOException { 33 | PayloadWriter.writeBlock(apkFile, id, bytes); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/V2Const.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.support.walle; 2 | 3 | /** 4 | * User: mcxiaoke 5 | * Date: 2017/5/17 6 | * Time: 15:08 7 | */ 8 | class V2Const { 9 | // V2 Scheme Constants 10 | public static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; 11 | public static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; 12 | public static final int APK_SIG_BLOCK_MIN_SIZE = 32; 13 | /** 14 | * The v2 signature of the APK is stored as an ID-value pair with ID 0x7109871a 15 | * (https://source.android.com/security/apksigning/v2.html#apk-signing-block) 16 | **/ 17 | public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; 18 | public static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; 19 | /** 20 | * APK Signing Block Magic Code: magic “APK Sig Block 42” (16 bytes) 21 | * "APK Sig Block 42" : 41 50 4B 20 53 69 67 20 42 6C 6F 63 6B 20 34 32 22 | */ 23 | public static final byte[] APK_SIGNING_BLOCK_MAGIC = 24 | new byte[]{ 25 | 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, 26 | 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, 27 | }; 28 | 29 | // ZIP Constants 30 | public static final int ZIP_EOCD_REC_MIN_SIZE = 22; 31 | public static final int ZIP_EOCD_REC_SIG = 0x06054b50; 32 | public static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; 33 | public static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; 34 | public static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; 35 | public static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; 36 | public static final int ZIP64_EOCD_LOCATOR_SIZE = 20; 37 | public static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50; 38 | public static final int UINT16_MAX_VALUE = 0xffff; 39 | } 40 | -------------------------------------------------------------------------------- /common/src/main/java/com/mcxiaoke/packer/support/walle/V2Utils.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.support.walle; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | 8 | /** 9 | * User: mcxiaoke 10 | * Date: 2017/5/26 11 | * Time: 12:10 12 | */ 13 | final class V2Utils { 14 | 15 | static byte[] getBytes(final ByteBuffer buf) { 16 | final byte[] array = buf.array(); 17 | final int arrayOffset = buf.arrayOffset(); 18 | return Arrays.copyOfRange(array, arrayOffset + buf.position(), 19 | arrayOffset + buf.limit()); 20 | } 21 | 22 | static void close(final Closeable c) { 23 | if (c == null) return; 24 | try { 25 | c.close(); 26 | } catch (IOException ignored) { 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.common; 2 | 3 | import com.android.apksig.ApkVerifier; 4 | import com.android.apksig.ApkVerifier.Builder; 5 | import com.android.apksig.ApkVerifier.IssueWithParams; 6 | import com.android.apksig.ApkVerifier.Result; 7 | import com.android.apksig.apk.ApkFormatException; 8 | import com.mcxiaoke.packer.support.walle.Support; 9 | import junit.framework.TestCase; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.ByteBuffer; 14 | import java.nio.ByteOrder; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | /** 21 | * User: mcxiaoke 22 | * Date: 2017/5/17 23 | * Time: 16:25 24 | */ 25 | public class PayloadTests extends TestCase { 26 | 27 | @Override 28 | protected void setUp() throws Exception { 29 | super.setUp(); 30 | } 31 | 32 | @Override 33 | protected void tearDown() throws Exception { 34 | super.tearDown(); 35 | } 36 | 37 | synchronized File newTestFile() throws IOException { 38 | return TestUtils.newTestFile(); 39 | } 40 | 41 | void checkApkVerified(File f) { 42 | try { 43 | assertTrue(TestUtils.apkVerified(f)); 44 | } catch (ApkFormatException e) { 45 | e.printStackTrace(); 46 | } catch (IOException e) { 47 | e.printStackTrace(); 48 | } catch (NoSuchAlgorithmException e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | 53 | public void testFileExists() { 54 | File file = new File("../tools/test.apk"); 55 | assertTrue(file.exists()); 56 | } 57 | 58 | public void testFileCopy() throws IOException { 59 | File f1 = new File("../tools/test.apk"); 60 | File f2 = newTestFile(); 61 | assertTrue(f2.exists()); 62 | assertTrue(f2.getName().endsWith(".apk")); 63 | assertEquals(f1.length(), f2.length()); 64 | assertEquals(f1.getParent(), f2.getParent()); 65 | } 66 | 67 | public void testFileSignature() throws IOException, 68 | ApkFormatException, 69 | NoSuchAlgorithmException { 70 | File f = newTestFile(); 71 | checkApkVerified(f); 72 | } 73 | 74 | public void testOverrideSignature() throws IOException, 75 | ApkFormatException, 76 | NoSuchAlgorithmException { 77 | File f = newTestFile(); 78 | // don't write with APK Signature Scheme v2 Block ID 0x7109871a 79 | PackerCommon.writeString(f, "OverrideSignatureSchemeBlock", 0x7109871a); 80 | assertEquals("OverrideSignatureSchemeBlock", PackerCommon.readString(f, 0x7109871a)); 81 | ApkVerifier verifier = new Builder(f).build(); 82 | Result result = verifier.verify(); 83 | final List<IssueWithParams> errors = result.getErrors(); 84 | if (errors != null && errors.size() > 0) { 85 | for (IssueWithParams error : errors) { 86 | System.out.println("testOverrideSignature " + error); 87 | } 88 | } 89 | assertTrue(result.containsErrors()); 90 | assertFalse(result.isVerified()); 91 | assertFalse(result.isVerifiedUsingV1Scheme()); 92 | assertFalse(result.isVerifiedUsingV2Scheme()); 93 | } 94 | 95 | public void testBytesWrite1() throws IOException { 96 | File f = newTestFile(); 97 | byte[] in = "Hello".getBytes(); 98 | Support.writeBlock(f, 0x12345, in); 99 | byte[] out = Support.readBytes(f, 0x12345); 100 | assertTrue(TestUtils.sameBytes(in, out)); 101 | checkApkVerified(f); 102 | } 103 | 104 | public void testBytesWrite2() throws IOException { 105 | File f = newTestFile(); 106 | byte[] in = "中文和特殊符号测试!@#¥%……*()《》?:【】、".getBytes("UTF-8"); 107 | Support.writeBlock(f, 0x12345, in); 108 | byte[] out = Support.readBytes(f, 0x12345); 109 | assertTrue(TestUtils.sameBytes(in, out)); 110 | checkApkVerified(f); 111 | } 112 | 113 | public void testStringWrite() throws IOException { 114 | File f = newTestFile(); 115 | PackerCommon.writeString(f, "Test String", 0x717a786b); 116 | assertEquals("Test String", PackerCommon.readString(f, 0x717a786b)); 117 | PackerCommon.writeString(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); 118 | assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", PackerCommon.readString(f, 0x717a786b)); 119 | checkApkVerified(f); 120 | } 121 | 122 | public void testValuesWrite() throws IOException { 123 | File f = newTestFile(); 124 | Map<String, String> in = new HashMap<>(); 125 | in.put("Channel", "HelloWorld"); 126 | in.put("名字", "哈哈啊哈哈哈"); 127 | in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); 128 | in.put("12345abcd", "2017"); 129 | PackerCommon.writeValues(f, in, 0x12345); 130 | Map<String, String> out = PackerCommon.readValues(f, 0x12345); 131 | assertNotNull(out); 132 | assertEquals(in.size(), out.size()); 133 | for (Map.Entry<String, String> entry : in.entrySet()) { 134 | assertEquals(entry.getValue(), out.get(entry.getKey())); 135 | } 136 | checkApkVerified(f); 137 | } 138 | 139 | public void testValuesMixedWrite() throws IOException { 140 | File f = newTestFile(); 141 | Map<String, String> in = new HashMap<>(); 142 | in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); 143 | in.put("12345abcd", "2017"); 144 | PackerCommon.writeValues(f, in, 0x123456); 145 | PackerCommon.writeValue(f, "hello", "Mixed", 0x8888); 146 | Map<String, String> out = PackerCommon.readValues(f, 0x123456); 147 | assertNotNull(out); 148 | assertEquals(in.size(), out.size()); 149 | for (Map.Entry<String, String> entry : in.entrySet()) { 150 | assertEquals(entry.getValue(), out.get(entry.getKey())); 151 | } 152 | assertEquals("Mixed", PackerCommon.readValue(f, "hello", 0x8888)); 153 | PackerCommon.writeString(f, "RawValue", 0x2017); 154 | assertEquals("RawValue", PackerCommon.readString(f, 0x2017)); 155 | PackerCommon.writeString(f, "OverrideValues", 0x123456); 156 | assertEquals("OverrideValues", PackerCommon.readString(f, 0x123456)); 157 | checkApkVerified(f); 158 | } 159 | 160 | public void testByteBuffer() throws IOException { 161 | byte[] string = "Hello".getBytes(); 162 | ByteBuffer buf = ByteBuffer.allocate(1024); 163 | buf.order(ByteOrder.LITTLE_ENDIAN); 164 | buf.putInt(123); 165 | buf.putChar('z'); 166 | buf.putShort((short) 2017); 167 | buf.putFloat(3.1415f); 168 | buf.put(string); 169 | buf.putLong(9876543210L); 170 | buf.putDouble(3.14159265); 171 | buf.put((byte) 5); 172 | buf.flip(); // important 173 | // TestUtils.showBuffer(buf); 174 | assertEquals(123, buf.getInt()); 175 | assertEquals('z', buf.getChar()); 176 | assertEquals(2017, buf.getShort()); 177 | assertEquals(3.1415f, buf.getFloat()); 178 | byte[] so = new byte[string.length]; 179 | buf.get(so); 180 | assertTrue(TestUtils.sameBytes(string, so)); 181 | assertEquals(9876543210L, buf.getLong()); 182 | assertEquals(3.14159265, buf.getDouble()); 183 | assertEquals((byte) 5, buf.get()); 184 | } 185 | 186 | public void testBufferWrite() throws IOException { 187 | File f = newTestFile(); 188 | byte[] string = "Hello".getBytes(); 189 | ByteBuffer in = ByteBuffer.allocate(1024); 190 | in.order(ByteOrder.LITTLE_ENDIAN); 191 | in.putInt(123); 192 | in.putChar('z'); 193 | in.putShort((short) 2017); 194 | in.putFloat(3.1415f); 195 | in.putLong(9876543210L); 196 | in.putDouble(3.14159265); 197 | in.put((byte) 5); 198 | in.put(string); 199 | in.flip(); // important 200 | // TestUtils.showBuffer(in); 201 | Support.writeBlock(f, 0x123456, in); 202 | ByteBuffer out = Support.readBlock(f, 0x123456); 203 | assertNotNull(out); 204 | // TestUtils.showBuffer(out); 205 | assertEquals(123, out.getInt()); 206 | assertEquals('z', out.getChar()); 207 | assertEquals(2017, out.getShort()); 208 | assertEquals(3.1415f, out.getFloat()); 209 | assertEquals(9876543210L, out.getLong()); 210 | assertEquals(3.14159265, out.getDouble()); 211 | assertEquals((byte) 5, out.get()); 212 | byte[] so = new byte[string.length]; 213 | out.get(so); 214 | assertTrue(TestUtils.sameBytes(string, so)); 215 | checkApkVerified(f); 216 | } 217 | 218 | public void testChannelWriteRead() throws IOException { 219 | File f = newTestFile(); 220 | PackerCommon.writeChannel(f, "Hello"); 221 | assertEquals("Hello", PackerCommon.readChannel(f)); 222 | PackerCommon.writeChannel(f, "中文"); 223 | assertEquals("中文", PackerCommon.readChannel(f)); 224 | PackerCommon.writeChannel(f, "中文 C"); 225 | assertEquals("中文 C", PackerCommon.readChannel(f)); 226 | checkApkVerified(f); 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.common; 2 | 3 | import com.android.apksig.ApkVerifier; 4 | import com.android.apksig.ApkVerifier.Builder; 5 | import com.android.apksig.ApkVerifier.Result; 6 | import com.android.apksig.apk.ApkFormatException; 7 | import org.apache.commons.io.FileUtils; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.ByteBuffer; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.util.Arrays; 14 | 15 | /** 16 | * User: mcxiaoke 17 | * Date: 2017/5/18 18 | * Time: 16:59 19 | */ 20 | public class TestUtils { 21 | private final static char[] CHARS = "0123456789ABCDEF".toCharArray(); 22 | 23 | public static boolean sameBytes(byte[] a, byte[] b) { 24 | if (a == null || b == null) { 25 | return false; 26 | } 27 | if (a.length != b.length) { 28 | return false; 29 | } 30 | for (int i = 0; i < a.length; i++) { 31 | if (a[i] != b[i]) { 32 | return false; 33 | } 34 | } 35 | return true; 36 | } 37 | 38 | public static String toHex(ByteBuffer buffer) { 39 | final byte[] array = buffer.array(); 40 | final int arrayOffset = buffer.arrayOffset(); 41 | byte[] data = Arrays.copyOfRange(array, arrayOffset + buffer.position(), 42 | arrayOffset + buffer.limit()); 43 | return toHex(data); 44 | } 45 | 46 | public static String toHex(byte[] bytes) { 47 | char[] hexChars = new char[bytes.length * 2]; 48 | for (int j = 0; j < bytes.length; j++) { 49 | int v = bytes[j] & 0xFF; 50 | hexChars[j * 2] = CHARS[v >>> 4]; 51 | hexChars[j * 2 + 1] = CHARS[v & 0x0F]; 52 | } 53 | return new String(hexChars); 54 | } 55 | 56 | public static File newTestFile() throws IOException { 57 | File dir = new File("../tools/"); 58 | File file = new File(dir, "test.apk"); 59 | File tf = new File(dir, System.currentTimeMillis() + "-test.apk"); 60 | FileUtils.copyFile(file, tf); 61 | return tf; 62 | } 63 | 64 | private static int counter = 0; 65 | 66 | public static void showBuffer(ByteBuffer b) { 67 | StringBuilder s = new StringBuilder(); 68 | s.append("------").append(++counter).append("------\n"); 69 | s.append("capacity=").append(b.capacity()); 70 | s.append(" position=").append(b.position()); 71 | s.append(" limit=").append(b.limit()); 72 | s.append(" remaining=").append(b.remaining()); 73 | s.append(" arrayOffset=").append(b.arrayOffset()); 74 | s.append(" arrayLength=").append(b.array().length).append("\n"); 75 | s.append("array=").append(toHex(b)).append("\n"); 76 | System.out.println(s.toString()); 77 | } 78 | 79 | public static void showBuffer2(final ByteBuffer buffer) { 80 | System.out.println("showBuffer capacity=" + buffer.capacity() 81 | + " position=" + buffer.position() 82 | + " limit=" + buffer.limit() 83 | + " remaining=" + buffer.remaining() 84 | + " arrayOffset=" + buffer.arrayOffset() 85 | + " arrayLength=" + buffer.array().length); 86 | // byte[] all = buffer.array(); 87 | // int offset = buffer.arrayOffset(); 88 | // int start = offset + buffer.position(); 89 | // int end = offset + buffer.limit(); 90 | // byte[] bytes = Arrays.copyOfRange(all, start, end); 91 | // System.out.println(Utils.toHex(bytes)); 92 | } 93 | 94 | public static boolean apkVerified(File f) throws ApkFormatException, 95 | NoSuchAlgorithmException, 96 | IOException { 97 | ApkVerifier verifier = new Builder(f).build(); 98 | Result result = verifier.verify(); 99 | return result.isVerified() 100 | && result.isVerifiedUsingV1Scheme() 101 | && result.isVerifiedUsingV2Scheme() 102 | && !result.containsErrors(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /compatibility.md: -------------------------------------------------------------------------------- 1 | # 兼容性问题 2 | 3 | ------ 4 | 5 | 更新时间:2016.08.05 6 | 7 | ## APK signature scheme v2 8 | 9 | - **使用最新版SDK(Android Gradle Plugin 2.2.0+)时,请务必在 `signingConfigs` 里加入 `v2SigningEnabled false` ,否则打包时会报错** 10 | 11 | ```groovy 12 | apply plugin: 'packer' 13 | 14 | dependencies { 15 | compile 'com.mcxiaoke.gradle:packer-helper:1.0.8' 16 | } 17 | 18 | android { 19 | //... 20 | signingConfigs { 21 | release { 22 | // 如果要支持最新版的系统 Android 7.0 23 | // 这一行必须加,否则安装时会提示没有签名 24 | // 作用是只使用旧版签名,禁用V2版签名模式 25 | v2SigningEnabled false 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | 为了提高Android系统的安全性,Google从Android 7.0开始增加一种新的增强签名模式,从`Android Gradle Plugin 2.2.0`开始,构建系统在打包应用后签名时默认使用`APK signature scheme v2`,该模式在原有的签名模式上,增加校验APK的SHA256哈希值,如果签名后对APK作了任何修改,安装时会校验失败,提示没有签名无法安装,使用本工具修改的APK会无法安装,**解决办法是在 `signingConfigs` 里增加 `v2SigningEnabled false`** ,禁用新版签名模式,技术细节请看官方文档:[APK signature scheme v2](https://developer.android.com/preview/api-overview.html#apk_signature_v2),还有这里 [Issue 31](https://github.com/mcxiaoke/packer-ng-plugin/issues/31) 的讨论 。 32 | -------------------------------------------------------------------------------- /deploy-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "deploy plugin artifacts to local repo" 3 | # rm -rf /tmp/repo/ 4 | ./gradlew -PRELEASE_REPOSITORY_URL=file:///tmp/repo -PSNAPSHOT_REPOSITORY_URL=file:///tmp/repo/ clean uploadArchives --stacktrace $1 5 | -------------------------------------------------------------------------------- /deploy-remote.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "build and deploy plugin artifacts to remote repo..." 3 | ./gradlew clean uploadArchives --stacktrace $1 4 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | PackerNg V2 2 | ======== 3 | 极速渠道打包工具 4 | 5 | - **v2.0.0 - 2017.06.23** - 全新发布,支持V2签名模式,包含多项优化 6 | 7 | ## 特别提示 8 | 9 | V2版只支持`APK Signature Scheme v2`,要求在 `signingConfigs` 里 `v2SigningEnabled true` 启用新版签名模式,并使用`2.2.0`以上版本的Gradle插件,如果你需要使用旧版本,看这里 [v1.0.9](https://github.com/mcxiaoke/packer-ng-plugin/tree/v1.0.9)。 10 | 11 | ## 项目介绍 12 | 13 | [**packer-ng-plugin**](https://github.com/mcxiaoke/packer-ng-plugin) 是下一代Android渠道打包工具Gradle插件,支持极速打包,**100**个渠道包只需要**10**秒钟,速度是 [**gradle-packer-plugin**](https://github.com/mcxiaoke/gradle-packer-plugin) 的**300**倍以上,可方便的用于CI系统集成,同时提供命令行打包脚本,渠道读取提供Python和C语言的实现。 14 | 15 | ## 使用指南 16 | 17 | [`Maven Central`](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22packer-ng%22) 18 | 19 | ### 修改项目根目录的 `build.gradle` 20 | 21 | ```groovy 22 | 23 | buildscript { 24 | dependencies{ 25 | classpath 'com.mcxiaoke.packer-ng:plugin:2.0.0' 26 | } 27 | } 28 | ``` 29 | 30 | ### 修改Android模块的 `build.gradle` 31 | 32 | ```groovy 33 | apply plugin: 'packer' 34 | 35 | dependencies { 36 | compile 'com.mcxiaoke.packer-ng:helper:2.0.0' 37 | } 38 | ``` 39 | 40 | **注意:`plugin` 和 `helper` 的版本号需要保持一致** 41 | 42 | ### 插件配置示例 43 | 44 | ```groovy 45 | packer { 46 | archiveNameFormat = '${buildType}-v${versionName}-${channel}' 47 | archiveOutput = new File(project.rootProject.buildDir, "apks") 48 | // channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', 49 | // 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] 50 | // channelFile = new File(project.rootDir, "markets.txt") 51 | channelMap = [ 52 | "Cat" : project.rootProject.file("channels/cat.txt"), 53 | "Dog" : project.rootProject.file("channels/dog.txt"), 54 | "Fish": project.rootProject.file("channels/channels.txt") 55 | ] 56 | } 57 | ``` 58 | 59 | * **archiveNameFormat** - 指定最终输出的渠道包文件名的格式模版,详细说明见后面,默认值是 `${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}` (可选) 60 | * **archiveOutput** - 指定最终输出的渠道包的存储位置,默认值是 `${project.buildDir}/archives` (可选) 61 | * **channelList** - 指定渠道列表,List类型,见示例 62 | * **channelMap** - 根据productFlavor指定不同的渠道列表文件,见示例 63 | * **channelFile** - 指定渠道列表文件,File类型,见示例 64 | 65 | 注意:`channelList` / `channelMap` / `channelFile` 不能同时使用,根据实际情况选择一种即可,三个属性同时存在时优先级为: `channelList` > `channelMap` > `channelFile `,另外,这三个属性会被命令行参数 `-Pchannels` 覆盖。 66 | 67 | ### 渠道列表格式 68 | 69 | 渠道名列表文件是纯文本文件,按行读取,每行一个渠道,行首和行尾的空白会被忽略,如果有注释,渠道名和注释之间用 `#` 分割。 70 | 71 | 渠道名建议尽量使用规范的**中英文和数字**,不要使用特殊字符和不可见字符。示例:[channels.txt](blob/v2dev/channels/channels.txt) 72 | 73 | ### 集成打包 74 | 75 | * 项目中没有使用 `productFlavors` 76 | 77 | ```shell 78 | ./gradlew clean apkRelease 79 | ``` 80 | 81 | * 项目中使用了 `productFlavors` 82 | 83 | 如果项目中指定了多个 `flavor` ,需要指定需要打渠道包的 `flavor` 名字,假设你有 `Paid` `Free` 两个 `flavor` ,打包的时候命令如下: 84 | 85 | ```shell 86 | ./gradlew clean apkPaidRelease 87 | ./gradlew clean apkFreeRelease 88 | ``` 89 | 90 | 直接使用 `./gradlew clean apkRelease` 会输出所有 `flavor` 的渠道包。 91 | 92 | * 通过参数直接指定渠道列表(会覆盖`build.gradle`中的属性): 93 | 94 | ```shell 95 | ./gradlew clean apkRelease -Pchannels=ch1,ch2,douban,google 96 | ``` 97 | 98 | 渠道数目很少时可以使用此种方式。 99 | 100 | * 通过参数指定渠道列表文件的位置(会覆盖`build.gradle`中的属性): 101 | 102 | ```shell 103 | ./gradlew clean apkRelease -Pchannels=@channels.txt 104 | ``` 105 | 106 | 使用@符号指定渠道列表文件的位置,使用相对于项目根目录的相对路径。 107 | 108 | * 还可以指定输出目录和文件名格式模版: 109 | 110 | ```shell 111 | ./gradlew clean apkRelease -Poutput=build/apks 112 | ./gradlew clean apkRelease -Pformat=${versionName}-${channel} 113 | ``` 114 | 115 | 这些参数 `channels` `output` `format` 可以组合使用,命令行参数会覆盖 `build.gradle` 对应的属性。 116 | 117 | * Gradle打包命令说明 118 | 119 | 渠道打包的Task名字是 `apk${flavor}${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,如果没有 `flavor` 可以忽略,使用时首字母需要大写,假设 `flavor` 是 `Paid`,`release`类型对应的任务名是 `apkPaidRelease`,`beta`类型对应的任务名是 `apkPaidBetaBeta`,其它的以此类推。 120 | 121 | * 特别提示 122 | 123 | 如果你同时使用其它的资源压缩工具或应用加固功能,请使用命令行脚本打包增加渠道信息,增加渠道信息需要放在APK处理过程的最后一步。 124 | 125 | ### 脚本打包 126 | 127 | 除了使用Gradle集成以外,还可以使用项目提供的Java脚本打包,Jar位于本项目的 `tools` 目录,请使用最新版,以下用 `packer-ng` 指代 `java -jar tools/packer-ng-2.0.0.jar`,下面是几个示例。 128 | 129 | * 参数说明: 130 | 131 | ``` 132 | packer-ng - 表示 java -jar packer-ng-2.0.0.jar 133 | channels.txt - 替换成你的渠道列表文件的实际路径 134 | build/archives - 替换成你指定的渠道包的输出路径 135 | app.apk - 替换成你要打渠道包的APK文件的实际路径 136 | ``` 137 | 138 | * 直接指定渠道列表打包: 139 | 140 | ```shell 141 | packer-ng generate --channels=ch1,ch2,ch3 --output=build/archives app.apk 142 | ``` 143 | 144 | * 指定渠道列表文件打包: 145 | 146 | ```shell 147 | packer-ng generate --channels=@channels.txt --output=build/archives app.apk 148 | ``` 149 | 150 | * 验证渠道信息: 151 | 152 | ```shell 153 | packer-ng verify app.apk 154 | ``` 155 | 156 | * 运行命令查看帮助 157 | 158 | ```shell 159 | java -jar tools/packer-ng-2.0.0.jar --help 160 | ``` 161 | 162 | * Python脚本读取渠道: 163 | 164 | ```shell 165 | python tools/packer-ng-v2.py app.apk 166 | ``` 167 | 168 | * C程序读取渠道: 169 | 170 | ```shell 171 | cd tools 172 | make 173 | make install 174 | packer app.apk 175 | ``` 176 | 177 | ### 代码中读取渠道 178 | 179 | ```java 180 | // 如果没有找到渠道信息或遇到错误,默认返回的是"" 181 | // com.mcxiaoke.packer.helper.PackerNg 182 | String channel = PackerNg.getChannel(Context) 183 | ``` 184 | 185 | ### 文件名格式模版 186 | 187 | 格式模版使用Groovy字符串模版引擎,默认文件名格式是: `${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}` 。 188 | 189 | 假如你的App包名是 `com.your.company` ,渠道名是 `Google_Play` ,`buildType` 是 `release` ,`versionName` 是 `2.1.15` ,`versionCode` 是 `200115` ,那么生成的默认APK的文件名是 `com.your.company-Google_Player-release-2.1.15-20015.apk` 。 190 | 191 | 可使用以下变量: 192 | 193 | * *projectName* - 项目名字 194 | * *appName* - App模块名字 195 | * *appPkg* - `applicationId` (App包名packageName) 196 | * *channel* - 打包时指定的渠道名 197 | * *buildType* - `buildType` (release/debug/beta等) 198 | * *flavorName* - `flavorName` (flavor名字,如paid/free等) 199 | * *versionName* - `versionName` (显示用的版本号) 200 | * *versionCode* - `versionCode` (内部版本号) 201 | * *buildTime* - `buildTime` (编译构建日期时间) 202 | * *fileSHA1* - `fileSHA1 ` (最终APK文件的SHA1哈希值) 203 | 204 | ------ 205 | 206 | ## 其它说明 207 | 208 | 渠道读取C语言实现使用 [GenericMakefile](https://github.com/mbcrawfo/GenericMakefile) 构建,[APK Signing Block](https://source.android.com/security/apksigning/v2) 读取和写入Java实现修改自 [apksig](https://android.googlesource.com/platform/tools/apksig/+/master) 和 [walle](https://github.com/Meituan-Dianping/walle/tree/master/payload_writer) ,特此致谢。 209 | 210 | 211 | ------ 212 | 213 | ## 关于作者 214 | 215 | #### 联系方式 216 | * Blog: <http://blog.mcxiaoke.com> 217 | * Github: <https://github.com/mcxiaoke> 218 | * Email: [packer-ng-plugin@mcxiaoke.com](mailto:packer-ng-plugin@mcxiaoke.com) 219 | 220 | #### 开源项目 221 | 222 | * Rx文档中文翻译: <https://github.com/mcxiaoke/RxDocs> 223 | * MQTT协议中文版: <https://github.com/mcxiaoke/mqtt> 224 | * Awesome-Kotlin: <https://github.com/mcxiaoke/awesome-kotlin> 225 | * Kotlin-Koi: <https://github.com/mcxiaoke/kotlin-koi> 226 | * Next公共组件库: <https://github.com/mcxiaoke/Android-Next> 227 | * Gradle渠道打包: <https://github.com/mcxiaoke/gradle-packer-plugin> 228 | * EventBus实现xBus: <https://github.com/mcxiaoke/xBus> 229 | * 蘑菇饭App: <https://github.com/mcxiaoke/minicat> 230 | * 饭否客户端: <https://github.com/mcxiaoke/fanfouapp-opensource> 231 | * Volley镜像: <https://github.com/mcxiaoke/android-volley> 232 | 233 | ------ 234 | 235 | ## License 236 | 237 | Copyright 2014 - 2017 Xiaoke Zhang 238 | 239 | Licensed under the Apache License, Version 2.0 (the "License"); 240 | you may not use this file except in compliance with the License. 241 | You may obtain a copy of the License at 242 | 243 | http://www.apache.org/licenses/LICENSE-2.0 244 | 245 | Unless required by applicable law or agreed to in writing, software 246 | distributed under the License is distributed on an "AS IS" BASIS, 247 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 248 | See the License for the specific language governing permissions and 249 | limitations under the License. 250 | 251 | -------------------------------------------------------------------------------- /gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Chris Banes 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'maven' 18 | apply plugin: 'signing' 19 | 20 | version = VERSION_NAME 21 | group = GROUP 22 | 23 | def isReleaseBuild() { 24 | return VERSION_NAME.contains("SNAPSHOT") == false 25 | } 26 | 27 | def getReleaseRepositoryUrl() { 28 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 29 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 30 | } 31 | 32 | def getSnapshotRepositoryUrl() { 33 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 34 | : "https://oss.sonatype.org/content/repositories/snapshots/" 35 | } 36 | 37 | def getRepositoryUsername() { 38 | return hasProperty('USERNAME') ? USERNAME : (hasProperty('NEXUS_USERNAME')?NEXUS_USERNAME:"") 39 | } 40 | 41 | def getRepositoryPassword() { 42 | return hasProperty('PASSWORD') ? PASSWORD : (hasProperty('NEXUS_PASSWORD')?NEXUS_PASSWORD:"") 43 | } 44 | 45 | afterEvaluate { project -> 46 | uploadArchives { 47 | repositories { 48 | mavenDeployer { 49 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 50 | 51 | pom.groupId = GROUP 52 | pom.artifactId = POM_ARTIFACT_ID 53 | pom.version = VERSION_NAME 54 | 55 | repository(url: getReleaseRepositoryUrl()) { 56 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 57 | } 58 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 59 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 60 | } 61 | 62 | pom.project { 63 | name POM_NAME 64 | packaging POM_PACKAGING 65 | description POM_DESCRIPTION 66 | url POM_URL 67 | 68 | scm { 69 | url POM_SCM_URL 70 | connection POM_SCM_CONNECTION 71 | developerConnection POM_SCM_DEV_CONNECTION 72 | } 73 | 74 | licenses { 75 | license { 76 | name POM_LICENCE_NAME 77 | url POM_LICENCE_URL 78 | distribution POM_LICENCE_DIST 79 | } 80 | } 81 | 82 | developers { 83 | developer { 84 | id POM_DEVELOPER_ID 85 | name POM_DEVELOPER_NAME 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | signing { 94 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 95 | sign configurations.archives 96 | } 97 | 98 | if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) { 99 | task install(type: Upload, dependsOn: assemble) { 100 | repositories.mavenInstaller { 101 | configuration = configurations.archives 102 | 103 | pom.groupId = GROUP 104 | pom.artifactId = POM_ARTIFACT_ID 105 | pom.version = VERSION_NAME 106 | 107 | pom.project { 108 | name POM_NAME 109 | packaging POM_PACKAGING 110 | description POM_DESCRIPTION 111 | url POM_URL 112 | 113 | scm { 114 | url POM_SCM_URL 115 | connection POM_SCM_CONNECTION 116 | developerConnection POM_SCM_DEV_CONNECTION 117 | } 118 | 119 | licenses { 120 | license { 121 | name POM_LICENCE_NAME 122 | url POM_LICENCE_URL 123 | distribution POM_LICENCE_DIST 124 | } 125 | } 126 | 127 | developers { 128 | developer { 129 | id POM_DEVELOPER_ID 130 | name POM_DEVELOPER_NAME 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | task androidJavadocs(type: Javadoc) { 138 | failOnError false 139 | source = android.sourceSets.main.java.source 140 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 141 | } 142 | 143 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 144 | classifier = 'javadoc' 145 | from androidJavadocs.destinationDir 146 | } 147 | 148 | task androidSourcesJar(type: Jar) { 149 | classifier = 'sources' 150 | from android.sourceSets.main.java.source 151 | } 152 | } else { 153 | install { 154 | repositories.mavenInstaller { 155 | pom.groupId = GROUP 156 | pom.artifactId = POM_ARTIFACT_ID 157 | pom.version = VERSION_NAME 158 | 159 | pom.project { 160 | name POM_NAME 161 | packaging POM_PACKAGING 162 | description POM_DESCRIPTION 163 | url POM_URL 164 | 165 | scm { 166 | url POM_SCM_URL 167 | connection POM_SCM_CONNECTION 168 | developerConnection POM_SCM_DEV_CONNECTION 169 | } 170 | 171 | licenses { 172 | license { 173 | name POM_LICENCE_NAME 174 | url POM_LICENCE_URL 175 | distribution POM_LICENCE_DIST 176 | } 177 | } 178 | 179 | developers { 180 | developer { 181 | id POM_DEVELOPER_ID 182 | name POM_DEVELOPER_NAME 183 | } 184 | } 185 | } 186 | } 187 | } 188 | 189 | task sourcesJar(type: Jar, dependsOn:classes) { 190 | classifier = 'sources' 191 | from sourceSets.main.allSource 192 | } 193 | 194 | task javadocJar(type: Jar, dependsOn:javadoc) { 195 | classifier = 'javadoc' 196 | from javadoc.destinationDir 197 | } 198 | } 199 | 200 | artifacts { 201 | if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) { 202 | archives androidSourcesJar 203 | archives androidJavadocsJar 204 | } else { 205 | archives sourcesJar 206 | archives javadocJar 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=2.0.1 2 | VERSION_CODE=200 3 | 4 | GROUP=com.mcxiaoke.packer-ng 5 | 6 | POM_DESCRIPTION=Next Generation Android Multi Packer Gradle Plugin 7 | POM_URL=https://github.com/mcxiaoke/packer-ng-plugin 8 | POM_SCM_URL=https://github.com/mcxiaoke/packer-ng-plugin.git 9 | POM_SCM_CONNECTION=scm:git:https://github.com/mcxiaoke/packer-ng-plugin.git 10 | POM_SCM_DEV_CONNECTION=scm:git:https://github.com/mcxiaoke/packer-ng-plugin.git 11 | POM_LICENCE_NAME=Apache License, Version 2.0 12 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0 13 | POM_LICENCE_DIST=repo 14 | POM_DEVELOPER_ID=mcxiaoke 15 | POM_DEVELOPER_NAME=Xiaoke Zhang 16 | POM_DEVELOPER_EMAIL=packer-ng@mcxiaoke.com 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/packer-ng-plugin/ffbe05a2d27406f3aea574d083cded27f0742160/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 16 11:37:35 CST 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)#39;` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now handle the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /helper/build.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | mavenCentral() 3 | jcenter() 4 | google() 5 | } 6 | 7 | apply plugin: 'com.android.library' 8 | 9 | dependencies { 10 | implementation project(":common") 11 | } 12 | 13 | android { 14 | 15 | compileOptions { 16 | sourceCompatibility JavaVersion.VERSION_1_7 17 | targetCompatibility JavaVersion.VERSION_1_7 18 | encoding "UTF-8" 19 | } 20 | 21 | compileSdkVersion project.compileSdkVersion 22 | buildToolsVersion project.buildToolsVersion 23 | 24 | defaultConfig { 25 | versionName project.VERSION_NAME 26 | versionCode Integer.parseInt(project.VERSION_CODE) 27 | minSdkVersion project.minSdkVersion 28 | targetSdkVersion project.targetSdkVersion 29 | } 30 | 31 | lintOptions { 32 | abortOnError false 33 | htmlReport true 34 | } 35 | } 36 | apply from: '../gradle-mvn-push.gradle' 37 | -------------------------------------------------------------------------------- /helper/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=helper 2 | POM_PACKAGING=jar 3 | POM_NAME=Helper Classes for Packer-Ng Android 4 | -------------------------------------------------------------------------------- /helper/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <manifest package="com.mcxiaoke.packer.helper"> 3 | 4 | </manifest> 5 | -------------------------------------------------------------------------------- /helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.helper; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import com.mcxiaoke.packer.common.PackerCommon; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | 10 | /** 11 | * User: mcxiaoke 12 | * Date: 15/11/23 13 | * Time: 13:12 14 | */ 15 | public final class PackerNg { 16 | private static final String TAG = "PackerNg"; 17 | private static final String EMPTY_STRING = ""; 18 | private static String sCachedChannel; 19 | 20 | public static String getChannel(final File file) { 21 | try { 22 | return PackerCommon.readChannel(file); 23 | } catch (Exception e) { 24 | return EMPTY_STRING; 25 | } 26 | } 27 | 28 | public static String getChannel(final Context context) { 29 | try { 30 | return getChannelOrThrow(context); 31 | } catch (Exception e) { 32 | return EMPTY_STRING; 33 | } 34 | } 35 | 36 | public static synchronized String getChannelOrThrow(final Context context) 37 | throws IOException { 38 | final ApplicationInfo info = context.getApplicationInfo(); 39 | return PackerCommon.readChannel(new File(info.sourceDir)); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /helper/src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: com.mcxiaoke.packer.helper.PackerNg 3 | 4 | -------------------------------------------------------------------------------- /huge_markets_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: mcxiaoke 4 | # @Date: 2015-11-25 14:30:02 5 | import subprocess 6 | import os 7 | import sys 8 | 9 | with open('huge_markets.txt', 'w') as f: 10 | for i in range(int(sys.argv[1])): 11 | f.write("Test Market %s#test market %s\n" % (i, i)) 12 | f.write("中文:MARKET%s#test market %s\n" % (i, i)) 13 | 14 | subprocess.check_output(["./gradlew", "-Pchannels=@huge_markets.txt", "-Poutput=tmp", "clean", "apkPaidRelease"]) 15 | os.remove('huge_markets.txt') 16 | -------------------------------------------------------------------------------- /markets.txt: -------------------------------------------------------------------------------- 1 | Google_Market#Google电子市场 2 | 安卓市场#安卓市场 3 | 91_market#91商城 4 | 5 | sony_market #sony商城 6 | UC浏览器 #UC浏览器 7 | 360SearchApp#360SearchApp 8 | HelloMarket 9 | OkMarket# 10 | #ErrorMarket 11 | ##### 12 | # 13 | 14 | 15 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | 3 | sourceCompatibility = 1.7 4 | targetCompatibility = 1.7 5 | 6 | buildscript { 7 | repositories { 8 | mavenCentral() 9 | } 10 | } 11 | 12 | repositories { 13 | mavenCentral() 14 | jcenter() 15 | google() 16 | } 17 | 18 | dependencies { 19 | compile localGroovy() 20 | compile gradleApi() 21 | compile project(':common') 22 | compile project(':cli') 23 | compile 'com.android.tools.build:gradle:2.3.3' 24 | compile 'com.android.tools.build:apksig:2.3.3' 25 | } 26 | 27 | apply from: '../gradle-mvn-push.gradle' 28 | 29 | -------------------------------------------------------------------------------- /plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=plugin 2 | POM_PACKAGING=jar 3 | POM_NAME=Next Generation Android Market Packer 4 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.ng 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.tasks.Input 5 | import org.gradle.api.tasks.TaskAction 6 | 7 | /** 8 | * User: mcxiaoke 9 | * Date: 14/12/19 10 | * Time: 11:29 11 | */ 12 | class CleanTask extends DefaultTask { 13 | 14 | @Input 15 | File target 16 | 17 | CleanTask() { 18 | description = 'clean all files in output dir' 19 | } 20 | 21 | @TaskAction 22 | void deleteAll() { 23 | logger.info("${name}: delete all files in ${target.absolutePath}") 24 | target.deleteDir() 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.ng 2 | 3 | /** 4 | * User: mcxiaoke 5 | * Date: 2017/6/2 6 | * Time: 12:02 7 | */ 8 | class Const { 9 | static final String HOME_PAGE = "https://github.com/mcxiaoke/packer-ng-plugin/" 10 | static final String PROP_CHANNELS = "channels" 11 | static final String PROP_OUTPUT = "output" 12 | static final String PROP_FORMAT = "format" 13 | 14 | static final String DEFAULT_OUTPUT = "archives" // in build dir 15 | 16 | /* 17 | * file name template string 18 | * 19 | * Available vars: 20 | * 1. projectName 21 | * 2. appName 22 | * 3. appPkg 23 | * 4. channel 24 | * 5. buildType 25 | * 6. versionName 26 | * 7. versionCode 27 | * 8. buildTime 28 | * 9. fileSHA1 29 | * 30 | * default value: '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' 31 | */ 32 | static final String DEFAULT_FORMAT = 33 | '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' 34 | } 35 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.ng 2 | 3 | class GradleExtension { 4 | File archiveOutput 5 | String archiveNameFormat 6 | Set<String> channelList; 7 | File channelFile; 8 | Map<String, File> channelMap; 9 | 10 | @Override 11 | String toString() { 12 | return "{" + 13 | "archiveOutput=" + archiveOutput + 14 | "\narchiveNameFormat='" + archiveNameFormat + '\'' + 15 | "\nchannelList=" + channelList + 16 | "\nchannelFile=" + channelFile + 17 | "\nchannelMap=" + channelMap + 18 | '}'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.ng 2 | 3 | import com.android.build.gradle.api.BaseVariant 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | 7 | // Android PackerNg Plugin Source 8 | class GradlePlugin implements Plugin<Project> { 9 | static final String TAG = "PackerNg" 10 | static final String PLUGIN_NAME = "packer" 11 | 12 | Project project 13 | 14 | @Override 15 | void apply(Project project) { 16 | this.project = project 17 | if (!project.plugins.hasPlugin("com.android.application")) { 18 | throw new PluginException( 19 | "'com.android.application' plugin must be applied", null) 20 | } 21 | project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) 22 | project.extensions.create(PLUGIN_NAME, GradleExtension) 23 | project.afterEvaluate { 24 | project.android.applicationVariants.all { BaseVariant variant -> 25 | addTasks(variant) 26 | } 27 | } 28 | } 29 | 30 | static boolean isV2SigningEnabled(BaseVariant vt) { 31 | boolean e1 = false 32 | boolean e2 = false 33 | def s1 = vt.buildType.signingConfig 34 | if (s1 && s1.signingReady) { 35 | e1 = s1.v2SigningEnabled 36 | } 37 | def s2 = vt.mergedFlavor.signingConfig 38 | if (s2 && s2.signingReady) { 39 | e2 = s2.v2SigningEnabled 40 | } 41 | return e1 || e2 42 | } 43 | 44 | void addTasks(BaseVariant vt) { 45 | debug("addTasks() for ${vt.name}") 46 | def variantTask = project.task("apk${vt.name.capitalize()}", 47 | type: GradleTask) { 48 | variant = vt 49 | extension = project.packer 50 | dependsOn vt.assemble 51 | } 52 | 53 | debug("addTasks() new variant task:${variantTask.name}") 54 | 55 | def buildTypeName = vt.buildType.name 56 | if (vt.name != buildTypeName) { 57 | def taskName = "apk${buildTypeName.capitalize()}" 58 | def task = project.tasks.findByName(taskName) 59 | if (task == null) { 60 | task = project.task(taskName) 61 | } 62 | task.dependsOn(variantTask) 63 | debug("addTasks() build type task ${taskName}") 64 | } 65 | 66 | } 67 | 68 | void debug(String msg) { 69 | project.logger.info(msg) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.ng 2 | 3 | import com.android.build.gradle.api.BaseVariant 4 | import com.mcxiaoke.packer.cli.Bridge 5 | import com.mcxiaoke.packer.cli.Helper 6 | import groovy.text.SimpleTemplateEngine 7 | import groovy.text.Template 8 | import org.gradle.api.DefaultTask 9 | import org.gradle.api.tasks.Input 10 | import org.gradle.api.tasks.TaskAction 11 | 12 | import java.text.SimpleDateFormat 13 | 14 | /** 15 | * User: mcxiaoke 16 | * Date: 15/11/23 17 | * Time: 14:40 18 | */ 19 | class GradleTask extends DefaultTask { 20 | 21 | @Input 22 | BaseVariant variant 23 | 24 | @Input 25 | GradleExtension extension 26 | 27 | GradleTask() { 28 | description = 'generate APK with channel info' 29 | } 30 | 31 | Template getNameTemplate() { 32 | String format 33 | String propValue = project.findProperty(Const.PROP_FORMAT) 34 | if (propValue != null) { 35 | format = propValue.toString() 36 | } else { 37 | format = extension.archiveNameFormat 38 | } 39 | if (format == null || format.isEmpty()) { 40 | format = Const.DEFAULT_FORMAT 41 | } 42 | def engine = new SimpleTemplateEngine() 43 | return engine.createTemplate(format) 44 | } 45 | 46 | File getOriginalApk() { 47 | variant.outputs.each { ot -> 48 | logger.info("Output APK: ${ot.name},${ot.outputFile}") 49 | } 50 | File file = variant.outputs[0].outputFile 51 | if (!Bridge.verifyApk(file)) { 52 | throw new PluginException("APK Signature Scheme v2 verify failed: '${file}'") 53 | } 54 | return file 55 | } 56 | 57 | File getOutputRoot() { 58 | File outputDir 59 | String propValue = project.findProperty(Const.PROP_OUTPUT) 60 | if (propValue != null) { 61 | String dirName = propValue.toString() 62 | outputDir = new File(project.rootDir, dirName) 63 | } else { 64 | outputDir = extension.archiveOutput 65 | } 66 | if (outputDir == null) { 67 | outputDir = new File(project.rootProject.buildDir, Const.DEFAULT_OUTPUT) 68 | } 69 | if (!outputDir.exists()) { 70 | outputDir.mkdirs() 71 | } 72 | return outputDir 73 | } 74 | 75 | File getVariantOutput() { 76 | File outputDir = getOutputRoot() 77 | String flavorName = variant.flavorName 78 | if (flavorName.length() > 0) { 79 | outputDir = new File(outputDir, flavorName) 80 | } 81 | if (!outputDir.exists()) { 82 | outputDir.mkdirs() 83 | } else { 84 | logger.info(":${name} delete old APKs in ${outputDir.absolutePath}") 85 | Helper.deleteAPKs(outputDir) 86 | } 87 | return outputDir 88 | } 89 | 90 | Set<String> getChannels() { 91 | // -P channels=ch1,ch2,ch3 92 | // -P channels=@channels.txt 93 | // channelList = [ch1,ch2,ch3] 94 | // channelFile = project.file("channels.txt") 95 | Collection<String> channels = [] 96 | // check command line property 97 | def propValue = project.findProperty(Const.PROP_CHANNELS) 98 | if (propValue != null) { 99 | String prop = propValue.toString() 100 | logger.info(":${project.name} channels property: '${prop}'") 101 | if (prop.startsWith("@")) { 102 | def fileName = prop.substring(1) 103 | if (fileName != null) { 104 | File f = new File(project.rootDir, fileName) 105 | if (!f.isFile() || !f.canRead()) { 106 | throw new PluginException("channel file not exists: '${f.absolutePath}'") 107 | } 108 | channels = Helper.parseChannels(f) 109 | } else { 110 | throw new PluginException("invalid channels property: '${prop}'") 111 | } 112 | } else { 113 | channels = Helper.parseChannels(prop); 114 | } 115 | if (channels == null || channels.isEmpty()) { 116 | throw new PluginException("invalid channels property: '${prop}'") 117 | } 118 | return channels 119 | } 120 | if (extension.channelList != null) { 121 | channels = Helper.escape(extension.channelList) 122 | logger.info(":${project.name} ext.channelList: ${channels}") 123 | } else if (extension.channelMap != null) { 124 | String flavorName = variant.flavorName 125 | File f = extension.channelMap.get(flavorName) 126 | logger.info(":${project.name} extension.channelMap file: ${f}") 127 | if (f == null || !f.isFile()) { 128 | throw new PluginException("channel file not exists: '${f.absolutePath}'") 129 | } 130 | if (f != null && f.isFile()) { 131 | channels = Helper.parseChannels(f) 132 | } 133 | } else if (extension.channelFile != null) { 134 | File f = extension.channelFile 135 | logger.info(":${project.name} extension.channelFile: ${f}") 136 | if (!f.isFile()) { 137 | throw new PluginException("channel file not exists: '${f.absolutePath}'") 138 | } 139 | channels = Helper.parseChannels(f) 140 | } 141 | if (channels == null || channels.isEmpty()) { 142 | throw new PluginException("No channels found") 143 | } 144 | return channels 145 | } 146 | 147 | 148 | void showProperties() { 149 | logger.info("Extension: ${extension}") 150 | logger.info("Property: ${Const.PROP_CHANNELS} = " + 151 | "${project.findProperty(Const.PROP_CHANNELS)}") 152 | logger.info("Property: ${Const.PROP_OUTPUT} = " + 153 | "${project.findProperty(Const.PROP_OUTPUT)}") 154 | logger.info("Property: ${Const.PROP_FORMAT} = " + 155 | "${project.findProperty(Const.PROP_FORMAT)}") 156 | } 157 | 158 | @TaskAction 159 | void generate() { 160 | println("============================================================") 161 | println("PackerNg - https://github.com/mcxiaoke/packer-ng-plugin") 162 | println("============================================================") 163 | showProperties() 164 | File apkFile = getOriginalApk() 165 | File rootDir = getOutputRoot() 166 | File outputDir = getVariantOutput() 167 | Collection<String> channels = getChannels() 168 | Template template = getNameTemplate() 169 | println("Variant: ${variant.name}") 170 | println("Input: ${apkFile.path}") 171 | println("Output: ${outputDir.path}") 172 | println("Channels: [${channels.join(' ')}]") 173 | for (String channel : channels) { 174 | File tempFile = new File(outputDir, "tmp-${channel}.apk") 175 | try { 176 | Helper.copyFile(apkFile, tempFile) 177 | Bridge.writeChannel(tempFile, channel) 178 | String apkName = buildApkName(channel, tempFile, template) 179 | File finalFile = new File(outputDir, apkName) 180 | if (Bridge.verifyChannel(tempFile, channel)) { 181 | println("Generating: ${apkName}") 182 | tempFile.renameTo(finalFile) 183 | logger.info("Generated: ${finalFile}") 184 | } else { 185 | throw new PluginException("${channel} APK verify failed") 186 | } 187 | } catch (IOException ex) { 188 | throw new PluginException("${channel} APK generate failed", ex) 189 | } finally { 190 | tempFile.delete() 191 | } 192 | } 193 | println("Outputs: ${rootDir.absolutePath}") 194 | println("============================================================") 195 | } 196 | 197 | String buildApkName(channel, file, template) { 198 | def buildTime = new SimpleDateFormat('yyyyMMdd-HHmmss').format(new Date()) 199 | def fileSHA1 = HASH.sha1(file) 200 | def nameMap = [ 201 | 'appName' : project.name, 202 | 'projectName': project.rootProject.name, 203 | 'fileSHA1' : fileSHA1, 204 | 'channel' : channel, 205 | 'flavor' : variant.flavorName, 206 | 'buildType' : variant.buildType.name, 207 | 'versionName': variant.versionName, 208 | 'versionCode': variant.versionCode, 209 | 'appPkg' : variant.applicationId, 210 | 'buildTime' : buildTime 211 | ] 212 | logger.info("nameMap: ${nameMap}") 213 | return template.make(nameMap).toString() + '.apk' 214 | } 215 | 216 | /* 217 | static Set<String> escape(Collection<String> cs) { 218 | // filter invalid chars for filename 219 | Pattern pattern = ~/[\/:*?"'<>|]/ 220 | return cs.collect { it.replaceAll(pattern, "_") }.toSet() 221 | } 222 | */ 223 | } 224 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.ng 2 | 3 | import org.gradle.api.GradleException 4 | 5 | /** 6 | * User: mcxiaoke 7 | * Date: 2017/6/5 8 | * Time: 15:29 9 | */ 10 | class PluginException extends GradleException { 11 | PluginException() { 12 | // super("See docs on ${Const.HOME_PAGE}") 13 | super() 14 | } 15 | 16 | PluginException(final String message) { 17 | super(message) 18 | } 19 | 20 | PluginException(final String message, final Throwable cause) { 21 | super(message, cause) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/java/com/mcxiaoke/packer/ng/HASH.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.ng; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.io.UnsupportedEncodingException; 10 | import java.security.MessageDigest; 11 | import java.security.NoSuchAlgorithmException; 12 | 13 | /** 14 | * User: mcxiaoke 15 | * Date: 16/5/30 16 | * Time: 10:53 17 | */ 18 | public final class HASH { 19 | private static final String ENC_UTF8 = "UTF-8"; 20 | private static final String MD5 = "MD5"; 21 | private static final String SHA_1 = "SHA-1"; 22 | private static final String SHA_256 = "SHA-256"; 23 | private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', 24 | '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 25 | private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', 26 | '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; 27 | 28 | private static final int IO_BUF_SIZE = 0x1000; // 4K 29 | 30 | private static long copy(InputStream in, OutputStream out) 31 | throws IOException { 32 | byte[] buf = new byte[IO_BUF_SIZE]; 33 | long total = 0; 34 | while (true) { 35 | int r = in.read(buf); 36 | if (r == -1) { 37 | break; 38 | } 39 | out.write(buf, 0, r); 40 | total += r; 41 | } 42 | return total; 43 | } 44 | 45 | private static byte[] getRawBytes(String text) { 46 | try { 47 | return text.getBytes(ENC_UTF8); 48 | } catch (UnsupportedEncodingException e) { 49 | return text.getBytes(); 50 | } 51 | } 52 | 53 | private static byte[] getRawBytes(File file) throws IOException { 54 | FileInputStream fis = null; 55 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 56 | ; 57 | try { 58 | fis = new FileInputStream(file); 59 | copy(fis, bos); 60 | } finally { 61 | bos.close(); 62 | if (fis != null) { 63 | fis.close(); 64 | } 65 | } 66 | return bos.toByteArray(); 67 | } 68 | 69 | private static String getString(byte[] data) { 70 | try { 71 | return new String(data, ENC_UTF8); 72 | } catch (UnsupportedEncodingException e) { 73 | return new String(data); 74 | } 75 | } 76 | 77 | public static String md5(File file) throws IOException { 78 | return md5(getRawBytes(file)); 79 | } 80 | 81 | public static String md5(byte[] data) { 82 | return new String(encodeHex(md5Bytes(data))); 83 | } 84 | 85 | public static String md5(String text) { 86 | return new String(encodeHex(md5Bytes(getRawBytes(text)))); 87 | } 88 | 89 | public static byte[] md5Bytes(byte[] data) { 90 | return getDigest(MD5).digest(data); 91 | } 92 | 93 | public static String sha1(File file) throws IOException { 94 | return sha1(getRawBytes(file)); 95 | } 96 | 97 | public static String sha1(byte[] data) { 98 | return new String(encodeHex(sha1Bytes(data))); 99 | } 100 | 101 | public static String sha1(String text) { 102 | return new String(encodeHex(sha1Bytes(getRawBytes(text)))); 103 | } 104 | 105 | public static byte[] sha1Bytes(byte[] data) { 106 | return getDigest(SHA_1).digest(data); 107 | } 108 | 109 | public static String sha256(File file) throws IOException { 110 | return sha256(getRawBytes(file)); 111 | } 112 | 113 | public static String sha256(byte[] data) { 114 | return new String(encodeHex(sha256Bytes(data))); 115 | } 116 | 117 | public static String sha256(String text) { 118 | return new String(encodeHex(sha256Bytes(getRawBytes(text)))); 119 | } 120 | 121 | public static byte[] sha256Bytes(byte[] data) { 122 | return getDigest(SHA_256).digest(data); 123 | } 124 | 125 | private static MessageDigest getDigest(String algorithm) { 126 | try { 127 | return MessageDigest.getInstance(algorithm); 128 | } catch (NoSuchAlgorithmException e) { 129 | throw new IllegalArgumentException(e); 130 | } 131 | } 132 | 133 | private static char[] encodeHex(byte[] data) { 134 | return encodeHex(data, true); 135 | } 136 | 137 | private static char[] encodeHex(byte[] data, boolean toLowerCase) { 138 | return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); 139 | } 140 | 141 | private static char[] encodeHex(byte[] data, char[] toDigits) { 142 | int l = data.length; 143 | char[] out = new char[l << 1]; 144 | for (int i = 0, j = 0; i < l; i++) { 145 | out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; 146 | out[j++] = toDigits[0x0F & data[i]]; 147 | } 148 | return out; 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /plugin/src/main/java/com/mcxiaoke/packer/ng/StringVersion.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.ng; 2 | 3 | /* 4 | * Licensed to the Apache Software Foundation (ASF) under one 5 | * or more contributor license agreements. See the NOTICE file 6 | * distributed with this work for additional information 7 | * regarding copyright ownership. The ASF licenses this file 8 | * to you under the Apache License, Version 2.0 (the 9 | * "License"); you may not use this file except in compliance 10 | * with the License. You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, 15 | * software distributed under the License is distributed on an 16 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | * KIND, either express or implied. See the License for the 18 | * specific language governing permissions and limitations 19 | * under the License. 20 | */ 21 | 22 | import java.math.BigInteger; 23 | import java.util.ArrayList; 24 | import java.util.Arrays; 25 | import java.util.Iterator; 26 | import java.util.List; 27 | import java.util.Locale; 28 | import java.util.Properties; 29 | import java.util.Stack; 30 | 31 | /** 32 | * <p> 33 | * Generic implementation of version comparison. 34 | * </p> 35 | * 36 | * Features: 37 | * <ul> 38 | * <li>mixing of '<code>-</code>' (hyphen) and '<code>.</code>' (dot) separators,</li> 39 | * <li>transition between characters and digits also constitutes a separator: 40 | * <code>1.0alpha1 => [1, 0, alpha, 1]</code></li> 41 | * <li>unlimited number of version components,</li> 42 | * <li>version components in the text can be digits or strings,</li> 43 | * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering. 44 | * Well-known qualifiers (case insensitive) are:<ul> 45 | * <li><code>alpha</code> or <code>a</code></li> 46 | * <li><code>beta</code> or <code>b</code></li> 47 | * <li><code>milestone</code> or <code>m</code></li> 48 | * <li><code>rc</code> or <code>cr</code></li> 49 | * <li><code>snapshot</code></li> 50 | * <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li> 51 | * <li><code>sp</code></li> 52 | * </ul> 53 | * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive), 54 | * </li> 55 | * <li>a hyphen usually precedes a qualifier, and is always less important than something preceded with a dot.</li> 56 | * </ul> 57 | * 58 | * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a> 59 | * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a> 60 | * @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a> 61 | */ 62 | class StringVersion 63 | implements Comparable<StringVersion> { 64 | private String value; 65 | 66 | private String canonical; 67 | 68 | private ListItem items; 69 | 70 | private interface Item { 71 | int INTEGER_ITEM = 0; 72 | int STRING_ITEM = 1; 73 | int LIST_ITEM = 2; 74 | 75 | int compareTo(Item item); 76 | 77 | int getType(); 78 | 79 | boolean isNull(); 80 | } 81 | 82 | /** 83 | * Represents a numeric item in the version item list. 84 | */ 85 | private static class IntegerItem 86 | implements Item { 87 | private static final BigInteger BIG_INTEGER_ZERO = new BigInteger("0"); 88 | 89 | private final BigInteger value; 90 | 91 | public static final IntegerItem ZERO = new IntegerItem(); 92 | 93 | private IntegerItem() { 94 | this.value = BIG_INTEGER_ZERO; 95 | } 96 | 97 | public IntegerItem(String str) { 98 | this.value = new BigInteger(str); 99 | } 100 | 101 | public int getType() { 102 | return INTEGER_ITEM; 103 | } 104 | 105 | public boolean isNull() { 106 | return BIG_INTEGER_ZERO.equals(value); 107 | } 108 | 109 | public int compareTo(Item item) { 110 | if (item == null) { 111 | return BIG_INTEGER_ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 112 | } 113 | 114 | switch (item.getType()) { 115 | case INTEGER_ITEM: 116 | return value.compareTo(((IntegerItem) item).value); 117 | 118 | case STRING_ITEM: 119 | return 1; // 1.1 > 1-sp 120 | 121 | case LIST_ITEM: 122 | return 1; // 1.1 > 1-1 123 | 124 | default: 125 | throw new RuntimeException("invalid item: " + item.getClass()); 126 | } 127 | } 128 | 129 | public String toString() { 130 | return value.toString(); 131 | } 132 | } 133 | 134 | /** 135 | * Represents a string in the version item list, usually a qualifier. 136 | */ 137 | private static class StringItem 138 | implements Item { 139 | private static final String[] QUALIFIERS = {"alpha", "beta", "milestone", "rc", "snapshot", "", "sp"}; 140 | 141 | @SuppressWarnings("checkstyle:constantname") 142 | private static final List<String> _QUALIFIERS = Arrays.asList(QUALIFIERS); 143 | 144 | private static final Properties ALIASES = new Properties(); 145 | 146 | static { 147 | ALIASES.put("ga", ""); 148 | ALIASES.put("final", ""); 149 | ALIASES.put("cr", "rc"); 150 | } 151 | 152 | /** 153 | * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes 154 | * the version older than one without a qualifier, or more recent. 155 | */ 156 | private static final String RELEASE_VERSION_INDEX = String.valueOf(_QUALIFIERS.indexOf("")); 157 | 158 | private String value; 159 | 160 | public StringItem(String value, boolean followedByDigit) { 161 | if (followedByDigit && value.length() == 1) { 162 | // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 163 | switch (value.charAt(0)) { 164 | case 'a': 165 | value = "alpha"; 166 | break; 167 | case 'b': 168 | value = "beta"; 169 | break; 170 | case 'm': 171 | value = "milestone"; 172 | break; 173 | default: 174 | } 175 | } 176 | this.value = ALIASES.getProperty(value, value); 177 | } 178 | 179 | public int getType() { 180 | return STRING_ITEM; 181 | } 182 | 183 | public boolean isNull() { 184 | return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); 185 | } 186 | 187 | /** 188 | * Returns a comparable value for a qualifier. 189 | * 190 | * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical 191 | * ordering. 192 | * 193 | * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1 194 | * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character, 195 | * so this is still fast. If more characters are needed then it requires a lexical sort anyway. 196 | * 197 | * @param qualifier 198 | * @return an equivalent value that can be used with lexical comparison 199 | */ 200 | public static String comparableQualifier(String qualifier) { 201 | int i = _QUALIFIERS.indexOf(qualifier); 202 | 203 | return i == -1 ? (_QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); 204 | } 205 | 206 | public int compareTo(Item item) { 207 | if (item == null) { 208 | // 1-rc < 1, 1-ga > 1 209 | return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); 210 | } 211 | switch (item.getType()) { 212 | case INTEGER_ITEM: 213 | return -1; // 1.any < 1.1 ? 214 | 215 | case STRING_ITEM: 216 | return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); 217 | 218 | case LIST_ITEM: 219 | return -1; // 1.any < 1-1 220 | 221 | default: 222 | throw new RuntimeException("invalid item: " + item.getClass()); 223 | } 224 | } 225 | 226 | public String toString() { 227 | return value; 228 | } 229 | } 230 | 231 | /** 232 | * Represents a version list item. This class is used both for the global item list and for sub-lists (which start 233 | * with '-(number)' in the version specification). 234 | */ 235 | private static class ListItem 236 | extends ArrayList<Item> 237 | implements Item { 238 | public int getType() { 239 | return LIST_ITEM; 240 | } 241 | 242 | public boolean isNull() { 243 | return (size() == 0); 244 | } 245 | 246 | void normalize() { 247 | for (int i = size() - 1; i >= 0; i--) { 248 | Item lastItem = get(i); 249 | 250 | if (lastItem.isNull()) { 251 | // remove null trailing items: 0, "", empty list 252 | remove(i); 253 | } else if (!(lastItem instanceof ListItem)) { 254 | break; 255 | } 256 | } 257 | } 258 | 259 | public int compareTo(Item item) { 260 | if (item == null) { 261 | if (size() == 0) { 262 | return 0; // 1-0 = 1- (normalize) = 1 263 | } 264 | Item first = get(0); 265 | return first.compareTo(null); 266 | } 267 | switch (item.getType()) { 268 | case INTEGER_ITEM: 269 | return -1; // 1-1 < 1.0.x 270 | 271 | case STRING_ITEM: 272 | return 1; // 1-1 > 1-sp 273 | 274 | case LIST_ITEM: 275 | Iterator<Item> left = iterator(); 276 | Iterator<Item> right = ((ListItem) item).iterator(); 277 | 278 | while (left.hasNext() || right.hasNext()) { 279 | Item l = left.hasNext() ? left.next() : null; 280 | Item r = right.hasNext() ? right.next() : null; 281 | 282 | // if this is shorter, then invert the compare and mul with -1 283 | int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); 284 | 285 | if (result != 0) { 286 | return result; 287 | } 288 | } 289 | 290 | return 0; 291 | 292 | default: 293 | throw new RuntimeException("invalid item: " + item.getClass()); 294 | } 295 | } 296 | 297 | public String toString() { 298 | StringBuilder buffer = new StringBuilder(); 299 | for (Item item : this) { 300 | if (buffer.length() > 0) { 301 | buffer.append((item instanceof ListItem) ? '-' : '.'); 302 | } 303 | buffer.append(item); 304 | } 305 | return buffer.toString(); 306 | } 307 | } 308 | 309 | public StringVersion(String version) { 310 | parseVersion(version); 311 | } 312 | 313 | public final void parseVersion(String version) { 314 | this.value = version; 315 | 316 | items = new ListItem(); 317 | 318 | version = version.toLowerCase(Locale.ENGLISH); 319 | 320 | ListItem list = items; 321 | 322 | Stack<Item> stack = new Stack<>(); 323 | stack.push(list); 324 | 325 | boolean isDigit = false; 326 | 327 | int startIndex = 0; 328 | 329 | for (int i = 0; i < version.length(); i++) { 330 | char c = version.charAt(i); 331 | 332 | if (c == '.') { 333 | if (i == startIndex) { 334 | list.add(IntegerItem.ZERO); 335 | } else { 336 | list.add(parseItem(isDigit, version.substring(startIndex, i))); 337 | } 338 | startIndex = i + 1; 339 | } else if (c == '-') { 340 | if (i == startIndex) { 341 | list.add(IntegerItem.ZERO); 342 | } else { 343 | list.add(parseItem(isDigit, version.substring(startIndex, i))); 344 | } 345 | startIndex = i + 1; 346 | 347 | list.add(list = new ListItem()); 348 | stack.push(list); 349 | } else if (Character.isDigit(c)) { 350 | if (!isDigit && i > startIndex) { 351 | list.add(new StringItem(version.substring(startIndex, i), true)); 352 | startIndex = i; 353 | 354 | list.add(list = new ListItem()); 355 | stack.push(list); 356 | } 357 | 358 | isDigit = true; 359 | } else { 360 | if (isDigit && i > startIndex) { 361 | list.add(parseItem(true, version.substring(startIndex, i))); 362 | startIndex = i; 363 | 364 | list.add(list = new ListItem()); 365 | stack.push(list); 366 | } 367 | 368 | isDigit = false; 369 | } 370 | } 371 | 372 | if (version.length() > startIndex) { 373 | list.add(parseItem(isDigit, version.substring(startIndex))); 374 | } 375 | 376 | while (!stack.isEmpty()) { 377 | list = (ListItem) stack.pop(); 378 | list.normalize(); 379 | } 380 | 381 | canonical = items.toString(); 382 | } 383 | 384 | private static Item parseItem(boolean isDigit, String buf) { 385 | return isDigit ? new IntegerItem(buf) : new StringItem(buf, false); 386 | } 387 | 388 | public int compareTo(StringVersion o) { 389 | return items.compareTo(o.items); 390 | } 391 | 392 | public String toString() { 393 | return value; 394 | } 395 | 396 | public String getCanonical() { 397 | return canonical; 398 | } 399 | 400 | public boolean equals(Object o) { 401 | return (o instanceof StringVersion) && canonical.equals(((StringVersion) o).canonical); 402 | } 403 | 404 | public int hashCode() { 405 | return canonical.hashCode(); 406 | } 407 | 408 | // CHECKSTYLE_OFF: LineLength 409 | 410 | /** 411 | * Main to test version parsing and comparison. 412 | * <p> 413 | * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue 414 | * <pre>java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"</pre> 415 | * command to command line. Result of given command will be something like this: 416 | * <pre> 417 | * Display parameters as parsed by Maven (in canonical form) and comparison result: 418 | * 1. 1.2.7 == 1.2.7 419 | * 1.2.7 > 1.2-SNAPSHOT 420 | * 2. 1.2-SNAPSHOT == 1.2-snapshot 421 | * </pre> 422 | * 423 | * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always 424 | * two adjacent will be compared 425 | */ 426 | // CHECKSTYLE_ON: LineLength 427 | public static void main(String... args) { 428 | System.out.println("Display parameters as parsed by Maven (in canonical form) and comparison result:"); 429 | if (args.length == 0) { 430 | return; 431 | } 432 | 433 | StringVersion prev = null; 434 | int i = 1; 435 | for (String version : args) { 436 | StringVersion c = new StringVersion(version); 437 | 438 | if (prev != null) { 439 | int compare = prev.compareTo(c); 440 | System.out.println(" " + prev.toString() + ' ' 441 | + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) + ' ' + version); 442 | } 443 | 444 | System.out.println(String.valueOf(i++) + ". " + version + " == " + c.getCanonical()); 445 | 446 | prev = c; 447 | } 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/gradle-plugins/packer.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.mcxiaoke.packer.ng.GradlePlugin -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Packer-NG Gradle Plugin V2 2 | 3 | ### 提示:本项目已停止新功能开发,有需要的请自行Fork修改 4 | 5 | ``` 6 | 对Gradle 7.x的支持,欢迎提PR,或者Fork自己修改(2022.06.27) 7 | ``` 8 | 9 | 极速渠道打包工具 10 | 11 | - **v2.0.1 - 2018.03.23** - 支持Android Plugin 3.x和Gradle 4.x 12 | - **v2.0.0 - 2017.06.23** - 全新发布,支持V2签名模式,包含多项优化 13 | 14 | <!-- TOC --> 15 | 16 | - [特别提示](#特别提示) 17 | - [项目介绍](#项目介绍) 18 | - [使用指南](#使用指南) 19 | - [修改项目配置](#修改项目配置) 20 | - [修改模块配置](#修改模块配置) 21 | - [插件配置示例](#插件配置示例) 22 | - [渠道列表格式](#渠道列表格式) 23 | - [集成打包](#集成打包) 24 | - [脚本打包](#脚本打包) 25 | - [代码中读取渠道](#代码中读取渠道) 26 | - [文件名格式模版](#文件名格式模版) 27 | - [其它说明](#其它说明) 28 | - [关于作者](#关于作者) 29 | - [联系方式](#联系方式) 30 | - [开源项目](#开源项目) 31 | - [License](#license) 32 | 33 | <!-- /TOC --> 34 | 35 | ## 特别提示 36 | 37 | V2版只支持`APK Signature Scheme v2`,要求在 `signingConfigs` 里 `v2SigningEnabled true` 启用新版签名模式,如果你需要使用旧版本,看这里 [v1.0.9](https://github.com/mcxiaoke/packer-ng-plugin/tree/v1.0.9)。 38 | 39 | ## 项目介绍 40 | 41 | [**packer-ng-plugin**](https://github.com/mcxiaoke/packer-ng-plugin) 是下一代Android渠道打包工具Gradle插件,支持极速打包,**100**个渠道包只需要**10**秒钟,速度是 [**gradle-packer-plugin**](https://github.com/mcxiaoke/gradle-packer-plugin) 的**300**倍以上,可方便的用于CI系统集成,同时提供命令行打包脚本,渠道读取提供Python和C语言的实现。 42 | 43 | ## 使用指南 44 | 45 | [`Maven Central`](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22packer-ng%22) 46 | 47 | ### 修改项目配置 48 | 49 | ```groovy 50 | // build.gradle 51 | buildscript { 52 | dependencies{ 53 | classpath 'com.mcxiaoke.packer-ng:plugin:2.0.1' 54 | } 55 | } 56 | ``` 57 | 58 | ### 修改模块配置 59 | 60 | ```groovy 61 | apply plugin: 'packer' 62 | // build.gradle 63 | dependencies { 64 | compile 'com.mcxiaoke.packer-ng:helper:2.0.1' 65 | } 66 | ``` 67 | 68 | **注意:`plugin` 和 `helper` 的版本号需要保持一致** 69 | 70 | ### 插件配置示例 71 | 72 | ```groovy 73 | packer { 74 | archiveNameFormat = '${buildType}-v${versionName}-${channel}' 75 | archiveOutput = new File(project.rootProject.buildDir, "apks") 76 | // channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', 77 | // 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] 78 | // channelFile = new File(project.rootDir, "markets.txt") 79 | channelMap = [ 80 | "Cat" : project.rootProject.file("channels/cat.txt"), 81 | "Dog" : project.rootProject.file("channels/dog.txt"), 82 | "Fish": project.rootProject.file("channels/channels.txt") 83 | ] 84 | } 85 | ``` 86 | 87 | * **archiveNameFormat** - 指定最终输出的渠道包文件名的格式模版,详细说明见后面,默认值是 `${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}` (可选) 88 | * **archiveOutput** - 指定最终输出的渠道包的存储位置,默认值是 `${project.buildDir}/archives` (可选) 89 | * **channelList** - 指定渠道列表,List类型,见示例 90 | * **channelMap** - 根据productFlavor指定不同的渠道列表文件,见示例 91 | * **channelFile** - 指定渠道列表文件,File类型,见示例 92 | 93 | 注意:`channelList` / `channelMap` / `channelFile` 不能同时使用,根据实际情况选择一种即可,三个属性同时存在时优先级为: `channelList` > `channelMap` > `channelFile `,另外,这三个属性会被命令行参数 `-Pchannels` 覆盖。 94 | 95 | ### 渠道列表格式 96 | 97 | 渠道名列表文件是纯文本文件,按行读取,每行一个渠道,行首和行尾的空白会被忽略,如果有注释,渠道名和注释之间用 `#` 分割。 98 | 99 | 渠道名建议尽量使用规范的**中英文和数字**,不要使用特殊字符和不可见字符。示例:[channels.txt](blob/v2dev/channels/channels.txt) 100 | 101 | ### 集成打包 102 | 103 | * 项目中没有使用 `productFlavors` 104 | 105 | ```shell 106 | ./gradlew clean apkRelease 107 | ``` 108 | 109 | * 项目中使用了 `productFlavors` 110 | 111 | 如果项目中指定了多个 `flavor` ,需要指定需要打渠道包的 `flavor` 名字,假设你有 `Paid` `Free` 两个 `flavor` ,打包的时候命令如下: 112 | 113 | ```shell 114 | ./gradlew clean apkPaidRelease 115 | ./gradlew clean apkFreeRelease 116 | ``` 117 | 118 | 直接使用 `./gradlew clean apkRelease` 会输出所有 `flavor` 的渠道包。 119 | 120 | * 通过参数直接指定渠道列表(会覆盖`build.gradle`中的属性): 121 | 122 | ```shell 123 | ./gradlew clean apkRelease -Pchannels=ch1,ch2,douban,google 124 | ``` 125 | 126 | 渠道数目很少时可以使用此种方式。 127 | 128 | * 通过参数指定渠道列表文件的位置(会覆盖`build.gradle`中的属性): 129 | 130 | ```shell 131 | ./gradlew clean apkRelease -Pchannels=@channels.txt 132 | ``` 133 | 134 | 使用@符号指定渠道列表文件的位置,使用相对于项目根目录的相对路径。 135 | 136 | * 还可以指定输出目录和文件名格式模版: 137 | 138 | ```shell 139 | ./gradlew clean apkRelease -Poutput=build/apks 140 | ./gradlew clean apkRelease -Pformat=${versionName}-${channel} 141 | ``` 142 | 143 | 这些参数 `channels` `output` `format` 可以组合使用,命令行参数会覆盖 `build.gradle` 对应的属性。 144 | 145 | * Gradle打包命令说明 146 | 147 | 渠道打包的Task名字是 `apk${flavor}${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,如果没有 `flavor` 可以忽略,使用时首字母需要大写,假设 `flavor` 是 `Paid`,`release`类型对应的任务名是 `apkPaidRelease`,`beta`类型对应的任务名是 `apkPaidBetaBeta`,其它的以此类推。 148 | 149 | * 特别提示 150 | 151 | 如果你同时使用其它的资源压缩工具或应用加固功能,请使用命令行脚本打包增加渠道信息,增加渠道信息需要放在APK处理过程的最后一步。 152 | 153 | ### 脚本打包 154 | 155 | 除了使用Gradle集成以外,还可以使用项目提供的Java脚本打包,Jar位于本项目的 `tools` 目录,请使用最新版,以下用 `packer-ng` 指代 `java -jar tools/packer-ng-2.0.1.jar`,下面是几个示例。 156 | 157 | * 参数说明: 158 | 159 | ``` 160 | packer-ng - 表示 java -jar packer-ng-2.0.1.jar 161 | channels.txt - 替换成你的渠道列表文件的实际路径 162 | build/archives - 替换成你指定的渠道包的输出路径 163 | app.apk - 替换成你要打渠道包的APK文件的实际路径 164 | ``` 165 | 166 | * 直接指定渠道列表打包: 167 | 168 | ```shell 169 | packer-ng generate --channels=ch1,ch2,ch3 --output=build/archives app.apk 170 | ``` 171 | 172 | * 指定渠道列表文件打包: 173 | 174 | ```shell 175 | packer-ng generate --channels=@channels.txt --output=build/archives app.apk 176 | ``` 177 | 178 | * 验证渠道信息: 179 | 180 | ```shell 181 | packer-ng verify app.apk 182 | ``` 183 | 184 | * 运行命令查看帮助 185 | 186 | ```shell 187 | java -jar tools/packer-ng-2.0.1.jar --help 188 | ``` 189 | 190 | * Python脚本读取渠道: 191 | 192 | ```shell 193 | python tools/packer-ng-v2.py app.apk 194 | ``` 195 | 196 | * C程序读取渠道: 197 | 198 | ```shell 199 | cd tools 200 | make 201 | make install 202 | packer app.apk 203 | ``` 204 | 205 | ### 代码中读取渠道 206 | 207 | ```java 208 | // 如果没有找到渠道信息或遇到错误,默认返回的是"" 209 | // com.mcxiaoke.packer.helper.PackerNg 210 | String channel = PackerNg.getChannel(Context) 211 | ``` 212 | 213 | ### 文件名格式模版 214 | 215 | 格式模版使用Groovy字符串模版引擎,默认文件名格式是: `${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}` 。 216 | 217 | 假如你的App包名是 `com.your.company` ,渠道名是 `Google_Play` ,`buildType` 是 `release` ,`versionName` 是 `2.1.15` ,`versionCode` 是 `200115` ,那么生成的默认APK的文件名是 `com.your.company-Google_Player-release-2.1.15-20015.apk` 。 218 | 219 | 可使用以下变量: 220 | 221 | * *projectName* - 项目名字 222 | * *appName* - App模块名字 223 | * *appPkg* - `applicationId` (App包名packageName) 224 | * *channel* - 打包时指定的渠道名 225 | * *buildType* - `buildType` (release/debug/beta等) 226 | * *flavor* - `flavor` (flavor名字,如paid/free等) 227 | * *versionName* - `versionName` (显示用的版本号) 228 | * *versionCode* - `versionCode` (内部版本号) 229 | * *buildTime* - `buildTime` (编译构建日期时间) 230 | * *fileSHA1* - `fileSHA1 ` (最终APK文件的SHA1哈希值) 231 | 232 | ------ 233 | 234 | ## 其它说明 235 | 236 | 渠道读取C语言实现使用 [GenericMakefile](https://github.com/mbcrawfo/GenericMakefile) 构建,[APK Signing Block](https://source.android.com/security/apksigning/v2) 读取和写入Java实现修改自 [apksig](https://android.googlesource.com/platform/tools/apksig/+/master) 和 [walle](https://github.com/Meituan-Dianping/walle/tree/master/payload_writer) ,特此致谢。 237 | 238 | 239 | ------ 240 | 241 | ## 关于作者 242 | 243 | ### 联系方式 244 | * Blog: <http://blog.mcxiaoke.com> 245 | * Github: <https://github.com/mcxiaoke> 246 | * Email: [packer-ng-plugin@mcxiaoke.com](mailto:packer-ng-plugin@mcxiaoke.com) 247 | 248 | ### 开源项目 249 | 250 | * Rx文档中文翻译: <https://github.com/mcxiaoke/RxDocs> 251 | * MQTT协议中文版: <https://github.com/mcxiaoke/mqtt> 252 | * Awesome-Kotlin: <https://github.com/mcxiaoke/awesome-kotlin> 253 | * Kotlin-Koi: <https://github.com/mcxiaoke/kotlin-koi> 254 | * Next公共组件库: <https://github.com/mcxiaoke/Android-Next> 255 | * Gradle渠道打包: <https://github.com/mcxiaoke/gradle-packer-plugin> 256 | * EventBus实现xBus: <https://github.com/mcxiaoke/xBus> 257 | * 蘑菇饭App: <https://github.com/mcxiaoke/minicat> 258 | 259 | ------ 260 | 261 | ## License 262 | 263 | Copyright 2014 - 2021 Xiaoke Zhang 264 | 265 | Licensed under the Apache License, Version 2.0 (the "License"); 266 | you may not use this file except in compliance with the License. 267 | You may obtain a copy of the License at 268 | 269 | http://www.apache.org/licenses/LICENSE-2.0 270 | 271 | Unless required by applicable law or agreed to in writing, software 272 | distributed under the License is distributed on an "AS IS" BASIS, 273 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 274 | See the License for the specific language governing permissions and 275 | limitations under the License. 276 | 277 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':common' 2 | include ':plugin' 3 | include ':cli' 4 | include ':helper' 5 | include ':app' 6 | -------------------------------------------------------------------------------- /test-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./deploy-local.sh 3 | echo "test clean build" 4 | ./gradlew clean assemblePaidRelease --stacktrace $1 $2 5 | -------------------------------------------------------------------------------- /test-market.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./deploy-local.sh 3 | echo "------ build for markets running..." 4 | ./gradlew -Pchannels=@channels/channels.txt clean apkRelease $1 $2 5 | echo "------ build for markets finished!" 6 | -------------------------------------------------------------------------------- /tools/build.sh: -------------------------------------------------------------------------------- 1 | # @Author: mcxiaoke 2 | # @Date: 2017-06-16 17:07:06 3 | # @Last Modified by: mcxiaoke 4 | # @Last Modified time: 2017-06-16 17:11:47 5 | #!/usr/bin/env bash 6 | cd src 7 | make && make install && make clean 8 | cd .. 9 | packer 10 | exit 11 | -------------------------------------------------------------------------------- /tools/packer-ng-2.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/packer-ng-plugin/ffbe05a2d27406f3aea574d083cded27f0742160/tools/packer-ng-2.0.1.jar -------------------------------------------------------------------------------- /tools/packer-ng-v2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author: mcxiaoke 3 | # @Date: 2017-06-06 14:03:18 4 | # @Last Modified by: mcxiaoke 5 | # @Last Modified time: 2018-03-23 15:36:57 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | import mmap 10 | import struct 11 | import zipfile 12 | import logging 13 | import time 14 | 15 | logging.basicConfig(format='%(levelname)s:%(lineno)s: %(funcName)s() %(message)s', 16 | level=logging.ERROR) 17 | logger = logging.getLogger(__name__) 18 | 19 | AUTHOR = 'mcxiaoke' 20 | VERSION = '2.0.1' 21 | try: 22 | props = dict(line.strip().split('=') for line in 23 | open('../gradle.properties') if line.strip()) 24 | VERSION = props.get('VERSION_NAME') 25 | except Exception as e: 26 | VERSION = '2.0.1' 27 | 28 | ##################################################################### 29 | 30 | 31 | # ref: https://android.googlesource.com/platform/tools/apksig/+/master 32 | # ref: https://source.android.com/security/apksigning/v2 33 | 34 | ZIP_EOCD_REC_MIN_SIZE = 22 35 | ZIP_EOCD_REC_SIG = 0x06054b50 36 | ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10 37 | ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12 38 | ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16 39 | ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20 40 | ZIP_EOCD_COMMENT_MIN_LENGTH = 0 41 | 42 | UINT16_MAX_VALUE = 0xffff # 65535 43 | 44 | BlOCK_MAX_SIZE = 0x100000 # 1m=1024k 45 | 46 | APK_SIG_BLOCK_MAGIC = 'APK Sig Block 42' 47 | APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42 48 | APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041 49 | APK_SIG_BLOCK_MIN_SIZE = 32 50 | APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a 51 | 52 | # plugin channel key 53 | PLUGIN_CHANNEL_KEY = 'CHANNEL' 54 | # plugin block id 55 | PLUGIN_BLOCK_ID = 0x7a786b21 56 | # plugin block magic 57 | PLUGIN_BLOCK_MAGIC = 'Packer Ng Sig V2' 58 | 59 | SEP_KV = '∘' 60 | SEP_LINE = '∙' 61 | 62 | ##################################################################### 63 | 64 | 65 | class ZipFormatException(Exception): 66 | '''ZipFormatException''' 67 | pass 68 | 69 | 70 | class SignatureNotFoundException(Exception): 71 | '''SignatureNotFoundException''' 72 | pass 73 | 74 | 75 | class MagicNotFoundException(Exception): 76 | '''MagicNotFoundException''' 77 | pass 78 | 79 | ##################################################################### 80 | 81 | 82 | class ByteDecoder(object): 83 | ''' 84 | byte array decoder 85 | https://docs.python.org/2/library/struct.html 86 | ''' 87 | 88 | def __init__(self, buf, littleEndian=True): 89 | self.buf = buf 90 | self.sign = '<' if littleEndian else '>' 91 | 92 | def getShort(self, offset=0): 93 | return struct.unpack('{}h'.format(self.sign), 94 | self.buf[offset:offset + 2])[0] 95 | 96 | def getUShort(self, offset=0): 97 | return struct.unpack('{}H'.format(self.sign), 98 | self.buf[offset:offset + 2])[0] 99 | 100 | def getInt(self, offset=0): 101 | return struct.unpack('{}i'.format(self.sign), 102 | self.buf[offset:offset + 4])[0] 103 | 104 | def getUInt(self, offset=0): 105 | return struct.unpack('{}I'.format(self.sign), 106 | self.buf[offset:offset + 4])[0] 107 | 108 | def getLong(self, offset=0): 109 | return struct.unpack('{}q'.format(self.sign), 110 | self.buf[offset:offset + 8])[0] 111 | 112 | def getULong(self, offset=0): 113 | return struct.unpack('{}Q'.format(self.sign), 114 | self.buf[offset:offset + 8])[0] 115 | 116 | def getFloat(self, offset=0): 117 | return struct.unpack('{}f'.format(self.sign), 118 | self.buf[offset:offset + 4])[0] 119 | 120 | def getDouble(self, offset=0): 121 | return struct.unpack('{}d'.format(self.sign), 122 | self.buf[offset:offset + 8])[0] 123 | 124 | def getChars(self, offset=0, size=16): 125 | return struct.unpack('{}{}'.format(self.sign, 's' * size), 126 | self.buf[offset:offset + size]) 127 | 128 | ##################################################################### 129 | 130 | 131 | class ZipSections(object): 132 | ''' 133 | long centralDirectoryOffset, 134 | long centralDirectorySizeBytes, 135 | int centralDirectoryRecordCount, 136 | long eocdOffset, 137 | ByteBuffer eocd 138 | ''' 139 | 140 | def __init__(self, cdStartOffset, 141 | cdSizeBytes, 142 | cdRecordCount, 143 | eocdOffset, 144 | eocd): 145 | self.cdStartOffset = cdStartOffset 146 | self.cdSizeBytes = cdSizeBytes 147 | self.cdRecordCount = cdRecordCount 148 | self.eocdOffset = eocdOffset 149 | self.eocd = eocd 150 | 151 | ##################################################################### 152 | 153 | 154 | def parseValues(content): 155 | ''' 156 | PLUGIN BLOCK LAYOUT 157 | OFFSET DATA TYPE DESCRIPTION 158 | @+0 magic string magic string 16 bytes 159 | @+16 payload length payload length int 4 bytes 160 | @+20 payload payload data bytes 161 | @-4 payload length same as @+16 4 bytes 162 | ''' 163 | magicLen = len(PLUGIN_BLOCK_MAGIC) 164 | logger.debug('content:%s', content) 165 | if not content or len(content) < magicLen + 4 * 2: 166 | return None 167 | content = content[magicLen + 4: -4] 168 | values = dict(line.split(SEP_KV) 169 | for line in content.split(SEP_LINE) if line.strip()) 170 | logger.debug('values:%s', values) 171 | return values 172 | 173 | 174 | def createMap(apk): 175 | with open(apk, "rb") as f: 176 | size = os.path.getsize(apk) 177 | offset = max(0, size - BlOCK_MAX_SIZE) 178 | length = min(size, BlOCK_MAX_SIZE) 179 | offset = offset - offset % mmap.PAGESIZE 180 | logger.debug('file size=%s', size) 181 | logger.debug('file offset=%s', offset) 182 | return mmap.mmap(f.fileno(), 183 | length=length, 184 | offset=offset, 185 | access=mmap.ACCESS_READ) 186 | 187 | 188 | def findBlockByPluginMagic(apk): 189 | mm = createMap(apk) 190 | magicLen = len(PLUGIN_BLOCK_MAGIC) 191 | start = mm.rfind(PLUGIN_BLOCK_MAGIC) 192 | if start == -1: 193 | return None 194 | d = ByteDecoder(mm) 195 | logger.debug('magic start offset=%s', start) 196 | magic = ''.join(d.getChars(start, magicLen)) 197 | logger.debug('magic start string=%s', magic) 198 | payloadLen = d.getInt(start + magicLen) 199 | logger.debug('magic payloadLen1=%s', payloadLen) 200 | 201 | end = start + magicLen + 4 + payloadLen + 4 202 | logger.debug('magic end offset=%s', end) 203 | logger.debug('magic payloadLen2=%s', d.getInt(end - 4)) 204 | 205 | block = mm[start:end] 206 | mm.close() 207 | return block 208 | 209 | 210 | def findBlockBySigningMagic(apk): 211 | # search APK Signing Block Magic words 212 | signingBlock = findBySigningMagic(apk) 213 | if signingBlock: 214 | return parseApkSigningBlock(signingBlock, PLUGIN_BLOCK_ID) 215 | 216 | 217 | def findBlockByZipSections(apk): 218 | # find zip centralDirectory, then find apkSigningBlock 219 | signingBlock = findByZipSections(apk) 220 | if signingBlock: 221 | return parseApkSigningBlock(signingBlock, PLUGIN_BLOCK_ID) 222 | 223 | 224 | def findBySigningMagic(apk): 225 | # findApkSigningBlockUsingSigningMagic 226 | mm = createMap(apk) 227 | index = mm.rfind(APK_SIG_BLOCK_MAGIC) 228 | if index == -1: 229 | raise MagicNotFoundException( 230 | 'APK Signing Block Magic not found') 231 | d = ByteDecoder(mm) 232 | logger.debug('magic index=%s', index) 233 | logger.debug('magic string=%s', ''.join(d.getChars(index, 16))) 234 | bEnd = index + 16 235 | logger.debug('block end=%s', bEnd) 236 | bSize = d.getLong(bEnd - 24) + 8 237 | logger.debug('block size=%s', bSize) 238 | bStart = bEnd - bSize 239 | logger.debug('block start=%s', bStart) 240 | block = mm[bStart:bEnd] 241 | mm.close() 242 | return block 243 | 244 | 245 | def findByZipSections(apk): 246 | # findApkSigningBlockUsingZipSections 247 | with open(apk, "rb") as f: 248 | mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) 249 | sections = findZipSections(mm) 250 | 251 | centralDirStartOffset = sections.cdStartOffset 252 | centralDirEndOffset = centralDirStartOffset + sections.cdSizeBytes 253 | eocdStartOffset = sections.eocdOffset 254 | logger.debug('centralDirStartOffset:%s', centralDirStartOffset) 255 | logger.debug('centralDirEndOffset:%s', centralDirEndOffset) 256 | logger.debug('eocdStartOffset:%s', eocdStartOffset) 257 | if centralDirEndOffset != eocdStartOffset: 258 | raise SignatureNotFoundException( 259 | "ZIP Central Directory is not " 260 | "immediately followed by " 261 | "End of Central Directory. CD end: {} eocd start: {}" 262 | .format(centralDirEndOffset, eocdStartOffset)) 263 | if centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE: 264 | raise SignatureNotFoundException( 265 | "APK too small for APK Signing Block. " 266 | "ZIP Central Directory offset:{} " 267 | .format(centralDirStartOffset)) 268 | 269 | fStart = centralDirStartOffset - 24 270 | mStart = centralDirStartOffset - 16 271 | fEnd = centralDirStartOffset 272 | logger.debug('fStart:%s', fStart) 273 | logger.debug('mStart:%s', mStart) 274 | logger.debug('fEnd:%s', fEnd) 275 | footer = mm[fStart:fEnd] 276 | footerSize = len(footer) 277 | # logger.debug('footer:%s',to_hex(footer)) 278 | fd = ByteDecoder(footer) 279 | magic = ''.join(fd.getChars(8, 16)) 280 | # logger.debug('magic str:%s', magic) 281 | lo = fd.getLong(8) 282 | hi = fd.getLong(16) 283 | logger.debug('magic lo:%s', hex(lo)) 284 | logger.debug('magic hi:%s', hex(hi)) 285 | 286 | if magic != APK_SIG_BLOCK_MAGIC: 287 | raise SignatureNotFoundException( 288 | "No APK Signing Block before ZIP Central Directory") 289 | # if lo != APK_SIG_BLOCK_MAGIC_LO or hi != APK_SIG_BLOCK_MAGIC_HI: 290 | # raise SignatureNotFoundException( 291 | # "No APK Signing Block before ZIP Central Directory") 292 | 293 | apkSigBlockSizeInFooter = fd.getLong(0) 294 | logger.debug('apkSigBlockSizeInFooter:%s', apkSigBlockSizeInFooter) 295 | 296 | if apkSigBlockSizeInFooter < footerSize or \ 297 | apkSigBlockSizeInFooter > sys.maxsize - 8: 298 | raise SignatureNotFoundException( 299 | "APK Signing Block size out of range: {}" 300 | .format(apkSigBlockSizeInFooter)) 301 | 302 | totalSize = apkSigBlockSizeInFooter + 8 303 | logger.debug('totalSize:%s', totalSize) 304 | apkSigBlockOffset = centralDirStartOffset - totalSize 305 | logger.debug('apkSigBlockOffset:%s', apkSigBlockOffset) 306 | 307 | if apkSigBlockOffset < 0: 308 | raise SignatureNotFoundException( 309 | "APK Signing Block offset out of range: " + apkSigBlockOffset) 310 | 311 | apkSigBlock = mm[apkSigBlockOffset:apkSigBlockOffset + 8] 312 | # logger.debug('apkSigBlock:%s', to_hex(apkSigBlock)) 313 | apkSigBlockSizeInHeader = ByteDecoder(apkSigBlock).getLong(0) 314 | logger.debug('apkSigBlockSizeInHeader:%s', apkSigBlockSizeInHeader) 315 | 316 | if apkSigBlockSizeInHeader != apkSigBlockSizeInFooter: 317 | raise SignatureNotFoundException( 318 | "APK Signing Block sizes in header and" 319 | "footer do not match: {} vs {}" 320 | .format(apkSigBlockSizeInHeader, apkSigBlockSizeInFooter)) 321 | 322 | block = mm[apkSigBlockOffset:apkSigBlockOffset + totalSize] 323 | mm.close() 324 | return block 325 | 326 | 327 | def parseApkSigningBlock(block, blockId): 328 | # parseApkSigningBlock 329 | if not block or not blockId: 330 | return None 331 | ''' 332 | // APK Signing Block 333 | // FORMAT: 334 | // OFFSET DATA TYPE DESCRIPTION 335 | // * @+0 bytes uint64: size in bytes(excluding this field) 336 | // * @+8 bytes payload 337 | // * @-24 bytes uint64: size in bytes(same as the one above) 338 | // * @-16 bytes uint128: magic 339 | ''' 340 | totalSize = len(block) 341 | bd0 = ByteDecoder(block) 342 | blockSizeInHeader = bd0.getULong(0) 343 | logger.debug('blockSizeInHeader:%s', blockSizeInHeader) 344 | blockSizeInFooter = bd0.getULong(totalSize - 24) 345 | logger.debug('blockSizeInFooter:%s', blockSizeInFooter) 346 | # slice only payload 347 | block = block[8:-24] 348 | bd = ByteDecoder(block) 349 | size = len(block) 350 | logger.debug('payloadSize:%s', size) 351 | 352 | entryCount = 0 353 | position = 0 354 | signingBlock = None 355 | channelBlock = None 356 | while position < size: 357 | entryCount += 1 358 | logger.debug('entryCount:%s', entryCount) 359 | if size - position < 8: 360 | raise SignatureNotFoundException( 361 | "Insufficient data to read size " 362 | "of APK Signing Block entry: {}" 363 | .format(entryCount)) 364 | lenLong = bd.getLong(position) 365 | logger.debug('lenLong:%s', lenLong) 366 | position += 8 367 | if lenLong < 4 or lenLong > sys.maxsize - 8: 368 | raise SignatureNotFoundException( 369 | "APK Signing Block entry #{} size out of range: {}" 370 | .format(entryCount, lenLong)) 371 | nextEntryPos = position + lenLong 372 | logger.debug('nextEntryPos:%s', nextEntryPos) 373 | if nextEntryPos > size: 374 | SignatureNotFoundException( 375 | "APK Signing Block entry #{}, available: {}" 376 | .format(entryCount, (size - position))) 377 | sid = bd.getInt(position) 378 | logger.debug('blockId:%s', hex(sid)) 379 | position += 4 380 | if sid == APK_SIGNATURE_SCHEME_V2_BLOCK_ID: 381 | logger.debug('found signingBlock') 382 | signingBlock = block[position:position + lenLong - 4] 383 | signingBlockSize = len(signingBlock) 384 | logger.debug('signingBlockSize:%s', signingBlockSize) 385 | # logger.debug('signingBlockHex:%s', to_hex(signingBlock[0:32])) 386 | elif sid == blockId: 387 | logger.debug('found pluginBlock') 388 | pluginBlock = block[position:position + lenLong - 4] 389 | pluginBlockSize = len(pluginBlock) 390 | logger.debug('pluginBlockSize:%s', pluginBlockSize) 391 | logger.debug('pluginBlock:%s', pluginBlock) 392 | # logger.debug('pluginBlockHex:%s', to_hex(pluginBlock)) 393 | return pluginBlock 394 | else: 395 | logger.debug('found unknown block:%s', hex(sid)) 396 | position = nextEntryPos 397 | 398 | 399 | def findZipSections(mm): 400 | eocd = findEocdRecord(mm) 401 | if not eocd: 402 | raise ZipFormatException( 403 | "ZIP End of Central Directory record not found") 404 | eocdOffset, eocdBuf = eocd 405 | ed = ByteDecoder(eocdBuf) 406 | # logger.debug('eocdBuf:%s', to_hex(eocdBuf)) 407 | cdStartOffset = ed.getUInt(ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET) 408 | logger.debug('cdStartOffset:%s', cdStartOffset) 409 | if cdStartOffset > eocdOffset: 410 | raise ZipFormatException( 411 | "ZIP Central Directory start offset out of range: {}" 412 | ". ZIP End of Central Directory offset: {}" 413 | .format(cdStartOffset, eocdOffset)) 414 | cdSizeBytes = ed.getUInt(ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET) 415 | logger.debug('cdSizeBytes:%s', cdSizeBytes) 416 | cdEndOffset = cdStartOffset + cdSizeBytes 417 | logger.debug('cdEndOffset:%s', cdEndOffset) 418 | if cdEndOffset > eocdOffset: 419 | raise ZipFormatException( 420 | "ZIP Central Directory overlaps with End of Central Directory" 421 | ". CD end: {}, EoCD start: {}" 422 | .format(cdEndOffset, eocdOffset)) 423 | cdRecordCount = ed.getUShort( 424 | ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET) 425 | logger.debug('cdRecordCount:%s', cdRecordCount) 426 | sections = ZipSections(cdStartOffset, 427 | cdSizeBytes, 428 | cdRecordCount, 429 | eocdOffset, 430 | eocdBuf) 431 | return sections 432 | 433 | 434 | def findEocdRecord(mm): 435 | fileSize = mm.size() 436 | logger.debug('fileSize:%s', fileSize) 437 | if fileSize < ZIP_EOCD_REC_MIN_SIZE: 438 | return None 439 | 440 | # 99.99% of APKs have a zero-length comment field 441 | maxCommentSize = min(UINT16_MAX_VALUE, fileSize - ZIP_EOCD_REC_MIN_SIZE) 442 | maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize 443 | logger.debug('maxCommentSize:%s', maxCommentSize) 444 | logger.debug('maxEocdSize:%s', maxEocdSize) 445 | bufOffsetInFile = fileSize - maxEocdSize 446 | logger.debug('bufOffsetInFile:%s', bufOffsetInFile) 447 | buf = mm[bufOffsetInFile:bufOffsetInFile + maxEocdSize] 448 | # logger.debug('buf:%s',to_hex(buf)) 449 | eocdOffsetInBuf = findEocdStartOffset(buf) 450 | logger.debug('eocdOffsetInBuf:%s', eocdOffsetInBuf) 451 | if eocdOffsetInBuf != -1: 452 | return bufOffsetInFile + eocdOffsetInBuf, buf[eocdOffsetInBuf:] 453 | 454 | 455 | def findEocdStartOffset(buf): 456 | archiveSize = len(buf) 457 | logger.debug('archiveSize:%s', archiveSize) 458 | maxCommentLength = min( 459 | archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE) 460 | logger.debug('maxCommentLength:%s', maxCommentLength) 461 | eocdEmptyCommentStartPos = archiveSize - ZIP_EOCD_REC_MIN_SIZE 462 | logger.debug('eocdEmptyCommentStartPos:%s', 463 | eocdEmptyCommentStartPos) 464 | expectedCommentLength = 0 465 | eocdOffsetInBuf = -1 466 | while expectedCommentLength <= maxCommentLength: 467 | eocdStartPos = eocdEmptyCommentStartPos - expectedCommentLength 468 | logger.debug('expectedCommentLength:%s', expectedCommentLength) 469 | # logger.debug('eocdStartPos:%s', eocdStartPos) 470 | seg = ByteDecoder(buf).getInt(eocdStartPos) 471 | logger.debug('seg:%s', hex(seg)) 472 | if seg == ZIP_EOCD_REC_SIG: 473 | actualCommentLength = ByteDecoder(buf).getUShort( 474 | eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET) 475 | logger.debug('actualCommentLength:%s', actualCommentLength) 476 | if actualCommentLength == expectedCommentLength: 477 | logger.debug('found eocdStartPos:%s', eocdStartPos) 478 | return eocdStartPos 479 | expectedCommentLength += 1 480 | return -1 481 | 482 | 483 | ##################################################################### 484 | 485 | 486 | def timeit(method): 487 | 488 | def timed(*args, **kw): 489 | ts = time.time() * 1000 490 | result = method(*args, **kw) 491 | te = time.time() * 1000 492 | 493 | print('%s() executed in %.2f msec' % (method.__name__, te - ts)) 494 | return result 495 | 496 | return timed 497 | 498 | 499 | def to_hex(s): 500 | return " ".join("{:02x}".format(ord(c)) for c in s) if s else "" 501 | 502 | 503 | def getChannel(apk): 504 | apk = os.path.abspath(apk) 505 | logger.debug('apk:%s', apk) 506 | try: 507 | zp = zipfile.ZipFile(apk) 508 | zp.testzip() 509 | content = findBlockByZipSections(apk) 510 | values = parseValues(content) 511 | if values: 512 | channel = values.get(PLUGIN_CHANNEL_KEY) 513 | logger.debug('channel:%s', channel) 514 | return channel 515 | else: 516 | logger.debug('channel not found') 517 | except Exception as e: 518 | logger.error('%s: %s', type(e).__name__, e) 519 | 520 | 521 | def showInfo(apk): 522 | try: 523 | from apkinfo import APK 524 | info = APK(apk) 525 | print('Package: \t{}'.format(info.get_package())) 526 | print('Version: \t{}'.format(info.get_version_name())) 527 | print('Build: \t\t{}'.format(info.get_version_code())) 528 | print('File: \t\t{}'.format(os.path.basename(apk))) 529 | print('Size: \t\t{}'.format(os.path.getsize(apk))) 530 | except Exception as e: 531 | pass 532 | 533 | 534 | def main(): 535 | logger.debug('AUTHOR:%s', AUTHOR) 536 | logger.debug('VERSION:%s', VERSION) 537 | prog = os.path.basename(sys.argv[0]) 538 | if len(sys.argv) < 2: 539 | print('Usage: {} app.apk'.format(prog)) 540 | sys.exit(1) 541 | apk = os.path.abspath(sys.argv[1]) 542 | channel = getChannel(apk) 543 | print('Channel: \t{}'.format(channel)) 544 | showInfo(apk) 545 | 546 | 547 | if __name__ == '__main__': 548 | main() 549 | -------------------------------------------------------------------------------- /tools/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.6) 2 | project(packer) 3 | 4 | set(VER_MAJOR 2) 5 | set(VER_MINOR 0) 6 | set(VER_PATCH 0) 7 | 8 | include (CheckFunctionExists) 9 | 10 | configure_file ( 11 | "${PROJECT_SOURCE_DIR}/Config.h.in" 12 | "${PROJECT_BINARY_DIR}/Config.h" 13 | ) 14 | 15 | include_directories("${PROJECT_BINARY_DIR}") 16 | 17 | aux_source_directory(. SOURCE) 18 | # add_subdirectory(math) 19 | add_executable(packer ${SOURCE}) 20 | # target_link_libraries(packer mathlib) 21 | 22 | # in sub dir CMakeLists.txt 23 | # aux_source_directory(. DIR_LIB_SRCS) 24 | # add_library (mathlib ${DIR_LIB_SRCS}) 25 | 26 | install (TARGETS packer DESTINATION bin) 27 | # install (FILES "${PROJECT_BINARY_DIR}/Config.h" DESTINATION include) -------------------------------------------------------------------------------- /tools/src/Makefile: -------------------------------------------------------------------------------- 1 | # for macos version compability 2 | export MACOSX_DEPLOYMENT_TARGET = 10.10 3 | 4 | #### PROJECT SETTINGS #### 5 | # The name of the executable to be created 6 | BIN_NAME := packer 7 | # Compiler used 8 | CC ?= gcc 9 | # Extension of source files used in the project 10 | SRC_EXT = c 11 | # Path to the source directory, relative to the makefile 12 | SRC_PATH = . 13 | # Space-separated pkg-config libraries used by this project 14 | LIBS = 15 | # General compiler flags 16 | COMPILE_FLAGS = -std=c99 -Wall -Wextra -g 17 | # Additional release-specific flags 18 | RCOMPILE_FLAGS = -D NDEBUG 19 | # Additional debug-specific flags 20 | DCOMPILE_FLAGS = -D DEBUG 21 | # Add additional include paths 22 | INCLUDES = -I $(SRC_PATH) 23 | # General linker settings 24 | LINK_FLAGS = 25 | # Additional release-specific linker settings 26 | RLINK_FLAGS = 27 | # Additional debug-specific linker settings 28 | DLINK_FLAGS = 29 | # Destination directory, like a jail or mounted system 30 | DESTDIR = / 31 | # Install path (bin/ is appended automatically) 32 | INSTALL_PREFIX = usr/local 33 | #### END PROJECT SETTINGS #### 34 | 35 | # Generally should not need to edit below this line 36 | 37 | # Obtains the OS type, either 'Darwin' (OS X) or 'Linux' 38 | UNAME_S:=$(shell uname -s) 39 | 40 | # Function used to check variables. Use on the command line: 41 | # make print-VARNAME 42 | # Useful for debugging and adding features 43 | print-%: ; @echo $*=$($*) 44 | 45 | # Shell used in this makefile 46 | # bash is used for 'echo -en' 47 | SHELL = /bin/bash 48 | # Clear built-in rules 49 | .SUFFIXES: 50 | # Programs for installation 51 | INSTALL = install 52 | INSTALL_PROGRAM = $(INSTALL) 53 | INSTALL_DATA = $(INSTALL) -m 644 54 | 55 | # Append pkg-config specific libraries if need be 56 | ifneq ($(LIBS),) 57 | COMPILE_FLAGS += $(shell pkg-config --cflags $(LIBS)) 58 | LINK_FLAGS += $(shell pkg-config --libs $(LIBS)) 59 | endif 60 | 61 | # Verbose option, to output compile and link commands 62 | export V := false 63 | export CMD_PREFIX := @ 64 | ifeq ($(V),true) 65 | CMD_PREFIX := 66 | endif 67 | 68 | # Combine compiler and linker flags 69 | release: export CFLAGS := $(CFLAGS) $(COMPILE_FLAGS) $(RCOMPILE_FLAGS) 70 | release: export LDFLAGS := $(LDFLAGS) $(LINK_FLAGS) $(RLINK_FLAGS) 71 | debug: export CFLAGS := $(CFLAGS) $(COMPILE_FLAGS) $(DCOMPILE_FLAGS) 72 | debug: export LDFLAGS := $(LDFLAGS) $(LINK_FLAGS) $(DLINK_FLAGS) 73 | 74 | # Build and output paths 75 | release: export BUILD_PATH := build/release 76 | release: export BIN_PATH := bin/release 77 | debug: export BUILD_PATH := build/debug 78 | debug: export BIN_PATH := bin/debug 79 | install: export BIN_PATH := bin/release 80 | 81 | # Find all source files in the source directory, sorted by most 82 | # recently modified 83 | ifeq ($(UNAME_S),Darwin) 84 | SOURCES = $(shell find $(SRC_PATH) -name '*.$(SRC_EXT)' | sort -k 1nr | cut -f2-) 85 | else 86 | SOURCES = $(shell find $(SRC_PATH) -name '*.$(SRC_EXT)' -printf '%T@\t%p\n' \ 87 | | sort -k 1nr | cut -f2-) 88 | endif 89 | 90 | # fallback in case the above fails 91 | rwildcard = $(foreach d, $(wildcard $1*), $(call rwildcard,$d/,$2) \ 92 | $(filter $(subst *,%,$2), $d)) 93 | ifeq ($(SOURCES),) 94 | SOURCES := $(call rwildcard, $(SRC_PATH), *.$(SRC_EXT)) 95 | endif 96 | 97 | # Set the object file names, with the source directory stripped 98 | # from the path, and the build path prepended in its place 99 | OBJECTS = $(SOURCES:$(SRC_PATH)/%.$(SRC_EXT)=$(BUILD_PATH)/%.o) 100 | # Set the dependency files that will be used to add header dependencies 101 | DEPS = $(OBJECTS:.o=.d) 102 | 103 | # Macros for timing compilation 104 | ifeq ($(UNAME_S),Darwin) 105 | CUR_TIME = awk 'BEGIN{srand(); print srand()}' 106 | TIME_FILE = $(dir $@).$(notdir $@)_time 107 | START_TIME = $(CUR_TIME) > $(TIME_FILE) 108 | END_TIME = read st < $(TIME_FILE) ; \ 109 | $(RM) $(TIME_FILE) ; \ 110 | st=$((`$(CUR_TIME)` - $st)) ; \ 111 | echo $st 112 | else 113 | TIME_FILE = $(dir $@).$(notdir $@)_time 114 | START_TIME = date '+%s' > $(TIME_FILE) 115 | END_TIME = read st < $(TIME_FILE) ; \ 116 | $(RM) $(TIME_FILE) ; \ 117 | st=$((`date '+%s'` - $st - 86400)) ; \ 118 | echo `date -u -d @$st '+%H:%M:%S'` 119 | endif 120 | 121 | # Version macros 122 | # Comment/remove this section to remove versioning 123 | USE_VERSION := false 124 | # If this isn't a git repo or the repo has no tags, git describe will return non-zero 125 | ifeq ($(shell git describe > /dev/null 2>&1 ; echo $?), 0) 126 | USE_VERSION := true 127 | VERSION := $(shell git describe --tags --long --dirty --always | \ 128 | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)-\?.*-\([0-9]*\)-\(.*\)/\1 \2 \3 \4 \5/g') 129 | VERSION_MAJOR := $(word 1, $(VERSION)) 130 | VERSION_MINOR := $(word 2, $(VERSION)) 131 | VERSION_PATCH := $(word 3, $(VERSION)) 132 | VERSION_REVISION := $(word 4, $(VERSION)) 133 | VERSION_HASH := $(word 5, $(VERSION)) 134 | VERSION_STRING := \ 135 | "$(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_PATCH).$(VERSION_REVISION)-$(VERSION_HASH)" 136 | override CFLAGS := $(CFLAGS) \ 137 | -D VERSION_MAJOR=$(VERSION_MAJOR) \ 138 | -D VERSION_MINOR=$(VERSION_MINOR) \ 139 | -D VERSION_PATCH=$(VERSION_PATCH) \ 140 | -D VERSION_REVISION=$(VERSION_REVISION) \ 141 | -D VERSION_HASH=\"$(VERSION_HASH)\" 142 | endif 143 | 144 | # Standard, non-optimized release build 145 | .PHONY: release 146 | release: dirs 147 | ifeq ($(USE_VERSION), true) 148 | @echo "Beginning release build v$(VERSION_STRING)" 149 | else 150 | @echo "Beginning release build" 151 | endif 152 | @$(START_TIME) 153 | @$(MAKE) all --no-print-directory 154 | @echo -n "Total build time: " 155 | @$(END_TIME) 156 | 157 | # Debug build for gdb debugging 158 | .PHONY: debug 159 | debug: dirs 160 | ifeq ($(USE_VERSION), true) 161 | @echo "Beginning debug build v$(VERSION_STRING)" 162 | else 163 | @echo "Beginning debug build" 164 | endif 165 | @$(START_TIME) 166 | @$(MAKE) all --no-print-directory 167 | @echo -n "Total build time: " 168 | @$(END_TIME) 169 | 170 | # Create the directories used in the build 171 | .PHONY: dirs 172 | dirs: 173 | @echo "Creating directories" 174 | @mkdir -p $(dir $(OBJECTS)) 175 | @mkdir -p $(BIN_PATH) 176 | 177 | # Installs to the set path 178 | .PHONY: install 179 | install: 180 | @echo "Installing to $(DESTDIR)$(INSTALL_PREFIX)/bin" 181 | @$(INSTALL_PROGRAM) $(BIN_PATH)/$(BIN_NAME) $(DESTDIR)$(INSTALL_PREFIX)/bin 182 | 183 | # Uninstalls the program 184 | .PHONY: uninstall 185 | uninstall: 186 | @echo "Removing $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BIN_NAME)" 187 | @$(RM) $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BIN_NAME) 188 | 189 | # Removes all build files 190 | .PHONY: clean 191 | clean: 192 | @echo "Deleting $(BIN_NAME) symlink" 193 | @$(RM) $(BIN_NAME) 194 | @echo "Deleting directories" 195 | @$(RM) -r build 196 | @$(RM) -r bin 197 | 198 | # Main rule, checks the executable and symlinks to the output 199 | all: $(BIN_PATH)/$(BIN_NAME) 200 | @echo "Making symlink: $(BIN_NAME) -> lt;" 201 | @$(RM) $(BIN_NAME) 202 | @ln -s $(BIN_PATH)/$(BIN_NAME) $(BIN_NAME) 203 | 204 | # Link the executable 205 | $(BIN_PATH)/$(BIN_NAME): $(OBJECTS) 206 | @echo "Linking: $@" 207 | @$(START_TIME) 208 | $(CMD_PREFIX)$(CC) $(OBJECTS) $(LDFLAGS) -o $@ 209 | @echo -en "\t Link time: " 210 | @$(END_TIME) 211 | 212 | # Add dependency files, if they exist 213 | -include $(DEPS) 214 | 215 | # Source file rules 216 | # After the first compilation they will be joined with the rules from the 217 | # dependency files to provide header dependencies 218 | $(BUILD_PATH)/%.o: $(SRC_PATH)/%.$(SRC_EXT) 219 | @echo "Compiling: lt; -> $@" 220 | @$(START_TIME) 221 | $(CMD_PREFIX)$(CC) $(CFLAGS) $(INCLUDES) -MP -MMD -c lt; -o $@ 222 | @echo -en "\t Compile time: " 223 | @$(END_TIME) 224 | -------------------------------------------------------------------------------- /tools/src/read.c: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: mcxiaoke 3 | * @Date: 2017-06-13 15:47:02 4 | * @Last Modified by: mcxiaoke 5 | * @Last Modified time: 2017-06-13 18:23:41 6 | */ 7 | //#include "config.h" 8 | #include <fcntl.h> 9 | #include <stdint.h> 10 | #include <stdio.h> 11 | #include <stdlib.h> 12 | #include <string.h> 13 | #include <sys/mman.h> 14 | #include <sys/stat.h> 15 | #include <time.h> 16 | #include <unistd.h> 17 | 18 | /* 19 | * http://man7.org/linux/man-pages/man2/mmap.2.html 20 | * https://en.wikipedia.org/wiki/Mmap 21 | */ 22 | 23 | static const char *apk_ext = ".apk"; 24 | static const off_t block_size = 0x100000; 25 | static const char *sep_kv = "∘"; 26 | static const char *sep_line = "∙"; 27 | static const char *magic = "Packer Ng Sig V2"; 28 | // static const char *key = "CHANNEL"; 29 | // static const char *version = "v2.0.0"; 30 | 31 | #define handle_error(msg) \ 32 | do { \ 33 | printf(msg); \ 34 | exit(EXIT_FAILURE); \ 35 | } while (0) 36 | 37 | #define handle_not_found() \ 38 | do { \ 39 | printf("Channel not found\n"); \ 40 | exit(EXIT_FAILURE); \ 41 | } while (0) 42 | 43 | /* find the overlap array for the given pattern */ 44 | void find_overlap(const char *word, size_t wlen, int *ptr) { 45 | size_t i = 2, j = 0, len = wlen; 46 | ptr[0] = -1; 47 | ptr[1] = 0; 48 | 49 | while (i < len) { 50 | if (word[i - 1] == word[j]) { 51 | j = j + 1; 52 | ptr[i] = j; 53 | i = i + 1; 54 | } else if (j > 0) { 55 | j = ptr[j]; 56 | } else { 57 | ptr[i] = 0; 58 | i = i + 1; 59 | } 60 | } 61 | return; 62 | } 63 | 64 | /* 65 | * finds the position of the pattern in the given target string 66 | * target - str, patter - word 67 | */ 68 | int32_t kmp_search(const char *str, int slen, const char *word, int wlen) { 69 | // printf("kmp_search() slen=%zu, wlen=%zu\n", slen, wlen); 70 | int *ptr = (int *)calloc(1, sizeof(int) * (strlen(magic))); 71 | find_overlap(magic, strlen(magic), ptr); 72 | int32_t i = 0, j = 0; 73 | 74 | while ((i + j) < slen) { 75 | /* match found on the target and pattern string char */ 76 | if (word[j] == str[i + j]) { 77 | if (j == (wlen - 1)) { 78 | return i + 1; 79 | } 80 | j = j + 1; 81 | } else { 82 | /* manipulating next indices to compare */ 83 | i = i + j - ptr[j]; 84 | if (ptr[j] > -1) { 85 | j = ptr[j]; 86 | } else { 87 | j = 0; 88 | } 89 | } 90 | } 91 | return -1; 92 | } 93 | 94 | int str_has_suffix(const char *str, const char *suf) { 95 | const char *a = str + strlen(str); 96 | const char *b = suf + strlen(suf); 97 | while (a != str && b != suf) { 98 | if (*--a != *--b) 99 | break; 100 | } 101 | return b == suf && *a == *b; 102 | } 103 | 104 | // ensure write '\0' at end 105 | // http://en.cppreference.com/w/c/string/byte/strncpy 106 | char *strncpy_2(char *dest, const char *src, size_t count) { 107 | char *ret = strncpy(dest, src, count); 108 | dest[count] = '\0'; 109 | return ret; 110 | } 111 | 112 | int main(int argc, char *argv[]) { 113 | char *addr; 114 | int fd; 115 | struct stat sb; 116 | off_t offset, pa_offset; 117 | size_t length; 118 | 119 | if (argc < 2) { 120 | // printf("Version: %d.%d.%d\n", VER_MAJOR, VER_MINOR, VER_PATCH); 121 | printf("Usage: %s app.apk (show apk channel)\n", argv[0]); 122 | exit(EXIT_FAILURE); 123 | } 124 | char *fn = argv[1]; 125 | // printf("file name: %s\n", fn); 126 | if (!str_has_suffix(fn, apk_ext)) { 127 | handle_error("Not apk file\n"); 128 | } 129 | fd = open(fn, O_RDONLY); 130 | if (fd == -1) { 131 | handle_error("No such file\n"); 132 | } 133 | if (fstat(fd, &sb) == -1) { 134 | handle_error("Can not read"); 135 | } 136 | // printf("file mode=%d\n", sb.st_mode); 137 | if (!S_ISREG(sb.st_mode)) { 138 | handle_error("Not regular file\n"); 139 | } 140 | if (sb.st_size < block_size) { 141 | offset = 0; 142 | } else { 143 | offset = sb.st_size - block_size; 144 | } 145 | pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1); 146 | /* offset for mmap() must be page aligned */ 147 | length = sb.st_size - offset; 148 | // printf("mmap file size=%zu\n", length); 149 | size_t pa_length = length + offset - pa_offset; 150 | // printf("mmap real size=%zu\n", pa_length); 151 | // printf("mmap real offset=%lld\n", pa_offset); 152 | addr = mmap(NULL, pa_length, PROT_READ, MAP_PRIVATE, fd, pa_offset); 153 | if (addr == MAP_FAILED) { 154 | handle_error("Can not mmap\n"); 155 | } 156 | 157 | int32_t index = kmp_search(addr, pa_length, magic, strlen(magic)); 158 | if (index == -1) { 159 | handle_not_found(); 160 | } 161 | // printf("magic index=%d\n", index); 162 | int32_t li = index + strlen(magic) - 1; 163 | // printf("magic lenindex=%d\n", li); 164 | int32_t payload_len; 165 | memcpy(&payload_len, &addr[li], 4); 166 | // printf("payload_len=%d\n", payload_len); 167 | if (payload_len < 0 || payload_len > block_size) { 168 | handle_not_found(); 169 | } 170 | // char *payload = malloc(payload_len + 1); 171 | char payload[payload_len + 1]; 172 | strncpy_2(payload, &addr[li + 4], payload_len); 173 | // payload[payload_len] = '\0'; 174 | // printf("payload=%s\n", payload); 175 | char *pos_start = strstr(payload, sep_kv); 176 | char *pos_end = strstr(payload, sep_line); 177 | if (pos_start == NULL || pos_end == NULL) { 178 | handle_not_found(); 179 | } 180 | size_t c_start = pos_start - payload + strlen(sep_kv); 181 | size_t c_end = pos_end - payload; 182 | size_t c_len = c_end - c_start; 183 | // printf("c_start=%zu, c_end=%zu, clen=%zu\n", c_start, c_end, clen); 184 | // char *channel = malloc(clen + 1); 185 | char channel[c_len + 1]; 186 | strncpy_2(channel, &payload[c_start], c_len); 187 | // channel[c_len] = '\0'; 188 | printf("%s\n", channel); 189 | // free(payload); 190 | // free(channel); 191 | munmap(addr, pa_length); 192 | close(fd); 193 | exit(EXIT_SUCCESS); 194 | } --------------------------------------------------------------------------------