├── .gitignore ├── ApkMultiChannelPlugin.jar ├── LICENSE ├── README.md ├── apksig └── src │ └── com │ └── android │ └── apksig │ ├── ApkSigner.java │ ├── ApkSignerEngine.java │ ├── ApkVerifier.java │ ├── DefaultApkSignerEngine.java │ ├── apk │ ├── AndroidBinXmlParser.java │ ├── ApkFormatException.java │ ├── ApkUtils.java │ ├── CodenameMinSdkVersionException.java │ └── MinSdkVersionException.java │ ├── internal │ ├── apk │ │ ├── v1 │ │ │ ├── DigestAlgorithm.java │ │ │ ├── V1SchemeSigner.java │ │ │ └── V1SchemeVerifier.java │ │ └── v2 │ │ │ ├── ContentDigestAlgorithm.java │ │ │ ├── SignatureAlgorithm.java │ │ │ ├── V2SchemeSigner.java │ │ │ └── V2SchemeVerifier.java │ ├── jar │ │ ├── ManifestParser.java │ │ ├── ManifestWriter.java │ │ └── SignatureFileWriter.java │ ├── util │ │ ├── AndroidSdkVersion.java │ │ ├── ByteArrayDataSink.java │ │ ├── ByteArrayOutputStreamSink.java │ │ ├── ByteBufferDataSource.java │ │ ├── ByteBufferSink.java │ │ ├── DelegatingX509Certificate.java │ │ ├── InclusiveIntRange.java │ │ ├── MessageDigestSink.java │ │ ├── OutputStreamDataSink.java │ │ ├── Pair.java │ │ ├── RandomAccessFileDataSink.java │ │ └── RandomAccessFileDataSource.java │ └── zip │ │ ├── CentralDirectoryRecord.java │ │ ├── EocdRecord.java │ │ ├── LocalFileRecord.java │ │ └── ZipUtils.java │ ├── util │ ├── DataSink.java │ ├── DataSinks.java │ ├── DataSource.java │ ├── DataSources.java │ └── ReadableDataSink.java │ └── zip │ └── ZipFormatException.java ├── art ├── build-setting.png ├── choose-apk.png ├── output.png ├── properties.png └── setting.png ├── library └── bcpkix-jdk15on-1.48.jar ├── resources ├── META-INF │ └── plugin.xml └── icons │ └── plugin.png └── src └── com └── github └── nukc └── plugin ├── MainAction.java ├── SettingAction.java ├── axml ├── ChannelEditor.java ├── decode │ ├── AXMLDoc.java │ ├── BNSNode.java │ ├── BTXTNode.java │ ├── BTagNode.java │ ├── BXMLNode.java │ ├── BXMLTree.java │ ├── IAXMLSerialize.java │ ├── IVisitable.java │ ├── IVisitor.java │ ├── IntReader.java │ ├── IntWriter.java │ ├── ResBlock.java │ ├── StringBlock.java │ └── XMLVisitor.java └── utils │ ├── CSString.java │ ├── Cast.java │ ├── Pair.java │ └── TypedValue.java ├── helper ├── ApkHelper.java ├── BuildHelper.java ├── CommandHelper.java ├── NotificationHelper.java ├── OptionsHelper.java └── ZipHelper.java ├── model └── Options.java └── ui ├── OptionForm.form └── OptionForm.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | out/ 4 | projectFilesBackup/ 5 | *.iml 6 | test/ -------------------------------------------------------------------------------- /ApkMultiChannelPlugin.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukc/ApkMultiChannelPlugin/28b389a37df3d7fcaf4a42987eecf0e32dd2de07/ApkMultiChannelPlugin.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApkMultiChannel Plugin 2 | 3 | 这是一个为了方便 Android 多渠道打包的 Android Studio / IDEA 插件: 4 | - 支持v2签名:jarsigner 和 apksigner 5 | - 支持2种渠道打包方式: 6 | - 修改 AndroidManifest.xml 的 meta-data (name 为 UMENG_CHANNEL) 的 value 值,没有则插入,然后重新打包签名 7 | - 美团的方式,直接往 apk 的 META-INF 目录里添加空文件(文件名为:c_渠道) 8 | - packer-ng-plugin的方式,往 apk 注释字段保存渠道号和验证标记字节 9 | 10 | ## 安装 11 | 12 | - 打开 Android Studio: 打开 ```Setting/Preferences -> Plugins -> Browse repositories``` 13 | 然后搜索 ```ApkMultiChannel``` 安装重启 14 | 15 | 或者 16 | 17 | - 下载 [ApkMultiChannelPlugin.jar](https://plugins.jetbrains.com/idea/plugin/9369) 18 | 然后 ```Setting/Preferences -> Plugins -> Install plugin from disk``` 选择 ```ApkMultiChannelPlugin.jar``` 安装重启 19 | 20 | 21 | ## 使用方式 22 | 23 | #### 1. 选择 apk 24 | 25 | 选择一个 apk 然后右键,点击 Build MultiChannel 26 | 27 | 28 | 29 | #### 2. 配置 30 | 31 | 配置签名信息,打包方式和渠道等 32 | 33 | 34 | 35 | 配置说明: 36 | 37 | **Key Store Path:** 签名文件的路径
38 | **Key Store Password:** 签名文件的密码
39 | **Key Alias:** 密钥别名
40 | **Key Password:** 密钥密码
41 | 42 | **Zipalign Path:** zipalign 文件的路径(用于优化 apk;zipalign 可以确保所有未压缩的数据均是以相对于文件开始部分的特定字节对齐开始,这样可减少应用消耗的 RAM 量。)
43 | **Signer Version:** 选择签名版本:apksigner 和 jarsigner
44 | **Build Type:** 打包方式
45 | 46 | **Channels:** 渠道列表,每行一个,最前面可加 ```>``` 或不加(保存信息的时候,程序会自行加上) 47 | 48 | #### 3. 开始打包 49 | 50 | 配置完成之后按 OK 就会开始进行渠道打包,文件会输出在选中的apk的当前目录下的channels目录中 51 | 52 | 53 | 54 | 55 | ## 说明 56 | 57 | 在开始打包前,配置信息先会保存在项目根目录的 channels.properties: 58 | 59 | 60 | 61 | 渠道可以直接在 channels.properties 添加(这里必须要在前面加 ```>```,为了方便解析,只有加了 ```>``` 才会参与打包),也可以删除和注释(#) 62 | 63 | 配置完后,想改配置但是又不想直接修改 channels.properties 文件的话,可以在 ```Build > Channel Setting``` 打开配置界面, 64 | 在这里打开配置界面按 OK 按钮后不会自动开始打包,仅用于修改配置。 65 | 66 | ### Build Type 说明 67 | 68 | #### update AndroidManifest.xml 69 | 先解压提取 AndroidManifest.xml 文件到 temp 文件夹中,然后根据渠道列表配置修改 meta-data,复制1个 apk 到 channels 文件夹下(除了 META-INF 目录下的签名文件), 70 | 同时替换 AndroidManifest.xml,最后重新签名。 71 | 72 | #### add channel file to META-INF 73 | 复制1个 apk,先检查签名版本,如果未签名则进行签名(配置选择 jarsigner 则在渠道打包前签名,apksigner 则是添加空文件到其 META-INF 目录后再签名)。 74 | 读取渠道:[ChannelHelper](https://gist.github.com/nukc/f777b54232be56f04171bcef56a627e1) 75 | 76 | #### write zip comment 77 | 先判断选中的 apk 中 comment 是否含有 SIGN 字节,如果有则不进行渠道打包并提示;之后检查是否是 v2 签名,如果是 v2,则复制1个不带签名文件的 apk 到 temp 文件夹并重新签名为 v1, 78 | 最后开始渠道打包。( v2 签名之后不能修改 ```ZIP End of Central Directory``` 区块,而且如果先修改 ```ZIP End of Central Directory``` 区块再 v2 签名,comment 会被抹掉。) 79 | 读取渠道:[ZipCommentHelper](https://gist.github.com/nukc/f762f73276c4ad02618147acd6978d16) 80 | 81 | ## 以后要加的功能 82 | 83 | - 添加支持选择项目路径外的 apk 文件进行多渠道打包 84 | - buildType 添加支持美团新一代渠道包生成方式 Walle 85 | 86 | 有什么问题欢迎大家在 [Issues](https://github.com/nukc/ApkMultiChannelPlugin/issues) 中提问 87 | 88 | ## 参考致谢 89 | 90 | - [ntop001/AXMLEditor](https://github.com/ntop001/AXMLEditor) 91 | - [Bilibili/apk-channelization](https://github.com/Bilibili/apk-channelization) 92 | - [美团Android自动化之旅—生成渠道包](http://tech.meituan.com/mt-apk-packaging.html) 93 | - [apksigner](https://developer.android.com/studio/command-line/apksigner.html) 94 | - [packer-ng-plugin](https://github.com/mcxiaoke/packer-ng-plugin) 95 | - [新一代开源Android渠道包生成工具Walle](http://tech.meituan.com/android-apk-v2-signature-scheme.html) 96 | - [apksig](https://android.googlesource.com/platform/tools/apksig/) 97 | 98 | 同时感谢 [dim](https://github.com/zzz40500) 和 [区长](https://github.com/lizhangqu) 的指点迷津。 99 | 100 | ## License 101 | 102 | GPL-3.0 -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/apk/ApkFormatException.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.android.apksig.apk; 18 | 19 | /** 20 | * Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a 21 | * well-formed ZIP archive, in which case {@link #getCause()} will return a 22 | * {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains 23 | * multiple ZIP entries with the same name. 24 | */ 25 | public class ApkFormatException extends Exception { 26 | private static final long serialVersionUID = 1L; 27 | 28 | public ApkFormatException(String message) { 29 | super(message); 30 | } 31 | 32 | public ApkFormatException(String message, Throwable cause) { 33 | super(message, cause); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/apk/CodenameMinSdkVersionException.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.android.apksig.apk; 18 | 19 | /** 20 | * Indicates that there was an issue determining the minimum Android platform version supported by 21 | * an APK because the version is specified as a codename, rather than as API Level number, and the 22 | * codename is in an unexpected format. 23 | */ 24 | public class CodenameMinSdkVersionException extends MinSdkVersionException { 25 | 26 | private static final long serialVersionUID = 1L; 27 | 28 | /** Encountered codename. */ 29 | private final String mCodename; 30 | 31 | /** 32 | * Constructs a new {@code MinSdkVersionCodenameException} with the provided message and 33 | * codename. 34 | */ 35 | public CodenameMinSdkVersionException(String message, String codename) { 36 | super(message); 37 | mCodename = codename; 38 | } 39 | 40 | /** 41 | * Returns the codename. 42 | */ 43 | public String getCodename() { 44 | return mCodename; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/apk/MinSdkVersionException.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.android.apksig.apk; 18 | 19 | /** 20 | * Indicates that there was an issue determining the minimum Android platform version supported by 21 | * an APK. 22 | */ 23 | public class MinSdkVersionException extends ApkFormatException { 24 | 25 | private static final long serialVersionUID = 1L; 26 | 27 | /** 28 | * Constructs a new {@code MinSdkVersionException} with the provided message. 29 | */ 30 | public MinSdkVersionException(String message) { 31 | super(message); 32 | } 33 | 34 | /** 35 | * Constructs a new {@code MinSdkVersionException} with the provided message and cause. 36 | */ 37 | public MinSdkVersionException(String message, Throwable cause) { 38 | super(message, cause); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/apk/v1/DigestAlgorithm.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.android.apksig.internal.apk.v1; 18 | 19 | import java.util.Comparator; 20 | 21 | /** 22 | * Digest algorithm used with JAR signing (aka v1 signing scheme). 23 | */ 24 | public enum DigestAlgorithm { 25 | /** SHA-1 */ 26 | SHA1("SHA-1"), 27 | 28 | /** SHA2-256 */ 29 | SHA256("SHA-256"); 30 | 31 | private final String mJcaMessageDigestAlgorithm; 32 | 33 | private DigestAlgorithm(String jcaMessageDigestAlgoritm) { 34 | mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm; 35 | } 36 | 37 | /** 38 | * Returns the {@link java.security.MessageDigest} algorithm represented by this digest 39 | * algorithm. 40 | */ 41 | String getJcaMessageDigestAlgorithm() { 42 | return mJcaMessageDigestAlgorithm; 43 | } 44 | 45 | public static Comparator BY_STRENGTH_COMPARATOR = new StrengthComparator(); 46 | 47 | private static class StrengthComparator implements Comparator { 48 | @Override 49 | public int compare(DigestAlgorithm a1, DigestAlgorithm a2) { 50 | switch (a1) { 51 | case SHA1: 52 | switch (a2) { 53 | case SHA1: 54 | return 0; 55 | case SHA256: 56 | return -1; 57 | } 58 | throw new RuntimeException("Unsupported algorithm: " + a2); 59 | 60 | case SHA256: 61 | switch (a2) { 62 | case SHA1: 63 | return 1; 64 | case SHA256: 65 | return 0; 66 | } 67 | throw new RuntimeException("Unsupported algorithm: " + a2); 68 | 69 | default: 70 | throw new RuntimeException("Unsupported algorithm: " + a1); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/apk/v2/ContentDigestAlgorithm.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.android.apksig.internal.apk.v2; 18 | 19 | /** 20 | * APK Signature Scheme v2 content digest algorithm. 21 | */ 22 | public enum ContentDigestAlgorithm { 23 | /** SHA2-256 over 1 MB chunks. */ 24 | CHUNKED_SHA256("SHA-256", 256 / 8), 25 | 26 | /** SHA2-512 over 1 MB chunks. */ 27 | CHUNKED_SHA512("SHA-512", 512 / 8); 28 | 29 | private final String mJcaMessageDigestAlgorithm; 30 | private final int mChunkDigestOutputSizeBytes; 31 | 32 | private ContentDigestAlgorithm( 33 | String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) { 34 | mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm; 35 | mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes; 36 | } 37 | 38 | /** 39 | * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of 40 | * chunks by this content digest algorithm. 41 | */ 42 | String getJcaMessageDigestAlgorithm() { 43 | return mJcaMessageDigestAlgorithm; 44 | } 45 | 46 | /** 47 | * Returns the size (in bytes) of the digest of a chunk of content. 48 | */ 49 | int getChunkDigestOutputSizeBytes() { 50 | return mChunkDigestOutputSizeBytes; 51 | } 52 | } -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/apk/v2/SignatureAlgorithm.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.android.apksig.internal.apk.v2; 18 | 19 | import com.android.apksig.internal.util.Pair; 20 | import java.security.spec.AlgorithmParameterSpec; 21 | import java.security.spec.MGF1ParameterSpec; 22 | import java.security.spec.PSSParameterSpec; 23 | 24 | /** 25 | * APK Signature Scheme v2 signature algorithm. 26 | */ 27 | public enum SignatureAlgorithm { 28 | /** 29 | * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content 30 | * digested using SHA2-256 in 1 MB chunks. 31 | */ 32 | RSA_PSS_WITH_SHA256( 33 | 0x0101, 34 | ContentDigestAlgorithm.CHUNKED_SHA256, 35 | "RSA", 36 | Pair.of("SHA256withRSA/PSS", 37 | new PSSParameterSpec( 38 | "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1))), 39 | 40 | /** 41 | * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content 42 | * digested using SHA2-512 in 1 MB chunks. 43 | */ 44 | RSA_PSS_WITH_SHA512( 45 | 0x0102, 46 | ContentDigestAlgorithm.CHUNKED_SHA512, 47 | "RSA", 48 | Pair.of( 49 | "SHA512withRSA/PSS", 50 | new PSSParameterSpec( 51 | "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1))), 52 | 53 | /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ 54 | RSA_PKCS1_V1_5_WITH_SHA256( 55 | 0x0103, 56 | ContentDigestAlgorithm.CHUNKED_SHA256, 57 | "RSA", 58 | Pair.of("SHA256withRSA", null)), 59 | 60 | /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ 61 | RSA_PKCS1_V1_5_WITH_SHA512( 62 | 0x0104, 63 | ContentDigestAlgorithm.CHUNKED_SHA512, 64 | "RSA", 65 | Pair.of("SHA512withRSA", null)), 66 | 67 | /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ 68 | ECDSA_WITH_SHA256( 69 | 0x0201, 70 | ContentDigestAlgorithm.CHUNKED_SHA256, 71 | "EC", 72 | Pair.of("SHA256withECDSA", null)), 73 | 74 | /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ 75 | ECDSA_WITH_SHA512( 76 | 0x0202, 77 | ContentDigestAlgorithm.CHUNKED_SHA512, 78 | "EC", 79 | Pair.of("SHA512withECDSA", null)), 80 | 81 | /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ 82 | DSA_WITH_SHA256( 83 | 0x0301, 84 | ContentDigestAlgorithm.CHUNKED_SHA256, 85 | "DSA", 86 | Pair.of("SHA256withDSA", null)); 87 | 88 | private final int mId; 89 | private final String mJcaKeyAlgorithm; 90 | private final ContentDigestAlgorithm mContentDigestAlgorithm; 91 | private final Pair mJcaSignatureAlgAndParams; 92 | 93 | private SignatureAlgorithm(int id, 94 | ContentDigestAlgorithm contentDigestAlgorithm, 95 | String jcaKeyAlgorithm, 96 | Pair jcaSignatureAlgAndParams) { 97 | mId = id; 98 | mContentDigestAlgorithm = contentDigestAlgorithm; 99 | mJcaKeyAlgorithm = jcaKeyAlgorithm; 100 | mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams; 101 | } 102 | 103 | /** 104 | * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format. 105 | */ 106 | int getId() { 107 | return mId; 108 | } 109 | 110 | /** 111 | * Returns the content digest algorithm associated with this signature algorithm. 112 | */ 113 | ContentDigestAlgorithm getContentDigestAlgorithm() { 114 | return mContentDigestAlgorithm; 115 | } 116 | 117 | /** 118 | * Returns the JCA {@link java.security.Key} algorithm used by this signature scheme. 119 | */ 120 | String getJcaKeyAlgorithm() { 121 | return mJcaKeyAlgorithm; 122 | } 123 | 124 | /** 125 | * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec} 126 | * (or null if not needed) to parameterize the {@code Signature}. 127 | */ 128 | Pair getJcaSignatureAlgorithmAndParams() { 129 | return mJcaSignatureAlgAndParams; 130 | } 131 | 132 | static SignatureAlgorithm findById(int id) { 133 | for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { 134 | if (alg.getId() == id) { 135 | return alg; 136 | } 137 | } 138 | 139 | return null; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/jar/ManifestParser.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.android.apksig.internal.jar; 18 | 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.jar.Attributes; 25 | 26 | /** 27 | * JAR manifest and signature file parser. 28 | * 29 | *

These files consist of a main section followed by individual sections. Individual sections 30 | * are named, their names referring to JAR entries. 31 | * 32 | * @see JAR Manifest format 33 | */ 34 | public class ManifestParser { 35 | 36 | private final byte[] mManifest; 37 | private int mOffset; 38 | private int mEndOffset; 39 | 40 | private byte[] mBufferedLine; 41 | 42 | /** 43 | * Constructs a new {@code ManifestParser} with the provided input. 44 | */ 45 | public ManifestParser(byte[] data) { 46 | this(data, 0, data.length); 47 | } 48 | 49 | /** 50 | * Constructs a new {@code ManifestParser} with the provided input. 51 | */ 52 | public ManifestParser(byte[] data, int offset, int length) { 53 | mManifest = data; 54 | mOffset = offset; 55 | mEndOffset = offset + length; 56 | } 57 | 58 | /** 59 | * Returns the remaining sections of this file. 60 | */ 61 | public List

readAllSections() { 62 | List
sections = new ArrayList<>(); 63 | Section section; 64 | while ((section = readSection()) != null) { 65 | sections.add(section); 66 | } 67 | return sections; 68 | } 69 | 70 | /** 71 | * Returns the next section from this file or {@code null} if end of file has been reached. 72 | */ 73 | public Section readSection() { 74 | // Locate the first non-empty line 75 | int sectionStartOffset; 76 | String attr; 77 | do { 78 | sectionStartOffset = mOffset; 79 | attr = readAttribute(); 80 | if (attr == null) { 81 | return null; 82 | } 83 | } while (attr.length() == 0); 84 | List attrs = new ArrayList<>(); 85 | attrs.add(parseAttr(attr)); 86 | 87 | // Read attributes until end of section reached 88 | while (true) { 89 | attr = readAttribute(); 90 | if ((attr == null) || (attr.length() == 0)) { 91 | // End of section 92 | break; 93 | } 94 | attrs.add(parseAttr(attr)); 95 | } 96 | 97 | int sectionEndOffset = mOffset; 98 | int sectionSizeBytes = sectionEndOffset - sectionStartOffset; 99 | 100 | return new Section(sectionStartOffset, sectionSizeBytes, attrs); 101 | } 102 | 103 | private static Attribute parseAttr(String attr) { 104 | // Name is separated from value by a semicolon followed by a single SPACE character. 105 | // This permits trailing spaces in names and leading and trailing spaces in values. 106 | // Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual 107 | // spaces to be able to parse such obfuscated APKs. 108 | int delimiterIndex = attr.indexOf(": "); 109 | if (delimiterIndex == -1) { 110 | return new Attribute(attr, ""); 111 | } else { 112 | return new Attribute( 113 | attr.substring(0, delimiterIndex), 114 | attr.substring(delimiterIndex + ": ".length())); 115 | } 116 | } 117 | 118 | /** 119 | * Returns the next attribute or empty {@code String} if end of section has been reached or 120 | * {@code null} if end of input has been reached. 121 | */ 122 | private String readAttribute() { 123 | byte[] bytes = readAttributeBytes(); 124 | if (bytes == null) { 125 | return null; 126 | } else if (bytes.length == 0) { 127 | return ""; 128 | } else { 129 | return new String(bytes, StandardCharsets.UTF_8); 130 | } 131 | } 132 | 133 | /** 134 | * Returns the next attribute or empty array if end of section has been reached or {@code null} 135 | * if end of input has been reached. 136 | */ 137 | private byte[] readAttributeBytes() { 138 | // Check whether end of section was reached during previous invocation 139 | if ((mBufferedLine != null) && (mBufferedLine.length == 0)) { 140 | mBufferedLine = null; 141 | return EMPTY_BYTE_ARRAY; 142 | } 143 | 144 | // Read the next line 145 | byte[] line = readLine(); 146 | if (line == null) { 147 | // End of input 148 | if (mBufferedLine != null) { 149 | byte[] result = mBufferedLine; 150 | mBufferedLine = null; 151 | return result; 152 | } 153 | return null; 154 | } 155 | 156 | // Consume the read line 157 | if (line.length == 0) { 158 | // End of section 159 | if (mBufferedLine != null) { 160 | byte[] result = mBufferedLine; 161 | mBufferedLine = EMPTY_BYTE_ARRAY; 162 | return result; 163 | } 164 | return EMPTY_BYTE_ARRAY; 165 | } 166 | byte[] attrLine; 167 | if (mBufferedLine == null) { 168 | attrLine = line; 169 | } else { 170 | if ((line.length == 0) || (line[0] != ' ')) { 171 | // The most common case: buffered line is a full attribute 172 | byte[] result = mBufferedLine; 173 | mBufferedLine = line; 174 | return result; 175 | } 176 | attrLine = mBufferedLine; 177 | mBufferedLine = null; 178 | attrLine = concat(attrLine, line, 1, line.length - 1); 179 | } 180 | 181 | // Everything's buffered in attrLine now. mBufferedLine is null 182 | 183 | // Read more lines 184 | while (true) { 185 | line = readLine(); 186 | if (line == null) { 187 | // End of input 188 | return attrLine; 189 | } else if (line.length == 0) { 190 | // End of section 191 | mBufferedLine = EMPTY_BYTE_ARRAY; // return "end of section" next time 192 | return attrLine; 193 | } 194 | if (line[0] == ' ') { 195 | // Continuation line 196 | attrLine = concat(attrLine, line, 1, line.length - 1); 197 | } else { 198 | // Next attribute 199 | mBufferedLine = line; 200 | return attrLine; 201 | } 202 | } 203 | } 204 | 205 | private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; 206 | 207 | private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) { 208 | byte[] result = new byte[arr1.length + length2]; 209 | System.arraycopy(arr1, 0, result, 0, arr1.length); 210 | System.arraycopy(arr2, offset2, result, arr1.length, length2); 211 | return result; 212 | } 213 | 214 | /** 215 | * Returns the next line (without line delimiter characters) or {@code null} if end of input has 216 | * been reached. 217 | */ 218 | private byte[] readLine() { 219 | if (mOffset >= mEndOffset) { 220 | return null; 221 | } 222 | int startOffset = mOffset; 223 | int newlineStartOffset = -1; 224 | int newlineEndOffset = -1; 225 | for (int i = startOffset; i < mEndOffset; i++) { 226 | byte b = mManifest[i]; 227 | if (b == '\r') { 228 | newlineStartOffset = i; 229 | int nextIndex = i + 1; 230 | if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) { 231 | newlineEndOffset = nextIndex + 1; 232 | break; 233 | } 234 | newlineEndOffset = nextIndex; 235 | break; 236 | } else if (b == '\n') { 237 | newlineStartOffset = i; 238 | newlineEndOffset = i + 1; 239 | break; 240 | } 241 | } 242 | if (newlineStartOffset == -1) { 243 | newlineStartOffset = mEndOffset; 244 | newlineEndOffset = mEndOffset; 245 | } 246 | mOffset = newlineEndOffset; 247 | 248 | if (newlineStartOffset == startOffset) { 249 | return EMPTY_BYTE_ARRAY; 250 | } 251 | return Arrays.copyOfRange(mManifest, startOffset, newlineStartOffset); 252 | } 253 | 254 | 255 | /** 256 | * Attribute. 257 | */ 258 | public static class Attribute { 259 | private final String mName; 260 | private final String mValue; 261 | 262 | /** 263 | * Constructs a new {@code Attribute} with the provided name and value. 264 | */ 265 | public Attribute(String name, String value) { 266 | mName = name; 267 | mValue = value; 268 | } 269 | 270 | /** 271 | * Returns this attribute's name. 272 | */ 273 | public String getName() { 274 | return mName; 275 | } 276 | 277 | /** 278 | * Returns this attribute's value. 279 | */ 280 | public String getValue() { 281 | return mValue; 282 | } 283 | } 284 | 285 | /** 286 | * Section. 287 | */ 288 | public static class Section { 289 | private final int mStartOffset; 290 | private final int mSizeBytes; 291 | private final String mName; 292 | private final List mAttributes; 293 | 294 | /** 295 | * Constructs a new {@code Section}. 296 | * 297 | * @param startOffset start offset (in bytes) of the section in the input file 298 | * @param sizeBytes size (in bytes) of the section in the input file 299 | * @param attrs attributes contained in the section 300 | */ 301 | public Section(int startOffset, int sizeBytes, List attrs) { 302 | mStartOffset = startOffset; 303 | mSizeBytes = sizeBytes; 304 | String sectionName = null; 305 | if (!attrs.isEmpty()) { 306 | Attribute firstAttr = attrs.get(0); 307 | if ("Name".equalsIgnoreCase(firstAttr.getName())) { 308 | sectionName = firstAttr.getValue(); 309 | } 310 | } 311 | mName = sectionName; 312 | mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs)); 313 | } 314 | 315 | public String getName() { 316 | return mName; 317 | } 318 | 319 | /** 320 | * Returns the offset (in bytes) at which this section starts in the input. 321 | */ 322 | public int getStartOffset() { 323 | return mStartOffset; 324 | } 325 | 326 | /** 327 | * Returns the size (in bytes) of this section in the input. 328 | */ 329 | public int getSizeBytes() { 330 | return mSizeBytes; 331 | } 332 | 333 | /** 334 | * Returns this section's attributes, in the order in which they appear in the input. 335 | */ 336 | public List getAttributes() { 337 | return mAttributes; 338 | } 339 | 340 | /** 341 | * Returns the value of the specified attribute in this section or {@code null} if this 342 | * section does not contain a matching attribute. 343 | */ 344 | public String getAttributeValue(Attributes.Name name) { 345 | return getAttributeValue(name.toString()); 346 | } 347 | 348 | /** 349 | * Returns the value of the specified attribute in this section or {@code null} if this 350 | * section does not contain a matching attribute. 351 | * 352 | * @param name name of the attribute. Attribute names are case-insensitive. 353 | */ 354 | public String getAttributeValue(String name) { 355 | for (Attribute attr : mAttributes) { 356 | if (attr.getName().equalsIgnoreCase(name)) { 357 | return attr.getValue(); 358 | } 359 | } 360 | return null; 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/jar/ManifestWriter.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.android.apksig.internal.jar; 18 | 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.nio.charset.StandardCharsets; 22 | import java.util.Map; 23 | import java.util.Set; 24 | import java.util.SortedMap; 25 | import java.util.TreeMap; 26 | import java.util.jar.Attributes; 27 | 28 | /** 29 | * Producer of {@code META-INF/MANIFEST.MF} file. 30 | * 31 | * @see JAR Manifest format 32 | */ 33 | public abstract class ManifestWriter { 34 | 35 | private static final byte[] CRLF = new byte[] {'\r', '\n'}; 36 | private static final int MAX_LINE_LENGTH = 70; 37 | 38 | private ManifestWriter() {} 39 | 40 | public static void writeMainSection(OutputStream out, Attributes attributes) 41 | throws IOException { 42 | 43 | // Main section must start with the Manifest-Version attribute. 44 | // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. 45 | String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION); 46 | if (manifestVersion == null) { 47 | throw new IllegalArgumentException( 48 | "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing"); 49 | } 50 | writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion); 51 | 52 | if (attributes.size() > 1) { 53 | SortedMap namedAttributes = getAttributesSortedByName(attributes); 54 | namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString()); 55 | writeAttributes(out, namedAttributes); 56 | } 57 | writeSectionDelimiter(out); 58 | } 59 | 60 | public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) 61 | throws IOException { 62 | writeAttribute(out, "Name", name); 63 | 64 | if (!attributes.isEmpty()) { 65 | writeAttributes(out, getAttributesSortedByName(attributes)); 66 | } 67 | writeSectionDelimiter(out); 68 | } 69 | 70 | static void writeSectionDelimiter(OutputStream out) throws IOException { 71 | out.write(CRLF); 72 | } 73 | 74 | static void writeAttribute(OutputStream out, Attributes.Name name, String value) 75 | throws IOException { 76 | writeAttribute(out, name.toString(), value); 77 | } 78 | 79 | private static void writeAttribute(OutputStream out, String name, String value) 80 | throws IOException { 81 | writeLine(out, name + ": " + value); 82 | } 83 | 84 | private static void writeLine(OutputStream out, String line) throws IOException { 85 | byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8); 86 | int offset = 0; 87 | int remaining = lineBytes.length; 88 | boolean firstLine = true; 89 | while (remaining > 0) { 90 | int chunkLength; 91 | if (firstLine) { 92 | // First line 93 | chunkLength = Math.min(remaining, MAX_LINE_LENGTH); 94 | } else { 95 | // Continuation line 96 | out.write(CRLF); 97 | out.write(' '); 98 | chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1); 99 | } 100 | out.write(lineBytes, offset, chunkLength); 101 | offset += chunkLength; 102 | remaining -= chunkLength; 103 | firstLine = false; 104 | } 105 | out.write(CRLF); 106 | } 107 | 108 | static SortedMap getAttributesSortedByName(Attributes attributes) { 109 | Set> attributesEntries = attributes.entrySet(); 110 | SortedMap namedAttributes = new TreeMap(); 111 | for (Map.Entry attribute : attributesEntries) { 112 | String attrName = attribute.getKey().toString(); 113 | String attrValue = attribute.getValue().toString(); 114 | namedAttributes.put(attrName, attrValue); 115 | } 116 | return namedAttributes; 117 | } 118 | 119 | static void writeAttributes( 120 | OutputStream out, SortedMap attributesSortedByName) throws IOException { 121 | for (Map.Entry attribute : attributesSortedByName.entrySet()) { 122 | String attrName = attribute.getKey(); 123 | String attrValue = attribute.getValue(); 124 | writeAttribute(out, attrName, attrValue); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/jar/SignatureFileWriter.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.android.apksig.internal.jar; 18 | 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.util.SortedMap; 22 | import java.util.jar.Attributes; 23 | 24 | /** 25 | * Producer of JAR signature file ({@code *.SF}). 26 | * 27 | * @see JAR Manifest format 28 | */ 29 | public abstract class SignatureFileWriter { 30 | private SignatureFileWriter() {} 31 | 32 | public static void writeMainSection(OutputStream out, Attributes attributes) 33 | throws IOException { 34 | 35 | // Main section must start with the Signature-Version attribute. 36 | // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. 37 | String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION); 38 | if (signatureVersion == null) { 39 | throw new IllegalArgumentException( 40 | "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing"); 41 | } 42 | ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion); 43 | 44 | if (attributes.size() > 1) { 45 | SortedMap namedAttributes = 46 | ManifestWriter.getAttributesSortedByName(attributes); 47 | namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString()); 48 | ManifestWriter.writeAttributes(out, namedAttributes); 49 | } 50 | writeSectionDelimiter(out); 51 | } 52 | 53 | public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) 54 | throws IOException { 55 | ManifestWriter.writeIndividualSection(out, name, attributes); 56 | } 57 | 58 | public static void writeSectionDelimiter(OutputStream out) throws IOException { 59 | ManifestWriter.writeSectionDelimiter(out); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/AndroidSdkVersion.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.android.apksig.internal.util; 18 | 19 | /** 20 | * Android SDK version / API Level constants. 21 | */ 22 | public abstract class AndroidSdkVersion { 23 | 24 | /** Hidden constructor to prevent instantiation. */ 25 | private AndroidSdkVersion() {} 26 | 27 | /** Android 2.3. */ 28 | public static final int GINGERBREAD = 9; 29 | 30 | /** Android 4.3. The revenge of the beans. */ 31 | public static final int JELLY_BEAN_MR2 = 18; 32 | 33 | /** Android 5.0. A flat one with beautiful shadows. But still tasty. */ 34 | public static final int LOLLIPOP = 21; 35 | 36 | /** Android 7.0. N is for Nougat. */ 37 | public static final int N = 24; 38 | } 39 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/ByteArrayDataSink.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.android.apksig.internal.util; 18 | 19 | import com.android.apksig.util.DataSink; 20 | import com.android.apksig.util.DataSource; 21 | import com.android.apksig.util.ReadableDataSink; 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | import java.util.Arrays; 25 | 26 | /** 27 | * Growable byte array which can be appended to via {@link DataSink} interface and read from via 28 | * {@link DataSource} interface. 29 | */ 30 | public class ByteArrayDataSink implements ReadableDataSink { 31 | 32 | private static final int MAX_READ_CHUNK_SIZE = 65536; 33 | 34 | private byte[] mArray; 35 | private int mSize; 36 | 37 | public ByteArrayDataSink() { 38 | this(65536); 39 | } 40 | 41 | public ByteArrayDataSink(int initialCapacity) { 42 | if (initialCapacity < 0) { 43 | throw new IllegalArgumentException("initial capacity: " + initialCapacity); 44 | } 45 | mArray = new byte[initialCapacity]; 46 | } 47 | 48 | @Override 49 | public void consume(byte[] buf, int offset, int length) throws IOException { 50 | if (length == 0) { 51 | return; 52 | } 53 | 54 | ensureAvailable(length); 55 | System.arraycopy(buf, offset, mArray, mSize, length); 56 | mSize += length; 57 | } 58 | 59 | @Override 60 | public void consume(ByteBuffer buf) throws IOException { 61 | if (!buf.hasRemaining()) { 62 | return; 63 | } 64 | 65 | if (buf.hasArray()) { 66 | consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); 67 | buf.position(buf.limit()); 68 | return; 69 | } 70 | 71 | ensureAvailable(buf.remaining()); 72 | byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)]; 73 | while (buf.hasRemaining()) { 74 | int chunkSize = Math.min(buf.remaining(), tmp.length); 75 | buf.get(tmp, 0, chunkSize); 76 | System.arraycopy(tmp, 0, mArray, mSize, chunkSize); 77 | mSize += chunkSize; 78 | } 79 | } 80 | 81 | private void ensureAvailable(int minAvailable) throws IOException { 82 | if (minAvailable <= 0) { 83 | return; 84 | } 85 | 86 | long minCapacity = ((long) mSize) + minAvailable; 87 | if (minCapacity <= mArray.length) { 88 | return; 89 | } 90 | if (minCapacity > Integer.MAX_VALUE) { 91 | throw new IOException( 92 | "Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE); 93 | } 94 | int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE); 95 | int newSize = (int) Math.max(minCapacity, doubleCurrentSize); 96 | mArray = Arrays.copyOf(mArray, newSize); 97 | } 98 | 99 | @Override 100 | public long size() { 101 | return mSize; 102 | } 103 | 104 | @Override 105 | public ByteBuffer getByteBuffer(long offset, int size) { 106 | checkChunkValid(offset, size); 107 | 108 | // checkChunkValid ensures that it's OK to cast offset to int. 109 | return ByteBuffer.wrap(mArray, (int) offset, size); 110 | } 111 | 112 | @Override 113 | public void feed(long offset, long size, DataSink sink) throws IOException { 114 | checkChunkValid(offset, size); 115 | 116 | // checkChunkValid ensures that it's OK to cast offset and size to int. 117 | sink.consume(mArray, (int) offset, (int) size); 118 | } 119 | 120 | @Override 121 | public void copyTo(long offset, int size, ByteBuffer dest) throws IOException { 122 | checkChunkValid(offset, size); 123 | 124 | // checkChunkValid ensures that it's OK to cast offset to int. 125 | dest.put(mArray, (int) offset, size); 126 | } 127 | 128 | private void checkChunkValid(long offset, long size) { 129 | if (offset < 0) { 130 | throw new IllegalArgumentException("offset: " + offset); 131 | } 132 | if (size < 0) { 133 | throw new IllegalArgumentException("size: " + size); 134 | } 135 | if (offset > mSize) { 136 | throw new IllegalArgumentException( 137 | "offset (" + offset + ") > source size (" + mSize + ")"); 138 | } 139 | long endOffset = offset + size; 140 | if (endOffset < offset) { 141 | throw new IllegalArgumentException( 142 | "offset (" + offset + ") + size (" + size + ") overflow"); 143 | } 144 | if (endOffset > mSize) { 145 | throw new IllegalArgumentException( 146 | "offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")"); 147 | } 148 | } 149 | 150 | @Override 151 | public DataSource slice(long offset, long size) { 152 | checkChunkValid(offset, size); 153 | // checkChunkValid ensures that it's OK to cast offset and size to int. 154 | return new SliceDataSource((int) offset, (int) size); 155 | } 156 | 157 | /** 158 | * Slice of the growable byte array. The slice's offset and size in the array are fixed. 159 | */ 160 | private class SliceDataSource implements DataSource { 161 | private final int mSliceOffset; 162 | private final int mSliceSize; 163 | 164 | private SliceDataSource(int offset, int size) { 165 | mSliceOffset = offset; 166 | mSliceSize = size; 167 | } 168 | 169 | @Override 170 | public long size() { 171 | return mSliceSize; 172 | } 173 | 174 | @Override 175 | public void feed(long offset, long size, DataSink sink) throws IOException { 176 | checkChunkValid(offset, size); 177 | // checkChunkValid combined with the way instances of this class are constructed ensures 178 | // that mSliceOffset + offset does not overflow and that it's fine to cast size to int. 179 | sink.consume(mArray, (int) (mSliceOffset + offset), (int) size); 180 | } 181 | 182 | @Override 183 | public ByteBuffer getByteBuffer(long offset, int size) throws IOException { 184 | checkChunkValid(offset, size); 185 | // checkChunkValid combined with the way instances of this class are constructed ensures 186 | // that mSliceOffset + offset does not overflow. 187 | return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size); 188 | } 189 | 190 | @Override 191 | public void copyTo(long offset, int size, ByteBuffer dest) throws IOException { 192 | checkChunkValid(offset, size); 193 | // checkChunkValid combined with the way instances of this class are constructed ensures 194 | // that mSliceOffset + offset does not overflow. 195 | dest.put(mArray, (int) (mSliceOffset + offset), size); 196 | } 197 | 198 | @Override 199 | public DataSource slice(long offset, long size) { 200 | checkChunkValid(offset, size); 201 | // checkChunkValid combined with the way instances of this class are constructed ensures 202 | // that mSliceOffset + offset does not overflow and that it's fine to cast size to int. 203 | return new SliceDataSource((int) (mSliceOffset + offset), (int) size); 204 | } 205 | 206 | private void checkChunkValid(long offset, long size) { 207 | if (offset < 0) { 208 | throw new IllegalArgumentException("offset: " + offset); 209 | } 210 | if (size < 0) { 211 | throw new IllegalArgumentException("size: " + size); 212 | } 213 | if (offset > mSliceSize) { 214 | throw new IllegalArgumentException( 215 | "offset (" + offset + ") > source size (" + mSliceSize + ")"); 216 | } 217 | long endOffset = offset + size; 218 | if (endOffset < offset) { 219 | throw new IllegalArgumentException( 220 | "offset (" + offset + ") + size (" + size + ") overflow"); 221 | } 222 | if (endOffset > mSliceSize) { 223 | throw new IllegalArgumentException( 224 | "offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize 225 | + ")"); 226 | } 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/ByteArrayOutputStreamSink.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.android.apksig.internal.util; 18 | 19 | import com.android.apksig.util.DataSink; 20 | 21 | import java.io.ByteArrayOutputStream; 22 | import java.nio.ByteBuffer; 23 | 24 | /** 25 | * Data sink which stores all input data into an internal {@link ByteArrayOutputStream}, thus 26 | * accepting an arbitrary amount of data. 27 | */ 28 | public class ByteArrayOutputStreamSink implements DataSink { 29 | 30 | private final ByteArrayOutputStream mBuf = new ByteArrayOutputStream(); 31 | 32 | @Override 33 | public void consume(byte[] buf, int offset, int length) { 34 | mBuf.write(buf, offset, length); 35 | } 36 | 37 | @Override 38 | public void consume(ByteBuffer buf) { 39 | if (!buf.hasRemaining()) { 40 | return; 41 | } 42 | 43 | if (buf.hasArray()) { 44 | mBuf.write( 45 | buf.array(), 46 | buf.arrayOffset() + buf.position(), 47 | buf.remaining()); 48 | buf.position(buf.limit()); 49 | } else { 50 | byte[] tmp = new byte[buf.remaining()]; 51 | buf.get(tmp); 52 | mBuf.write(tmp, 0, tmp.length); 53 | } 54 | } 55 | 56 | /** 57 | * Returns the data received so far. 58 | */ 59 | public byte[] getData() { 60 | return mBuf.toByteArray(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/ByteBufferDataSource.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.android.apksig.internal.util; 18 | 19 | import com.android.apksig.util.DataSink; 20 | import com.android.apksig.util.DataSource; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | 24 | /** 25 | * {@link DataSource} backed by a {@link ByteBuffer}. 26 | */ 27 | public class ByteBufferDataSource implements DataSource { 28 | 29 | private final ByteBuffer mBuffer; 30 | private final int mSize; 31 | 32 | /** 33 | * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided 34 | * buffer between the buffer's position and limit. 35 | */ 36 | public ByteBufferDataSource(ByteBuffer buffer) { 37 | this(buffer, true); 38 | } 39 | 40 | /** 41 | * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided 42 | * buffer between the buffer's position and limit. 43 | */ 44 | private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) { 45 | mBuffer = (sliceRequired) ? buffer.slice() : buffer; 46 | mSize = buffer.remaining(); 47 | } 48 | 49 | @Override 50 | public long size() { 51 | return mSize; 52 | } 53 | 54 | @Override 55 | public ByteBuffer getByteBuffer(long offset, int size) { 56 | checkChunkValid(offset, size); 57 | 58 | // checkChunkValid ensures that it's OK to cast offset to int. 59 | int chunkPosition = (int) offset; 60 | int chunkLimit = chunkPosition + size; 61 | // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position 62 | // and limit fields, to be more specific). We thus use synchronization around these 63 | // state-changing operations to make instances of this class thread-safe. 64 | synchronized (mBuffer) { 65 | // ByteBuffer.limit(int) and .position(int) check that that the position >= limit 66 | // invariant is not broken. Thus, the only way to safely change position and limit 67 | // without caring about their current values is to first set position to 0 or set the 68 | // limit to capacity. 69 | mBuffer.position(0); 70 | 71 | mBuffer.limit(chunkLimit); 72 | mBuffer.position(chunkPosition); 73 | return mBuffer.slice(); 74 | } 75 | } 76 | 77 | @Override 78 | public void copyTo(long offset, int size, ByteBuffer dest) { 79 | dest.put(getByteBuffer(offset, size)); 80 | } 81 | 82 | @Override 83 | public void feed(long offset, long size, DataSink sink) throws IOException { 84 | if ((size < 0) || (size > mSize)) { 85 | throw new IllegalArgumentException("size: " + size + ", source size: " + mSize); 86 | } 87 | sink.consume(getByteBuffer(offset, (int) size)); 88 | } 89 | 90 | @Override 91 | public ByteBufferDataSource slice(long offset, long size) { 92 | if ((offset == 0) && (size == mSize)) { 93 | return this; 94 | } 95 | if ((size < 0) || (size > mSize)) { 96 | throw new IllegalArgumentException("size: " + size + ", source size: " + mSize); 97 | } 98 | return new ByteBufferDataSource( 99 | getByteBuffer(offset, (int) size), 100 | false // no need to slice -- it's already a slice 101 | ); 102 | } 103 | 104 | private void checkChunkValid(long offset, long size) { 105 | if (offset < 0) { 106 | throw new IllegalArgumentException("offset: " + offset); 107 | } 108 | if (size < 0) { 109 | throw new IllegalArgumentException("size: " + size); 110 | } 111 | if (offset > mSize) { 112 | throw new IllegalArgumentException( 113 | "offset (" + offset + ") > source size (" + mSize + ")"); 114 | } 115 | long endOffset = offset + size; 116 | if (endOffset < offset) { 117 | throw new IllegalArgumentException( 118 | "offset (" + offset + ") + size (" + size + ") overflow"); 119 | } 120 | if (endOffset > mSize) { 121 | throw new IllegalArgumentException( 122 | "offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")"); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/ByteBufferSink.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.android.apksig.internal.util; 18 | 19 | import com.android.apksig.util.DataSink; 20 | import java.io.IOException; 21 | import java.nio.BufferOverflowException; 22 | import java.nio.ByteBuffer; 23 | 24 | /** 25 | * Data sink which stores all received data into the associated {@link ByteBuffer}. 26 | */ 27 | public class ByteBufferSink implements DataSink { 28 | 29 | private final ByteBuffer mBuffer; 30 | 31 | public ByteBufferSink(ByteBuffer buffer) { 32 | mBuffer = buffer; 33 | } 34 | 35 | @Override 36 | public void consume(byte[] buf, int offset, int length) throws IOException { 37 | try { 38 | mBuffer.put(buf, offset, length); 39 | } catch (BufferOverflowException e) { 40 | throw new IOException( 41 | "Insufficient space in output buffer for " + length + " bytes", e); 42 | } 43 | } 44 | 45 | @Override 46 | public void consume(ByteBuffer buf) throws IOException { 47 | int length = buf.remaining(); 48 | try { 49 | mBuffer.put(buf); 50 | } catch (BufferOverflowException e) { 51 | throw new IOException( 52 | "Insufficient space in output buffer for " + length + " bytes", e); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/DelegatingX509Certificate.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.android.apksig.internal.util; 18 | 19 | import java.math.BigInteger; 20 | import java.security.InvalidKeyException; 21 | import java.security.NoSuchAlgorithmException; 22 | import java.security.NoSuchProviderException; 23 | import java.security.Principal; 24 | import java.security.Provider; 25 | import java.security.PublicKey; 26 | import java.security.SignatureException; 27 | import java.security.cert.CertificateEncodingException; 28 | import java.security.cert.CertificateException; 29 | import java.security.cert.CertificateExpiredException; 30 | import java.security.cert.CertificateNotYetValidException; 31 | import java.security.cert.CertificateParsingException; 32 | import java.security.cert.X509Certificate; 33 | import java.util.Collection; 34 | import java.util.Date; 35 | import java.util.List; 36 | import java.util.Set; 37 | import javax.security.auth.x500.X500Principal; 38 | 39 | /** 40 | * {@link X509Certificate} which delegates all method invocations to the provided delegate 41 | * {@code X509Certificate}. 42 | */ 43 | public class DelegatingX509Certificate extends X509Certificate { 44 | private final X509Certificate mDelegate; 45 | 46 | public DelegatingX509Certificate(X509Certificate delegate) { 47 | this.mDelegate = delegate; 48 | } 49 | 50 | @Override 51 | public Set getCriticalExtensionOIDs() { 52 | return mDelegate.getCriticalExtensionOIDs(); 53 | } 54 | 55 | @Override 56 | public byte[] getExtensionValue(String oid) { 57 | return mDelegate.getExtensionValue(oid); 58 | } 59 | 60 | @Override 61 | public Set getNonCriticalExtensionOIDs() { 62 | return mDelegate.getNonCriticalExtensionOIDs(); 63 | } 64 | 65 | @Override 66 | public boolean hasUnsupportedCriticalExtension() { 67 | return mDelegate.hasUnsupportedCriticalExtension(); 68 | } 69 | 70 | @Override 71 | public void checkValidity() 72 | throws CertificateExpiredException, CertificateNotYetValidException { 73 | mDelegate.checkValidity(); 74 | } 75 | 76 | @Override 77 | public void checkValidity(Date date) 78 | throws CertificateExpiredException, CertificateNotYetValidException { 79 | mDelegate.checkValidity(date); 80 | } 81 | 82 | @Override 83 | public int getVersion() { 84 | return mDelegate.getVersion(); 85 | } 86 | 87 | @Override 88 | public BigInteger getSerialNumber() { 89 | return mDelegate.getSerialNumber(); 90 | } 91 | 92 | @Override 93 | public Principal getIssuerDN() { 94 | return mDelegate.getIssuerDN(); 95 | } 96 | 97 | @Override 98 | public Principal getSubjectDN() { 99 | return mDelegate.getSubjectDN(); 100 | } 101 | 102 | @Override 103 | public Date getNotBefore() { 104 | return mDelegate.getNotBefore(); 105 | } 106 | 107 | @Override 108 | public Date getNotAfter() { 109 | return mDelegate.getNotAfter(); 110 | } 111 | 112 | @Override 113 | public byte[] getTBSCertificate() throws CertificateEncodingException { 114 | return mDelegate.getTBSCertificate(); 115 | } 116 | 117 | @Override 118 | public byte[] getSignature() { 119 | return mDelegate.getSignature(); 120 | } 121 | 122 | @Override 123 | public String getSigAlgName() { 124 | return mDelegate.getSigAlgName(); 125 | } 126 | 127 | @Override 128 | public String getSigAlgOID() { 129 | return mDelegate.getSigAlgOID(); 130 | } 131 | 132 | @Override 133 | public byte[] getSigAlgParams() { 134 | return mDelegate.getSigAlgParams(); 135 | } 136 | 137 | @Override 138 | public boolean[] getIssuerUniqueID() { 139 | return mDelegate.getIssuerUniqueID(); 140 | } 141 | 142 | @Override 143 | public boolean[] getSubjectUniqueID() { 144 | return mDelegate.getSubjectUniqueID(); 145 | } 146 | 147 | @Override 148 | public boolean[] getKeyUsage() { 149 | return mDelegate.getKeyUsage(); 150 | } 151 | 152 | @Override 153 | public int getBasicConstraints() { 154 | return mDelegate.getBasicConstraints(); 155 | } 156 | 157 | @Override 158 | public byte[] getEncoded() throws CertificateEncodingException { 159 | return mDelegate.getEncoded(); 160 | } 161 | 162 | @Override 163 | public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, 164 | InvalidKeyException, NoSuchProviderException, SignatureException { 165 | mDelegate.verify(key); 166 | } 167 | 168 | @Override 169 | public void verify(PublicKey key, String sigProvider) 170 | throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, 171 | NoSuchProviderException, SignatureException { 172 | mDelegate.verify(key, sigProvider); 173 | } 174 | 175 | @Override 176 | public String toString() { 177 | return mDelegate.toString(); 178 | } 179 | 180 | @Override 181 | public PublicKey getPublicKey() { 182 | return mDelegate.getPublicKey(); 183 | } 184 | 185 | @Override 186 | public X500Principal getIssuerX500Principal() { 187 | return mDelegate.getIssuerX500Principal(); 188 | } 189 | 190 | @Override 191 | public X500Principal getSubjectX500Principal() { 192 | return mDelegate.getSubjectX500Principal(); 193 | } 194 | 195 | @Override 196 | public List getExtendedKeyUsage() throws CertificateParsingException { 197 | return mDelegate.getExtendedKeyUsage(); 198 | } 199 | 200 | @Override 201 | public Collection> getSubjectAlternativeNames() throws CertificateParsingException { 202 | return mDelegate.getSubjectAlternativeNames(); 203 | } 204 | 205 | @Override 206 | public Collection> getIssuerAlternativeNames() throws CertificateParsingException { 207 | return mDelegate.getIssuerAlternativeNames(); 208 | } 209 | 210 | @Override 211 | public void verify(PublicKey key, Provider sigProvider) throws CertificateException, 212 | NoSuchAlgorithmException, InvalidKeyException, SignatureException { 213 | mDelegate.verify(key, sigProvider); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/InclusiveIntRange.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.android.apksig.internal.util; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.List; 22 | 23 | /** 24 | * Inclusive interval of integers. 25 | */ 26 | public class InclusiveIntRange { 27 | private final int min; 28 | private final int max; 29 | 30 | private InclusiveIntRange(int min, int max) { 31 | this.min = min; 32 | this.max = max; 33 | } 34 | 35 | public int getMin() { 36 | return min; 37 | } 38 | 39 | public int getMax() { 40 | return max; 41 | } 42 | 43 | public static InclusiveIntRange fromTo(int min, int max) { 44 | return new InclusiveIntRange(min, max); 45 | } 46 | 47 | public static InclusiveIntRange from(int min) { 48 | return new InclusiveIntRange(min, Integer.MAX_VALUE); 49 | } 50 | 51 | public List getValuesNotIn( 52 | List sortedNonOverlappingRanges) { 53 | if (sortedNonOverlappingRanges.isEmpty()) { 54 | return Collections.singletonList(this); 55 | } 56 | 57 | int testValue = min; 58 | List result = null; 59 | for (InclusiveIntRange range : sortedNonOverlappingRanges) { 60 | int rangeMax = range.max; 61 | if (testValue > rangeMax) { 62 | continue; 63 | } 64 | int rangeMin = range.min; 65 | if (testValue < range.min) { 66 | if (result == null) { 67 | result = new ArrayList<>(); 68 | } 69 | result.add(fromTo(testValue, rangeMin - 1)); 70 | } 71 | if (rangeMax >= max) { 72 | return (result != null) ? result : Collections.emptyList(); 73 | } 74 | testValue = rangeMax + 1; 75 | } 76 | if (testValue <= max) { 77 | if (result == null) { 78 | result = new ArrayList<>(1); 79 | } 80 | result.add(fromTo(testValue, max)); 81 | } 82 | return (result != null) ? result : Collections.emptyList(); 83 | } 84 | 85 | @Override 86 | public String toString() { 87 | return "[" + min + ", " + ((max < Integer.MAX_VALUE) ? (max + "]") : "\u221e)"); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/MessageDigestSink.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 | package com.android.apksig.internal.util; 17 | 18 | import com.android.apksig.util.DataSink; 19 | import java.nio.ByteBuffer; 20 | import java.security.MessageDigest; 21 | 22 | /** 23 | * Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each 24 | * {@code MessageDigest} instance receives the same data. 25 | */ 26 | public class MessageDigestSink implements DataSink { 27 | 28 | private final MessageDigest[] mMessageDigests; 29 | 30 | public MessageDigestSink(MessageDigest[] digests) { 31 | mMessageDigests = digests; 32 | } 33 | 34 | @Override 35 | public void consume(byte[] buf, int offset, int length) { 36 | for (MessageDigest md : mMessageDigests) { 37 | md.update(buf, offset, length); 38 | } 39 | } 40 | 41 | @Override 42 | public void consume(ByteBuffer buf) { 43 | int originalPosition = buf.position(); 44 | for (MessageDigest md : mMessageDigests) { 45 | // Reset the position back to the original because the previous iteration's 46 | // MessageDigest.update set the buffer's position to the buffer's limit. 47 | buf.position(originalPosition); 48 | md.update(buf); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/OutputStreamDataSink.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.android.apksig.internal.util; 18 | 19 | import com.android.apksig.util.DataSink; 20 | import java.io.IOException; 21 | import java.io.OutputStream; 22 | import java.nio.ByteBuffer; 23 | 24 | /** 25 | * {@link DataSink} which outputs received data into the associated {@link OutputStream}. 26 | */ 27 | public class OutputStreamDataSink implements DataSink { 28 | 29 | private static final int MAX_READ_CHUNK_SIZE = 65536; 30 | 31 | private final OutputStream mOut; 32 | 33 | /** 34 | * Constructs a new {@code OutputStreamDataSink} which outputs received data into the provided 35 | * {@link OutputStream}. 36 | */ 37 | public OutputStreamDataSink(OutputStream out) { 38 | if (out == null) { 39 | throw new NullPointerException("out == null"); 40 | } 41 | mOut = out; 42 | } 43 | 44 | /** 45 | * Returns {@link OutputStream} into which this data sink outputs received data. 46 | */ 47 | public OutputStream getOutputStream() { 48 | return mOut; 49 | } 50 | 51 | @Override 52 | public void consume(byte[] buf, int offset, int length) throws IOException { 53 | mOut.write(buf, offset, length); 54 | } 55 | 56 | @Override 57 | public void consume(ByteBuffer buf) throws IOException { 58 | if (!buf.hasRemaining()) { 59 | return; 60 | } 61 | 62 | if (buf.hasArray()) { 63 | mOut.write( 64 | buf.array(), 65 | buf.arrayOffset() + buf.position(), 66 | buf.remaining()); 67 | buf.position(buf.limit()); 68 | } else { 69 | byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)]; 70 | while (buf.hasRemaining()) { 71 | int chunkSize = Math.min(buf.remaining(), tmp.length); 72 | buf.get(tmp, 0, chunkSize); 73 | mOut.write(tmp, 0, chunkSize); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/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.android.apksig.internal.util; 18 | 19 | /** 20 | * Pair of two elements. 21 | */ 22 | public final class Pair { 23 | private final A mFirst; 24 | private final B mSecond; 25 | 26 | private Pair(A first, B second) { 27 | mFirst = first; 28 | mSecond = second; 29 | } 30 | 31 | public static Pair of(A first, B second) { 32 | return new Pair(first, second); 33 | } 34 | 35 | public A getFirst() { 36 | return mFirst; 37 | } 38 | 39 | public B getSecond() { 40 | return mSecond; 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | final int prime = 31; 46 | int result = 1; 47 | result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); 48 | result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); 49 | return result; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object obj) { 54 | if (this == obj) { 55 | return true; 56 | } 57 | if (obj == null) { 58 | return false; 59 | } 60 | if (getClass() != obj.getClass()) { 61 | return false; 62 | } 63 | @SuppressWarnings("rawtypes") 64 | Pair other = (Pair) obj; 65 | if (mFirst == null) { 66 | if (other.mFirst != null) { 67 | return false; 68 | } 69 | } else if (!mFirst.equals(other.mFirst)) { 70 | return false; 71 | } 72 | if (mSecond == null) { 73 | if (other.mSecond != null) { 74 | return false; 75 | } 76 | } else if (!mSecond.equals(other.mSecond)) { 77 | return false; 78 | } 79 | return true; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/RandomAccessFileDataSink.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.android.apksig.internal.util; 18 | 19 | import com.android.apksig.util.DataSink; 20 | import java.io.IOException; 21 | import java.io.RandomAccessFile; 22 | import java.nio.ByteBuffer; 23 | import java.nio.channels.FileChannel; 24 | 25 | /** 26 | * {@link DataSink} which outputs received data into the associated file, sequentially. 27 | */ 28 | public class RandomAccessFileDataSink implements DataSink { 29 | 30 | private final RandomAccessFile mFile; 31 | private final FileChannel mFileChannel; 32 | private long mPosition; 33 | 34 | /** 35 | * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the 36 | * beginning of the provided file. 37 | */ 38 | public RandomAccessFileDataSink(RandomAccessFile file) { 39 | this(file, 0); 40 | } 41 | 42 | /** 43 | * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the 44 | * specified position of the provided file. 45 | */ 46 | public RandomAccessFileDataSink(RandomAccessFile file, long startPosition) { 47 | if (file == null) { 48 | throw new NullPointerException("file == null"); 49 | } 50 | if (startPosition < 0) { 51 | throw new IllegalArgumentException("startPosition: " + startPosition); 52 | } 53 | mFile = file; 54 | mFileChannel = file.getChannel(); 55 | mPosition = startPosition; 56 | } 57 | 58 | @Override 59 | public void consume(byte[] buf, int offset, int length) throws IOException { 60 | if (length == 0) { 61 | return; 62 | } 63 | 64 | synchronized (mFile) { 65 | mFile.seek(mPosition); 66 | mFile.write(buf, offset, length); 67 | mPosition += length; 68 | } 69 | } 70 | 71 | @Override 72 | public void consume(ByteBuffer buf) throws IOException { 73 | int length = buf.remaining(); 74 | if (length == 0) { 75 | return; 76 | } 77 | 78 | synchronized (mFile) { 79 | mFile.seek(mPosition); 80 | while (buf.hasRemaining()) { 81 | mFileChannel.write(buf); 82 | } 83 | mPosition += length; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/util/RandomAccessFileDataSource.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.android.apksig.internal.util; 18 | 19 | import com.android.apksig.util.DataSink; 20 | import com.android.apksig.util.DataSource; 21 | import java.io.IOException; 22 | import java.io.RandomAccessFile; 23 | import java.nio.ByteBuffer; 24 | import java.nio.channels.FileChannel; 25 | 26 | /** 27 | * {@link DataSource} backed by a {@link RandomAccessFile}. 28 | */ 29 | public class RandomAccessFileDataSource implements DataSource { 30 | 31 | private static final int MAX_READ_CHUNK_SIZE = 65536; 32 | 33 | private final RandomAccessFile mFile; 34 | private final long mOffset; 35 | private final long mSize; 36 | 37 | /** 38 | * Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the 39 | * specified the whole file. Changes to the contents of the file, including the size of the 40 | * file, will be visible in this data source. 41 | */ 42 | public RandomAccessFileDataSource(RandomAccessFile file) { 43 | mFile = file; 44 | mOffset = 0; 45 | mSize = -1; 46 | } 47 | 48 | /** 49 | * Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the 50 | * specified region of the provided file. Changes to the contents of the file will be visible in 51 | * this data source. 52 | */ 53 | public RandomAccessFileDataSource(RandomAccessFile file, long offset, long size) { 54 | if (offset < 0) { 55 | throw new IllegalArgumentException("offset: " + size); 56 | } 57 | if (size < 0) { 58 | throw new IllegalArgumentException("size: " + size); 59 | } 60 | mFile = file; 61 | mOffset = offset; 62 | mSize = size; 63 | } 64 | 65 | @Override 66 | public long size() { 67 | if (mSize == -1) { 68 | try { 69 | return mFile.length(); 70 | } catch (IOException e) { 71 | return 0; 72 | } 73 | } else { 74 | return mSize; 75 | } 76 | } 77 | 78 | @Override 79 | public RandomAccessFileDataSource slice(long offset, long size) { 80 | long sourceSize = size(); 81 | checkChunkValid(offset, size, sourceSize); 82 | if ((offset == 0) && (size == sourceSize)) { 83 | return this; 84 | } 85 | 86 | return new RandomAccessFileDataSource(mFile, mOffset + offset, size); 87 | } 88 | 89 | @Override 90 | public void feed(long offset, long size, DataSink sink) throws IOException { 91 | long sourceSize = size(); 92 | checkChunkValid(offset, size, sourceSize); 93 | if (size == 0) { 94 | return; 95 | } 96 | 97 | long chunkOffsetInFile = mOffset + offset; 98 | long remaining = size; 99 | byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)]; 100 | while (remaining > 0) { 101 | int chunkSize = (int) Math.min(remaining, buf.length); 102 | synchronized (mFile) { 103 | mFile.seek(chunkOffsetInFile); 104 | mFile.readFully(buf, 0, chunkSize); 105 | } 106 | sink.consume(buf, 0, chunkSize); 107 | chunkOffsetInFile += chunkSize; 108 | remaining -= chunkSize; 109 | } 110 | } 111 | 112 | @Override 113 | public void copyTo(long offset, int size, ByteBuffer dest) throws IOException { 114 | long sourceSize = size(); 115 | checkChunkValid(offset, size, sourceSize); 116 | if (size == 0) { 117 | return; 118 | } 119 | 120 | long offsetInFile = mOffset + offset; 121 | int remaining = size; 122 | int prevLimit = dest.limit(); 123 | try { 124 | dest.limit(dest.position() + size); 125 | FileChannel fileChannel = mFile.getChannel(); 126 | while (remaining > 0) { 127 | int chunkSize; 128 | synchronized (mFile) { 129 | fileChannel.position(offsetInFile); 130 | chunkSize = fileChannel.read(dest); 131 | } 132 | offsetInFile += chunkSize; 133 | remaining -= chunkSize; 134 | } 135 | } finally { 136 | dest.limit(prevLimit); 137 | } 138 | } 139 | 140 | @Override 141 | public ByteBuffer getByteBuffer(long offset, int size) throws IOException { 142 | ByteBuffer result = ByteBuffer.allocate(size); 143 | copyTo(offset, size, result); 144 | result.flip(); 145 | return result; 146 | } 147 | 148 | private static void checkChunkValid(long offset, long size, long sourceSize) { 149 | if (offset < 0) { 150 | throw new IllegalArgumentException("offset: " + offset); 151 | } 152 | if (size < 0) { 153 | throw new IllegalArgumentException("size: " + size); 154 | } 155 | if (offset > sourceSize) { 156 | throw new IllegalArgumentException( 157 | "offset (" + offset + ") > source size (" + sourceSize + ")"); 158 | } 159 | long endOffset = offset + size; 160 | if (endOffset < offset) { 161 | throw new IllegalArgumentException( 162 | "offset (" + offset + ") + size (" + size + ") overflow"); 163 | } 164 | if (endOffset > sourceSize) { 165 | throw new IllegalArgumentException( 166 | "offset (" + offset + ") + size (" + size 167 | + ") > source size (" + sourceSize +")"); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/zip/CentralDirectoryRecord.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.android.apksig.internal.zip; 18 | 19 | import com.android.apksig.zip.ZipFormatException; 20 | import java.nio.BufferUnderflowException; 21 | import java.nio.ByteBuffer; 22 | import java.nio.ByteOrder; 23 | import java.nio.charset.StandardCharsets; 24 | import java.util.Comparator; 25 | 26 | /** 27 | * ZIP Central Directory (CD) Record. 28 | */ 29 | public class CentralDirectoryRecord { 30 | 31 | /** 32 | * Comparator which compares records by the offset of the corresponding Local File Header in the 33 | * archive. 34 | */ 35 | public static final Comparator BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR = 36 | new ByLocalFileHeaderOffsetComparator(); 37 | 38 | private static final int RECORD_SIGNATURE = 0x02014b50; 39 | private static final int HEADER_SIZE_BYTES = 46; 40 | 41 | private static final int LAST_MODIFICATION_TIME_OFFSET = 12; 42 | private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42; 43 | private static final int NAME_OFFSET = HEADER_SIZE_BYTES; 44 | 45 | private final ByteBuffer mData; 46 | private final int mLastModificationTime; 47 | private final int mLastModificationDate; 48 | private final long mCrc32; 49 | private final long mCompressedSize; 50 | private final long mUncompressedSize; 51 | private final long mLocalFileHeaderOffset; 52 | private final String mName; 53 | private final int mNameSizeBytes; 54 | 55 | private CentralDirectoryRecord( 56 | ByteBuffer data, 57 | int lastModificationTime, 58 | int lastModificationDate, 59 | long crc32, 60 | long compressedSize, 61 | long uncompressedSize, 62 | long localFileHeaderOffset, 63 | String name, 64 | int nameSizeBytes) { 65 | mData = data; 66 | mLastModificationDate = lastModificationDate; 67 | mLastModificationTime = lastModificationTime; 68 | mCrc32 = crc32; 69 | mCompressedSize = compressedSize; 70 | mUncompressedSize = uncompressedSize; 71 | mLocalFileHeaderOffset = localFileHeaderOffset; 72 | mName = name; 73 | mNameSizeBytes = nameSizeBytes; 74 | } 75 | 76 | public int getSize() { 77 | return mData.remaining(); 78 | } 79 | 80 | public String getName() { 81 | return mName; 82 | } 83 | 84 | public int getNameSizeBytes() { 85 | return mNameSizeBytes; 86 | } 87 | 88 | public int getLastModificationTime() { 89 | return mLastModificationTime; 90 | } 91 | 92 | public int getLastModificationDate() { 93 | return mLastModificationDate; 94 | } 95 | 96 | public long getCrc32() { 97 | return mCrc32; 98 | } 99 | 100 | public long getCompressedSize() { 101 | return mCompressedSize; 102 | } 103 | 104 | public long getUncompressedSize() { 105 | return mUncompressedSize; 106 | } 107 | 108 | public long getLocalFileHeaderOffset() { 109 | return mLocalFileHeaderOffset; 110 | } 111 | 112 | /** 113 | * Returns the Central Directory Record starting at the current position of the provided buffer 114 | * and advances the buffer's position immediately past the end of the record. 115 | */ 116 | public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException { 117 | ZipUtils.assertByteOrderLittleEndian(buf); 118 | if (buf.remaining() < HEADER_SIZE_BYTES) { 119 | throw new ZipFormatException( 120 | "Input too short. Need at least: " + HEADER_SIZE_BYTES 121 | + " bytes, available: " + buf.remaining() + " bytes", 122 | new BufferUnderflowException()); 123 | } 124 | int originalPosition = buf.position(); 125 | int recordSignature = buf.getInt(); 126 | if (recordSignature != RECORD_SIGNATURE) { 127 | throw new ZipFormatException( 128 | "Not a Central Directory record. Signature: 0x" 129 | + Long.toHexString(recordSignature & 0xffffffffL)); 130 | } 131 | buf.position(originalPosition + LAST_MODIFICATION_TIME_OFFSET); 132 | int lastModificationTime = ZipUtils.getUnsignedInt16(buf); 133 | int lastModificationDate = ZipUtils.getUnsignedInt16(buf); 134 | long crc32 = ZipUtils.getUnsignedInt32(buf); 135 | long compressedSize = ZipUtils.getUnsignedInt32(buf); 136 | long uncompressedSize = ZipUtils.getUnsignedInt32(buf); 137 | int nameSize = ZipUtils.getUnsignedInt16(buf); 138 | int extraSize = ZipUtils.getUnsignedInt16(buf); 139 | int commentSize = ZipUtils.getUnsignedInt16(buf); 140 | buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET); 141 | long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf); 142 | buf.position(originalPosition); 143 | int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize; 144 | if (recordSize > buf.remaining()) { 145 | throw new ZipFormatException( 146 | "Input too short. Need: " + recordSize + " bytes, available: " 147 | + buf.remaining() + " bytes", 148 | new BufferUnderflowException()); 149 | } 150 | String name = getName(buf, originalPosition + NAME_OFFSET, nameSize); 151 | buf.position(originalPosition); 152 | int originalLimit = buf.limit(); 153 | int recordEndInBuf = originalPosition + recordSize; 154 | ByteBuffer recordBuf; 155 | try { 156 | buf.limit(recordEndInBuf); 157 | recordBuf = buf.slice(); 158 | } finally { 159 | buf.limit(originalLimit); 160 | } 161 | // Consume this record 162 | buf.position(recordEndInBuf); 163 | return new CentralDirectoryRecord( 164 | recordBuf, 165 | lastModificationTime, 166 | lastModificationDate, 167 | crc32, 168 | compressedSize, 169 | uncompressedSize, 170 | localFileHeaderOffset, 171 | name, 172 | nameSize); 173 | } 174 | 175 | public void copyTo(ByteBuffer output) { 176 | output.put(mData.slice()); 177 | } 178 | 179 | public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset( 180 | long localFileHeaderOffset) { 181 | ByteBuffer result = ByteBuffer.allocate(mData.remaining()); 182 | result.put(mData.slice()); 183 | result.flip(); 184 | result.order(ByteOrder.LITTLE_ENDIAN); 185 | ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset); 186 | return new CentralDirectoryRecord( 187 | result, 188 | mLastModificationTime, 189 | mLastModificationDate, 190 | mCrc32, 191 | mCompressedSize, 192 | mUncompressedSize, 193 | localFileHeaderOffset, 194 | mName, 195 | mNameSizeBytes); 196 | } 197 | 198 | public static CentralDirectoryRecord createWithDeflateCompressedData( 199 | String name, 200 | int lastModifiedTime, 201 | int lastModifiedDate, 202 | long crc32, 203 | long compressedSize, 204 | long uncompressedSize, 205 | long localFileHeaderOffset) { 206 | byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); 207 | int recordSize = HEADER_SIZE_BYTES + nameBytes.length; 208 | ByteBuffer result = ByteBuffer.allocate(recordSize); 209 | result.order(ByteOrder.LITTLE_ENDIAN); 210 | result.putInt(RECORD_SIGNATURE); 211 | ZipUtils.putUnsignedInt16(result, 0x14); // Version made by 212 | ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract 213 | result.putShort(ZipUtils.GP_FLAG_EFS); // UTF-8 character encoding used for entry name 214 | result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED); 215 | ZipUtils.putUnsignedInt16(result, lastModifiedTime); 216 | ZipUtils.putUnsignedInt16(result, lastModifiedDate); 217 | ZipUtils.putUnsignedInt32(result, crc32); 218 | ZipUtils.putUnsignedInt32(result, compressedSize); 219 | ZipUtils.putUnsignedInt32(result, uncompressedSize); 220 | ZipUtils.putUnsignedInt16(result, nameBytes.length); 221 | ZipUtils.putUnsignedInt16(result, 0); // Extra field length 222 | ZipUtils.putUnsignedInt16(result, 0); // File comment length 223 | ZipUtils.putUnsignedInt16(result, 0); // Disk number 224 | ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes 225 | ZipUtils.putUnsignedInt32(result, 0); // External file attributes 226 | ZipUtils.putUnsignedInt32(result, localFileHeaderOffset); 227 | result.put(nameBytes); 228 | 229 | if (result.hasRemaining()) { 230 | throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); 231 | } 232 | result.flip(); 233 | return new CentralDirectoryRecord( 234 | result, 235 | lastModifiedTime, 236 | lastModifiedDate, 237 | crc32, 238 | compressedSize, 239 | uncompressedSize, 240 | localFileHeaderOffset, 241 | name, 242 | nameBytes.length); 243 | } 244 | 245 | static String getName(ByteBuffer record, int position, int nameLengthBytes) { 246 | byte[] nameBytes; 247 | int nameBytesOffset; 248 | if (record.hasArray()) { 249 | nameBytes = record.array(); 250 | nameBytesOffset = record.arrayOffset() + position; 251 | } else { 252 | nameBytes = new byte[nameLengthBytes]; 253 | nameBytesOffset = 0; 254 | int originalPosition = record.position(); 255 | try { 256 | record.position(position); 257 | record.get(nameBytes); 258 | } finally { 259 | record.position(originalPosition); 260 | } 261 | } 262 | return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8); 263 | } 264 | 265 | private static class ByLocalFileHeaderOffsetComparator 266 | implements Comparator { 267 | @Override 268 | public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) { 269 | long offset1 = r1.getLocalFileHeaderOffset(); 270 | long offset2 = r2.getLocalFileHeaderOffset(); 271 | if (offset1 > offset2) { 272 | return 1; 273 | } else if (offset1 < offset2) { 274 | return -1; 275 | } else { 276 | return 0; 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/internal/zip/EocdRecord.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.android.apksig.internal.zip; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.nio.ByteOrder; 21 | 22 | /** 23 | * ZIP End of Central Directory record. 24 | */ 25 | public class EocdRecord { 26 | private static final int CD_RECORD_COUNT_ON_DISK_OFFSET = 8; 27 | private static final int CD_RECORD_COUNT_TOTAL_OFFSET = 10; 28 | private static final int CD_SIZE_OFFSET = 12; 29 | private static final int CD_OFFSET_OFFSET = 16; 30 | 31 | public static ByteBuffer createWithModifiedCentralDirectoryInfo( 32 | ByteBuffer original, 33 | int centralDirectoryRecordCount, 34 | long centralDirectorySizeBytes, 35 | long centralDirectoryOffset) { 36 | ByteBuffer result = ByteBuffer.allocate(original.remaining()); 37 | result.order(ByteOrder.LITTLE_ENDIAN); 38 | result.put(original.slice()); 39 | result.flip(); 40 | ZipUtils.setUnsignedInt16( 41 | result, CD_RECORD_COUNT_ON_DISK_OFFSET, centralDirectoryRecordCount); 42 | ZipUtils.setUnsignedInt16( 43 | result, CD_RECORD_COUNT_TOTAL_OFFSET, centralDirectoryRecordCount); 44 | ZipUtils.setUnsignedInt32(result, CD_SIZE_OFFSET, centralDirectorySizeBytes); 45 | ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset); 46 | return result; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/util/DataSink.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.android.apksig.util; 18 | 19 | import java.io.IOException; 20 | import java.nio.ByteBuffer; 21 | 22 | /** 23 | * Consumer of input data which may be provided in one go or in chunks. 24 | */ 25 | public interface DataSink { 26 | 27 | /** 28 | * Consumes the provided chunk of data. 29 | * 30 | *

This data sink guarantees to not hold references to the provided buffer after this method 31 | * terminates. 32 | */ 33 | void consume(byte[] buf, int offset, int length) throws IOException; 34 | 35 | /** 36 | * Consumes all remaining data in the provided buffer and advances the buffer's position 37 | * to the buffer's limit. 38 | * 39 | *

This data sink guarantees to not hold references to the provided buffer after this method 40 | * terminates. 41 | */ 42 | void consume(ByteBuffer buf) throws IOException; 43 | } 44 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/util/DataSinks.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.android.apksig.util; 18 | 19 | import com.android.apksig.internal.util.ByteArrayDataSink; 20 | import com.android.apksig.internal.util.OutputStreamDataSink; 21 | import com.android.apksig.internal.util.RandomAccessFileDataSink; 22 | import java.io.OutputStream; 23 | import java.io.RandomAccessFile; 24 | 25 | /** 26 | * Utility methods for working with {@link DataSink} abstraction. 27 | */ 28 | public abstract class DataSinks { 29 | private DataSinks() {} 30 | 31 | /** 32 | * Returns a {@link DataSink} which outputs received data into the provided 33 | * {@link OutputStream}. 34 | */ 35 | public static DataSink asDataSink(OutputStream out) { 36 | return new OutputStreamDataSink(out); 37 | } 38 | 39 | /** 40 | * Returns a {@link DataSink} which outputs received data into the provided file, sequentially, 41 | * starting at the beginning of the file. 42 | */ 43 | public static DataSink asDataSink(RandomAccessFile file) { 44 | return new RandomAccessFileDataSink(file); 45 | } 46 | 47 | /** 48 | * Returns a new in-memory {@link DataSink} which exposes all data consumed so far via the 49 | * {@link DataSource} interface. 50 | */ 51 | public static ReadableDataSink newInMemoryDataSink() { 52 | return new ByteArrayDataSink(); 53 | } 54 | 55 | /** 56 | * Returns a new in-memory {@link DataSink} which exposes all data consumed so far via the 57 | * {@link DataSource} interface. 58 | * 59 | * @param initialCapacity initial capacity in bytes 60 | */ 61 | public static ReadableDataSink newInMemoryDataSink(int initialCapacity) { 62 | return new ByteArrayDataSink(initialCapacity); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/util/DataSource.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.android.apksig.util; 18 | 19 | import java.io.IOException; 20 | import java.nio.ByteBuffer; 21 | 22 | /** 23 | * Abstract representation of a source of data. 24 | * 25 | *

This abstraction serves three purposes: 26 | *

    27 | *
  • Transparent handling of different types of sources, such as {@code byte[]}, 28 | * {@link ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.
  • 29 | *
  • Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer} 30 | * may have worked as the unifying abstraction.
  • 31 | *
  • Support sources which do not fit into logical memory as a contiguous region.
  • 32 | *
33 | * 34 | *

There are following ways to obtain a chunk of data from the data source: 35 | *

    36 | *
  • Stream the chunk's data into a {@link DataSink} using 37 | * {@link #feed(long, long, DataSink) feed}. This is best suited for scenarios where there is no 38 | * need to have the chunk's data accessible at the same time, for example, when computing the 39 | * digest of the chunk. If you need to keep the chunk's data around after {@code feed} 40 | * completes, you must create a copy during {@code feed}. However, in that case the following 41 | * methods of obtaining the chunk's data may be more appropriate.
  • 42 | *
  • Obtain a {@link ByteBuffer} containing the chunk's data using 43 | * {@link #getByteBuffer(long, int) getByteBuffer}. Depending on the data source, the chunk's 44 | * data may or may not be copied by this operation. This is best suited for scenarios where 45 | * you need to access the chunk's data in arbitrary order, but don't need to modify the data and 46 | * thus don't require a copy of the data.
  • 47 | *
  • Copy the chunk's data to a {@link ByteBuffer} using 48 | * {@link #copyTo(long, int, ByteBuffer) copyTo}. This is best suited for scenarios where 49 | * you require a copy of the chunk's data, such as to when you need to modify the data. 50 | *
  • 51 | *
52 | */ 53 | public interface DataSource { 54 | 55 | /** 56 | * Returns the amount of data (in bytes) contained in this data source. 57 | */ 58 | long size(); 59 | 60 | /** 61 | * Feeds the specified chunk from this data source into the provided sink. 62 | * 63 | * @param offset index (in bytes) at which the chunk starts inside data source 64 | * @param size size (in bytes) of the chunk 65 | */ 66 | void feed(long offset, long size, DataSink sink) throws IOException; 67 | 68 | /** 69 | * Returns a buffer holding the contents of the specified chunk of data from this data source. 70 | * Changes to the data source are not guaranteed to be reflected in the returned buffer. 71 | * Similarly, changes in the buffer are not guaranteed to be reflected in the data source. 72 | * 73 | *

The returned buffer's position is {@code 0}, and the buffer's limit and capacity is 74 | * {@code size}. 75 | * 76 | * @param offset index (in bytes) at which the chunk starts inside data source 77 | * @param size size (in bytes) of the chunk 78 | */ 79 | ByteBuffer getByteBuffer(long offset, int size) throws IOException; 80 | 81 | /** 82 | * Copies the specified chunk from this data source into the provided destination buffer, 83 | * advancing the destination buffer's position by {@code size}. 84 | * 85 | * @param offset index (in bytes) at which the chunk starts inside data source 86 | * @param size size (in bytes) of the chunk 87 | */ 88 | void copyTo(long offset, int size, ByteBuffer dest) throws IOException; 89 | 90 | /** 91 | * Returns a data source representing the specified region of data of this data source. Changes 92 | * to data represented by this data source will also be visible in the returned data source. 93 | * 94 | * @param offset index (in bytes) at which the region starts inside data source 95 | * @param size size (in bytes) of the region 96 | */ 97 | DataSource slice(long offset, long size); 98 | } 99 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/util/DataSources.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.android.apksig.util; 18 | 19 | import com.android.apksig.internal.util.ByteBufferDataSource; 20 | import com.android.apksig.internal.util.RandomAccessFileDataSource; 21 | import java.io.RandomAccessFile; 22 | import java.nio.ByteBuffer; 23 | 24 | /** 25 | * Utility methods for working with {@link DataSource} abstraction. 26 | */ 27 | public abstract class DataSources { 28 | private DataSources() {} 29 | 30 | /** 31 | * Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source 32 | * represents the data contained between the position and limit of the buffer. Changes to the 33 | * buffer's contents will be visible in the data source. 34 | */ 35 | public static DataSource asDataSource(ByteBuffer buffer) { 36 | if (buffer == null) { 37 | throw new NullPointerException(); 38 | } 39 | return new ByteBufferDataSource(buffer); 40 | } 41 | 42 | /** 43 | * Returns a {@link DataSource} backed by the provided {@link RandomAccessFile}. Changes to the 44 | * file, including changes to size of file, will be visible in the data source. 45 | */ 46 | public static DataSource asDataSource(RandomAccessFile file) { 47 | if (file == null) { 48 | throw new NullPointerException(); 49 | } 50 | return new RandomAccessFileDataSource(file); 51 | } 52 | 53 | /** 54 | * Returns a {@link DataSource} backed by the provided region of the {@link RandomAccessFile}. 55 | * Changes to the file will be visible in the data source. 56 | */ 57 | public static DataSource asDataSource(RandomAccessFile file, long offset, long size) { 58 | if (file == null) { 59 | throw new NullPointerException(); 60 | } 61 | return new RandomAccessFileDataSource(file, offset, size); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/util/ReadableDataSink.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.android.apksig.util; 18 | 19 | /** 20 | * {@link DataSink} which exposes all data consumed so far as a {@link DataSource}. This abstraction 21 | * offers append-only write access and random read access. 22 | */ 23 | public interface ReadableDataSink extends DataSink, DataSource { 24 | } 25 | -------------------------------------------------------------------------------- /apksig/src/com/android/apksig/zip/ZipFormatException.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.android.apksig.zip; 18 | 19 | /** 20 | * Indicates that a ZIP archive is not well-formed. 21 | */ 22 | public class ZipFormatException extends Exception { 23 | private static final long serialVersionUID = 1L; 24 | 25 | public ZipFormatException(String message) { 26 | super(message); 27 | } 28 | 29 | public ZipFormatException(String message, Throwable cause) { 30 | super(message, cause); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /art/build-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukc/ApkMultiChannelPlugin/28b389a37df3d7fcaf4a42987eecf0e32dd2de07/art/build-setting.png -------------------------------------------------------------------------------- /art/choose-apk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukc/ApkMultiChannelPlugin/28b389a37df3d7fcaf4a42987eecf0e32dd2de07/art/choose-apk.png -------------------------------------------------------------------------------- /art/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukc/ApkMultiChannelPlugin/28b389a37df3d7fcaf4a42987eecf0e32dd2de07/art/output.png -------------------------------------------------------------------------------- /art/properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukc/ApkMultiChannelPlugin/28b389a37df3d7fcaf4a42987eecf0e32dd2de07/art/properties.png -------------------------------------------------------------------------------- /art/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukc/ApkMultiChannelPlugin/28b389a37df3d7fcaf4a42987eecf0e32dd2de07/art/setting.png -------------------------------------------------------------------------------- /library/bcpkix-jdk15on-1.48.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukc/ApkMultiChannelPlugin/28b389a37df3d7fcaf4a42987eecf0e32dd2de07/library/bcpkix-jdk15on-1.48.jar -------------------------------------------------------------------------------- /resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.github.nukc.plugin.apkmultichannel 3 | ApkMultiChannel 4 | 1.2 5 | Nukc 6 | 7 | 9 | most HTML tags may be used 10 | ]]> 11 | 12 | 1.2

14 |
    15 |
  • Optimization logic and fix bug
  • 16 |
17 |

1.1

18 |
    19 |
  • support write zip comment
  • 20 |
21 |

1.0

22 |
    23 |
  • Initial
  • 24 |
25 | ]]> 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | com.intellij.modules.lang 35 | 36 | 37 | 38 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /resources/icons/plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukc/ApkMultiChannelPlugin/28b389a37df3d7fcaf4a42987eecf0e32dd2de07/resources/icons/plugin.png -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/MainAction.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin; 2 | 3 | import com.github.nukc.plugin.helper.BuildHelper; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.actionSystem.CommonDataKeys; 7 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 8 | import com.intellij.openapi.diagnostic.Logger; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.openapi.vfs.VirtualFile; 11 | 12 | /** 13 | * Created by Nukc. 14 | */ 15 | public class MainAction extends AnAction { 16 | 17 | @Override 18 | public void actionPerformed(AnActionEvent e) { 19 | Project project = e.getData(PlatformDataKeys.PROJECT); 20 | VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE); 21 | BuildHelper.build(project, virtualFile); 22 | } 23 | 24 | @Override 25 | public void update(AnActionEvent e) { 26 | VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE); 27 | boolean visible = virtualFile != null && "apk".equals(virtualFile.getExtension()); 28 | e.getPresentation().setVisible(visible); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/SettingAction.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin; 2 | 3 | import com.github.nukc.plugin.ui.OptionForm; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.diagnostic.Logger; 7 | import com.intellij.openapi.project.Project; 8 | 9 | /** 10 | * Created by Nukc on 19/12/2016. 11 | */ 12 | public class SettingAction extends AnAction { 13 | 14 | @Override 15 | public void actionPerformed(AnActionEvent event) { 16 | Project project = event.getProject(); 17 | OptionForm.show(project); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/ChannelEditor.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml; 2 | 3 | import com.github.nukc.plugin.axml.decode.AXMLDoc; 4 | import com.github.nukc.plugin.axml.decode.BTagNode; 5 | import com.github.nukc.plugin.axml.decode.BTagNode.Attribute; 6 | import com.github.nukc.plugin.axml.decode.BXMLNode; 7 | import com.github.nukc.plugin.axml.decode.StringBlock; 8 | import com.github.nukc.plugin.axml.utils.TypedValue; 9 | 10 | import java.util.List; 11 | 12 | public class ChannelEditor { 13 | private final String NAME_SPACE = "http://schemas.android.com/apk/res/android"; 14 | private final String META_DATA = "meta-data"; 15 | private final String NAME = "name"; 16 | private final String VALUE = "value"; 17 | private final String UMENG_CHANNEL = "UMENG_CHANNEL"; 18 | 19 | private String mChannelValue = "channel"; 20 | 21 | private int namespace; 22 | private int meta_data; 23 | private int attr_name; 24 | private int attr_value; 25 | private int channel_name; 26 | private int channel_value = -1; 27 | 28 | private AXMLDoc mDoc; 29 | 30 | public ChannelEditor(AXMLDoc doc) { 31 | this.mDoc = doc; 32 | } 33 | 34 | public void setChannel(String channel) { 35 | mChannelValue = channel; 36 | } 37 | 38 | //First add resource and get mapping ids 39 | private void registStringBlock(StringBlock sb) { 40 | namespace = sb.putString(NAME_SPACE); 41 | meta_data = sb.putString(META_DATA); 42 | attr_name = sb.putString(NAME); 43 | attr_value = sb.putString(VALUE); 44 | channel_name = sb.putString(UMENG_CHANNEL); 45 | 46 | if (channel_value == -1) { 47 | channel_value = sb.addString(mChannelValue);//now we have a seat in StringBlock 48 | } 49 | } 50 | 51 | //put string to the seat 52 | private void replaceValue(StringBlock sb) { 53 | sb.setString(channel_value, mChannelValue); 54 | } 55 | 56 | //Second find&change meta-data's value or add a new one 57 | private void editNode(AXMLDoc doc) { 58 | BXMLNode application = doc.getApplicationNode(); //manifest node 59 | List children = application.getChildren(); 60 | 61 | BTagNode umeng_meta = null; 62 | 63 | end: 64 | for (BXMLNode node : children) { 65 | BTagNode m = (BTagNode) node; 66 | //it's a risk that the value for "android:name" maybe not String 67 | if ((meta_data == m.getName()) && (m.getAttrStringForKey(attr_name) == channel_name)) { 68 | umeng_meta = m; 69 | break end; 70 | } 71 | } 72 | 73 | if (umeng_meta != null) { 74 | umeng_meta.setAttrStringForKey(attr_value, channel_value); 75 | } else { 76 | Attribute name_attr = new Attribute(namespace, attr_name, TypedValue.TYPE_STRING); 77 | name_attr.setString(channel_name); 78 | Attribute value_attr = new Attribute(namespace, attr_value, TypedValue.TYPE_STRING); 79 | value_attr.setString(channel_value); 80 | 81 | umeng_meta = new BTagNode(-1, meta_data); 82 | umeng_meta.setAttribute(name_attr); 83 | umeng_meta.setAttribute(value_attr); 84 | 85 | children.add(umeng_meta); 86 | } 87 | } 88 | 89 | public void commit() { 90 | registStringBlock(mDoc.getStringBlock()); 91 | editNode(mDoc); 92 | 93 | replaceValue(mDoc.getStringBlock()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/AXMLDoc.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.util.List; 7 | 8 | import static java.lang.System.out; 9 | 10 | public class AXMLDoc { 11 | private final int MAGIC_NUMBER = 0X00080003; 12 | private final int CHUNK_STRING_BLOCK = 0X001C0001; 13 | private final int CHUNK_RESOURCE_ID = 0X00080180; 14 | private final int CHUNK_XML_TREE = 0X00100100; 15 | 16 | private final String MANIFEST = "manifest"; 17 | private final String APPLICATION = "application"; 18 | 19 | private int mDocSize ; 20 | private StringBlock mStringBlock; 21 | private ResBlock mResBlock; 22 | 23 | private BXMLTree mXMLTree; 24 | 25 | public AXMLDoc(){ 26 | } 27 | 28 | public StringBlock getStringBlock(){ 29 | return mStringBlock; 30 | } 31 | 32 | public ResBlock getResBlock(){ 33 | return mResBlock; 34 | } 35 | 36 | public BXMLTree getBXMLTree(){ 37 | return mXMLTree; 38 | } 39 | 40 | public BXMLNode getManifestNode(){ 41 | List children = mXMLTree.getRoot().getChildren(); 42 | 43 | for(BXMLNode node : children){ 44 | if( MANIFEST.equals( mStringBlock.getStringFor(((BTagNode)node).getName() ) )){ 45 | return node; 46 | } 47 | } 48 | 49 | return null; 50 | } 51 | 52 | public BXMLNode getApplicationNode(){ 53 | BXMLNode manifest = getManifestNode(); 54 | if(manifest == null){ 55 | return null; 56 | } 57 | 58 | for(BXMLNode node : manifest.getChildren()){ 59 | if( APPLICATION.equals( mStringBlock.getStringFor(((BTagNode)node).getName() ))){ 60 | return node; 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | 67 | /** 68 | * Prepare() should be called, if any resource has changes. 69 | * @param os 70 | * @throws IOException 71 | */ 72 | public void build(OutputStream os) throws IOException{ 73 | IntWriter writer = new IntWriter(os, false); 74 | mStringBlock.prepare(); 75 | mResBlock.prepare(); 76 | mXMLTree.prepare(); 77 | 78 | int base = 8; 79 | mDocSize = base + mStringBlock.getSize() + mResBlock.getSize() + mXMLTree.getSize(); 80 | 81 | writer.writeInt(MAGIC_NUMBER); 82 | writer.writeInt(mDocSize); 83 | 84 | mStringBlock.write(writer); 85 | mResBlock.write(writer); 86 | mXMLTree.write(writer); 87 | 88 | os.flush(); 89 | os.close(); 90 | } 91 | 92 | public void testSize() throws IOException{ 93 | out.println("string block size:" + mStringBlock.getSize()); 94 | mStringBlock.prepare(); 95 | out.println("string block size:" + mStringBlock.getSize()); 96 | 97 | out.println("res block size:" + mResBlock.getSize()); 98 | mResBlock.prepare(); 99 | out.println("res size:" + mResBlock.getSize()); 100 | 101 | out.println("xml size:" + mXMLTree.getSize()); 102 | mXMLTree.prepare(); 103 | out.println("xml size:" + mXMLTree.getSize()); 104 | 105 | out.println("doc size:" + mDocSize); 106 | int base = 8; 107 | int size = base + mStringBlock.getSize() + mResBlock.getSize() + mXMLTree.getSize(); 108 | out.println("doc size:" + size); 109 | } 110 | 111 | public void print(){ 112 | out.println("size:" + mDocSize); 113 | mXMLTree.print(new XMLVisitor(mStringBlock)); 114 | } 115 | 116 | public void parse(InputStream is) throws Exception{ 117 | IntReader reader = new IntReader(is, false); 118 | 119 | int magicNum = reader.readInt(); 120 | 121 | if(magicNum != MAGIC_NUMBER){ 122 | throw new RuntimeException("Not valid AXML format"); 123 | } 124 | 125 | int size = reader.readInt(); 126 | 127 | mDocSize = size; 128 | 129 | int chunkType = reader.readInt(); 130 | 131 | if(chunkType == CHUNK_STRING_BLOCK){ 132 | parseStringBlock(reader); 133 | } 134 | 135 | chunkType = reader.readInt(); 136 | 137 | if(chunkType == CHUNK_RESOURCE_ID){ 138 | parseResourceBlock(reader); 139 | } 140 | 141 | chunkType = reader.readInt(); 142 | 143 | if(chunkType == CHUNK_XML_TREE){ 144 | parseXMLTree(reader); 145 | } 146 | } 147 | 148 | private void parseStringBlock(IntReader reader) throws Exception{ 149 | StringBlock block = new StringBlock(); 150 | block.read(reader); 151 | 152 | mStringBlock = block; 153 | } 154 | 155 | private void parseResourceBlock(IntReader reader) throws IOException{ 156 | ResBlock block = new ResBlock(); 157 | block.read(reader); 158 | mResBlock = block; 159 | } 160 | 161 | private void parseXMLTree(IntReader reader) throws Exception{ 162 | BXMLTree tree = new BXMLTree(); 163 | tree.read(reader); 164 | 165 | mXMLTree = tree; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/BNSNode.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import java.io.IOException; 4 | 5 | public class BNSNode extends BXMLNode { 6 | private final int TAG_START = 0x00100100; 7 | private final int TAG_END = 0x00100101; 8 | 9 | private int mPrefix; 10 | private int mUri; 11 | 12 | public void checkStartTag(int tag) throws IOException{ 13 | checkTag(TAG_START, tag); 14 | } 15 | 16 | public void checkEndTag(int tag) throws IOException{ 17 | checkTag(TAG_END, tag); 18 | } 19 | 20 | @SuppressWarnings("unused") 21 | public void readStart(IntReader reader) throws IOException{ 22 | super.readStart(reader); 23 | 24 | int ffffx0 = reader.readInt(); //unused int value(0xFFFF) 25 | mPrefix = reader.readInt(); 26 | mUri = reader.readInt(); 27 | } 28 | 29 | @SuppressWarnings("unused") 30 | public void readEnd(IntReader reader) throws IOException{ 31 | super.readEnd(reader); 32 | 33 | int ffffx0 = reader.readInt();//skip unused value 34 | int prefix = reader.readInt(); 35 | int uri = reader.readInt(); 36 | 37 | if((prefix != mPrefix) || (uri != mUri) ){ 38 | throw new IOException("Invalid end element"); 39 | } 40 | } 41 | 42 | public void prepare(){ 43 | //TODO line number 44 | } 45 | 46 | public void writeStart(IntWriter writer) throws IOException{ 47 | writer.writeInt(TAG_START); 48 | super.writeStart(writer); 49 | 50 | writer.writeInt(0xFFFFFFFF); 51 | writer.writeInt(mPrefix); 52 | writer.writeInt(mUri); 53 | } 54 | 55 | public void writeEnd(IntWriter writer) throws IOException{ 56 | writer.writeInt(TAG_END); 57 | super.writeEnd(writer); 58 | 59 | writer.writeInt(0xFFFFFFFF); 60 | writer.writeInt(mPrefix); 61 | writer.writeInt(mUri); 62 | } 63 | 64 | public int getPrefix(){ 65 | return mPrefix; 66 | } 67 | 68 | public int getUri(){ 69 | return mUri; 70 | } 71 | 72 | @Override 73 | public void accept(IVisitor v) { 74 | v.visit(this); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/BTXTNode.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | public class BTXTNode extends BXMLNode implements IVisitable{ 7 | private final int TAG = 0x00100104; 8 | private int mRawName; 9 | 10 | public void checkTag(int value) throws IOException{ 11 | super.checkTag(TAG, value); 12 | } 13 | 14 | @SuppressWarnings("unused") 15 | public void readStart(IntReader reader) throws IOException{ 16 | super.readStart(reader); 17 | 18 | mRawName = reader.readInt(); 19 | 20 | int skip0 = reader.readInt(); 21 | int skip1 = reader.readInt(); 22 | } 23 | 24 | public void readEnd(IntReader reader) throws IOException{ 25 | } 26 | 27 | public void prepare(){ 28 | 29 | } 30 | 31 | public void writeStart(IntWriter writer) throws IOException{ 32 | writer.writeInt(TAG); 33 | super.writeStart(writer); 34 | writer.writeInt(mRawName); 35 | 36 | writer.writeInt(0);//skiped 37 | writer.writeInt(0);//skiped 38 | } 39 | 40 | public void writeEnd(IntWriter writer){ 41 | 42 | } 43 | 44 | public int getName(){ 45 | return mRawName; 46 | } 47 | 48 | public boolean hasChild(){ 49 | return false; 50 | } 51 | 52 | public List getChildren(){ 53 | throw new RuntimeException("Text node has no child"); 54 | } 55 | 56 | public void addChild(BXMLNode node){ 57 | throw new RuntimeException("Can't add child to Text node"); 58 | } 59 | 60 | @Override 61 | public void accept(IVisitor v) { 62 | v.visit(this); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/BTagNode.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import com.github.nukc.plugin.axml.utils.TypedValue; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class BTagNode extends BXMLNode { 10 | private final int TAG_START = 0x00100102; 11 | private final int TAG_END = 0x00100103; 12 | 13 | private int mRawNSUri; 14 | private int mRawName; 15 | 16 | private short mRawAttrCount; //(id attr)<<16 + (normal attr ?) 17 | 18 | private short mRawClassAttr; //'class=' 19 | private short mRawIdAttr; //'android:id=' 20 | private short mRawStyleAttr; //'style=' 21 | 22 | private List mRawAttrs; 23 | 24 | public BTagNode(){} 25 | public BTagNode(int ns, int name){ 26 | mRawName = name; 27 | mRawNSUri = ns; 28 | } 29 | 30 | public void checkStartTag(int tag) throws IOException{ 31 | checkTag(TAG_START, tag); 32 | } 33 | 34 | public void checkEndTag(int tag) throws IOException{ 35 | checkTag(TAG_END, tag); 36 | } 37 | 38 | @SuppressWarnings("unused") 39 | public void readStart(IntReader reader) throws IOException{ 40 | super.readStart(reader); 41 | 42 | int xffff_ffff = reader.readInt(); //unused int value(0xFFFF_FFFF) 43 | mRawNSUri = reader.readInt(); //TODO maybe not ns uri (0xFFFF) 44 | mRawName = reader.readInt(); //name for element 45 | int x0014_0014 = reader.readInt(); //TODO unknown field 46 | 47 | mRawAttrCount = (short)reader.readShort(); //attribute count 48 | 49 | mRawIdAttr = (short)reader.readShort(); //id attribute 50 | mRawClassAttr = (short)reader.readShort(); //class 51 | mRawStyleAttr = (short)reader.readShort(); 52 | 53 | if(mRawAttrCount > 0){ 54 | if(mRawName == 62 ){ 55 | System.out.println(); 56 | } 57 | mRawAttrs = new ArrayList(); 58 | int [] attrs = reader.readIntArray(mRawAttrCount* Attribute.SIZE); //namespace, name, value(string),value(type),value(data) 59 | for(int i=0; i< mRawAttrCount; i++){ 60 | mRawAttrs.add(new Attribute(subArray(attrs, i* Attribute.SIZE, Attribute.SIZE))); 61 | 62 | Attribute attr = mRawAttrs.get(i); 63 | } 64 | } 65 | } 66 | 67 | @SuppressWarnings("unused") 68 | public void readEnd(IntReader reader) throws IOException{ 69 | super.readEnd(reader); 70 | 71 | int xffff_ffff = reader.readInt(); //unused int value(0xFFFF_FFFF) 72 | int ns_uri = reader.readInt(); 73 | int name = reader.readInt(); 74 | 75 | if((ns_uri != mRawNSUri) || (name != mRawName) ){ 76 | throw new IOException("Invalid end element"); 77 | } 78 | } 79 | 80 | private static final int INT_SIZE = 4; 81 | 82 | //chunsize, attr count 83 | public void prepare(){ 84 | int base_first = INT_SIZE * 9; 85 | //System.out.println("chunksize origin 1:" + mChunkSize.first + " 2:"+mChunkSize.second); 86 | mRawAttrCount =(short)(mRawAttrs == null ? 0: mRawAttrs.size()); 87 | //ignore id, class, style attribute's bee's way 88 | 89 | int attrSize = mRawAttrs == null ? 0: mRawAttrs.size()* Attribute.SIZE*INT_SIZE; 90 | mChunkSize.first = base_first + attrSize; 91 | mChunkSize.second = INT_SIZE*6; 92 | //System.out.println("chunksize after 1:" + mChunkSize.first + " 2:"+mChunkSize.second); 93 | //TODO ~ line number ~ 94 | } 95 | 96 | public void writeStart(IntWriter writer) throws IOException{ 97 | writer.writeInt(TAG_START); 98 | super.writeStart(writer); 99 | writer.writeInt(0xFFFFFFFF); 100 | writer.writeInt(mRawNSUri); 101 | writer.writeInt(mRawName); 102 | writer.writeInt(0x00140014); 103 | 104 | writer.writeShort(mRawAttrCount); 105 | writer.writeShort(mRawIdAttr);//id 106 | writer.writeShort(mRawClassAttr);//class 107 | writer.writeShort(mRawStyleAttr);//style 108 | 109 | if(mRawAttrCount > 0){ 110 | for(Attribute attr : mRawAttrs){ 111 | writer.writeInt(attr.mNameSpace); 112 | writer.writeInt(attr.mName); 113 | writer.writeInt(attr.mString); 114 | writer.writeInt(attr.mType); 115 | writer.writeInt(attr.mValue); 116 | } 117 | } 118 | } 119 | 120 | public void writeEnd(IntWriter writer) throws IOException{ 121 | writer.writeInt(TAG_END); 122 | super.writeEnd(writer); 123 | writer.writeInt(0xFFFFFFFF); 124 | writer.writeInt(mRawNSUri); 125 | writer.writeInt(mRawName); 126 | } 127 | 128 | /** 129 | * Eg:android:id="@+id/xxx". Equivalent to getAttributeValue(null, "id"). 130 | * @return Attribute(name="id").mString 131 | */ 132 | public int getIdAttr(){ 133 | return getAttrStringForKey(mRawIdAttr); 134 | } 135 | /** 136 | * Eg:android:class="com.foo.example". Equivalent to getAttributeValue(null, "class"). 137 | * @return Attribute(name="class").mString 138 | */ 139 | public int getClassAttr(){ 140 | return getAttrStringForKey(mRawClassAttr); 141 | } 142 | /** 143 | * Eg:style=""@style/Button". Equivalent to getAttributeValue(null, "style"). 144 | * @return Attribute(name="style").mString 145 | */ 146 | public int getStyleAttr(){ 147 | return getAttrStringForKey(mRawStyleAttr); 148 | } 149 | 150 | public Attribute[] getAttribute(){ 151 | if(mRawAttrs == null){ 152 | return new Attribute[0]; 153 | }else{ 154 | return mRawAttrs.toArray(new Attribute[mRawAttrs.size()]); 155 | } 156 | } 157 | 158 | public void setAttribute(Attribute attr){ 159 | if(mRawAttrs == null){ 160 | mRawAttrs = new ArrayList(); 161 | } 162 | 163 | mRawAttrs.add(attr); 164 | } 165 | 166 | /** 167 | * 168 | * @param key 169 | * @return String mapping id 170 | */ 171 | public int getAttrStringForKey(int key){ 172 | Attribute[] attrs = getAttribute(); 173 | 174 | for(Attribute attr : attrs){ 175 | if(attr.mName == key){ 176 | return attr.mString; 177 | } 178 | } 179 | 180 | return -1; 181 | } 182 | 183 | public boolean setAttrStringForKey(int key, int string_value){ 184 | Attribute[] attrs = getAttribute(); 185 | 186 | for(Attribute attr : attrs){ 187 | if(attr.mName == key){ 188 | attr.setValue(TypedValue.TYPE_STRING, string_value); 189 | return true; 190 | } 191 | } 192 | 193 | return false; 194 | } 195 | 196 | /** 197 | * return scalar type for key 198 | * @param key 199 | * @return int[]{Type, Value} 200 | */ 201 | public int[] getAttrValueForKey(int key){ 202 | Attribute[] attrs = getAttribute(); 203 | 204 | for(Attribute attr : attrs){ 205 | if(attr.mName == key){ 206 | int type_value [] = new int[2]; 207 | type_value[0] = attr.mType; 208 | type_value[1] = attr.mValue; 209 | return type_value; 210 | } 211 | } 212 | 213 | return null; 214 | } 215 | 216 | /** 217 | * Don't support now 218 | * @param key 219 | * @param type 220 | * @param value 221 | * @return 222 | */ 223 | public boolean setAttrValueForKey(int key, int type, int value){ 224 | return false; 225 | } 226 | 227 | public int getName(){ 228 | return mRawName; 229 | } 230 | 231 | public void setName(int name){ 232 | mRawName = name; 233 | } 234 | 235 | public int getNamesapce(){ 236 | return mRawNSUri; 237 | } 238 | 239 | public void setNamespace(int ns){ 240 | mRawNSUri = ns; 241 | } 242 | 243 | public static class Attribute { 244 | public static final int SIZE = 5; 245 | 246 | public int mNameSpace; 247 | public int mName; 248 | public int mString; 249 | public int mType; 250 | public int mValue; 251 | 252 | public Attribute(int ns, int name, int type){ 253 | mNameSpace = ns; 254 | mName = name; 255 | mType = type<<24; 256 | } 257 | 258 | public void setString(int str){ 259 | if((mType>>24) != TypedValue.TYPE_STRING){ 260 | throw new RuntimeException("Can't set string for none string type"); 261 | } 262 | 263 | mString = str; 264 | mValue = str; 265 | } 266 | 267 | /** 268 | * TODO type >>> 16 = real type , so how to fix it 269 | * @param type 270 | * @param value 271 | */ 272 | public void setValue(int type, int value){ 273 | mType = type<<24; 274 | if(type == TypedValue.TYPE_STRING){ 275 | mValue = value; 276 | mString = value; 277 | }else{ 278 | mValue = value; 279 | mString = -1; 280 | } 281 | 282 | } 283 | 284 | public Attribute(int[] raw){ 285 | mNameSpace = raw[0]; 286 | mName = raw[1]; 287 | mString = raw[2]; 288 | mType = raw[3]; 289 | mValue = raw[4]; 290 | } 291 | 292 | public boolean hasNamespace(){ 293 | return (mNameSpace != -1); 294 | } 295 | } 296 | 297 | private int[] subArray(int[] src, int start, int len){ 298 | if((start + len) > src.length ){ 299 | throw new RuntimeException("OutOfArrayBound"); 300 | } 301 | 302 | int[] des = new int[len]; 303 | System.arraycopy(src, start, des, 0, len); 304 | 305 | return des; 306 | } 307 | 308 | @Override 309 | public void accept(IVisitor v) { 310 | v.visit(this); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/BXMLNode.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import com.github.nukc.plugin.axml.utils.Pair; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public abstract class BXMLNode implements IVisitable{ 10 | public Pair mChunkSize = new Pair(); 11 | public Pair mLineNumber= new Pair(); 12 | { 13 | mLineNumber.first = 0; 14 | mLineNumber.second = 0; 15 | } 16 | private List mChild; 17 | 18 | public void checkTag(int expect, int value) throws IOException{ 19 | if(value != expect){ 20 | throw new IOException("Can't read current node"); 21 | } 22 | } 23 | 24 | public void readStart(IntReader reader) throws IOException{ 25 | mChunkSize.first = reader.readInt(); 26 | mLineNumber.first = reader.readInt(); 27 | } 28 | 29 | public void readEnd(IntReader reader) throws IOException{ 30 | mChunkSize.second = reader.readInt(); 31 | mLineNumber.second = reader.readInt(); 32 | } 33 | 34 | public void writeStart(IntWriter writer) throws IOException{ 35 | writer.writeInt(mChunkSize.first); 36 | writer.writeInt(mLineNumber.first); 37 | } 38 | 39 | public void writeEnd(IntWriter writer) throws IOException{ 40 | writer.writeInt(mChunkSize.second); 41 | writer.writeInt(mLineNumber.second); 42 | } 43 | 44 | public boolean hasChild(){ 45 | return (mChild != null && !mChild.isEmpty()); 46 | } 47 | 48 | public List getChildren(){ 49 | return mChild; 50 | } 51 | 52 | public void addChild(BXMLNode node){ 53 | if(mChild == null) mChild = new ArrayList(); 54 | if(node != null){ 55 | mChild.add(node); 56 | } 57 | } 58 | 59 | public abstract void prepare(); 60 | 61 | public Pair getSize(){ 62 | return mChunkSize; 63 | } 64 | 65 | public Pair getLineNumber(){ 66 | return mLineNumber; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/BXMLTree.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import com.github.nukc.plugin.axml.utils.Pair; 4 | 5 | import java.io.IOException; 6 | import java.util.Stack; 7 | 8 | public class BXMLTree implements IAXMLSerialize{ 9 | private final int NS_START = 0x00100100; 10 | private final int NS_END = 0x00100101; 11 | private final int NODE_START= 0x00100102; 12 | private final int NODE_END = 0x00100103; 13 | private final int TEXT = 0x00100104; 14 | 15 | private Stack mVisitor; 16 | private BNSNode mRoot; 17 | private int mSize; 18 | 19 | public BXMLTree(){ 20 | mRoot = new BNSNode(); 21 | mVisitor = new Stack(); 22 | } 23 | 24 | public void print(IVisitor visitor){ 25 | mRoot.accept(visitor); 26 | } 27 | 28 | public void write(IntWriter writer) throws IOException{ 29 | write(mRoot, writer); 30 | } 31 | 32 | public void prepare(){ 33 | mSize = 0; 34 | prepare(mRoot); 35 | } 36 | 37 | private void write(BXMLNode node, IntWriter writer) throws IOException{ 38 | node.writeStart(writer); 39 | 40 | if(node.hasChild()){ 41 | for(BXMLNode child : node.getChildren()){ 42 | write(child, writer); 43 | } 44 | } 45 | node.writeEnd(writer); 46 | } 47 | 48 | private void prepare(BXMLNode node){ 49 | node.prepare(); 50 | Pair p = node.getSize(); 51 | mSize += p.first + p.second; 52 | 53 | if(node.hasChild()){ 54 | for(BXMLNode child:node.getChildren()){ 55 | prepare(child); 56 | } 57 | } 58 | } 59 | 60 | public int getSize(){ 61 | return mSize; 62 | } 63 | 64 | public BXMLNode getRoot(){ 65 | return mRoot; 66 | } 67 | 68 | public void read(IntReader reader) throws IOException{ 69 | mRoot.checkStartTag(NS_START); 70 | mVisitor.push(mRoot); 71 | mRoot.readStart(reader); 72 | 73 | int chunkType; 74 | 75 | end:while(true){ 76 | chunkType = reader.readInt(); 77 | 78 | switch(chunkType){ 79 | case NODE_START: 80 | { 81 | BTagNode node = new BTagNode(); 82 | node.checkStartTag(NODE_START); 83 | BXMLNode parent = mVisitor.peek(); 84 | parent.addChild(node); 85 | mVisitor.push(node); 86 | 87 | node.readStart(reader); 88 | } 89 | break; 90 | case NODE_END: 91 | { 92 | BTagNode node = (BTagNode)mVisitor.pop(); 93 | node.checkEndTag(NODE_END); 94 | node.readEnd(reader); 95 | } 96 | break; 97 | case TEXT: 98 | { 99 | System.out.println("Hello Text"); 100 | 101 | } 102 | break; 103 | case NS_END: 104 | break end; 105 | } 106 | } 107 | 108 | if( !mRoot.equals(mVisitor.pop())){ 109 | throw new IOException("doc has invalid end"); 110 | } 111 | 112 | mRoot.checkEndTag(chunkType); 113 | mRoot.readEnd(reader); 114 | } 115 | 116 | @Override 117 | public int getType() { 118 | return 0; 119 | } 120 | 121 | @Override 122 | public void setSize(int size) { 123 | } 124 | 125 | @Override 126 | public void setType(int type) { 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/IAXMLSerialize.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import java.io.IOException; 4 | 5 | public interface IAXMLSerialize { 6 | public int getSize(); 7 | public int getType(); 8 | 9 | public void setSize(int size); 10 | public void setType(int type); 11 | 12 | public void read(IntReader reader) throws IOException; 13 | public void write(IntWriter writer) throws IOException; 14 | } 15 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/IVisitable.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | public interface IVisitable { 4 | public void accept(IVisitor v); 5 | } 6 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/IVisitor.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | public interface IVisitor { 4 | public void visit(BNSNode node); 5 | public void visit(BTagNode node); 6 | public void visit(BTXTNode node); 7 | } 8 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/IntReader.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.EOFException; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | /** 9 | * @author Dmitry Skiba 10 | * 11 | * Simple helper class that allows reading of integers. 12 | */ 13 | public final class IntReader { 14 | 15 | public IntReader() { 16 | } 17 | public IntReader(InputStream stream,boolean bigEndian) { 18 | reset(stream,bigEndian); 19 | } 20 | 21 | public final void reset(InputStream stream,boolean bigEndian) { 22 | m_stream= new BufferedInputStream(stream); 23 | m_bigEndian=bigEndian; 24 | m_position=0; 25 | } 26 | 27 | public final void close() { 28 | if (m_stream==null) { 29 | return; 30 | } 31 | try { 32 | m_stream.close(); 33 | } 34 | catch (IOException e) { 35 | } 36 | reset(null,false); 37 | } 38 | 39 | public final InputStream getStream() { 40 | return m_stream; 41 | } 42 | 43 | public final boolean isBigEndian() { 44 | return m_bigEndian; 45 | } 46 | public final void setBigEndian(boolean bigEndian) { 47 | m_bigEndian=bigEndian; 48 | } 49 | 50 | public final int readByte() throws IOException { 51 | return readInt(1); 52 | } 53 | public final int readShort() throws IOException { 54 | return readInt(2); 55 | } 56 | public final int readInt() throws IOException { 57 | return readInt(4); 58 | } 59 | 60 | public final int readInt(int length) throws IOException { 61 | if (length<0 || length>4) { 62 | throw new IllegalArgumentException(); 63 | } 64 | int result=0; 65 | if (m_bigEndian) { 66 | for (int i=(length-1)*8;i>=0;i-=8) { 67 | int b=m_stream.read(); 68 | if (b==-1) { 69 | throw new EOFException(); 70 | } 71 | m_position+=1; 72 | result|=(b<0;length-=1) { 97 | array[offset++]=readInt(); 98 | } 99 | } 100 | 101 | public final byte[] readByteArray(int length) throws IOException { 102 | byte[] array=new byte[length]; 103 | int read=m_stream.read(array); 104 | m_position+=read; 105 | if (read!=length) { 106 | throw new EOFException(); 107 | } 108 | return array; 109 | } 110 | 111 | public final void skip(int bytes) throws IOException { 112 | if (bytes<=0) { 113 | return; 114 | } 115 | long skipped=m_stream.skip(bytes); 116 | m_position+=skipped; 117 | if (skipped!=bytes) { 118 | throw new EOFException(); 119 | } 120 | } 121 | 122 | public final void skipInt() throws IOException { 123 | skip(4); 124 | } 125 | 126 | public final int available() throws IOException { 127 | return m_stream.available(); 128 | } 129 | 130 | public final int getPosition() { 131 | return m_position; 132 | } 133 | 134 | /////////////////////////////////// data 135 | 136 | private InputStream m_stream; 137 | private boolean m_bigEndian; 138 | private int m_position; 139 | } 140 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/IntWriter.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.nio.ByteBuffer; 6 | import java.nio.ByteOrder; 7 | 8 | public class IntWriter { 9 | public IntWriter() { 10 | } 11 | public IntWriter(OutputStream stream,boolean bigEndian) { 12 | reset(stream,bigEndian); 13 | } 14 | 15 | public final void reset(OutputStream stream,boolean bigEndian) { 16 | m_stream=stream; 17 | m_bigEndian=bigEndian; 18 | m_position=0; 19 | 20 | ByteOrder order = m_bigEndian ? ByteOrder.BIG_ENDIAN: ByteOrder.LITTLE_ENDIAN; 21 | shortBB.order(order); 22 | intBB.order(order); 23 | } 24 | 25 | public final void close() { 26 | if (m_stream==null) { 27 | return; 28 | } 29 | try { 30 | m_stream.flush(); 31 | m_stream.close(); 32 | } 33 | catch (IOException e) { 34 | } 35 | reset(null,false); 36 | } 37 | 38 | public final OutputStream getStream() { 39 | return m_stream; 40 | } 41 | 42 | public final boolean isBigEndian() { 43 | return m_bigEndian; 44 | } 45 | public final void setBigEndian(boolean bigEndian) { 46 | m_bigEndian=bigEndian; 47 | } 48 | 49 | public final void writeByte(byte b) throws IOException { 50 | m_stream.write(b); 51 | m_position += 1; 52 | } 53 | 54 | public final int writeShort(short s) throws IOException { 55 | shortBB.clear(); 56 | shortBB.putShort(s); 57 | 58 | m_stream.write(shortBB.array()); 59 | m_position += 2; 60 | 61 | return 2; 62 | } 63 | 64 | public final int writeInt(int i) throws IOException { 65 | intBB.clear(); 66 | intBB.putInt(i); 67 | 68 | m_stream.write(intBB.array()); 69 | m_position += 4; 70 | 71 | return 4; 72 | } 73 | 74 | public final void writeIntArray(int[] array) throws IOException { 75 | for(int i : array){ 76 | writeInt(i); 77 | } 78 | } 79 | 80 | public final void writeIntArray(int[] array,int offset,int length) throws IOException { 81 | int limit = offset + length; 82 | for(int i = offset; i< limit; i++){ 83 | writeInt(i); 84 | } 85 | } 86 | 87 | public final int writeByteArray(byte[] array) throws IOException { 88 | m_stream.write(array); 89 | m_position += array.length; 90 | 91 | return array.length; 92 | } 93 | 94 | public final void skip(int n, byte def) throws IOException { 95 | for(int i =0; i< n; i++){ 96 | m_stream.write(def); 97 | } 98 | 99 | m_position += n; 100 | } 101 | 102 | public final void skipIntFFFF() throws IOException { 103 | writeInt(Integer.MAX_VALUE); 104 | } 105 | 106 | public final void skipInt0000() throws IOException { 107 | writeInt(0); 108 | } 109 | 110 | public final int getPosition() { 111 | return m_position; 112 | } 113 | 114 | /////////////////////////////////// data 115 | 116 | private OutputStream m_stream; 117 | private boolean m_bigEndian; 118 | private int m_position; 119 | 120 | private ByteBuffer shortBB = ByteBuffer.allocate(2); 121 | private ByteBuffer intBB = ByteBuffer.allocate(4); 122 | } 123 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/ResBlock.java: -------------------------------------------------------------------------------- 1 | package com.github.nukc.plugin.axml.decode; 2 | 3 | import java.io.IOException; 4 | 5 | public class ResBlock implements IAXMLSerialize{ 6 | private static final int TAG = 0x00080180; 7 | 8 | private int mChunkSize; 9 | private int[] mRawResIds; 10 | 11 | public void print(){ 12 | StringBuilder sb = new StringBuilder(); 13 | 14 | for(int id : getResourceIds()){ 15 | sb.append(id); 16 | sb.append(" "); 17 | } 18 | 19 | System.out.println(sb.toString()); 20 | } 21 | 22 | public void read(IntReader reader) throws IOException{ 23 | mChunkSize = reader.readInt(); 24 | 25 | if(mChunkSize < 8 || (mChunkSize % 4)!= 0){ 26 | throw new IOException("Invalid resource ids size ("+mChunkSize+")."); 27 | } 28 | 29 | mRawResIds = reader.readIntArray(mChunkSize/4 - 2);//subtract base offset (type + size) 30 | } 31 | 32 | private final int INT_SIZE = 4; 33 | public void prepare(){ 34 | int base = 2*INT_SIZE; 35 | int resSize = mRawResIds == null ? 0:mRawResIds.length*INT_SIZE; 36 | mChunkSize = base + resSize; 37 | } 38 | 39 | @Override 40 | public void write(IntWriter writer) throws IOException { 41 | writer.writeInt(TAG); 42 | writer.writeInt(mChunkSize); 43 | 44 | if(mRawResIds != null){ 45 | for(int id : mRawResIds){ 46 | writer.writeInt(id); 47 | } 48 | } 49 | } 50 | 51 | public int[] getResourceIds(){ 52 | return mRawResIds; 53 | } 54 | 55 | public int getResourceIdAt(int index){ 56 | return mRawResIds[index]; 57 | } 58 | 59 | @Override 60 | public int getSize() { 61 | return mChunkSize; 62 | } 63 | 64 | @Override 65 | public int getType() { 66 | return TAG; 67 | } 68 | 69 | @Override 70 | public void setSize(int size) { 71 | // TODO Auto-generated method stub 72 | 73 | } 74 | 75 | @Override 76 | public void setType(int type) { 77 | // TODO Auto-generated method stub 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/com/github/nukc/plugin/axml/decode/StringBlock.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2011 Ryszard Wiśniewski 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.github.nukc.plugin.axml.decode; 18 | 19 | import java.io.IOException; 20 | import java.io.UnsupportedEncodingException; 21 | import java.nio.charset.Charset; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | /** 26 | * write and read StringBlock 27 | * @author NTOOOOOP 28 | */ 29 | public class StringBlock implements IAXMLSerialize{ 30 | private static final int TAG = 0x001C0001; 31 | private static final int INT_SIZE = 4; 32 | 33 | private int mChunkSize; 34 | private int mStringsCount; 35 | private int mStylesCount; 36 | private int mEncoder; 37 | 38 | private int mStrBlockOffset; 39 | private int mStyBlockOffset; 40 | 41 | private int[] mPerStrOffset; 42 | private int[] mPerStyOffset; 43 | 44 | /** 45 | * raw String 46 | */ 47 | private List mStrings; 48 | /** 49 | * android can identify HTML tags in a string,all the styles are kept here 50 | */ 51 | private List