The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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 =&gt; [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 &gt; 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 | }


--------------------------------------------------------------------------------