From 9bbf4bb32b30888926085920f0edd0b475277772 Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Sat, 19 Apr 2025 01:51:12 +0800 Subject: [PATCH] =?UTF-8?q?6.6.3=20-=20Alpha=20-=20=E4=BF=AE=E5=A4=8D=20AP?= =?UTF-8?q?K=20=E6=96=87=E4=BB=B6=E7=B1=BB=E5=9E=8B=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E5=8F=AF=E8=83=BD=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=BA=94=E7=94=A8=E5=90=8D=E7=A7=B0=E5=8F=8A?= =?UTF-8?q?=20SDK=20=E4=BF=A1=E6=81=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_zh-Hans.json | 1 + .readme/README-ar.md | 2 +- .readme/README-en.md | 2 +- .readme/README-es.md | 2 +- .readme/README-fr.md | 2 +- .readme/README-ja.md | 2 +- .readme/README-ko.md | 2 +- .readme/README-ru.md | 2 +- .readme/README-zh-Hans.md | 3 +- .readme/README-zh-Hant-HK.md | 2 +- .readme/README-zh-Hant-TW.md | 2 +- README.md | 3 +- app/build.gradle.kts | 6 +- .../main/assets-app/doc/CHANGELOG-zh-Hans.md | 1 + app/src/main/assets-app/doc/CHANGELOG.md | 1 + .../ui/main/scripts/ApkInfoDialogManager.kt | 7 +- .../autojs/ui/project/BuildActivity.java | 21 +- modules/apk-parser/.gitignore | 1 + modules/apk-parser/build.gradle | 34 + modules/apk-parser/consumer-rules.pro | 4 + modules/apk-parser/proguard-rules.pro | 21 + .../apk-parser/src/main/AndroidManifest.xml | 1 + .../dongliu/apk/parser/AbstractApkFile.java | 428 ++++++ .../java/net/dongliu/apk/parser/ApkFile.java | 124 ++ .../net/dongliu/apk/parser/ApkParsers.java | 145 ++ .../dongliu/apk/parser/ByteArrayApkFile.java | 83 ++ .../java/net/dongliu/apk/parser/Main.java | 35 + .../dongliu/apk/parser/bean/AdaptiveIcon.java | 53 + .../net/dongliu/apk/parser/bean/ApkMeta.java | 336 +++++ .../dongliu/apk/parser/bean/ApkSignStatus.kt | 14 + .../net/dongliu/apk/parser/bean/ApkSigner.kt | 24 + .../dongliu/apk/parser/bean/ApkV2Signer.java | 21 + .../apk/parser/bean/CertificateMeta.java | 76 ++ .../dongliu/apk/parser/bean/ColorIcon.java | 26 + .../net/dongliu/apk/parser/bean/DexClass.java | 73 + .../dongliu/apk/parser/bean/GlEsVersion.java | 27 + .../net/dongliu/apk/parser/bean/Icon.java | 65 + .../net/dongliu/apk/parser/bean/IconFace.java | 30 + .../net/dongliu/apk/parser/bean/IconPath.java | 34 + .../dongliu/apk/parser/bean/Permission.java | 35 + .../dongliu/apk/parser/bean/UseFeature.java | 24 + .../apk/parser/cert/asn1/Asn1BerParser.java | 593 ++++++++ .../apk/parser/cert/asn1/Asn1Class.java | 28 + .../cert/asn1/Asn1DecodingException.java | 32 + .../apk/parser/cert/asn1/Asn1DerEncoder.java | 511 +++++++ .../cert/asn1/Asn1EncodingException.java | 32 + .../apk/parser/cert/asn1/Asn1Field.java | 55 + .../parser/cert/asn1/Asn1OpaqueObject.java | 41 + .../apk/parser/cert/asn1/Asn1TagClass.kt | 26 + .../apk/parser/cert/asn1/Asn1Tagging.kt | 20 + .../dongliu/apk/parser/cert/asn1/Asn1Type.kt | 20 + .../parser/cert/asn1/ber/BerDataValue.java | 110 ++ .../asn1/ber/BerDataValueFormatException.java | 34 + .../cert/asn1/ber/BerDataValueReader.java | 37 + .../apk/parser/cert/asn1/ber/BerEncoding.java | 189 +++ .../ber/ByteBufferBerDataValueReader.java | 204 +++ .../ber/InputStreamBerDataValueReader.java | 308 +++++ .../cert/pkcs7/AlgorithmIdentifier.java | 43 + .../apk/parser/cert/pkcs7/Attribute.java | 37 + .../apk/parser/cert/pkcs7/ContentInfo.java | 36 + .../cert/pkcs7/EncapsulatedContentInfo.java | 48 + .../cert/pkcs7/IssuerAndSerialNumber.java | 45 + .../apk/parser/cert/pkcs7/Pkcs7Constants.java | 30 + .../apk/parser/cert/pkcs7/SignedData.java | 59 + .../parser/cert/pkcs7/SignerIdentifier.java | 44 + .../apk/parser/cert/pkcs7/SignerInfo.java | 62 + .../apk/parser/exception/ParserException.java | 30 + .../apk/parser/parser/AdaptiveIconParser.java | 70 + .../apk/parser/parser/ApkMetaTranslator.java | 271 ++++ .../apk/parser/parser/ApkSignBlockParser.kt | 134 ++ .../apk/parser/parser/AttributeValues.kt | 150 ++ .../parser/parser/BCCertificateParser.java | 65 + .../apk/parser/parser/BinaryXmlParser.java | 317 +++++ .../apk/parser/parser/CertificateMetas.kt | 73 + .../apk/parser/parser/CertificateParser.kt | 28 + .../apk/parser/parser/CompositeXmlStreamer.kt | 41 + .../dongliu/apk/parser/parser/DexParser.java | 248 ++++ .../parser/parser/JSSECertificateParser.kt | 44 + .../parser/parser/ResourceTableParser.java | 230 ++++ .../apk/parser/parser/StringPoolEntry.kt | 6 + .../apk/parser/parser/XmlNamespaces.kt | 64 + .../dongliu/apk/parser/parser/XmlStreamer.kt | 20 + .../apk/parser/parser/XmlTranslator.java | 126 ++ .../apk/parser/struct/AndroidConstants.kt | 33 + .../dongliu/apk/parser/struct/ChunkHeader.kt | 52 + .../dongliu/apk/parser/struct/ChunkType.kt | 51 + .../net/dongliu/apk/parser/struct/ResValue.kt | 184 +++ .../apk/parser/struct/ResourceValue.java | 316 +++++ .../dongliu/apk/parser/struct/StringPool.java | 25 + .../apk/parser/struct/StringPoolHeader.kt | 56 + .../apk/parser/struct/dex/DexClassStruct.java | 110 ++ .../apk/parser/struct/dex/DexHeader.java | 271 ++++ .../apk/parser/struct/resource/Densities.kt | 17 + .../parser/struct/resource/LibraryEntry.java | 23 + .../parser/struct/resource/LibraryHeader.kt | 23 + .../apk/parser/struct/resource/NullHeader.kt | 6 + .../parser/struct/resource/PackageHeader.java | 95 ++ .../struct/resource/ResTableConfig.java | 262 ++++ .../parser/struct/resource/ResourceEntry.java | 91 ++ .../struct/resource/ResourceMapEntry.java | 56 + .../struct/resource/ResourcePackage.java | 110 ++ .../parser/struct/resource/ResourceTable.java | 106 ++ .../struct/resource/ResourceTableHeader.java | 32 + .../struct/resource/ResourceTableMap.java | 149 ++ .../apk/parser/struct/resource/Type.java | 150 ++ .../parser/struct/resource/TypeHeader.java | 93 ++ .../apk/parser/struct/resource/TypeSpec.java | 32 + .../struct/resource/TypeSpecHeader.java | 65 + .../struct/signingv2/ApkSigningBlock.java | 23 + .../apk/parser/struct/signingv2/Digest.java | 13 + .../parser/struct/signingv2/Signature.java | 14 + .../parser/struct/signingv2/SignerBlock.java | 19 + .../apk/parser/struct/xml/Attribute.kt | 65 + .../apk/parser/struct/xml/Attributes.kt | 70 + .../apk/parser/struct/xml/NullHeader.kt | 11 + .../apk/parser/struct/xml/XmlCData.java | 78 ++ .../apk/parser/struct/xml/XmlHeader.java | 15 + .../parser/struct/xml/XmlNamespaceEndTag.java | 25 + .../struct/xml/XmlNamespaceStartTag.java | 26 + .../apk/parser/struct/xml/XmlNodeEndTag.java | 39 + .../apk/parser/struct/xml/XmlNodeHeader.java | 29 + .../parser/struct/xml/XmlNodeStartTag.java | 57 + .../parser/struct/xml/XmlResourceMapHeader.kt | 9 + .../dongliu/apk/parser/struct/zip/EOCD.java | 94 ++ .../net/dongliu/apk/parser/utils/Buffers.java | 133 ++ .../net/dongliu/apk/parser/utils/Inputs.kt | 24 + .../net/dongliu/apk/parser/utils/Locales.kt | 49 + .../net/dongliu/apk/parser/utils/Pair.java | 33 + .../dongliu/apk/parser/utils/ParseUtils.java | 195 +++ .../apk/parser/utils/ResourceFetcher.java | 142 ++ .../apk/parser/utils/ResourceLoader.kt | 64 + .../net/dongliu/apk/parser/utils/Strings.kt | 65 + .../net/dongliu/apk/parser/utils/Unsigned.kt | 52 + .../parser/utils/xml/AggregateTranslator.java | 55 + .../utils/xml/CharSequenceTranslator.java | 128 ++ .../parser/utils/xml/CodePointTranslator.java | 49 + .../apk/parser/utils/xml/EntityArrays.java | 60 + .../parser/utils/xml/LookupTranslator.java | 82 ++ .../utils/xml/NumericEntityEscaper.java | 122 ++ .../xml/UnicodeUnpairedSurrogateRemover.java | 36 + .../apk/parser/utils/xml/XmlEscaper.java | 62 + .../src/main/resources/r_styles.ini | 720 ++++++++++ .../src/main/resources/r_values.ini | 1207 +++++++++++++++++ settings.gradle.kts | 1 + version.properties | 6 +- 145 files changed, 12589 insertions(+), 27 deletions(-) create mode 100644 modules/apk-parser/.gitignore create mode 100644 modules/apk-parser/build.gradle create mode 100644 modules/apk-parser/consumer-rules.pro create mode 100644 modules/apk-parser/proguard-rules.pro create mode 100644 modules/apk-parser/src/main/AndroidManifest.xml create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/AbstractApkFile.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/ApkFile.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/ApkParsers.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/ByteArrayApkFile.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/Main.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/AdaptiveIcon.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkMeta.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkSignStatus.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkSigner.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkV2Signer.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/CertificateMeta.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ColorIcon.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/DexClass.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/GlEsVersion.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/Icon.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/IconFace.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/IconPath.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/Permission.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/UseFeature.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1BerParser.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Class.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DecodingException.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DerEncoder.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/exception/ParserException.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/DexParser.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkType.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResValue.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPool.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Buffers.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Inputs.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Locales.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Pair.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Strings.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Unsigned.kt create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java create mode 100644 modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java create mode 100644 modules/apk-parser/src/main/resources/r_styles.ini create mode 100644 modules/apk-parser/src/main/resources/r_values.ini diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index a6bb8642..8e7160ae 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -5,6 +5,7 @@ "fix": [ "主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题", "部分设备代码编辑器空行显示方框字符的问题", + "APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题", "console.setContentTextColor 方法导致日志字体颜色丢失默认值的问题 _[`issue #346`](http://issues.autojs6.com/346)_" ], "improvement": [ diff --git a/.readme/README-ar.md b/.readme/README-ar.md index c5784d84..18cf4d12 100644 --- a/.readme/README-ar.md +++ b/.readme/README-ar.md @@ -389,7 +389,7 @@ SDK Tools (أدوات SDK) (في النافذة اليمنى) يعتمد مشروع AutoJs6 على إصدار `JDK (مجموعة تطوير جافا)` بإصدار لا يقل عن `17`، ولكن يفضل الإصدار الذي لا يقل عن `19`. -اعتبارًا من 18 April 2025، الإصدار الأقصى المدعوم من JDK لمشروع AutoJs6 هو `24`. +اعتبارًا من 19 April 2025، الإصدار الأقصى المدعوم من JDK لمشروع AutoJs6 هو `24`. > ملاحظة: إذا كان نظام الكمبيوتر يحتوي على JDK والإصدار يفي بالمتطلبات المذكورة أعلاه، فيمكنك تخطي هذا القسم. diff --git a/.readme/README-en.md b/.readme/README-en.md index 94bddcca..e9cebc69 100644 --- a/.readme/README-en.md +++ b/.readme/README-en.md @@ -389,7 +389,7 @@ Check `Show Package Details`, click NDK and CMake respectively to ensure the cor The `JDK (Java Development Kit)` version required for the AutoJs6 project should be at least `17`, but `19` or higher is recommended. -As of Apr 18 , 2025, AutoJs6 supports up to version `24` of the JDK. +As of Apr 19 , 2025, AutoJs6 supports up to version `24` of the JDK. > Note: If the JDK is already installed on the computer system and the version meets the above requirements, this section can be skipped. diff --git a/.readme/README-es.md b/.readme/README-es.md index f7e016b3..85f6c260 100644 --- a/.readme/README-es.md +++ b/.readme/README-es.md @@ -389,7 +389,7 @@ Marca `Show Package Details (Mostrar detalles del paquete)`, haz clic en NDK y C La versión mínima requerida del `JDK (Kit de Desarrollo de Java)` para el proyecto AutoJs6 es `17`, pero se recomienda usar una versión no inferior a `19`. -Hasta el 18 de April de 2025, AutoJs6 soporta hasta la versión `24` de JDK. +Hasta el 19 de April de 2025, AutoJs6 soporta hasta la versión `24` de JDK. > Nota: Si el sistema ya tiene instalado JDK y cumple con los requisitos anteriores, puedes omitir esta sección. diff --git a/.readme/README-fr.md b/.readme/README-fr.md index 246bb7ce..62087e36 100644 --- a/.readme/README-fr.md +++ b/.readme/README-fr.md @@ -389,7 +389,7 @@ Cochez `Show Package Details (afficher les détails du package)`, puis cliquez r La version de JDK (Kit de développement Java) requise pour le projet AutoJs6 doit être au moins `17`, mais il est recommandé d'avoir au moins `19`. -À partir du 18 April 2025, AutoJs6 prend en charge la version maximale de JDK `24`. +À partir du 19 April 2025, AutoJs6 prend en charge la version maximale de JDK `24`. > Note : Si le JDK est déjà installé sur le système informatique et que la version répond aux exigences ci-dessus, vous pouvez ignorer cette section. diff --git a/.readme/README-ja.md b/.readme/README-ja.md index 839a6e6d..addea4b7 100644 --- a/.readme/README-ja.md +++ b/.readme/README-ja.md @@ -389,7 +389,7 @@ SDK Tools (SDK ツール) (右側のウィンドウにあります) AutoJs6プロジェクトが依存する `JDK (Java開発キット)` のリリースバージョンは少なくとも `17` で, それ以下のバージョンは推奨されませんが, 最低でも `19` の使用を推奨します. -2025 年 4 月 18 日 現在, AutoJs6がサポートする最大のJDKバージョンは `24` です. +2025 年 4 月 19 日 現在, AutoJs6がサポートする最大のJDKバージョンは `24` です. > 注: コンピュータシステムに適切なバージョンのJDKがインストールされている場合, この節の内容をスキップできます. diff --git a/.readme/README-ko.md b/.readme/README-ko.md index 88a7cdff..016e7bf0 100644 --- a/.readme/README-ko.md +++ b/.readme/README-ko.md @@ -389,7 +389,7 @@ SDK Tools (SDK 도구) (오른쪽 창에 위치) AutoJs6 프로젝트는 `JDK (Java 개발 도구 키트)` 버전이 `17` 이상이어야 하지만, `19` 이상을 권장합니다. -2025 년 4 월 18 일 기준으로, AutoJs6이 지원하는 최대 JDK 버전은 `24`입니다. +2025 년 4 월 19 일 기준으로, AutoJs6이 지원하는 최대 JDK 버전은 `24`입니다. > 메모: 시스템에 JDK가 설치되어 있고, 버전이 위의 요구 사항을 충족하는 경우, 이 섹션을 건너뛸 수 있습니다. diff --git a/.readme/README-ru.md b/.readme/README-ru.md index 2f7c601f..320a2fe7 100644 --- a/.readme/README-ru.md +++ b/.readme/README-ru.md @@ -389,7 +389,7 @@ SDK Tools (SDK Инструменты) (находится в правом ок Для проекта AutoJs6 требуется версия `JDK (Java Development Kit)` не ниже `17`, но рекомендуется не ниже `19`. -По состоянию на 18 April 2025 г. AutoJs6 поддерживает JDK максимальной версии `24`. +По состоянию на 19 April 2025 г. AutoJs6 поддерживает JDK максимальной версии `24`. > Примечание: Если на компьютере уже установлен JDK и версия соответствует указанным требованиям, этот раздел можно пропустить. diff --git a/.readme/README-zh-Hans.md b/.readme/README-zh-Hans.md index 3c7563a2..2c81d25c 100644 --- a/.readme/README-zh-Hans.md +++ b/.readme/README-zh-Hans.md @@ -161,6 +161,7 @@ AutoJs6 在 Auto.js 最终项目的基础上, 于 `2021/12/01` 进行二次开 * `修复` 主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题 * `修复` 部分设备代码编辑器空行显示方框字符的问题 +* `修复` APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题 * `修复` console.setContentTextColor 方法导致日志字体颜色丢失默认值的问题 _[`issue #346`](http://issues.autojs6.com/346)_ * `优化` 打包单文件时自动读取并勾选已安装应用的声明权限 _[`issue #362`](http://issues.autojs6.com/362)_ * `优化` 使用异步加载方式一定程度提升文件管理器列表滑动流畅性 @@ -310,7 +311,7 @@ SDK Tools (SDK 工具) (位于右侧窗口) AutoJs6 项目依赖的 `JDK (Java 开发工具包)` 发行版本不低于 `17`, 但建议不低于 `19`. -截至 2025 年 4 月 18 日, AutoJs6 可支持 JDK 最高版本为 `24`. +截至 2025 年 4 月 19 日, AutoJs6 可支持 JDK 最高版本为 `24`. > 注: 如果计算机系统已安装 JDK 且版本满足上述要求, 则可跳过此小节内容. diff --git a/.readme/README-zh-Hant-HK.md b/.readme/README-zh-Hant-HK.md index 29b5abda..6ae40fad 100644 --- a/.readme/README-zh-Hant-HK.md +++ b/.readme/README-zh-Hant-HK.md @@ -389,7 +389,7 @@ SDK Tools (SDK 工具) (位於右側窗口) AutoJs6 項目依賴的 `JDK (Java 開發工具包)` 發行版本不低於 `17`, 但建議不低於 `19`. -截至 2025 年 4 月 18 日, AutoJs6 可支持 JDK 最高版本為 `24`. +截至 2025 年 4 月 19 日, AutoJs6 可支持 JDK 最高版本為 `24`. > 注: 如果計算機系統已安裝 JDK 且版本滿足上述要求, 則可跳過此小節內容. diff --git a/.readme/README-zh-Hant-TW.md b/.readme/README-zh-Hant-TW.md index c83151dc..ead0868a 100644 --- a/.readme/README-zh-Hant-TW.md +++ b/.readme/README-zh-Hant-TW.md @@ -389,7 +389,7 @@ SDK Tools (SDK 工具) (位於右側視窗) AutoJs6 專案依賴的 `JDK (Java 開發工具包)` 發行版本不低於 `17`, 但建議不低於 `19`. -截至 2025 年 4 月 18 日, AutoJs6 可支援 JDK 最高版本為 `24`. +截至 2025 年 4 月 19 日, AutoJs6 可支援 JDK 最高版本為 `24`. > 注: 如果計算機系統已安裝 JDK 且版本滿足上述要求, 則可跳過此小節內容. diff --git a/README.md b/README.md index 3c7563a2..2c81d25c 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ AutoJs6 在 Auto.js 最终项目的基础上, 于 `2021/12/01` 进行二次开 * `修复` 主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题 * `修复` 部分设备代码编辑器空行显示方框字符的问题 +* `修复` APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题 * `修复` console.setContentTextColor 方法导致日志字体颜色丢失默认值的问题 _[`issue #346`](http://issues.autojs6.com/346)_ * `优化` 打包单文件时自动读取并勾选已安装应用的声明权限 _[`issue #362`](http://issues.autojs6.com/362)_ * `优化` 使用异步加载方式一定程度提升文件管理器列表滑动流畅性 @@ -310,7 +311,7 @@ SDK Tools (SDK 工具) (位于右侧窗口) AutoJs6 项目依赖的 `JDK (Java 开发工具包)` 发行版本不低于 `17`, 但建议不低于 `19`. -截至 2025 年 4 月 18 日, AutoJs6 可支持 JDK 最高版本为 `24`. +截至 2025 年 4 月 19 日, AutoJs6 可支持 JDK 最高版本为 `24`. > 注: 如果计算机系统已安装 JDK 且版本满足上述要求, 则可跳过此小节内容. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 11801daf..7f621b89 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -163,8 +163,10 @@ dependencies /* Unclassified */ { // Android Job implementation(project(":libs:android-job-simplified-1.4.3")) - // APK Parser (https://github.com/jaredrummler/APKParser) - implementation(project(":libs:apk-parser-1.0.2")) + // APK Parser + // implementation("com.jaredrummler:apk-parser:1.0.2") + // implementation("com.github.AndroidDeveloperLB:apk-parser:6") + implementation(project(":modules:apk-parser")) // Prism4j implementation(files("$rootDir/libs/prism4j-2.0.0.jar")) diff --git a/app/src/main/assets-app/doc/CHANGELOG-zh-Hans.md b/app/src/main/assets-app/doc/CHANGELOG-zh-Hans.md index 1fc990a9..ce972d52 100644 --- a/app/src/main/assets-app/doc/CHANGELOG-zh-Hans.md +++ b/app/src/main/assets-app/doc/CHANGELOG-zh-Hans.md @@ -10,6 +10,7 @@ * `修复` 主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题 * `修复` 部分设备代码编辑器空行显示方框字符的问题 +* `修复` APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题 * `修复` console.setContentTextColor 方法导致日志字体颜色丢失默认值的问题 _[`issue #346`](http://issues.autojs6.com/346)_ * `优化` 打包单文件时自动读取并勾选已安装应用的声明权限 _[`issue #362`](http://issues.autojs6.com/362)_ * `优化` 使用异步加载方式一定程度提升文件管理器列表滑动流畅性 diff --git a/app/src/main/assets-app/doc/CHANGELOG.md b/app/src/main/assets-app/doc/CHANGELOG.md index 1fc990a9..ce972d52 100644 --- a/app/src/main/assets-app/doc/CHANGELOG.md +++ b/app/src/main/assets-app/doc/CHANGELOG.md @@ -10,6 +10,7 @@ * `修复` 主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题 * `修复` 部分设备代码编辑器空行显示方框字符的问题 +* `修复` APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题 * `修复` console.setContentTextColor 方法导致日志字体颜色丢失默认值的问题 _[`issue #346`](http://issues.autojs6.com/346)_ * `优化` 打包单文件时自动读取并勾选已安装应用的声明权限 _[`issue #362`](http://issues.autojs6.com/362)_ * `优化` 使用异步加载方式一定程度提升文件管理器列表滑动流畅性 diff --git a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt index c5ecee45..0afbbd9f 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt @@ -6,16 +6,17 @@ import android.content.pm.PackageManager.GET_META_DATA import android.os.Build import android.view.LayoutInflater import android.view.View.MeasureSpec.UNSPECIFIED +import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog import com.android.apksig.ApkVerifier -import com.jaredrummler.apkparser.ApkParser import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import net.dongliu.apk.parser.ApkFile import org.autojs.autojs.extension.MaterialDialogExtensions.makeSettingsLaunchable import org.autojs.autojs.extension.MaterialDialogExtensions.makeTextCopyable import org.autojs.autojs.extension.MaterialDialogExtensions.setCopyableTextIfAbsent @@ -115,7 +116,7 @@ object ApkInfoDialogManager { dialog.setIcon(applicationInfo?.apply { sourceDir = apkFilePath publicSourceDir = apkFilePath - }?.loadIcon(packageManager) ?: context.getDrawable(R.drawable.ic_packaging)) + }?.loadIcon(packageManager) ?: AppCompatResources.getDrawable(context, R.drawable.ic_packaging)) dialog.makeSettingsLaunchable({ it.iconView }, installedPackageName) dialog.makeTextCopyable { it.titleView } @@ -138,7 +139,7 @@ object ApkInfoDialogManager { private suspend fun getApkInfo(apkFile: File): ApkInfo? = withContext(Dispatchers.IO) { runCatching { - ApkParser.create(apkFile).use { parser -> + ApkFile(apkFile).use { parser -> val meta = runCatching { parser.apkMeta }.getOrNull() val label = meta?.label val packageName = meta?.packageName diff --git a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java index da8e4f36..a6c1cea6 100644 --- a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java +++ b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java @@ -27,11 +27,11 @@ import androidx.lifecycle.ViewModelProvider; import com.afollestad.materialdialogs.MaterialDialog; import com.google.android.flexbox.FlexboxLayout; import com.google.android.material.textfield.TextInputLayout; -import com.jaredrummler.apkparser.ApkParser; -import com.jaredrummler.apkparser.model.ApkMeta; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; +import net.dongliu.apk.parser.ApkFile; +import net.dongliu.apk.parser.bean.ApkMeta; import org.autojs.autojs.apkbuilder.ApkBuilder; import org.autojs.autojs.apkbuilder.keystore.KeyStore; import org.autojs.autojs.core.pref.Language; @@ -63,6 +63,7 @@ import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; @@ -614,16 +615,16 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa } private void updatePermissionsCheckboxes(String packageName) { - ApkParser parser; + ApkFile apkFile; try { - parser = ApkParser.create(getPackageManager(), packageName); - } catch (PackageManager.NameNotFoundException e) { + apkFile = new ApkFile(new File(getPackageManager().getApplicationInfo(packageName, 0).sourceDir)); + } catch (PackageManager.NameNotFoundException | IOException e) { return; } try { - ApkMeta meta = parser.getApkMeta(); - if (meta == null || meta.usesPermissions == null) { + ApkMeta meta = apkFile.getApkMeta(); + if (meta == null) { return; } var initialPermissions = meta.usesPermissions; @@ -649,7 +650,11 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa } catch (Exception ignored) { /* Ignored. */ } finally { - parser.close(); + try { + apkFile.close(); + } catch (IOException e) { + /* Ignored. */ + } } } diff --git a/modules/apk-parser/.gitignore b/modules/apk-parser/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/modules/apk-parser/.gitignore @@ -0,0 +1 @@ +/build diff --git a/modules/apk-parser/build.gradle b/modules/apk-parser/build.gradle new file mode 100644 index 00000000..043fb4d3 --- /dev/null +++ b/modules/apk-parser/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + namespace 'net.dongliu.apk.parser' + compileSdk = project.ext.compileSdk + + defaultConfig { + minSdk = project.ext.minSdk + targetSdk = project.ext.targetSdk + + versionName '6' + + consumerProguardFiles "consumer-rules.pro" + multiDexEnabled true + } + + compileOptions { + sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion) + targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion) + } + + kotlinOptions { + jvmTarget = project.ext.javaVersion + } +} + +dependencies { + implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' + implementation 'androidx.annotation:annotation:1.9.1' +} diff --git a/modules/apk-parser/consumer-rules.pro b/modules/apk-parser/consumer-rules.pro new file mode 100644 index 00000000..2fb6dc27 --- /dev/null +++ b/modules/apk-parser/consumer-rules.pro @@ -0,0 +1,4 @@ +-dontwarn java.awt.Component +-dontwarn java.awt.GraphicsEnvironment +-dontwarn java.awt.HeadlessException +-dontwarn java.awt.Window diff --git a/modules/apk-parser/proguard-rules.pro b/modules/apk-parser/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/modules/apk-parser/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/modules/apk-parser/src/main/AndroidManifest.xml b/modules/apk-parser/src/main/AndroidManifest.xml new file mode 100644 index 00000000..cc947c56 --- /dev/null +++ b/modules/apk-parser/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/AbstractApkFile.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/AbstractApkFile.java new file mode 100644 index 00000000..43aa99ab --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/AbstractApkFile.java @@ -0,0 +1,428 @@ +package net.dongliu.apk.parser; + +import static java.lang.System.arraycopy; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.bean.AdaptiveIcon; +import net.dongliu.apk.parser.bean.ApkMeta; +import net.dongliu.apk.parser.bean.ApkSignStatus; +import net.dongliu.apk.parser.bean.ApkSigner; +import net.dongliu.apk.parser.bean.ApkV2Signer; +import net.dongliu.apk.parser.bean.CertificateMeta; +import net.dongliu.apk.parser.bean.DexClass; +import net.dongliu.apk.parser.bean.Icon; +import net.dongliu.apk.parser.bean.IconFace; +import net.dongliu.apk.parser.bean.IconPath; +import net.dongliu.apk.parser.exception.ParserException; +import net.dongliu.apk.parser.parser.AdaptiveIconParser; +import net.dongliu.apk.parser.parser.ApkMetaTranslator; +import net.dongliu.apk.parser.parser.ApkSignBlockParser; +import net.dongliu.apk.parser.parser.BinaryXmlParser; +import net.dongliu.apk.parser.parser.CertificateMetas; +import net.dongliu.apk.parser.parser.CertificateParser; +import net.dongliu.apk.parser.parser.CompositeXmlStreamer; +import net.dongliu.apk.parser.parser.DexParser; +import net.dongliu.apk.parser.parser.ResourceTableParser; +import net.dongliu.apk.parser.parser.XmlStreamer; +import net.dongliu.apk.parser.parser.XmlTranslator; +import net.dongliu.apk.parser.struct.AndroidConstants; +import net.dongliu.apk.parser.struct.resource.ResourceTable; +import net.dongliu.apk.parser.struct.signingv2.ApkSigningBlock; +import net.dongliu.apk.parser.struct.signingv2.SignerBlock; +import net.dongliu.apk.parser.struct.zip.EOCD; +import net.dongliu.apk.parser.utils.Buffers; +import net.dongliu.apk.parser.utils.Unsigned; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +/** + * Common Apk Parser methods. + * This Class is not thread-safe. + * + * @author Liu Dong + */ +public abstract class AbstractApkFile implements Closeable { + private DexClass[] dexClasses; + + private boolean resourceTableParsed; + private ResourceTable resourceTable; + private Set locales; + + private boolean manifestParsed; + private String manifestXml; + private ApkMeta apkMeta; + private List iconPaths; + private List apkSigners; + private List apkV2Signers; + + private static final Locale DEFAULT_LOCALE = Locale.US; + + /** + * default use empty locale + */ + private Locale preferredLocale = AbstractApkFile.DEFAULT_LOCALE; + + /** + * return decoded AndroidManifest.xml + * + * @return decoded AndroidManifest.xml + */ + public String getManifestXml() throws IOException { + this.parseManifest(); + return this.manifestXml; + } + + /** + * return decoded AndroidManifest.xml + * + * @return decoded AndroidManifest.xml + */ + public ApkMeta getApkMeta() throws IOException { + this.parseManifest(); + return this.apkMeta; + } + + /** + * get locales supported from resource file + * + * @return decoded AndroidManifest.xml + */ + public Set getLocales() throws IOException { + this.parseResourceTable(); + return this.locales; + } + + /** + * Get the apk's all cert file info, of apk v1 signing. + * If cert faile not exist, return empty list. + */ + public List getApkSigners() throws IOException, CertificateException { + if (this.apkSigners == null) { + this.parseCertificates(); + } + return this.apkSigners; + } + + private void parseCertificates() throws IOException, CertificateException { + this.apkSigners = new ArrayList<>(); + for (final CertificateFile file : this.getAllCertificateData()) { + final CertificateParser parser = CertificateParser.getInstance(file.data); + final List certificateMetas = parser.parse(); + this.apkSigners.add(new ApkSigner(file.path, certificateMetas)); + } + } + + /** + * Get the apk's all signer in apk sign block, using apk singing v2 scheme. + * If apk v2 signing block not exists, return empty list. + */ + public List getApkV2Singers() throws IOException, CertificateException { + if (this.apkV2Signers == null) { + this.parseApkSigningBlock(); + } + return this.apkV2Signers; + } + + private void parseApkSigningBlock() throws IOException, CertificateException { + final List list = new ArrayList<>(); + final ByteBuffer apkSignBlockBuf = this.findApkSignBlock(); + if (apkSignBlockBuf != null) { + final ApkSignBlockParser parser = new ApkSignBlockParser(apkSignBlockBuf); + final ApkSigningBlock apkSigningBlock = parser.parse(); + for (final SignerBlock signerBlock : apkSigningBlock.signerBlocks) { + final List certificates = signerBlock.certificates; + final List certificateMetas = CertificateMetas.from(certificates); + final ApkV2Signer apkV2Signer = new ApkV2Signer(certificateMetas); + list.add(apkV2Signer); + } + } + this.apkV2Signers = list; + } + + @NonNull + protected abstract List getAllCertificateData() throws IOException; + + protected static class CertificateFile { + @NonNull + public final String path; + @NonNull + public final byte[] data; + + public CertificateFile(@NonNull final String path, @NonNull final byte[] data) { + this.path = path; + this.data = data; + } + } + + private void parseManifest() throws IOException { + if (this.manifestParsed) { + return; + } + this.parseResourceTable(); + final XmlTranslator xmlTranslator = new XmlTranslator(); + final ApkMetaTranslator apkTranslator = new ApkMetaTranslator(this.resourceTable, this.preferredLocale); + final XmlStreamer xmlStreamer = new CompositeXmlStreamer(xmlTranslator, apkTranslator); + final byte[] data = this.getFileData(AndroidConstants.MANIFEST_FILE); + if (data == null) { + throw new ParserException("Manifest file not found"); + } + this.transBinaryXml(data, xmlStreamer); + this.manifestXml = xmlTranslator.getXml(); + this.apkMeta = apkTranslator.getApkMeta(); + this.iconPaths = apkTranslator.getIconPaths(); + this.manifestParsed = true; + } + + /** + * read file in apk into bytes + */ + @Nullable + public abstract byte[] getFileData(@NonNull String path) throws IOException; + + /** + * return the whole apk file as ByteBuffer + */ + @NonNull + protected abstract ByteBuffer fileData() throws IOException; + + /** + * trans binary xml file to text xml file. + * + * @param path the xml file path in apk file + * @return the text. null if file not exists + */ + @Nullable + public String transBinaryXml(final @NonNull String path) throws IOException { + final byte[] data = this.getFileData(path); + if (data == null) { + return null; + } + this.parseResourceTable(); + final XmlTranslator xmlTranslator = new XmlTranslator(); + this.transBinaryXml(data, xmlTranslator); + return xmlTranslator.getXml(); + } + + private void transBinaryXml(final byte[] data, @NonNull final XmlStreamer xmlStreamer) throws IOException { + this.parseResourceTable(); + final ByteBuffer buffer = ByteBuffer.wrap(data); + final BinaryXmlParser binaryXmlParser = new BinaryXmlParser(buffer, this.resourceTable, xmlStreamer, this.preferredLocale); + binaryXmlParser.parse(); + } + + /** + * This method return icons specified in android manifest file, application. + * The icons could be file icon, color icon, or adaptive icon, etc. + * + * @return icon files. + */ + @NonNull + public List getAllIcons() throws IOException { + this.parseManifest(); + final List iconPaths = this.iconPaths; + if (iconPaths.isEmpty()) { + return Collections.emptyList(); + } + final List iconFaces = new ArrayList<>(iconPaths.size()); + for (final IconPath iconPath : iconPaths) { + final String filePath = iconPath.path; + if (filePath != null && filePath.endsWith(".xml")) { + // adaptive icon? + final byte[] data = this.getFileData(filePath); + if (data == null) { + continue; + } + this.parseResourceTable(); + final AdaptiveIconParser iconParser = new AdaptiveIconParser(); + this.transBinaryXml(data, iconParser); + Icon backgroundIcon = null; + final String background = iconParser.getBackground(); + if (background != null) { + backgroundIcon = this.newFileIcon(background, iconPath.density); + } + Icon foregroundIcon = null; + final String foreground = iconParser.getForeground(); + if (foreground != null) { + foregroundIcon = this.newFileIcon(foreground, iconPath.density); + } + final AdaptiveIcon icon = new AdaptiveIcon(foregroundIcon, backgroundIcon); + iconFaces.add(icon); + } else { + final Icon icon = this.newFileIcon(filePath, iconPath.density); + iconFaces.add(icon); + } + } + return iconFaces; + } + + @NonNull + private Icon newFileIcon(@NonNull final String filePath, final int density) throws IOException { + return new Icon(filePath, density, this.getFileData(filePath)); + } + + /** + * get class infos form dex file. currently only class name + */ + public DexClass[] getDexClasses() throws IOException { + if (this.dexClasses == null) { + this.parseDexFiles(); + } + return this.dexClasses; + } + + private DexClass[] mergeDexClasses(@NonNull final DexClass[] first, @NonNull final DexClass[] second) { + final DexClass[] result = new DexClass[first.length + second.length]; + arraycopy(first, 0, result, 0, first.length); + arraycopy(second, 0, result, first.length, second.length); + return result; + } + + @NonNull + private DexClass[] parseDexFile(@NonNull final String path) throws IOException { + final byte[] data = this.getFileData(path); + if (data == null) { + final String msg = String.format("Dex file %s not found", path); + throw new ParserException(msg); + } + final ByteBuffer buffer = ByteBuffer.wrap(data); + final DexParser dexParser = new DexParser(buffer); + return dexParser.parse(); + } + + private void parseDexFiles() throws IOException { + this.dexClasses = this.parseDexFile(AndroidConstants.DEX_FILE); + for (int i = 2; i < 1000; i++) { + final String path = String.format(Locale.ROOT, AndroidConstants.DEX_ADDITIONAL, i); + try { + final DexClass[] classes = this.parseDexFile(path); + this.dexClasses = this.mergeDexClasses(this.dexClasses, classes); + } catch (final ParserException e) { + break; + } + } + } + + /** + * parse resource table. + */ + private void parseResourceTable() throws IOException { + if (this.resourceTableParsed) { + return; + } + this.resourceTableParsed = true; + final byte[] data = this.getFileData(AndroidConstants.RESOURCE_FILE); + if (data == null) { + // if no resource entry has been found, we assume it is not needed by this APK + this.resourceTable = new ResourceTable(null); + this.locales = Collections.emptySet(); + return; + } + final ByteBuffer buffer = ByteBuffer.wrap(data); + final ResourceTableParser resourceTableParser = new ResourceTableParser(buffer); + resourceTableParser.parse(); + this.resourceTable = resourceTableParser.getResourceTable(); + this.locales = resourceTableParser.locales; + } + + /** + * Check apk sign. This method only use apk v1 scheme verifier + * + * @deprecated using google official ApkVerifier of apksig lib instead. + */ + @NonNull + @Deprecated + public abstract ApkSignStatus verifyApk() throws IOException; + + @Override + public void close() throws IOException { + this.apkSigners = null; + this.resourceTable = null; + this.iconPaths = null; + } + + /** + * The local used to parse apk + */ + public Locale getPreferredLocale() { + return this.preferredLocale; + } + + /** + * The locale preferred. Will cause getManifestXml / getApkMeta to return different values. + * The default value is from os default locale setting. + */ + public void setPreferredLocale(final Locale preferredLocale) { + if (!Objects.equals(this.preferredLocale, preferredLocale)) { + this.preferredLocale = preferredLocale; + this.manifestXml = null; + this.apkMeta = null; + this.manifestParsed = false; + } + } + + /** + * Create ApkSignBlockParser for this apk file. + * + * @return null if do not have sign block + */ + @Nullable + protected ByteBuffer findApkSignBlock() throws IOException { + final ByteBuffer buffer = this.fileData().order(ByteOrder.LITTLE_ENDIAN); + final int len = buffer.limit(); + // first find zip end of central directory entry + if (len < 22) { + // should not happen + throw new RuntimeException("Not zip file"); + } + final int maxEOCDSize = 1024 * 100; + EOCD eocd = null; + for (int i = len - 22; i > Math.max(0, len - maxEOCDSize); i--) { + final int v = buffer.getInt(i); + if (v == EOCD.SIGNATURE) { + Buffers.position(buffer, i + 4); + eocd = new EOCD(); + eocd.setDiskNum(Buffers.readUShort(buffer)); + eocd.setCdStartDisk(Buffers.readUShort(buffer)); + eocd.setCdRecordNum(Buffers.readUShort(buffer)); + eocd.setTotalCDRecordNum(Buffers.readUShort(buffer)); + eocd.setCdSize(Buffers.readUInt(buffer)); + eocd.setCdStart(Buffers.readUInt(buffer)); + eocd.setCommentLen(Buffers.readUShort(buffer)); + } + } + if (eocd == null) { + return null; + } + final int magicStrLen = 16; + final long cdStart = eocd.getCdStart(); + // find apk sign block + Buffers.position(buffer, cdStart - magicStrLen); + final String magic = Buffers.readAsciiString(buffer, magicStrLen); + if (!magic.equals(ApkSigningBlock.MAGIC)) { + return null; + } + Buffers.position(buffer, cdStart - 24); + final int blockSize = Unsigned.ensureUInt(buffer.getLong()); + Buffers.position(buffer, cdStart - blockSize - 8); + final long size2 = Unsigned.ensureULong(buffer.getLong()); + if (blockSize != size2) { + return null; + } + // now at the start of signing block + return Buffers.sliceAndSkip(buffer, blockSize - magicStrLen); + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ApkFile.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ApkFile.java new file mode 100644 index 00000000..4b7559e7 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ApkFile.java @@ -0,0 +1,124 @@ +package net.dongliu.apk.parser; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.bean.ApkSignStatus; +import net.dongliu.apk.parser.utils.Inputs; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * ApkFile, for parsing apk file info. + * This class is not thread-safe. + * + * @author dongliu + */ +public class ApkFile extends AbstractApkFile implements Closeable { + + @NonNull + private final ZipFile zf; + private final File apkFile; + + public ApkFile(final @NonNull File apkFile) throws IOException { + this.apkFile = apkFile; + // create zip file cost time, use one zip file for apk parser life cycle + this.zf = new ZipFile(apkFile); + } + + public ApkFile(@NonNull final String filePath) throws IOException { + this(new File(filePath)); + } + + @NonNull + @Override + protected List getAllCertificateData() throws IOException { + final Enumeration enu = this.zf.entries(); + final List list = new ArrayList<>(); + while (enu.hasMoreElements()) { + final ZipEntry ne = enu.nextElement(); + if (ne.isDirectory()) { + continue; + } + final String name = ne.getName().toUpperCase(); + if (name.endsWith(".RSA") || name.endsWith(".DSA")) { + list.add(new CertificateFile(name, Inputs.readAllAndClose(this.zf.getInputStream(ne)))); + } + } + return list; + } + + @Nullable + @Override + public byte[] getFileData(final @NonNull String path) throws IOException { + final ZipEntry entry = this.zf.getEntry(path); + if (entry == null) { + return null; + } + final InputStream inputStream = this.zf.getInputStream(entry); + return Inputs.readAllAndClose(inputStream); + } + + @NonNull + @Override + protected ByteBuffer fileData() throws IOException { + final FileChannel channel = new FileInputStream(this.apkFile).getChannel(); + return channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); + } + + /** + * {@inheritDoc} + * + * @deprecated using google official ApkVerifier of apksig lib instead. + */ + @NonNull + @Override + @Deprecated + public ApkSignStatus verifyApk() throws IOException { + final ZipEntry entry = this.zf.getEntry("META-INF/MANIFEST.MF"); + if (entry == null) { + // apk is not signed; + return ApkSignStatus.NotSigned; + } + try (final JarFile jarFile = new JarFile(this.apkFile)) { + final Enumeration entries = jarFile.entries(); + final byte[] buffer = new byte[8192]; + while (entries.hasMoreElements()) { + final JarEntry e = entries.nextElement(); + if (e.isDirectory()) { + continue; + } + try (final InputStream in = jarFile.getInputStream(e)) { + // Read in each jar entry. A security exception will be thrown if a signature/digest check fails. + //noinspection StatementWithEmptyBody + while (in.read(buffer, 0, buffer.length) != -1) { + // Don't care + } + } catch (final SecurityException se) { + return ApkSignStatus.Incorrect; + } + } + } + return ApkSignStatus.Signed; + } + + @Override + public void close() throws IOException { + try (final Closeable superClosable = ApkFile.super::close) { + } + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ApkParsers.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ApkParsers.java new file mode 100644 index 00000000..acdce7a9 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ApkParsers.java @@ -0,0 +1,145 @@ +package net.dongliu.apk.parser; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.bean.ApkMeta; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +/** + * Convenient utils method for parse apk file + * + * @author Liu Dong + */ +public class ApkParsers { + + private static boolean useBouncyCastle; + + public static boolean useBouncyCastle() { + return ApkParsers.useBouncyCastle; + } + + /** + * Use BouncyCastle instead of JSSE to parse X509 certificate. + * If want to use BouncyCastle, you will also need to add bcprov and bcpkix lib to your project. + */ + public static void useBouncyCastle(final boolean useBouncyCastle) { + ApkParsers.useBouncyCastle = useBouncyCastle; + } + + /** + * Get apk meta info for apk file + */ + public static ApkMeta getMetaInfo(final @NonNull String apkFilePath) throws IOException { + try (final ApkFile apkFile = new ApkFile(apkFilePath)) { + return apkFile.getApkMeta(); + } + } + + /** + * Get apk meta info for apk file + */ + public static ApkMeta getMetaInfo(final @NonNull File file) throws IOException { + try (final ApkFile apkFile = new ApkFile(file)) { + return apkFile.getApkMeta(); + } + } + + /** + * Get apk meta info for apk file + */ + public static ApkMeta getMetaInfo(final byte[] apkData) throws IOException { + try (final ByteArrayApkFile apkFile = new ByteArrayApkFile(apkData)) { + return apkFile.getApkMeta(); + } + } + + /** + * Get apk meta info for apk file, with locale + */ + public static ApkMeta getMetaInfo(final @NonNull String apkFilePath, final Locale locale) throws IOException { + try (final ApkFile apkFile = new ApkFile(apkFilePath)) { + apkFile.setPreferredLocale(locale); + return apkFile.getApkMeta(); + } + } + + /** + * Get apk meta info for apk file + */ + public static ApkMeta getMetaInfo(final @NonNull File file, final Locale locale) throws IOException { + try (final ApkFile apkFile = new ApkFile(file)) { + apkFile.setPreferredLocale(locale); + return apkFile.getApkMeta(); + } + } + + /** + * Get apk meta info for apk file + */ + public static ApkMeta getMetaInfo(final byte[] apkData, final Locale locale) throws IOException { + try (final ByteArrayApkFile apkFile = new ByteArrayApkFile(apkData)) { + apkFile.setPreferredLocale(locale); + return apkFile.getApkMeta(); + } + } + + /** + * Get apk manifest xml file as text + */ + public static String getManifestXml(final @NonNull String apkFilePath) throws IOException { + try (final ApkFile apkFile = new ApkFile(apkFilePath)) { + return apkFile.getManifestXml(); + } + } + + /** + * Get apk manifest xml file as text + */ + public static String getManifestXml(final @NonNull File file) throws IOException { + try (final ApkFile apkFile = new ApkFile(file)) { + return apkFile.getManifestXml(); + } + } + + /** + * Get apk manifest xml file as text + */ + public static String getManifestXml(final byte[] apkData) throws IOException { + try (final ByteArrayApkFile apkFile = new ByteArrayApkFile(apkData)) { + return apkFile.getManifestXml(); + } + } + + /** + * Get apk manifest xml file as text + */ + public static String getManifestXml(final @NonNull String apkFilePath, final Locale locale) throws IOException { + try (final ApkFile apkFile = new ApkFile(apkFilePath)) { + apkFile.setPreferredLocale(locale); + return apkFile.getManifestXml(); + } + } + + /** + * Get apk manifest xml file as text + */ + public static String getManifestXml(final @NonNull File file, final Locale locale) throws IOException { + try (final ApkFile apkFile = new ApkFile(file)) { + apkFile.setPreferredLocale(locale); + return apkFile.getManifestXml(); + } + } + + /** + * Get apk manifest xml file as text + */ + public static String getManifestXml(final byte[] apkData, final Locale locale) throws IOException { + try (final ByteArrayApkFile apkFile = new ByteArrayApkFile(apkData)) { + apkFile.setPreferredLocale(locale); + return apkFile.getManifestXml(); + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ByteArrayApkFile.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ByteArrayApkFile.java new file mode 100644 index 00000000..3ec1cf7f --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/ByteArrayApkFile.java @@ -0,0 +1,83 @@ +package net.dongliu.apk.parser; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.bean.ApkSignStatus; +import net.dongliu.apk.parser.utils.Inputs; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Parse apk file from byte array. + * This class is not thread-safe + * + * @author Liu Dong + */ +public class ByteArrayApkFile extends AbstractApkFile implements Closeable { + + private byte[] apkData; + + public ByteArrayApkFile(final byte[] apkData) { + this.apkData = apkData; + } + + @NonNull + @Override + protected List getAllCertificateData() throws IOException { + final List list = new ArrayList<>(); + try (final InputStream in = new ByteArrayInputStream(this.apkData); + final ZipInputStream zis = new ZipInputStream(in)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + final String name = entry.getName(); + if (name.toUpperCase().endsWith(".RSA") || name.toUpperCase().endsWith(".DSA")) { + list.add(new CertificateFile(name, Inputs.readAll(zis))); + } + } + } + return list; + } + + @Nullable + @Override + public byte[] getFileData(@NonNull final String path) throws IOException { + try (final InputStream in = new ByteArrayInputStream(this.apkData); + final ZipInputStream zis = new ZipInputStream(in)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (path.equals(entry.getName())) { + return Inputs.readAll(zis); + } + } + } + return null; + } + + @NonNull + @Override + protected ByteBuffer fileData() { + return ByteBuffer.wrap(this.apkData).asReadOnlyBuffer(); + } + + @NonNull + @Deprecated + @Override + public ApkSignStatus verifyApk() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() throws IOException { + super.close(); + this.apkData = null; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/Main.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/Main.java new file mode 100644 index 00000000..3db1c545 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/Main.java @@ -0,0 +1,35 @@ +package net.dongliu.apk.parser; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.security.cert.CertificateException; +import java.util.Locale; + +/** + * Main method for parser apk + * + * @author Liu Dong {@literal } + */ +public class Main { + public static void main(final @NonNull String[] args) throws IOException, CertificateException { + final String action = args[0]; + final String apkPath = args[1]; + try (final ApkFile apkFile = new ApkFile(apkPath)) { + apkFile.setPreferredLocale(Locale.getDefault()); + switch (action) { + case "meta": + System.out.println(apkFile.getApkMeta()); + break; + case "manifest": + System.out.println(apkFile.getManifestXml()); + break; + case "signer": + System.out.println(apkFile.getApkSigners()); + break; + default: + } + + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/AdaptiveIcon.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/AdaptiveIcon.java new file mode 100644 index 00000000..eef88357 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/AdaptiveIcon.java @@ -0,0 +1,53 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.Serializable; + +/** + * Android adaptive icon, from android 8.0 + */ +public class AdaptiveIcon implements IconFace, Serializable { + private static final long serialVersionUID = 4185750290211529320L; + /** + * The foreground icon + */ + @Nullable + public final Icon foreground; + /** + * The background icon + */ + @Nullable + public final Icon background; + + public AdaptiveIcon(@Nullable final Icon foreground, @Nullable final Icon background) { + this.foreground = foreground; + this.background = background; + } + + @NonNull + @Override + public String toString() { + return "AdaptiveIcon{" + + "foreground=" + this.foreground + + ", background=" + this.background + + '}'; + } + + @Override + public boolean isFile() { + return this.foreground.isFile(); + } + + @Override + @Nullable + public byte[] getData() { + return this.foreground.getData(); + } + + @Override + public String getPath() { + return this.foreground.getPath(); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkMeta.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkMeta.java new file mode 100644 index 00000000..35eadcf6 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkMeta.java @@ -0,0 +1,336 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Apk meta info + * + * @author dongliu + */ +public class ApkMeta { + + public final String packageName; + /** + * get the apk's title(name) + */ + public final String label; + private final String icon; + public final String versionName; + public final long versionCode; + public final Long revisionCode; + public final String sharedUserId; + public final String sharedUserLabel; + public final String split; + public final String configForSplit; + public final boolean isFeatureSplit; + public final boolean isSplitRequired; + public final boolean isolatedSplits; + public final String installLocation; + public final String minSdkVersion; + public final String targetSdkVersion; + @Nullable + public final String maxSdkVersion; + @Nullable + public final String compileSdkVersion; + @Nullable + public final String compileSdkVersionCodename; + @Nullable + public final String platformBuildVersionCode; + @Nullable + public final String platformBuildVersionName; + public final GlEsVersion glEsVersion; + public final boolean isAnyDensity; + public final boolean isSmallScreens; + public final boolean isNormalScreens; + public final boolean isLargeScreens; + public final boolean isDebuggable; + @NonNull + public final List usesPermissions; + public final List usesFeatures; + public final List permissions; + + private ApkMeta(final Builder builder) { + this.packageName = builder.packageName; + this.label = builder.label; + this.icon = builder.icon; + this.versionName = builder.versionName; + this.versionCode = builder.versionCode; + this.revisionCode = builder.revisionCode; + this.sharedUserId = builder.sharedUserId; + this.sharedUserLabel = builder.sharedUserLabel; + this.split = builder.split; + this.configForSplit = builder.configForSplit; + this.isFeatureSplit = builder.isFeatureSplit; + this.isSplitRequired = builder.isSplitRequired; + this.isolatedSplits = builder.isolatedSplits; + this.installLocation = builder.installLocation; + this.minSdkVersion = builder.minSdkVersion; + this.targetSdkVersion = builder.targetSdkVersion; + this.maxSdkVersion = builder.maxSdkVersion; + this.compileSdkVersion = builder.compileSdkVersion; + this.compileSdkVersionCodename = builder.compileSdkVersionCodename; + this.platformBuildVersionCode = builder.platformBuildVersionCode; + this.platformBuildVersionName = builder.platformBuildVersionName; + this.glEsVersion = builder.glEsVersion; + this.isAnyDensity = builder.anyDensity; + this.isSmallScreens = builder.smallScreens; + this.isNormalScreens = builder.normalScreens; + this.isLargeScreens = builder.largeScreens; + this.isDebuggable = builder.debuggable; + this.usesPermissions = builder.usesPermissions; + this.usesFeatures = builder.usesFeatures; + this.permissions = builder.permissions; + } + + @NonNull + public static Builder newBuilder() { + return new Builder(); + } + + @NonNull + @Override + public String toString() { + return "packageName: \t" + this.packageName + "\n" + + "label: \t" + this.label + "\n" + + "icon: \t" + this.icon + "\n" + + "versionName: \t" + this.versionName + "\n" + + "versionCode: \t" + this.versionCode + "\n" + + "minSdkVersion: \t" + this.minSdkVersion + "\n" + + "targetSdkVersion: \t" + this.targetSdkVersion + "\n" + + "maxSdkVersion: \t" + this.maxSdkVersion; + } + + public static final class Builder { + public String applicationClassRelativePath; + private String packageName; + private String label; + private String icon; + private String versionName; + private long versionCode = 0L; + private Long revisionCode; + private String sharedUserId; + private String sharedUserLabel; + public String split; + public String configForSplit; + public boolean isFeatureSplit; + public boolean isSplitRequired; + public boolean isolatedSplits; + private String installLocation; + private String minSdkVersion; + private String targetSdkVersion; + private String maxSdkVersion; + private String compileSdkVersion; + private String compileSdkVersionCodename; + private String platformBuildVersionCode; + private String platformBuildVersionName; + private GlEsVersion glEsVersion; + private boolean anyDensity; + private boolean smallScreens; + private boolean normalScreens; + private boolean largeScreens; + private boolean debuggable; + private final List usesPermissions = new ArrayList<>(); + private final List usesFeatures = new ArrayList<>(); + private final List permissions = new ArrayList<>(); + + private Builder() { + } + + @NonNull + public Builder setPackageName(final @Nullable String packageName) { + this.packageName = packageName; + return this; + } + + public String getPackageName() { + return packageName; + } + + @NonNull + public Builder setLabel(final @NonNull String label) { + this.label = label; + return this; + } + + public String getLabel() { + return label; + } + + @NonNull + public Builder setIcon(final @Nullable String icon) { + this.icon = icon; + return this; + } + + @NonNull + public Builder setVersionName(final @Nullable String versionName) { + this.versionName = versionName; + return this; + } + + @NonNull + public Builder setVersionCode(final long versionCode) { + this.versionCode = versionCode; + return this; + } + + @NonNull + public Builder setRevisionCode(final @Nullable Long revisionCode) { + this.revisionCode = revisionCode; + return this; + } + + @NonNull + public Builder setSharedUserId(final @Nullable String sharedUserId) { + this.sharedUserId = sharedUserId; + return this; + } + + @NonNull + public Builder setSharedUserLabel(final @Nullable String sharedUserLabel) { + this.sharedUserLabel = sharedUserLabel; + return this; + } + + @NonNull + public Builder setSplit(final @Nullable String split) { + this.split = split; + return this; + } + + @NonNull + public Builder setConfigForSplit(final @Nullable String configForSplit) { + this.configForSplit = configForSplit; + return this; + } + + @NonNull + public Builder setIsFeatureSplit(final boolean isFeatureSplit) { + this.isFeatureSplit = isFeatureSplit; + return this; + } + + @NonNull + public Builder setIsSplitRequired(final boolean isSplitRequired) { + this.isSplitRequired = isSplitRequired; + return this; + } + + @NonNull + public Builder setIsolatedSplits(final boolean isolatedSplits) { + this.isolatedSplits = isolatedSplits; + return this; + } + + @NonNull + public Builder setInstallLocation(final @NonNull String installLocation) { + this.installLocation = installLocation; + return this; + } + + @NonNull + public Builder setMinSdkVersion(final @NonNull String minSdkVersion) { + this.minSdkVersion = minSdkVersion; + return this; + } + + @NonNull + public Builder setTargetSdkVersion(final @NonNull String targetSdkVersion) { + this.targetSdkVersion = targetSdkVersion; + return this; + } + + @NonNull + public Builder setMaxSdkVersion(final @NonNull String maxSdkVersion) { + this.maxSdkVersion = maxSdkVersion; + return this; + } + + @NonNull + public Builder setCompileSdkVersion(final @Nullable String compileSdkVersion) { + this.compileSdkVersion = compileSdkVersion; + return this; + } + + @NonNull + public Builder setCompileSdkVersionCodename(final @Nullable String compileSdkVersionCodename) { + this.compileSdkVersionCodename = compileSdkVersionCodename; + return this; + } + + @NonNull + public Builder setPlatformBuildVersionCode(final @Nullable String platformBuildVersionCode) { + this.platformBuildVersionCode = platformBuildVersionCode; + return this; + } + + @NonNull + public Builder setPlatformBuildVersionName(final @Nullable String platformBuildVersionName) { + this.platformBuildVersionName = platformBuildVersionName; + return this; + } + + @NonNull + public Builder setGlEsVersion(final @NonNull GlEsVersion glEsVersion) { + this.glEsVersion = glEsVersion; + return this; + } + + @NonNull + public Builder setAnyDensity(final boolean anyDensity) { + this.anyDensity = anyDensity; + return this; + } + + @NonNull + public Builder setSmallScreens(final boolean smallScreens) { + this.smallScreens = smallScreens; + return this; + } + + @NonNull + public Builder setNormalScreens(final boolean normalScreens) { + this.normalScreens = normalScreens; + return this; + } + + @NonNull + public Builder setLargeScreens(final boolean largeScreens) { + this.largeScreens = largeScreens; + return this; + } + + @NonNull + public Builder setDebuggable(final boolean debuggable) { + this.debuggable = debuggable; + return this; + } + + @NonNull + public Builder addUsesPermission(final @Nullable String usesPermission) { + this.usesPermissions.add(usesPermission); + return this; + } + + @NonNull + public Builder addUsesFeature(final @NonNull UseFeature usesFeature) { + this.usesFeatures.add(usesFeature); + return this; + } + + @NonNull + public Builder addPermissions(final @NonNull Permission permission) { + this.permissions.add(permission); + return this; + } + + @NonNull + public ApkMeta build() { + return new ApkMeta(this); + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkSignStatus.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkSignStatus.kt new file mode 100644 index 00000000..0b98e304 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkSignStatus.kt @@ -0,0 +1,14 @@ +package net.dongliu.apk.parser.bean + +/** + * Apk sign status. + * + * @author dongliu + */ +enum class ApkSignStatus { + NotSigned, + + /** invalid signing */ + Incorrect, + Signed +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkSigner.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkSigner.kt new file mode 100644 index 00000000..f89bbf67 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkSigner.kt @@ -0,0 +1,24 @@ +package net.dongliu.apk.parser.bean + +import net.dongliu.apk.parser.bean.CertificateMeta + +/** + * ApkSignV1 certificate file. + */ +class ApkSigner( + /** + * The cert file path in apk file + */ + val path: String, + /** + * The meta info of certificate contained in this cert file. + */ + val certificateMetas: List +) { + override fun toString(): String { + return "ApkSigner{" + + "path='" + path + '\'' + + ", certificateMetas=" + certificateMetas + + '}' + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkV2Signer.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkV2Signer.java new file mode 100644 index 00000000..ff06d524 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ApkV2Signer.java @@ -0,0 +1,21 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; + +import java.util.List; + +/** + * ApkSignV1 certificate file. + */ +public class ApkV2Signer { + /** + * The meta info of certificate contained in this cert file. + */ + @NonNull + public final List certificateMetas; + + public ApkV2Signer(final @NonNull List certificateMetas) { + this.certificateMetas = certificateMetas; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/CertificateMeta.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/CertificateMeta.java new file mode 100644 index 00000000..25eb8e7b --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/CertificateMeta.java @@ -0,0 +1,76 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * basic certificate info. + * + * @author dongliu + */ +public class CertificateMeta { + + /** + * the sign algorithm name + */ + public final String signAlgorithm; + + /** + * the signature algorithm OID string. + * An OID is represented by a set of non-negative whole numbers separated by periods. + * For example, the string "1.2.840.10040.4.3" identifies the SHA-1 with DSA signature algorithm defined in + * + * RFC 3279: Algorithms and Identifiers for the Internet X.509 Public Key Infrastructure Certificate and CRL Profile + * . + */ + public final String signAlgorithmOID; + + /** + * the start date of the validity period. + */ + public final Date startDate; + + /** + * the end date of the validity period. + */ + public final Date endDate; + + /** + * certificate binary data. + */ + public final byte[] data; + + /** + * first use base64 to encode certificate binary data, and then calculate md5 of base64b string. + * some programs use this as the certMd5 of certificate + */ + public final String certBase64Md5; + + /** + * use md5 to calculate certificate's certMd5. + */ + public final String certMd5; + + public CertificateMeta(final @NonNull String signAlgorithm, final @NonNull String signAlgorithmOID, final @NonNull Date startDate, final @NonNull Date endDate, + final @NonNull byte[] data, final @NonNull String certBase64Md5, final @NonNull String certMd5) { + this.signAlgorithm = signAlgorithm; + this.signAlgorithmOID = signAlgorithmOID; + this.startDate = startDate; + this.endDate = endDate; + this.data = data; + this.certBase64Md5 = certBase64Md5; + this.certMd5 = certMd5; + } + + @NonNull + @Override + public String toString() { + final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT); + return "CertificateMeta{signAlgorithm=" + this.signAlgorithm + ", " + + "certBase64Md5=" + this.certBase64Md5 + ", " + + "startDate=" + df.format(this.startDate) + ", " + "endDate=" + df.format(this.endDate) + "}"; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ColorIcon.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ColorIcon.java new file mode 100644 index 00000000..eeced531 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/ColorIcon.java @@ -0,0 +1,26 @@ +package net.dongliu.apk.parser.bean; + +import java.io.Serializable; + +/** + * The plain icon, using color drawable resource. + */ +public class ColorIcon implements IconFace, Serializable { + private static final long serialVersionUID = -7913024425268466186L; + + @Override + public boolean isFile() { + return false; + } + + @Override + public byte[] getData() { + throw new UnsupportedOperationException(); + } + + @Override + public String getPath() { + throw new UnsupportedOperationException(); + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/DexClass.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/DexClass.java new file mode 100644 index 00000000..2cbe76dd --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/DexClass.java @@ -0,0 +1,73 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.dex.DexClassStruct; + +/** + * @author dongliu + */ +public class DexClass { + /** + * the class name + */ + public final String classType; + @Nullable + public final String superClass; + private final int accessFlags; + + public DexClass(final String classType, final String superClass, final int accessFlags) { + this.classType = classType; + this.superClass = superClass; + this.accessFlags = accessFlags; + } + + public String getPackageName() { + String packageName = this.classType; + if (packageName.length() > 0) { + if (packageName.charAt(0) == 'L') { + packageName = packageName.substring(1); + } + } + if (packageName.length() > 0) { + final int idx = this.classType.lastIndexOf('/'); + if (idx > 0) { + packageName = packageName.substring(0, this.classType.lastIndexOf('/') - 1); + } else if (packageName.charAt(packageName.length() - 1) == ';') { + packageName = packageName.substring(0, packageName.length() - 1); + } + } + return packageName.replace('/', '.'); + } + + public boolean isInterface() { + return (this.accessFlags & DexClassStruct.ACC_INTERFACE) != 0; + } + + public boolean isEnum() { + return (this.accessFlags & DexClassStruct.ACC_ENUM) != 0; + } + + public boolean isAnnotation() { + return (this.accessFlags & DexClassStruct.ACC_ANNOTATION) != 0; + } + + public boolean isPublic() { + return (this.accessFlags & DexClassStruct.ACC_PUBLIC) != 0; + } + + public boolean isProtected() { + return (this.accessFlags & DexClassStruct.ACC_PROTECTED) != 0; + } + + public boolean isStatic() { + return (this.accessFlags & DexClassStruct.ACC_STATIC) != 0; + } + + @NonNull + @Override + public String toString() { + return "" + this.classType; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/GlEsVersion.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/GlEsVersion.java new file mode 100644 index 00000000..82424f98 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/GlEsVersion.java @@ -0,0 +1,27 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; + +/** + * the glEsVersion apk used. + * + * @author dongliu + */ +public class GlEsVersion { + public final int major; + public final int minor; + public final boolean isRequired; + + public GlEsVersion(final int major, final int minor, final boolean isRequired) { + this.major = major; + this.minor = minor; + this.isRequired = isRequired; + } + + @NonNull + @Override + public String toString() { + return this.major + "." + this.minor; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/Icon.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/Icon.java new file mode 100644 index 00000000..388e6c8d --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/Icon.java @@ -0,0 +1,65 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.Serializable; + +/** + * The plain file apk icon. + * + * @author Liu Dong + */ +public class Icon implements IconFace, Serializable { + + private static final long serialVersionUID = 8680309892249769701L; + @NonNull + private final String path; + private final int density; + + @Nullable + private final byte[] data; + + public Icon(@NonNull final String path, final int density, final @Nullable byte[] data) { + this.path = path; + this.density = density; + this.data = data; + } + + /** + * The icon path in apk file + */ + @NonNull + @Override + public String getPath() { + return this.path; + } + + /** + * Return the density this icon for. 0 means default icon. + * see {@link net.dongliu.apk.parser.struct.resource.Densities} for more density values. + */ + public int getDensity() { + return this.density; + } + + @Override + public boolean isFile() { + return true; + } + + /** + * Icon data may be null, due to some apk missing the icon file. + */ + @Override + @Nullable + public byte[] getData() { + return this.data; + } + + @NonNull + @Override + public String toString() { + return "Icon{path='" + this.path + '\'' + ", density=" + this.density + ", size=" + (this.data == null ? 0 : this.data.length) + '}'; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/IconFace.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/IconFace.java new file mode 100644 index 00000000..4162dbe1 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/IconFace.java @@ -0,0 +1,30 @@ +package net.dongliu.apk.parser.bean; + +import java.io.Serializable; + +import androidx.annotation.Nullable; + +/** + * The icon interface + */ +public interface IconFace extends Serializable { + + /** + * If icon is file resource + */ + boolean isFile(); + + /** + * Return the icon file as bytes. This method is valid only when {@link #isFile()} return true. + * Otherwise, {@link UnsupportedOperationException} should be thrown. + */ + @Nullable + byte[] getData(); + + + /** + * Return the icon file path in apk file. This method is valid only when {@link #isFile()} return true. + * Otherwise, {@link UnsupportedOperationException} should be thrown. + */ + String getPath(); +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/IconPath.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/IconPath.java new file mode 100644 index 00000000..9eedc8b5 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/IconPath.java @@ -0,0 +1,34 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Icon path, and density + */ +public class IconPath { + /** + * The icon path in apk file + */ + @Nullable + public final String path; + /** + * Return the density this icon for. 0 means default icon. + * see {@link net.dongliu.apk.parser.struct.resource.Densities} for more density values. + */ + public final int density; + + public IconPath(final @Nullable String path, final int density) { + this.path = path; + this.density = density; + } + + @NonNull + @Override + public String toString() { + return "IconPath{" + + "path='" + this.path + '\'' + + ", density=" + this.density + + '}'; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/Permission.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/Permission.java new file mode 100644 index 00000000..ae66ce85 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/Permission.java @@ -0,0 +1,35 @@ +package net.dongliu.apk.parser.bean; + + +import androidx.annotation.Nullable; + +/** + * permission provided by the app + * + * @author Liu Dong + */ +public class Permission { + @Nullable + public final String name; + @Nullable + public final String label; + @Nullable + public final String icon; + @Nullable + public final String description; + @Nullable + public final String group; + @Nullable + public final String protectionLevel; + + public Permission(final @Nullable String name, final @Nullable String label, final @Nullable String icon, final @Nullable String description, final @Nullable String group, + final @Nullable String protectionLevel) { + this.name = name; + this.label = label; + this.icon = icon; + this.description = description; + this.group = group; + this.protectionLevel = protectionLevel; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/UseFeature.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/UseFeature.java new file mode 100644 index 00000000..696e537c --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/bean/UseFeature.java @@ -0,0 +1,24 @@ +package net.dongliu.apk.parser.bean; + +import androidx.annotation.NonNull; + +/** + * the permission used by apk + * + * @author dongliu + */ +public class UseFeature { + public final String name; + public final boolean isRequired; + + public UseFeature(final String name, final boolean required) { + this.name = name; + this.isRequired = required; + } + + @NonNull + @Override + public String toString() { + return this.name; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1BerParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1BerParser.java new file mode 100644 index 00000000..a19be086 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1BerParser.java @@ -0,0 +1,593 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.cert.asn1.ber.BerDataValue; +import net.dongliu.apk.parser.cert.asn1.ber.BerDataValueFormatException; +import net.dongliu.apk.parser.cert.asn1.ber.BerDataValueReader; +import net.dongliu.apk.parser.cert.asn1.ber.BerEncoding; +import net.dongliu.apk.parser.cert.asn1.ber.ByteBufferBerDataValueReader; +import net.dongliu.apk.parser.utils.Buffers; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Parser of ASN.1 BER-encoded structures. + *

+ *

Structure is described to the parser by providing a class annotated with {@link Asn1Class}, + * containing fields annotated with {@link Asn1Field}. + */ +public final class Asn1BerParser { + private Asn1BerParser() { + } + + /** + * Returns the ASN.1 structure contained in the BER encoded input. + * + * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer + * is advanced to the first position following the end of the consumed structure. + * @param containerClass class describing the structure of the input. The class must meet the + * following requirements: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • The class must expose a public no-arg constructor.
  • + *
  • Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.
  • + *
+ * @throws Asn1DecodingException if the input could not be decoded into the specified Java + * object + */ + public static T parse(final @NonNull ByteBuffer encoded, final Class containerClass) + throws Asn1DecodingException { + final BerDataValue containerDataValue; + try { + containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue(); + } catch (final BerDataValueFormatException e) { + throw new Asn1DecodingException("Failed to decode top-level data value", e); + } + if (containerDataValue == null) { + throw new Asn1DecodingException("Empty input"); + } + return Asn1BerParser.parse(containerDataValue, containerClass); + } + + /** + * Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means + * that this method does not care whether the tag number of this data structure is + * {@code SET OF} and whether the tag class is {@code UNIVERSAL}. + *

+ *

Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1 + * SET may contain duplicate elements. + * + * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer + * is advanced to the first position following the end of the consumed structure. + * @param elementClass class describing the structure of the values/elements contained in this + * container. The class must meet the following requirements: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • The class must expose a public no-arg constructor.
  • + *
  • Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.
  • + *
+ * @throws Asn1DecodingException if the input could not be decoded into the specified Java + * object + */ + public static List parseImplicitSetOf(final @NonNull ByteBuffer encoded, final Class elementClass) + throws Asn1DecodingException { + final BerDataValue containerDataValue; + try { + containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue(); + } catch (final BerDataValueFormatException e) { + throw new Asn1DecodingException("Failed to decode top-level data value", e); + } + if (containerDataValue == null) { + throw new Asn1DecodingException("Empty input"); + } + return Asn1BerParser.parseSetOf(containerDataValue, elementClass); + } + + private static T parse(final BerDataValue container, final Class containerClass) + throws Asn1DecodingException { + if (container == null) { + throw new NullPointerException("container == null"); + } + if (containerClass == null) { + throw new NullPointerException("containerClass == null"); + } + final Asn1Type dataType = Asn1BerParser.getContainerAsn1Type(containerClass); + switch (dataType) { + case Choice: + return Asn1BerParser.parseChoice(container, containerClass); + case Sequence: { + final int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL; + final int expectedTagNumber = BerEncoding.getTagNumber(dataType); + if ((container.tagClass != expectedTagClass) + || (container.tagNumber != expectedTagNumber)) { + throw new Asn1UnexpectedTagException( + "Unexpected data value read as " + containerClass.getName() + + ". Expected " + BerEncoding.tagClassAndNumberToString( + expectedTagClass, expectedTagNumber) + + ", but read: " + BerEncoding.tagClassAndNumberToString( + container.tagClass, container.tagNumber)); + } + return Asn1BerParser.parseSequence(container, containerClass); + } + default: + throw new Asn1DecodingException("Parsing container " + dataType + " not supported"); + } + } + + private static T parseChoice(final BerDataValue dataValue, final Class containerClass) + throws Asn1DecodingException { + final List fields = Asn1BerParser.getAnnotatedFields(containerClass); + if (fields.isEmpty()) { + throw new Asn1DecodingException( + "No fields annotated with " + Asn1Field.class.getName() + + " in CHOICE class " + containerClass.getName()); + } + // Check that class + tagNumber don't clash between the choices + for (int i = 0; i < fields.size() - 1; i++) { + final AnnotatedField f1 = fields.get(i); + final int tagNumber1 = f1.berTagNumber; + final int tagClass1 = f1.berTagClass; + for (int j = i + 1; j < fields.size(); j++) { + final AnnotatedField f2 = fields.get(j); + final int tagNumber2 = f2.berTagNumber; + final int tagClass2 = f2.berTagClass; + if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) { + throw new Asn1DecodingException( + "CHOICE fields are indistinguishable because they have the same tag" + + " class and number: " + containerClass.getName() + + "." + f1.field.getName() + + " and ." + f2.field.getName()); + } + } + } + // Instantiate the container object / result + final T obj; + try { + obj = containerClass.getConstructor().newInstance(); + } catch (final IllegalArgumentException | ReflectiveOperationException e) { + throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e); + } + // Set the matching field's value from the data value + for (final AnnotatedField field : fields) { + try { + field.setValueFrom(dataValue, obj); + return obj; + } catch (final Asn1UnexpectedTagException expected) { + // not a match + } + } + throw new Asn1DecodingException( + "No options of CHOICE " + containerClass.getName() + " matched"); + } + + private static T parseSequence(final BerDataValue container, final Class containerClass) + throws Asn1DecodingException { + final List fields = Asn1BerParser.getAnnotatedFields(containerClass); + Collections.sort( + fields, Comparator.comparingInt(f -> f.annotation.index())); + // Check that there are no fields with the same index + if (fields.size() > 1) { + AnnotatedField lastField = null; + for (final AnnotatedField field : fields) { + if ((lastField != null) + && (lastField.annotation.index() == field.annotation.index())) { + throw new Asn1DecodingException( + "Fields have the same index: " + containerClass.getName() + + "." + lastField.field.getName() + + " and ." + field.field.getName()); + } + lastField = field; + } + } + // Instantiate the container object / result + final T t; + try { + t = containerClass.getConstructor().newInstance(); + } catch (final IllegalArgumentException | ReflectiveOperationException e) { + throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e); + } + // Parse fields one by one. A complication is that there may be optional fields. + int nextUnreadFieldIndex = 0; + final BerDataValueReader elementsReader = container.contentsReader(); + while (nextUnreadFieldIndex < fields.size()) { + final BerDataValue dataValue; + try { + dataValue = elementsReader.readDataValue(); + } catch (final BerDataValueFormatException e) { + throw new Asn1DecodingException("Malformed data value", e); + } + if (dataValue == null) { + break; + } + for (int i = nextUnreadFieldIndex; i < fields.size(); i++) { + final AnnotatedField field = fields.get(i); + try { + if (field.isOptional) { + // Optional field -- might not be present and we may thus be trying to set + // it from the wrong tag. + try { + field.setValueFrom(dataValue, t); + nextUnreadFieldIndex = i + 1; + break; + } catch (final Asn1UnexpectedTagException e) { + // This field is not present, attempt to use this data value for the + // next / iteration of the loop + } + } else { + // Mandatory field -- if we can't set its value from this data value, then + // it's an error + field.setValueFrom(dataValue, t); + nextUnreadFieldIndex = i + 1; + break; + } + } catch (final Asn1DecodingException e) { + throw new Asn1DecodingException( + "Failed to parse " + containerClass.getName() + + "." + field.field.getName(), + e); + } + } + } + return t; + } + + /** + * NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness + * of elements -- it's an unordered collection. + */ + @SuppressWarnings("unchecked") + private static List parseSetOf(final BerDataValue container, final Class elementClass) + throws Asn1DecodingException { + final List result = new ArrayList<>(); + final BerDataValueReader elementsReader = container.contentsReader(); + while (true) { + final BerDataValue dataValue; + try { + dataValue = elementsReader.readDataValue(); + } catch (final BerDataValueFormatException e) { + throw new Asn1DecodingException("Malformed data value", e); + } + if (dataValue == null) { + break; + } + final T element; + if (ByteBuffer.class.equals(elementClass)) { + element = (T) dataValue.getEncodedContents(); + } else if (Asn1OpaqueObject.class.equals(elementClass)) { + element = (T) new Asn1OpaqueObject(dataValue.getEncoded()); + } else { + element = Asn1BerParser.parse(dataValue, elementClass); + } + result.add(element); + } + return result; + } + + private static Asn1Type getContainerAsn1Type(final Class containerClass) + throws Asn1DecodingException { + final Asn1Class containerAnnotation = containerClass.getAnnotation(Asn1Class.class); + if (containerAnnotation == null) { + throw new Asn1DecodingException( + containerClass.getName() + " is not annotated with " + + Asn1Class.class.getName()); + } + switch (containerAnnotation.type()) { + case Choice: + case Sequence: + return containerAnnotation.type(); + default: + throw new Asn1DecodingException( + "Unsupported ASN.1 container annotation type: " + + containerAnnotation.type()); + } + } + + private static Class getElementType(final Field field) + throws Asn1DecodingException, ClassNotFoundException { + final String type = field.getGenericType().toString(); + final int delimiterIndex = type.indexOf('<'); + if (delimiterIndex == -1) { + throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); + } + final int startIndex = delimiterIndex + 1; + final int endIndex = type.indexOf('>', startIndex); + // TODO: handle comma? + if (endIndex == -1) { + throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); + } + final String elementClassName = type.substring(startIndex, endIndex); + return Class.forName(elementClassName); + } + + private static final class AnnotatedField { + public final Field field; + public final Asn1Field annotation; + private final Asn1Type dataType; + public final int berTagClass; + public final int berTagNumber; + private final Asn1Tagging tagging; + public final boolean isOptional; + + public AnnotatedField(@NonNull final Field field, @NonNull final Asn1Field annotation) throws Asn1DecodingException { + this.field = field; + this.annotation = annotation; + this.dataType = annotation.type(); + Asn1TagClass tagClass = annotation.cls(); + if (tagClass == Asn1TagClass.Automatic) { + if (annotation.tagNumber() != -1) { + tagClass = Asn1TagClass.ContextSpecific; + } else { + tagClass = Asn1TagClass.Universal; + } + } + this.berTagClass = BerEncoding.getTagClass(tagClass); + final int tagNumber; + if (annotation.tagNumber() != -1) { + tagNumber = annotation.tagNumber(); + } else if ((this.dataType == Asn1Type.Choice) || (this.dataType == Asn1Type.Any)) { + tagNumber = -1; + } else { + tagNumber = BerEncoding.getTagNumber(this.dataType); + } + this.berTagNumber = tagNumber; + this.tagging = annotation.tagging(); + if (((this.tagging == Asn1Tagging.Explicit) || (this.tagging == Asn1Tagging.Implicit)) + && (annotation.tagNumber() == -1)) { + throw new Asn1DecodingException( + "Tag number must be specified when tagging mode is " + this.tagging); + } + this.isOptional = annotation.optional(); + } + + public void setValueFrom(@NonNull BerDataValue dataValue, final Object obj) throws Asn1DecodingException { + final int readTagClass = dataValue.tagClass; + if (this.berTagNumber != -1) { + final int readTagNumber = dataValue.tagNumber; + if ((readTagClass != this.berTagClass) || (readTagNumber != this.berTagNumber)) { + throw new Asn1UnexpectedTagException( + "Tag mismatch. Expected: " + + BerEncoding.tagClassAndNumberToString(this.berTagClass, this.berTagNumber) + + ", but found " + + BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber)); + } + } else { + if (readTagClass != this.berTagClass) { + throw new Asn1UnexpectedTagException( + "Tag mismatch. Expected class: " + + BerEncoding.tagClassToString(this.berTagClass) + + ", but found " + + BerEncoding.tagClassToString(readTagClass)); + } + } + if (this.tagging == Asn1Tagging.Explicit) { + try { + dataValue = dataValue.contentsReader().readDataValue(); + } catch (final BerDataValueFormatException e) { + throw new Asn1DecodingException( + "Failed to read contents of EXPLICIT data value", e); + } + } + BerToJavaConverter.setFieldValue(obj, this.field, this.dataType, dataValue); + } + } + + private static class Asn1UnexpectedTagException extends Asn1DecodingException { + private static final long serialVersionUID = 1L; + + public Asn1UnexpectedTagException(final String message) { + super(message); + } + } + + private static String oidToString(final ByteBuffer encodedOid) throws Asn1DecodingException { + if (!encodedOid.hasRemaining()) { + throw new Asn1DecodingException("Empty OBJECT IDENTIFIER"); + } + // First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2 + final long firstComponent = Asn1BerParser.decodeBase128UnsignedLong(encodedOid); + final int firstNode = (int) Math.min(firstComponent / 40, 2); + final long secondNode = firstComponent - firstNode * 40L; + final StringBuilder result = new StringBuilder(); + result.append(Long.toString(firstNode)).append('.') + .append(secondNode); + // Each consecutive node is encoded as a separate component + while (encodedOid.hasRemaining()) { + final long node = Asn1BerParser.decodeBase128UnsignedLong(encodedOid); + result.append('.').append(node); + } + return result.toString(); + } + + private static long decodeBase128UnsignedLong(final ByteBuffer encoded) throws Asn1DecodingException { + if (!encoded.hasRemaining()) { + return 0; + } + long result = 0; + while (encoded.hasRemaining()) { + if (result > Long.MAX_VALUE >>> 7) { + throw new Asn1DecodingException("Base-128 number too large"); + } + final int b = encoded.get() & 0xff; + result <<= 7; + result |= b & 0x7f; + if ((b & 0x80) == 0) { + return result; + } + } + throw new Asn1DecodingException( + "Truncated base-128 encoded input: missing terminating byte, with highest bit not" + + " set"); + } + + @NonNull + private static BigInteger integerToBigInteger(final ByteBuffer encoded) { + if (!encoded.hasRemaining()) { + return BigInteger.ZERO; + } + return new BigInteger(Buffers.readBytes(encoded)); + } + + private static int integerToInt(final ByteBuffer encoded) throws Asn1DecodingException { + final BigInteger value = Asn1BerParser.integerToBigInteger(encoded); + try { + return value.intValue(); + } catch (final ArithmeticException e) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e); + } + } + + private static long integerToLong(final ByteBuffer encoded) throws Asn1DecodingException { + final BigInteger value = Asn1BerParser.integerToBigInteger(encoded); + try { + return value.intValue(); + } catch (final ArithmeticException e) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value), + e); + } + } + + private static List getAnnotatedFields(final Class containerClass) + throws Asn1DecodingException { + final Field[] declaredFields = containerClass.getDeclaredFields(); + final List result = new ArrayList<>(declaredFields.length); + for (final Field field : declaredFields) { + final Asn1Field annotation = field.getAnnotation(Asn1Field.class); + if (annotation == null) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + throw new Asn1DecodingException( + Asn1Field.class.getName() + " used on a static field: " + + containerClass.getName() + "." + field.getName()); + } + final AnnotatedField annotatedField; + try { + annotatedField = new AnnotatedField(field, annotation); + } catch (final Asn1DecodingException e) { + throw new Asn1DecodingException( + "Invalid ASN.1 annotation on " + + containerClass.getName() + "." + field.getName(), + e); + } + result.add(annotatedField); + } + return result; + } + + private static final class BerToJavaConverter { + private BerToJavaConverter() { + } + + public static void setFieldValue( + final Object obj, final Field field, final Asn1Type type, final BerDataValue dataValue) + throws Asn1DecodingException { + try { + switch (type) { + case SetOf: + case SequenceOf: + if (Asn1OpaqueObject.class.equals(field.getType())) { + field.set(obj, BerToJavaConverter.convert(type, dataValue, field.getType())); + } else { + field.set(obj, Asn1BerParser.parseSetOf(dataValue, Asn1BerParser.getElementType(field))); + } + return; + default: + field.set(obj, BerToJavaConverter.convert(type, dataValue, field.getType())); + break; + } + } catch (final ReflectiveOperationException e) { + throw new Asn1DecodingException( + "Failed to set value of " + obj.getClass().getName() + + "." + field.getName(), + e); + } + } + + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + @SuppressWarnings("unchecked") + public static T convert( + final Asn1Type sourceType, + final BerDataValue dataValue, + final Class targetType) throws Asn1DecodingException { + if (ByteBuffer.class.equals(targetType)) { + return (T) dataValue.getEncodedContents(); + } else if (byte[].class.equals(targetType)) { + final ByteBuffer resultBuf = dataValue.getEncodedContents(); + if (!resultBuf.hasRemaining()) { + return (T) BerToJavaConverter.EMPTY_BYTE_ARRAY; + } + final byte[] result = new byte[resultBuf.remaining()]; + resultBuf.get(result); + return (T) result; + } else if (Asn1OpaqueObject.class.equals(targetType)) { + return (T) new Asn1OpaqueObject(dataValue.getEncoded()); + } + final ByteBuffer encodedContents = dataValue.getEncodedContents(); + switch (sourceType) { + case Integer: + if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) { + return (T) Integer.valueOf(Asn1BerParser.integerToInt(encodedContents)); + } else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) { + return (T) Long.valueOf(Asn1BerParser.integerToLong(encodedContents)); + } else if (BigInteger.class.equals(targetType)) { + return (T) Asn1BerParser.integerToBigInteger(encodedContents); + } + break; + case ObjectIdentifier: + if (String.class.equals(targetType)) { + return (T) Asn1BerParser.oidToString(encodedContents); + } + break; + case Sequence: { + final Asn1Class containerAnnotation = targetType.getAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.Sequence)) { + return Asn1BerParser.parseSequence(dataValue, targetType); + } + break; + } + case Choice: { + final Asn1Class containerAnnotation = targetType.getAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.Choice)) { + return Asn1BerParser.parseChoice(dataValue, targetType); + } + break; + } + default: + break; + } + throw new Asn1DecodingException( + "Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName()); + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Class.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Class.java new file mode 100644 index 00000000..bc888ffc --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Class.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Asn1Class { + Asn1Type type(); +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DecodingException.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DecodingException.java new file mode 100644 index 00000000..63bc752f --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DecodingException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +/** + * Indicates that input could not be decoded into intended ASN.1 structure. + */ +public class Asn1DecodingException extends Exception { + private static final long serialVersionUID = 1L; + + public Asn1DecodingException(final String message) { + super(message); + } + + public Asn1DecodingException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DerEncoder.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DerEncoder.java new file mode 100644 index 00000000..8da5f20d --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DerEncoder.java @@ -0,0 +1,511 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.cert.asn1.ber.BerEncoding; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Encoder of ASN.1 structures into DER-encoded form. + *

+ *

Structure is described to the encoder by providing a class annotated with {@link Asn1Class}, + * containing fields annotated with {@link Asn1Field}. + */ +public final class Asn1DerEncoder { + private Asn1DerEncoder() { + } + + /** + * Returns the DER-encoded form of the provided ASN.1 structure. + * + * @param container container to be encoded. The container's class must meet the following + * requirements: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • Member fields of the class which are to be encoded must be annotated with + * {@link Asn1Field} and be public.
  • + *
+ * @throws Asn1EncodingException if the input could not be encoded + */ + @Nullable + public static byte[] encode(final @NonNull Object container) throws Asn1EncodingException { + final Class containerClass = container.getClass(); + final Asn1Class containerAnnotation = containerClass.getAnnotation(Asn1Class.class); + if (containerAnnotation == null) { + throw new Asn1EncodingException( + containerClass.getName() + " not annotated with " + Asn1Class.class.getName()); + } + final Asn1Type containerType = containerAnnotation.type(); + switch (containerType) { + case Choice: + return Asn1DerEncoder.toChoice(container); + case Sequence: + return Asn1DerEncoder.toSequence(container); + default: + throw new Asn1EncodingException("Unsupported container type: " + containerType); + } + } + + @Nullable + private static byte[] toChoice(@NonNull final Object container) throws Asn1EncodingException { + final Class containerClass = container.getClass(); + final List fields = Asn1DerEncoder.getAnnotatedFields(container); + if (fields.isEmpty()) { + throw new Asn1EncodingException( + "No fields annotated with " + Asn1Field.class.getName() + + " in CHOICE class " + containerClass.getName()); + } + AnnotatedField resultField = null; + for (final AnnotatedField field : fields) { + final Object fieldValue = Asn1DerEncoder.getMemberFieldValue(container, field.field); + if (fieldValue != null) { + if (resultField != null) { + throw new Asn1EncodingException( + "Multiple non-null fields in CHOICE class " + containerClass.getName() + + ": " + resultField.field.getName() + + ", " + field.field.getName()); + } + resultField = field; + } + } + if (resultField == null) { + throw new Asn1EncodingException( + "No non-null fields in CHOICE class " + containerClass.getName()); + } + return resultField.toDer(); + } + + private static byte[] toSequence(final Object container) throws Asn1EncodingException { + final Class containerClass = container.getClass(); + final List fields = Asn1DerEncoder.getAnnotatedFields(container); + Collections.sort( + fields, Comparator.comparingInt(f -> f.annotation.index())); + if (fields.size() > 1) { + AnnotatedField lastField = null; + for (final AnnotatedField field : fields) { + if ((lastField != null) + && (lastField.annotation.index() == field.annotation.index())) { + throw new Asn1EncodingException( + "Fields have the same index: " + containerClass.getName() + + "." + lastField.field.getName() + + " and ." + field.field.getName()); + } + lastField = field; + } + } + final List serializedFields = new ArrayList<>(fields.size()); + for (final AnnotatedField field : fields) { + final byte[] serializedField; + try { + serializedField = field.toDer(); + } catch (final Asn1EncodingException e) { + throw new Asn1EncodingException( + "Failed to encode " + containerClass.getName() + + "." + field.field.getName(), + e); + } + if (serializedField != null) { + serializedFields.add(serializedField); + } + } + return Asn1DerEncoder.createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE, + serializedFields.toArray(new byte[0][])); + } + + private static byte[] toSetOf(final Collection values, final Asn1Type elementType) + throws Asn1EncodingException { + final List serializedValues = new ArrayList<>(values.size()); + for (final Object value : values) { + serializedValues.add(JavaToDerConverter.toDer(value, elementType, null)); + } + if (serializedValues.size() > 1) { + Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE); + } + return Asn1DerEncoder.createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SET, + serializedValues.toArray(new byte[0][])); + } + + /** + * Compares two bytes arrays based on their lexicographic order. Corresponding elements of the + * two arrays are compared in ascending order. Elements at out of range indices are assumed to + * be smaller than the smallest possible value for an element. + */ + private static class ByteArrayLexicographicComparator implements Comparator { + private static final ByteArrayLexicographicComparator INSTANCE = + new ByteArrayLexicographicComparator(); + + @Override + public int compare(final byte[] arr1, final byte[] arr2) { + final int commonLength = Math.min(arr1.length, arr2.length); + for (int i = 0; i < commonLength; i++) { + final int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff); + if (diff != 0) { + return diff; + } + } + return arr1.length - arr2.length; + } + } + + private static List getAnnotatedFields(@NonNull final Object container) + throws Asn1EncodingException { + final Class containerClass = container.getClass(); + final Field[] declaredFields = containerClass.getDeclaredFields(); + final List result = new ArrayList<>(declaredFields.length); + for (final Field field : declaredFields) { + final Asn1Field annotation = field.getAnnotation(Asn1Field.class); + if (annotation == null) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + throw new Asn1EncodingException( + Asn1Field.class.getName() + " used on a static field: " + + containerClass.getName() + "." + field.getName()); + } + final AnnotatedField annotatedField; + try { + annotatedField = new AnnotatedField(container, field, annotation); + } catch (final Asn1EncodingException e) { + throw new Asn1EncodingException( + "Invalid ASN.1 annotation on " + + containerClass.getName() + "." + field.getName(), + e); + } + result.add(annotatedField); + } + return result; + } + + private static byte[] toInteger(final int value) { + return Asn1DerEncoder.toInteger((long) value); + } + + private static byte[] toInteger(final long value) { + return Asn1DerEncoder.toInteger(BigInteger.valueOf(value)); + } + + private static byte[] toInteger(final BigInteger value) { + return Asn1DerEncoder.createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER, + value.toByteArray()); + } + + private static byte[] toOid(final String oid) throws Asn1EncodingException { + final ByteArrayOutputStream encodedValue = new ByteArrayOutputStream(); + final String[] nodes = oid.split("\\."); + if (nodes.length < 2) { + throw new Asn1EncodingException( + "OBJECT IDENTIFIER must contain at least two nodes: " + oid); + } + final int firstNode; + try { + firstNode = Integer.parseInt(nodes[0]); + } catch (final NumberFormatException e) { + throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]); + } + if ((firstNode > 6) || (firstNode < 0)) { + throw new Asn1EncodingException("Invalid value for node #1: " + firstNode); + } + final int secondNode; + try { + secondNode = Integer.parseInt(nodes[1]); + } catch (final NumberFormatException e) { + throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]); + } + if ((secondNode >= 40) || (secondNode < 0)) { + throw new Asn1EncodingException("Invalid value for node #2: " + secondNode); + } + final int firstByte = firstNode * 40 + secondNode; + if (firstByte > 0xff) { + throw new Asn1EncodingException( + "First two nodes out of range: " + firstNode + "." + secondNode); + } + encodedValue.write(firstByte); + for (int i = 2; i < nodes.length; i++) { + final String nodeString = nodes[i]; + final int node; + try { + node = Integer.parseInt(nodeString); + } catch (final NumberFormatException e) { + throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString); + } + if (node < 0) { + throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node); + } + if (node <= 0x7f) { + encodedValue.write(node); + continue; + } + if (node < 1 << 14) { + encodedValue.write(0x80 | (node >> 7)); + encodedValue.write(node & 0x7f); + continue; + } + if (node < 1 << 21) { + encodedValue.write(0x80 | (node >> 14)); + encodedValue.write(0x80 | ((node >> 7) & 0x7f)); + encodedValue.write(node & 0x7f); + continue; + } + throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node); + } + return Asn1DerEncoder.createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER, + encodedValue.toByteArray()); + } + + private static Object getMemberFieldValue(final Object obj, final Field field) + throws Asn1EncodingException { + try { + return field.get(obj); + } catch (final ReflectiveOperationException e) { + throw new Asn1EncodingException( + "Failed to read " + obj.getClass().getName() + "." + field.getName(), e); + } + } + + private static final class AnnotatedField { + public final Field field; + private final Object mObject; + public final Asn1Field annotation; + private final Asn1Type mDataType; + private final Asn1Type mElementDataType; + private final int mDerTagClass; + private final int mDerTagNumber; + private final Asn1Tagging mTagging; + private final boolean mOptional; + + public AnnotatedField(@NonNull final Object obj, @NonNull final Field field, @NonNull final Asn1Field annotation) + throws Asn1EncodingException { + this.mObject = obj; + this.field = field; + this.annotation = annotation; + this.mDataType = annotation.type(); + this.mElementDataType = annotation.elementType(); + Asn1TagClass tagClass = annotation.cls(); + if (tagClass == Asn1TagClass.Automatic) { + if (annotation.tagNumber() != -1) { + tagClass = Asn1TagClass.ContextSpecific; + } else { + tagClass = Asn1TagClass.Universal; + } + } + this.mDerTagClass = BerEncoding.getTagClass(tagClass); + final int tagNumber; + if (annotation.tagNumber() != -1) { + tagNumber = annotation.tagNumber(); + } else if ((this.mDataType == Asn1Type.Choice) || (this.mDataType == Asn1Type.Any)) { + tagNumber = -1; + } else { + tagNumber = BerEncoding.getTagNumber(this.mDataType); + } + this.mDerTagNumber = tagNumber; + this.mTagging = annotation.tagging(); + if (((this.mTagging == Asn1Tagging.Explicit) || (this.mTagging == Asn1Tagging.Implicit)) + && (annotation.tagNumber() == -1)) { + throw new Asn1EncodingException( + "Tag number must be specified when tagging mode is " + this.mTagging); + } + this.mOptional = annotation.optional(); + } + + @Nullable + public byte[] toDer() throws Asn1EncodingException { + final Object fieldValue = Asn1DerEncoder.getMemberFieldValue(this.mObject, this.field); + if (fieldValue == null) { + if (this.mOptional) { + return null; + } + throw new Asn1EncodingException("Required field not set"); + } + final byte[] encoded = JavaToDerConverter.toDer(fieldValue, this.mDataType, this.mElementDataType); + switch (this.mTagging) { + case Normal: + return encoded; + case Explicit: + return Asn1DerEncoder.createTag(this.mDerTagClass, true, this.mDerTagNumber, encoded); + case Implicit: + final int originalTagNumber = BerEncoding.getTagNumber(encoded[0]); + if (originalTagNumber == 0x1f) { + throw new Asn1EncodingException("High-tag-number form not supported"); + } + if (this.mDerTagNumber >= 0x1f) { + throw new Asn1EncodingException( + "Unsupported high tag number: " + this.mDerTagNumber); + } + encoded[0] = BerEncoding.setTagNumber(encoded[0], this.mDerTagNumber); + encoded[0] = BerEncoding.setTagClass(encoded[0], this.mDerTagClass); + return encoded; + default: + throw new RuntimeException("Unknown tagging mode: " + this.mTagging); + } + } + } + + private static byte[] createTag( + final int tagClass, final boolean constructed, final int tagNumber, final byte[]... contents) { + if (tagNumber >= 0x1f) { + throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber); + } + // tag class & number fit into the first byte + final byte firstIdentifierByte = + (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber); + int contentsLength = 0; + for (final byte[] c : contents) { + contentsLength += c.length; + } + int contentsPosInResult; + final byte[] result; + if (contentsLength < 0x80) { + // Length fits into one byte + contentsPosInResult = 2; + result = new byte[contentsPosInResult + contentsLength]; + result[0] = firstIdentifierByte; + result[1] = (byte) contentsLength; + } else { + // Length is represented as multiple bytes + // The low 7 bits of the first byte represent the number of length bytes (following the + // first byte) in which the length is in big-endian base-256 form + if (contentsLength <= 0xff) { + contentsPosInResult = 3; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x81; + // 1 length byte + result[2] = (byte) contentsLength; + } else if (contentsLength <= 0xffff) { + contentsPosInResult = 4; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x82; + // 2 length bytes + result[2] = (byte) (contentsLength >> 8); + result[3] = (byte) (contentsLength & 0xff); + } else if (contentsLength <= 0xffffff) { + contentsPosInResult = 5; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x83; + // 3 length bytes + result[2] = (byte) (contentsLength >> 16); + result[3] = (byte) ((contentsLength >> 8) & 0xff); + result[4] = (byte) (contentsLength & 0xff); + } else { + contentsPosInResult = 6; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x84; + // 4 length bytes + result[2] = (byte) (contentsLength >> 24); + result[3] = (byte) ((contentsLength >> 16) & 0xff); + result[4] = (byte) ((contentsLength >> 8) & 0xff); + result[5] = (byte) (contentsLength & 0xff); + } + result[0] = firstIdentifierByte; + } + for (final byte[] c : contents) { + System.arraycopy(c, 0, result, contentsPosInResult, c.length); + contentsPosInResult += c.length; + } + return result; + } + + private static final class JavaToDerConverter { + private JavaToDerConverter() { + } + + public static byte[] toDer(@NonNull final Object source, final Asn1Type targetType, @Nullable final Asn1Type targetElementType) + throws Asn1EncodingException { + final Class sourceType = source.getClass(); + if (Asn1OpaqueObject.class.equals(sourceType)) { + final ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded(); + final byte[] result = new byte[buf.remaining()]; + buf.get(result); + return result; + } + if ((targetType == null) || (targetType == Asn1Type.Any)) { + return Asn1DerEncoder.encode(source); + } + switch (targetType) { + case OctetString: + byte[] value = null; + if (source instanceof ByteBuffer) { + final ByteBuffer buf = (ByteBuffer) source; + value = new byte[buf.remaining()]; + buf.slice().get(value); + } else if (source instanceof byte[]) { + value = (byte[]) source; + } + if (value != null) { + return Asn1DerEncoder.createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, + false, + BerEncoding.TAG_NUMBER_OCTET_STRING, + value); + } + break; + case Integer: + if (source instanceof Integer) { + return Asn1DerEncoder.toInteger((Integer) source); + } else if (source instanceof Long) { + return Asn1DerEncoder.toInteger((Long) source); + } else if (source instanceof BigInteger) { + return Asn1DerEncoder.toInteger((BigInteger) source); + } + break; + case ObjectIdentifier: + if (source instanceof String) { + return Asn1DerEncoder.toOid((String) source); + } + break; + case Sequence: { + final Asn1Class containerAnnotation = sourceType.getAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.Sequence)) { + return Asn1DerEncoder.toSequence(source); + } + break; + } + case Choice: { + final Asn1Class containerAnnotation = sourceType.getAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.Choice)) { + return Asn1DerEncoder.toChoice(source); + } + break; + } + case SetOf: + return Asn1DerEncoder.toSetOf((Collection) source, targetElementType); + default: + break; + } + throw new Asn1EncodingException( + "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType); + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java new file mode 100644 index 00000000..a04f7529 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +/** + * Indicates that an ASN.1 structure could not be encoded. + */ +public class Asn1EncodingException extends Exception { + private static final long serialVersionUID = 1L; + + public Asn1EncodingException(final String message) { + super(message); + } + + public Asn1EncodingException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java new file mode 100644 index 00000000..b54adcb7 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Asn1Field { + /** + * Index used to order fields in a container. Required for fields of SEQUENCE containers. + */ + int index() default 0; + + Asn1TagClass cls() default Asn1TagClass.Automatic; + + Asn1Type type(); + + /** + * Tagging mode. Default: NORMAL. + */ + Asn1Tagging tagging() default Asn1Tagging.Normal; + + /** + * Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used. + */ + int tagNumber() default -1; + + /** + * {@code true} if this field is optional. Ignored for fields of CHOICE containers. + */ + boolean optional() default false; + + /** + * Type of elements. Used only for SET_OF or SEQUENCE_OF. + */ + Asn1Type elementType() default Asn1Type.Any; +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java new file mode 100644 index 00000000..2b24bd4e --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; + +/** + * Opaque holder of encoded ASN.1 stuff. + */ +public class Asn1OpaqueObject { + private final ByteBuffer encoded; + + public Asn1OpaqueObject(final @NonNull ByteBuffer encoded) { + this.encoded = encoded.slice(); + } + + public Asn1OpaqueObject(final @NonNull byte[] encoded) { + this.encoded = ByteBuffer.wrap(encoded); + } + + @NonNull + public ByteBuffer getEncoded() { + return this.encoded.slice(); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.kt new file mode 100644 index 00000000..246fa87e --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.cert.asn1 + +enum class Asn1TagClass { + Universal, Application, ContextSpecific, Private, + + /** + * Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class + * automatically. + */ + Automatic +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.kt new file mode 100644 index 00000000..b8f45764 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.cert.asn1 + +enum class Asn1Tagging { + Normal, Explicit, Implicit +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.kt new file mode 100644 index 00000000..28401aea --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.cert.asn1 + +enum class Asn1Type { + Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence, SequenceOf, SetOf +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java new file mode 100644 index 00000000..71d764ef --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; + +/** + * ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}. + */ +public class BerDataValue { + private final ByteBuffer encoded; + private final ByteBuffer encodedContents; + /** + * Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS} + * constants. + */ + public final int tagClass; + /** + * Returns {@code true} if the content octets of this data value are the complete BER encoding + * of one or more data values, {@code false} if the content octets of this data value directly + * represent the value. + */ + public final boolean isConstructed; + /** + * Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER} + * constants. + */ + public final int tagNumber; + + BerDataValue( + @NonNull final ByteBuffer encoded, + @NonNull final ByteBuffer encodedContents, + final int tagClass, + final boolean constructed, + final int tagNumber) { + this.encoded = encoded; + this.encodedContents = encodedContents; + this.tagClass = tagClass; + this.isConstructed = constructed; + this.tagNumber = tagNumber; + } + + /** + * Returns the encoded form of this data value. + */ + public ByteBuffer getEncoded() { + return this.encoded.slice(); + } + + /** + * Returns the encoded contents of this data value. + */ + @NonNull + public ByteBuffer getEncodedContents() { + return this.encodedContents.slice(); + } + + /** + * Returns a new reader of the contents of this data value. + */ + @NonNull + public BerDataValueReader contentsReader() { + return new ByteBufferBerDataValueReader(this.getEncodedContents()); + } + + /** + * Returns a new reader which returns just this data value. This may be useful for re-reading + * this value in different contexts. + */ + @NonNull + public BerDataValueReader dataValueReader() { + return new ParsedValueReader(this); + } + + private static final class ParsedValueReader implements BerDataValueReader { + private final BerDataValue mValue; + private boolean mValueOutput; + + public ParsedValueReader(final BerDataValue value) { + this.mValue = value; + } + + @Nullable + @Override + public BerDataValue readDataValue() { + if (this.mValueOutput) { + return null; + } + this.mValueOutput = true; + return this.mValue; + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java new file mode 100644 index 00000000..56ffcf18 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +/** + * Indicates that an ASN.1 data value being read could not be decoded using + * Basic Encoding Rules (BER). + */ +public class BerDataValueFormatException extends Exception { + + private static final long serialVersionUID = 1L; + + public BerDataValueFormatException(final String message) { + super(message); + } + + public BerDataValueFormatException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java new file mode 100644 index 00000000..2407c804 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +import androidx.annotation.Nullable; + +/** + * Reader of ASN.1 Basic Encoding Rules (BER) data values. + * + *

BER data value reader returns data values, one by one, from a source. The interpretation of + * data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract + * the elements of a SEQUENCE value) is left to clients of the reader. + */ +public interface BerDataValueReader { + + /** + * Returns the next data value or {@code null} if end of input has been reached. + * + * @throws BerDataValueFormatException if the value being read is malformed. + */ + @Nullable + BerDataValue readDataValue() throws BerDataValueFormatException; +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java new file mode 100644 index 00000000..f13cec92 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +import net.dongliu.apk.parser.cert.asn1.Asn1TagClass; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +/** + * ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}. + */ +public abstract class BerEncoding { + private BerEncoding() { + } + + /** + * Constructed vs primitive flag in the first identifier byte. + */ + public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5; + + /** + * Tag class: UNIVERSAL + */ + public static final int TAG_CLASS_UNIVERSAL = 0; + + /** + * Tag class: APPLICATION + */ + public static final int TAG_CLASS_APPLICATION = 1; + + /** + * Tag class: CONTEXT SPECIFIC + */ + public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2; + + /** + * Tag class: PRIVATE + */ + public static final int TAG_CLASS_PRIVATE = 3; + + /** + * Tag number: INTEGER + */ + public static final int TAG_NUMBER_INTEGER = 0x2; + + /** + * Tag number: OCTET STRING + */ + public static final int TAG_NUMBER_OCTET_STRING = 0x4; + + /** + * Tag number: NULL + */ + public static final int TAG_NUMBER_NULL = 0x05; + + /** + * Tag number: OBJECT IDENTIFIER + */ + public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6; + + /** + * Tag number: SEQUENCE + */ + public static final int TAG_NUMBER_SEQUENCE = 0x10; + + /** + * Tag number: SET + */ + public static final int TAG_NUMBER_SET = 0x11; + + public static int getTagNumber(final Asn1Type dataType) { + switch (dataType) { + case Integer: + return BerEncoding.TAG_NUMBER_INTEGER; + case ObjectIdentifier: + return BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER; + case OctetString: + return BerEncoding.TAG_NUMBER_OCTET_STRING; + case SetOf: + return BerEncoding.TAG_NUMBER_SET; + case Sequence: + case SequenceOf: + return BerEncoding.TAG_NUMBER_SEQUENCE; + default: + throw new IllegalArgumentException("Unsupported data type: " + dataType); + } + } + + public static int getTagClass(final Asn1TagClass tagClass) { + switch (tagClass) { + case Application: + return BerEncoding.TAG_CLASS_APPLICATION; + case ContextSpecific: + return BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC; + case Private: + return BerEncoding.TAG_CLASS_PRIVATE; + case Universal: + return BerEncoding.TAG_CLASS_UNIVERSAL; + default: + throw new IllegalArgumentException("Unsupported tag class: " + tagClass); + } + } + + public static String tagClassToString(final int typeClass) { + switch (typeClass) { + case BerEncoding.TAG_CLASS_APPLICATION: + return "APPLICATION"; + case BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC: + return ""; + case BerEncoding.TAG_CLASS_PRIVATE: + return "PRIVATE"; + case BerEncoding.TAG_CLASS_UNIVERSAL: + return "UNIVERSAL"; + default: + throw new IllegalArgumentException("Unsupported type class: " + typeClass); + } + } + + public static String tagClassAndNumberToString(final int tagClass, final int tagNumber) { + final String classString = BerEncoding.tagClassToString(tagClass); + final String numberString = BerEncoding.tagNumberToString(tagNumber); + return classString.isEmpty() ? numberString : classString + " " + numberString; + } + + public static String tagNumberToString(final int tagNumber) { + switch (tagNumber) { + case BerEncoding.TAG_NUMBER_INTEGER: + return "INTEGER"; + case BerEncoding.TAG_NUMBER_OCTET_STRING: + return "OCTET STRING"; + case BerEncoding.TAG_NUMBER_NULL: + return "NULL"; + case BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER: + return "OBJECT IDENTIFIER"; + case BerEncoding.TAG_NUMBER_SEQUENCE: + return "SEQUENCE"; + case BerEncoding.TAG_NUMBER_SET: + return "SET"; + default: + return "0x" + Integer.toHexString(tagNumber); + } + } + + /** + * Returns {@code true} if the provided first identifier byte indicates that the data value uses + * constructed encoding for its contents, or {@code false} if the data value uses primitive + * encoding for its contents. + */ + public static boolean isConstructed(final byte firstIdentifierByte) { + return (firstIdentifierByte & BerEncoding.ID_FLAG_CONSTRUCTED_ENCODING) != 0; + } + + /** + * Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS} + * constants. + */ + public static int getTagClass(final byte firstIdentifierByte) { + return (firstIdentifierByte & 0xff) >> 6; + } + + public static byte setTagClass(final byte firstIdentifierByte, final int tagClass) { + return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6)); + } + + /** + * Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER} + * constants. + */ + public static int getTagNumber(final byte firstIdentifierByte) { + return firstIdentifierByte & 0x1f; + } + + public static byte setTagNumber(final byte firstIdentifierByte, final int tagNumber) { + return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java new file mode 100644 index 00000000..1c064734 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; + +/** + * {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data + * values. See {@code X.690} for the encoding. + */ +public class ByteBufferBerDataValueReader implements BerDataValueReader { + private final ByteBuffer mBuf; + + public ByteBufferBerDataValueReader(final @NonNull ByteBuffer buf) { + this.mBuf = buf; + } + + @Nullable + @Override + public BerDataValue readDataValue() throws BerDataValueFormatException { + final int startPosition = this.mBuf.position(); + if (!this.mBuf.hasRemaining()) { + return null; + } + final byte firstIdentifierByte = this.mBuf.get(); + final int tagNumber = this.readTagNumber(firstIdentifierByte); + final boolean constructed = BerEncoding.isConstructed(firstIdentifierByte); + if (!this.mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Missing length"); + } + final int firstLengthByte = this.mBuf.get() & 0xff; + final int contentsLength; + final int contentsOffsetInTag; + if ((firstLengthByte & 0x80) == 0) { + // short form length + contentsLength = this.readShortFormLength(firstLengthByte); + contentsOffsetInTag = this.mBuf.position() - startPosition; + this.skipDefiniteLengthContents(contentsLength); + } else if (firstLengthByte != 0x80) { + // long form length + contentsLength = this.readLongFormLength(firstLengthByte); + contentsOffsetInTag = this.mBuf.position() - startPosition; + this.skipDefiniteLengthContents(contentsLength); + } else { + // indefinite length -- value ends with 0x00 0x00 + contentsOffsetInTag = this.mBuf.position() - startPosition; + contentsLength = + constructed + ? this.skipConstructedIndefiniteLengthContents() + : this.skipPrimitiveIndefiniteLengthContents(); + } + // Create the encoded data value ByteBuffer + final int endPosition = this.mBuf.position(); + this.mBuf.position(startPosition); + final int bufOriginalLimit = this.mBuf.limit(); + this.mBuf.limit(endPosition); + final ByteBuffer encoded = this.mBuf.slice(); + this.mBuf.position(this.mBuf.limit()); + this.mBuf.limit(bufOriginalLimit); + // Create the encoded contents ByteBuffer + encoded.position(contentsOffsetInTag); + encoded.limit(contentsOffsetInTag + contentsLength); + final ByteBuffer encodedContents = encoded.slice(); + encoded.clear(); + return new BerDataValue( + encoded, + encodedContents, + BerEncoding.getTagClass(firstIdentifierByte), + constructed, + tagNumber); + } + + private int readTagNumber(final byte firstIdentifierByte) throws BerDataValueFormatException { + final int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte); + if (tagNumber == 0x1f) { + // high-tag-number form, where the tag number follows this byte in base-128 + // big-endian form, where each byte has the highest bit set, except for the last + // byte + return this.readHighTagNumber(); + } else { + // low-tag-number form + return tagNumber; + } + } + + private int readHighTagNumber() throws BerDataValueFormatException { + // Base-128 big-endian form, where each byte has the highest bit set, except for the last + // byte + int b; + int result = 0; + do { + if (!this.mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Truncated tag number"); + } + b = this.mBuf.get(); + if (result > Integer.MAX_VALUE >>> 7) { + throw new BerDataValueFormatException("Tag number too large"); + } + result <<= 7; + result |= b & 0x7f; + } while ((b & 0x80) != 0); + return result; + } + + private int readShortFormLength(final int firstLengthByte) { + return firstLengthByte & 0x7f; + } + + private int readLongFormLength(final int firstLengthByte) throws BerDataValueFormatException { + // The low 7 bits of the first byte represent the number of bytes (following the first + // byte) in which the length is in big-endian base-256 form + final int byteCount = firstLengthByte & 0x7f; + if (byteCount > 4) { + throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes"); + } + int result = 0; + for (int i = 0; i < byteCount; i++) { + if (!this.mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Truncated length"); + } + final int b = this.mBuf.get(); + if (result > Integer.MAX_VALUE >>> 8) { + throw new BerDataValueFormatException("Length too large"); + } + result <<= 8; + result |= b & 0xff; + } + return result; + } + + private void skipDefiniteLengthContents(final int contentsLength) throws BerDataValueFormatException { + if (this.mBuf.remaining() < contentsLength) { + throw new BerDataValueFormatException( + "Truncated contents. Need: " + contentsLength + " bytes, available: " + + this.mBuf.remaining()); + } + this.mBuf.position(this.mBuf.position() + contentsLength); + } + + private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00 + boolean prevZeroByte = false; + int bytesRead = 0; + while (true) { + if (!this.mBuf.hasRemaining()) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + bytesRead + " bytes read"); + + } + final int b = this.mBuf.get(); + bytesRead++; + if (bytesRead < 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + if (b == 0) { + if (prevZeroByte) { + // End of contents reached -- we've read the value and its terminator 0x00 0x00 + return bytesRead - 2; + } + prevZeroByte = true; + } else { + prevZeroByte = false; + } + } + } + + private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it + // can contain data values which are themselves indefinite length encoded. As a result, we + // must parse the direct children of this data value to correctly skip over the contents of + // this data value. + final int startPos = this.mBuf.position(); + while (this.mBuf.hasRemaining()) { + // Check whether the 0x00 0x00 terminator is at current position + if ((this.mBuf.remaining() > 1) && (this.mBuf.getShort(this.mBuf.position()) == 0)) { + final int contentsLength = this.mBuf.position() - startPos; + this.mBuf.position(this.mBuf.position() + 2); + return contentsLength; + } + // No luck. This must be a BER-encoded data value -- skip over it by parsing it + this.readDataValue(); + } + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + + (this.mBuf.position() - startPos) + " bytes read"); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java new file mode 100644 index 00000000..05f021c2 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data + * values. See {@code X.690} for the encoding. + */ +public class InputStreamBerDataValueReader implements BerDataValueReader { + private final InputStream mIn; + + public InputStreamBerDataValueReader(final InputStream in) { + if (in == null) { + throw new NullPointerException("in == null"); + } + this.mIn = in; + } + + @Override + @Nullable + public BerDataValue readDataValue() throws BerDataValueFormatException { + return InputStreamBerDataValueReader.readDataValue(this.mIn); + } + + /** + * Returns the next data value or {@code null} if end of input has been reached. + * + * @throws BerDataValueFormatException if the value being read is malformed. + */ + @Nullable + private static BerDataValue readDataValue(final InputStream input) + throws BerDataValueFormatException { + final RecordingInputStream in = new RecordingInputStream(input); + + try { + final int firstIdentifierByte = in.read(); + if (firstIdentifierByte == -1) { + // End of input + return null; + } + final int tagNumber = InputStreamBerDataValueReader.readTagNumber(in, firstIdentifierByte); + + final int firstLengthByte = in.read(); + if (firstLengthByte == -1) { + throw new BerDataValueFormatException("Missing length"); + } + + final boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte); + final int contentsLength; + final int contentsOffsetInDataValue; + if ((firstLengthByte & 0x80) == 0) { + // short form length + contentsLength = InputStreamBerDataValueReader.readShortFormLength(firstLengthByte); + contentsOffsetInDataValue = in.getReadByteCount(); + InputStreamBerDataValueReader.skipDefiniteLengthContents(in, contentsLength); + } else if ((firstLengthByte & 0xff) != 0x80) { + // long form length + contentsLength = InputStreamBerDataValueReader.readLongFormLength(in, firstLengthByte); + contentsOffsetInDataValue = in.getReadByteCount(); + InputStreamBerDataValueReader.skipDefiniteLengthContents(in, contentsLength); + } else { + // indefinite length + contentsOffsetInDataValue = in.getReadByteCount(); + contentsLength = + constructed + ? InputStreamBerDataValueReader.skipConstructedIndefiniteLengthContents(in) + : InputStreamBerDataValueReader.skipPrimitiveIndefiniteLengthContents(in); + } + + final byte[] encoded = in.getReadBytes(); + final ByteBuffer encodedContents = + ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength); + return new BerDataValue( + ByteBuffer.wrap(encoded), + encodedContents, + BerEncoding.getTagClass((byte) firstIdentifierByte), + constructed, + tagNumber); + } catch (final IOException e) { + throw new BerDataValueFormatException("Failed to read data value", e); + } + } + + private static int readTagNumber(final InputStream in, final int firstIdentifierByte) + throws IOException, BerDataValueFormatException { + final int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte); + if (tagNumber == 0x1f) { + // high-tag-number form + return InputStreamBerDataValueReader.readHighTagNumber(in); + } else { + // low-tag-number form + return tagNumber; + } + } + + private static int readHighTagNumber(final InputStream in) + throws IOException, BerDataValueFormatException { + // Base-128 big-endian form, where each byte has the highest bit set, except for the last + // byte where the highest bit is not set + int b; + int result = 0; + do { + b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException("Truncated tag number"); + } + if (result > Integer.MAX_VALUE >>> 7) { + throw new BerDataValueFormatException("Tag number too large"); + } + result <<= 7; + result |= b & 0x7f; + } while ((b & 0x80) != 0); + return result; + } + + private static int readShortFormLength(final int firstLengthByte) { + return firstLengthByte & 0x7f; + } + + private static int readLongFormLength(final InputStream in, final int firstLengthByte) + throws IOException, BerDataValueFormatException { + // The low 7 bits of the first byte represent the number of bytes (following the first + // byte) in which the length is in big-endian base-256 form + final int byteCount = firstLengthByte & 0x7f; + if (byteCount > 4) { + throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes"); + } + int result = 0; + for (int i = 0; i < byteCount; i++) { + final int b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException("Truncated length"); + } + if (result > Integer.MAX_VALUE >>> 8) { + throw new BerDataValueFormatException("Length too large"); + } + result <<= 8; + result |= b & 0xff; + } + return result; + } + + private static void skipDefiniteLengthContents(final InputStream in, int len) + throws IOException, BerDataValueFormatException { + long bytesRead = 0; + while (len > 0) { + final int skipped = (int) in.skip(len); + if (skipped <= 0) { + throw new BerDataValueFormatException( + "Truncated definite-length contents: " + bytesRead + " bytes read" + + ", " + len + " missing"); + } + len -= skipped; + bytesRead += skipped; + } + } + + private static int skipPrimitiveIndefiniteLengthContents(final InputStream in) + throws IOException, BerDataValueFormatException { + // Contents are terminated by 0x00 0x00 + boolean prevZeroByte = false; + int bytesRead = 0; + while (true) { + final int b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + bytesRead + " bytes read"); + } + bytesRead++; + if (bytesRead < 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + if (b == 0) { + if (prevZeroByte) { + // End of contents reached -- we've read the value and its terminator 0x00 0x00 + return bytesRead - 2; + } + prevZeroByte = true; + } else { + prevZeroByte = false; + } + } + } + + private static int skipConstructedIndefiniteLengthContents(final RecordingInputStream in) + throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it + // can contain data values which are indefinite length encoded as well. As a result, we + // must parse the direct children of this data value to correctly skip over the contents of + // this data value. + final int readByteCountBefore = in.getReadByteCount(); + while (true) { + // We can't easily peek for the 0x00 0x00 terminator using the provided InputStream. + // Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we + // then check below to see whether it's 0x00 0x00. + final BerDataValue dataValue = InputStreamBerDataValueReader.readDataValue(in); + if (dataValue == null) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + + (in.getReadByteCount() - readByteCountBefore) + " bytes read"); + } + if (in.getReadByteCount() <= 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + final ByteBuffer encoded = dataValue.getEncoded(); + if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) { + // 0x00 0x00 encountered + return in.getReadByteCount() - readByteCountBefore - 2; + } + } + } + + private static class RecordingInputStream extends InputStream { + private final InputStream mIn; + private final ByteArrayOutputStream mBuf; + + private RecordingInputStream(final InputStream in) { + this.mIn = in; + this.mBuf = new ByteArrayOutputStream(); + } + + public byte[] getReadBytes() { + return this.mBuf.toByteArray(); + } + + public int getReadByteCount() { + return this.mBuf.size(); + } + + @Override + public int read() throws IOException { + final int b = this.mIn.read(); + if (b != -1) { + this.mBuf.write(b); + } + return b; + } + + @Override + public int read(@NonNull final byte[] b) throws IOException { + final int len = this.mIn.read(b); + if (len > 0) { + this.mBuf.write(b, 0, len); + } + return len; + } + + @Override + public int read(@NonNull final byte[] b, final int off, int len) throws IOException { + len = this.mIn.read(b, off, len); + if (len > 0) { + this.mBuf.write(b, off, len); + } + return len; + } + + @Override + public long skip(final long n) throws IOException { + if (n <= 0) { + return 0; + } + + final byte[] buf = new byte[4096]; + final int len = this.mIn.read(buf, 0, (int) Math.min(buf.length, n)); + if (len > 0) { + this.mBuf.write(buf, 0, len); + } + return Math.max(len, 0); + } + + @Override + public synchronized void mark(final int readlimit) { + } + + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java new file mode 100644 index 00000000..495351f0 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +/** + * PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.Sequence) +public class AlgorithmIdentifier { + + @Asn1Field(index = 0, type = Asn1Type.ObjectIdentifier) + public String algorithm; + + @Asn1Field(index = 1, type = Asn1Type.Any, optional = true) + public Asn1OpaqueObject parameters; + + public AlgorithmIdentifier() { + } + + public AlgorithmIdentifier(final String algorithmOid, final Asn1OpaqueObject parameters) { + this.algorithm = algorithmOid; + this.parameters = parameters; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java new file mode 100644 index 00000000..794bef76 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.util.List; + +/** + * PKCS #7 {@code Attribute} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.Sequence) +public class Attribute { + + @Asn1Field(index = 0, type = Asn1Type.ObjectIdentifier) + public String attrType; + + @Asn1Field(index = 1, type = Asn1Type.SetOf) + public List attrValues; +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java new file mode 100644 index 00000000..8fa3be92 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Tagging; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +/** + * PKCS #7 {@code ContentInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.Sequence) +public class ContentInfo { + + @Asn1Field(index = 1, type = Asn1Type.ObjectIdentifier) + public String contentType; + + @Asn1Field(index = 2, type = Asn1Type.Any, tagging = Asn1Tagging.Explicit, tagNumber = 0) + public Asn1OpaqueObject content; +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java new file mode 100644 index 00000000..33d89cef --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1Tagging; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.nio.ByteBuffer; + +/** + * PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.Sequence) +public class EncapsulatedContentInfo { + + @Asn1Field(index = 0, type = Asn1Type.ObjectIdentifier) + public String contentType; + + @Asn1Field( + index = 1, + type = Asn1Type.OctetString, + tagging = Asn1Tagging.Explicit, tagNumber = 0, + optional = true) + public ByteBuffer content; + + public EncapsulatedContentInfo() { + } + + public EncapsulatedContentInfo(final String contentTypeOid) { + this.contentType = contentTypeOid; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java new file mode 100644 index 00000000..f19071b7 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.math.BigInteger; + +/** + * PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.Sequence) +public class IssuerAndSerialNumber { + + @Asn1Field(index = 0, type = Asn1Type.Any) + public Asn1OpaqueObject issuer; + + @Asn1Field(index = 1, type = Asn1Type.Integer) + public BigInteger certificateSerialNumber; + + public IssuerAndSerialNumber() { + } + + public IssuerAndSerialNumber(final Asn1OpaqueObject issuer, final BigInteger certificateSerialNumber) { + this.issuer = issuer; + this.certificateSerialNumber = certificateSerialNumber; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java new file mode 100644 index 00000000..7062b51f --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +/** + * Assorted PKCS #7 constants from RFC 5652. + */ +public abstract class Pkcs7Constants { + private Pkcs7Constants() { + } + + public static final String OID_DATA = "1.2.840.113549.1.7.1"; + public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2"; + public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3"; + public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4"; +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java new file mode 100644 index 00000000..8f65e35b --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Tagging; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * PKCS #7 {@code SignedData} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.Sequence) +public class SignedData { + + @Asn1Field(index = 0, type = Asn1Type.Integer) + public int version; + + @Asn1Field(index = 1, type = Asn1Type.SetOf) + public List digestAlgorithms; + + @Asn1Field(index = 2, type = Asn1Type.Sequence) + public EncapsulatedContentInfo encapContentInfo; + + @Asn1Field( + index = 3, + type = Asn1Type.SetOf, + tagging = Asn1Tagging.Implicit, tagNumber = 0, + optional = true) + public List certificates; + + @Asn1Field( + index = 4, + type = Asn1Type.SetOf, + tagging = Asn1Tagging.Implicit, tagNumber = 1, + optional = true) + public List crls; + + @Asn1Field(index = 5, type = Asn1Type.SetOf) + public List signerInfos; +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java new file mode 100644 index 00000000..ed2b4f12 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1Tagging; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.nio.ByteBuffer; + +/** + * PKCS #7 {@code SignerIdentifier} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.Choice) +public class SignerIdentifier { + + @Asn1Field(type = Asn1Type.Sequence) + public IssuerAndSerialNumber issuerAndSerialNumber; + + @Asn1Field(type = Asn1Type.OctetString, tagging = Asn1Tagging.Implicit, tagNumber = 0) + public ByteBuffer subjectKeyIdentifier; + + public SignerIdentifier() { + } + + public SignerIdentifier(final IssuerAndSerialNumber issuerAndSerialNumber) { + this.issuerAndSerialNumber = issuerAndSerialNumber; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java new file mode 100644 index 00000000..26f63fdc --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Tagging; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * PKCS #7 {@code SignerInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.Sequence) +public class SignerInfo { + + @Asn1Field(index = 0, type = Asn1Type.Integer) + public int version; + + @Asn1Field(index = 1, type = Asn1Type.Choice) + public SignerIdentifier sid; + + @Asn1Field(index = 2, type = Asn1Type.Sequence) + public AlgorithmIdentifier digestAlgorithm; + + @Asn1Field( + index = 3, + type = Asn1Type.SetOf, + tagging = Asn1Tagging.Implicit, tagNumber = 0, + optional = true) + public Asn1OpaqueObject signedAttrs; + + @Asn1Field(index = 4, type = Asn1Type.Sequence) + public AlgorithmIdentifier signatureAlgorithm; + + @Asn1Field(index = 5, type = Asn1Type.OctetString) + public ByteBuffer signature; + + @Asn1Field( + index = 6, + type = Asn1Type.SetOf, + tagging = Asn1Tagging.Implicit, tagNumber = 1, + optional = true) + public List unsignedAttrs; +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/exception/ParserException.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/exception/ParserException.java new file mode 100644 index 00000000..91d0bba2 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/exception/ParserException.java @@ -0,0 +1,30 @@ +package net.dongliu.apk.parser.exception; + +/** + * throwed when parse failed. + * + * @author dongliu + */ +public class ParserException extends RuntimeException { + private static final long serialVersionUID = -669279149141454276L; + + public ParserException(final String msg) { + super(msg); + } + + public ParserException(final String message, final Throwable cause) { + super(message, cause); + } + + public ParserException(final Throwable cause) { + super(cause); + } + + public ParserException(final String message, final Throwable cause, final boolean enableSuppression, + final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ParserException() { + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java new file mode 100644 index 00000000..3b439c96 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java @@ -0,0 +1,70 @@ +package net.dongliu.apk.parser.parser; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.xml.Attribute; +import net.dongliu.apk.parser.struct.xml.Attributes; +import net.dongliu.apk.parser.struct.xml.XmlCData; +import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag; +import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag; +import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag; +import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag; + +/** + * Parse adaptive icon xml file. + * + * @author Liu Dong dongliu@live.cn + */ +public class AdaptiveIconParser implements XmlStreamer { + @Nullable + private String foreground; + @Nullable + private String background; + + @Nullable + public String getForeground() { + return this.foreground; + } + + @Nullable + public String getBackground() { + return this.background; + } + + @Override + public void onStartTag(final @NonNull XmlNodeStartTag xmlNodeStartTag) { + if ("background".equals(xmlNodeStartTag.name)) { + this.background = this.getDrawable(xmlNodeStartTag); + } else if ("foreground".equals(xmlNodeStartTag.name)) { + this.foreground = this.getDrawable(xmlNodeStartTag); + } + } + + @Nullable + private String getDrawable(final XmlNodeStartTag xmlNodeStartTag) { + final Attributes attributes = xmlNodeStartTag.attributes; + for (final Attribute attribute : attributes.attributes) { + if (attribute.name.equals("drawable")) { + return attribute.value; + } + } + return null; + } + + @Override + public void onEndTag(@NonNull final XmlNodeEndTag xmlNodeEndTag) { + } + + @Override + public void onCData(@NonNull final XmlCData xmlCData) { + } + + @Override + public void onNamespaceStart(@NonNull final XmlNamespaceStartTag tag) { + } + + @Override + public void onNamespaceEnd(@NonNull final XmlNamespaceEndTag tag) { + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java new file mode 100644 index 00000000..5f29ae1e --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java @@ -0,0 +1,271 @@ +package net.dongliu.apk.parser.parser; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.bean.ApkMeta; +import net.dongliu.apk.parser.bean.GlEsVersion; +import net.dongliu.apk.parser.bean.IconPath; +import net.dongliu.apk.parser.bean.Permission; +import net.dongliu.apk.parser.bean.UseFeature; +import net.dongliu.apk.parser.struct.ResourceValue; +import net.dongliu.apk.parser.struct.resource.Densities; +import net.dongliu.apk.parser.struct.resource.ResourceEntry; +import net.dongliu.apk.parser.struct.resource.ResourceTable; +import net.dongliu.apk.parser.struct.resource.Type; +import net.dongliu.apk.parser.struct.xml.Attribute; +import net.dongliu.apk.parser.struct.xml.Attributes; +import net.dongliu.apk.parser.struct.xml.XmlCData; +import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag; +import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag; +import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag; +import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * trans binary xml to apk meta info + * + * @author Liu Dong dongliu@live.cn + */ +public class ApkMetaTranslator implements XmlStreamer { + private final String[] tagStack = new String[100]; + private int depth = 0; + @NonNull + private final ApkMeta.Builder apkMetaBuilder = ApkMeta.newBuilder(); + private List iconPaths = Collections.emptyList(); + + private final ResourceTable resourceTable; + @Nullable + private final Locale locale; + + public ApkMetaTranslator(final @NonNull ResourceTable resourceTable, @Nullable final Locale locale) { + this.resourceTable = resourceTable; + this.locale = locale; + } + + @Override + public void onStartTag(final @NonNull XmlNodeStartTag xmlNodeStartTag) { + final Attributes attributes = xmlNodeStartTag.attributes; + final String xmlNodeStartTagName = xmlNodeStartTag.name; + switch (xmlNodeStartTagName) { + case "application": { + this.apkMetaBuilder.setDebuggable(attributes.getBoolean("debuggable", false)); + //TODO fix this part in a better way. Workaround for this: https://github.com/hsiafan/apk-parser/issues/119 + if (this.apkMetaBuilder.split == null) + this.apkMetaBuilder.setSplit(attributes.getString("split")); + if (this.apkMetaBuilder.configForSplit == null) + this.apkMetaBuilder.setConfigForSplit(attributes.getString("configForSplit")); + if (!this.apkMetaBuilder.isFeatureSplit) + this.apkMetaBuilder.setIsFeatureSplit(attributes.getBoolean("isFeatureSplit", false)); + if (!this.apkMetaBuilder.isSplitRequired) + this.apkMetaBuilder.setIsSplitRequired(attributes.getBoolean("isSplitRequired", false)); + if (!this.apkMetaBuilder.isolatedSplits) + this.apkMetaBuilder.setIsolatedSplits(attributes.getBoolean("isolatedSplits", false)); + final String label = attributes.getString("label"); + if (label != null) { + this.apkMetaBuilder.setLabel(label); + } else { + //workaround in case the real label can't be found, so we at least try to use the package name with the application class + final String packageName = this.apkMetaBuilder.getPackageName(); + if (!TextUtils.isEmpty(packageName)) { + final String applicationClassRelativePath = attributes.getString("name"); + if (TextUtils.isEmpty(applicationClassRelativePath)) { + this.apkMetaBuilder.setLabel(packageName); + } else { + this.apkMetaBuilder.applicationClassRelativePath = applicationClassRelativePath; + final String newLabel = packageName + applicationClassRelativePath; + this.apkMetaBuilder.setLabel(newLabel); + } + } + } + final Attribute iconAttr = attributes.get("icon"); + if (iconAttr != null) { + final ResourceValue resourceValue = iconAttr.typedValue; + if (resourceValue instanceof ResourceValue.ReferenceResourceValue) { + final long resourceId = ((ResourceValue.ReferenceResourceValue) resourceValue).getReferenceResourceId(); + final List resources = this.resourceTable.getResourcesById(resourceId); + if (!resources.isEmpty()) { + final List icons = new ArrayList<>(); + boolean hasDefault = false; + for (final ResourceTable.Resource resource : resources) { + final Type type = resource.type; + final ResourceEntry resourceEntry = resource.resourceEntry; + final String path = resourceEntry.toStringValue(this.resourceTable, this.locale); + if (type.density == Densities.DEFAULT) { + hasDefault = true; + this.apkMetaBuilder.setIcon(path); + } + final IconPath iconPath = new IconPath(path, type.density); + icons.add(iconPath); + } + if (!hasDefault) { + this.apkMetaBuilder.setIcon(icons.get(0).path); + } + this.iconPaths = icons; + } + } else { + final String value = iconAttr.value; + if (value != null) { + this.apkMetaBuilder.setIcon(value); + final IconPath iconPath = new IconPath(value, Densities.DEFAULT); + this.iconPaths = Collections.singletonList(iconPath); + } + } + } + break; + } + case "manifest": { + final String packageName = attributes.getString("package"); + this.apkMetaBuilder.setPackageName(packageName); + if (TextUtils.isEmpty(this.apkMetaBuilder.getLabel()) && !TextUtils.isEmpty(packageName)) { + //workaround in case the real label can't be found, so we at least try to use the package name with the application class + final String applicationClassRelativePath = this.apkMetaBuilder.applicationClassRelativePath; + if (TextUtils.isEmpty(applicationClassRelativePath)) { + this.apkMetaBuilder.setLabel(packageName); + } else { + final String newLabel = packageName + applicationClassRelativePath; + this.apkMetaBuilder.setLabel(newLabel); + } + } + this.apkMetaBuilder.setVersionName(attributes.getString("versionName")); + this.apkMetaBuilder.setRevisionCode(attributes.getLong("revisionCode")); + this.apkMetaBuilder.setSharedUserId(attributes.getString("sharedUserId")); + this.apkMetaBuilder.setSharedUserLabel(attributes.getString("sharedUserLabel")); + if (this.apkMetaBuilder.split == null) + this.apkMetaBuilder.setSplit(attributes.getString("split")); + if (this.apkMetaBuilder.configForSplit == null) + this.apkMetaBuilder.setConfigForSplit(attributes.getString("configForSplit")); + if (!this.apkMetaBuilder.isFeatureSplit) + this.apkMetaBuilder.setIsFeatureSplit(attributes.getBoolean("isFeatureSplit", false)); + if (!this.apkMetaBuilder.isSplitRequired) + this.apkMetaBuilder.setIsSplitRequired(attributes.getBoolean("isSplitRequired", false)); + if (!this.apkMetaBuilder.isolatedSplits) + this.apkMetaBuilder.setIsolatedSplits(attributes.getBoolean("isolatedSplits", false)); + final Long majorVersionCode = attributes.getLong("versionCodeMajor"); + Long versionCode = attributes.getLong("versionCode"); + if (majorVersionCode != null) { + if (versionCode == null) { + versionCode = 0L; + } + versionCode = (majorVersionCode << 32) | (versionCode & 0xFFFFFFFFL); + } + if (versionCode != null) + this.apkMetaBuilder.setVersionCode(versionCode); + final String installLocation = attributes.getString("installLocation"); + if (installLocation != null) { + this.apkMetaBuilder.setInstallLocation(installLocation); + } + this.apkMetaBuilder.setCompileSdkVersion(attributes.getString("compileSdkVersion")); + this.apkMetaBuilder.setCompileSdkVersionCodename(attributes.getString("compileSdkVersionCodename")); + this.apkMetaBuilder.setPlatformBuildVersionCode(attributes.getString("platformBuildVersionCode")); + this.apkMetaBuilder.setPlatformBuildVersionName(attributes.getString("platformBuildVersionName")); + break; + } + case "uses-sdk": { + final String minSdkVersion = attributes.getString("minSdkVersion"); + if (minSdkVersion != null) { + this.apkMetaBuilder.setMinSdkVersion(minSdkVersion); + } + final String targetSdkVersion = attributes.getString("targetSdkVersion"); + if (targetSdkVersion != null) { + this.apkMetaBuilder.setTargetSdkVersion(targetSdkVersion); + } + final String maxSdkVersion = attributes.getString("maxSdkVersion"); + if (maxSdkVersion != null) { + this.apkMetaBuilder.setMaxSdkVersion(maxSdkVersion); + } + break; + } + case "supports-screens": { + this.apkMetaBuilder.setAnyDensity(attributes.getBoolean("anyDensity", false)); + this.apkMetaBuilder.setSmallScreens(attributes.getBoolean("smallScreens", false)); + this.apkMetaBuilder.setNormalScreens(attributes.getBoolean("normalScreens", false)); + this.apkMetaBuilder.setLargeScreens(attributes.getBoolean("largeScreens", false)); + break; + } + case "uses-feature": { + final String name = attributes.getString("name"); + final boolean required = attributes.getBoolean("required", false); + if (name != null) { + final UseFeature useFeature = new UseFeature(name, required); + this.apkMetaBuilder.addUsesFeature(useFeature); + } else { + final Integer gl = attributes.getInt("glEsVersion"); + if (gl != null) { + final int v = gl; + final GlEsVersion glEsVersion = new GlEsVersion(v >> 16, v & 0xffff, required); + this.apkMetaBuilder.setGlEsVersion(glEsVersion); + } + } + break; + } + case "uses-permission": { + this.apkMetaBuilder.addUsesPermission(attributes.getString("name")); + break; + } + case "permission": { + final Permission permission = new Permission( + attributes.getString("name"), + attributes.getString("label"), + attributes.getString("icon"), + attributes.getString("description"), + attributes.getString("group"), + attributes.getString("android:protectionLevel")); + this.apkMetaBuilder.addPermissions(permission); + break; + } + } + this.tagStack[this.depth++] = xmlNodeStartTagName; + } + + @Override + public void onEndTag(@NonNull final XmlNodeEndTag xmlNodeEndTag) { + this.depth--; + } + + @Override + public void onCData(@NonNull final XmlCData xmlCData) { + } + + @Override + public void onNamespaceStart(@NonNull final XmlNamespaceStartTag tag) { + } + + @Override + public void onNamespaceEnd(@NonNull final XmlNamespaceEndTag tag) { + } + + @NonNull + public ApkMeta getApkMeta() { + return this.apkMetaBuilder.build(); + } + + @NonNull + public List getIconPaths() { + return this.iconPaths; + } + + private boolean matchTagPath(final String... tags) { + // the root should always be "manifest" + if (this.depth != tags.length + 1) { + return false; + } + for (int i = 1; i < this.depth; i++) { + if (!this.tagStack[i].equals(tags[i - 1])) { + return false; + } + } + return true; + } + + private boolean matchLastTag(final String tag) { + // the root should always be "manifest" + return this.tagStack[this.depth - 1].endsWith(tag); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.kt new file mode 100644 index 00000000..e8bf5213 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.kt @@ -0,0 +1,134 @@ +package net.dongliu.apk.parser.parser + +import net.dongliu.apk.parser.struct.signingv2.* +import net.dongliu.apk.parser.utils.* +import java.io.ByteArrayInputStream +import java.nio.* +import java.security.cert.* + +/** + * The Apk Sign Block V2 Parser. + * see https://source.android.com/security/apksigning/v2 + */ +class ApkSignBlockParser(data: ByteBuffer) { + private val data: ByteBuffer + + init { + this.data = data.order(ByteOrder.LITTLE_ENDIAN) + } + + @Throws(CertificateException::class) + fun parse(): ApkSigningBlock { + // sign block found, read pairs + val signerBlocks: MutableList = ArrayList() + while (data.remaining() >= 8) { + val id = data.int + val size = Unsigned.ensureUInt(data.int.toLong()) + if (id == ApkSigningBlock.SIGNING_V2_ID) { + val signingV2Buffer = Buffers.sliceAndSkip(data, size) + // now only care about apk signing v2 entry + while (signingV2Buffer.hasRemaining()) { + val signerBlock = readSigningV2(signingV2Buffer) + signerBlocks.add(signerBlock) + } + } else { + // just ignore now + Buffers.position(data, data.position() + size) + } + } + return ApkSigningBlock(signerBlocks) + } + + @Throws(CertificateException::class) + private fun readSigningV2(inputBuffer: ByteBuffer): SignerBlock { + val buffer = readLenPrefixData(inputBuffer) + val signedData = readLenPrefixData(buffer) + val digestsData = readLenPrefixData(signedData) + val digests = readDigests(digestsData) + val certificateData = readLenPrefixData(signedData) + val certificates = readCertificates(certificateData) + val attributesData = readLenPrefixData(signedData) + readAttributes(attributesData) + val signaturesData = readLenPrefixData(buffer) + val signatures = readSignatures(signaturesData) + val publicKeyData = readLenPrefixData(buffer) + return SignerBlock(digests, certificates, signatures) + } + + private fun readDigests(buffer: ByteBuffer): List { + val list: MutableList = ArrayList() + while (buffer.hasRemaining()) { + val digestData = readLenPrefixData(buffer) + val algorithmID = digestData.int + val digest = Buffers.readBytes(digestData) + list.add(Digest(algorithmID, digest)) + } + return list + } + + @Throws(CertificateException::class) + private fun readCertificates(buffer: ByteBuffer): List { + val certificateFactory = CertificateFactory.getInstance("X.509") + val certificates: MutableList = ArrayList() + while (buffer.hasRemaining()) { + val certificateData = readLenPrefixData(buffer) + val certificate = certificateFactory.generateCertificate( + ByteArrayInputStream(Buffers.readBytes(certificateData)) + ) + certificates.add(certificate as X509Certificate) + } + return certificates + } + + private fun readAttributes(buffer: ByteBuffer) { + while (buffer.hasRemaining()) { + val attributeData = readLenPrefixData(buffer) + val id = attributeData.int + // byte[] value = Buffers.readBytes(attributeData); + } + } + + private fun readSignatures(buffer: ByteBuffer): List { + val signatures: MutableList = ArrayList() + while (buffer.hasRemaining()) { + val signatureData = readLenPrefixData(buffer) + val algorithmID = signatureData.int + val signatureDataLen = Unsigned.ensureUInt(signatureData.int.toLong()) + val signature = Buffers.readBytes(signatureData, signatureDataLen) + signatures.add(Signature(algorithmID, signature)) + } + return signatures + } + + private fun readLenPrefixData(buffer: ByteBuffer): ByteBuffer { + val len = Unsigned.ensureUInt(buffer.int.toLong()) + return Buffers.sliceAndSkip(buffer, len) + } // /** + // * 0x0101—RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc + // */ + // private static final int PSS_SHA_256 = 0x0101; + // /** + // * 0x0102—RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc + // */ + // private static final int PSS_SHA_512 = 0x0102; + // /** + // * 0x0103—RSASSA-PKCS1-v1_5 with SHA2-256 digest. This is for build systems which require deterministic signatures. + // */ + // private static final int PKCS1_SHA_256 = 0x0103; + // /** + // * 0x0104—RSASSA-PKCS1-v1_5 with SHA2-512 digest. This is for build systems which require deterministic signatures. + // */ + // private static final int PKCS1_SHA_512 = 0x0104; + // /** + // * 0x0201—ECDSA with SHA2-256 digest + // */ + // private static final int ECDSA_SHA_256 = 0x0201; + // /** + // * 0x0202—ECDSA with SHA2-512 digest + // */ + // private static final int ECDSA_SHA_512 = 0x0202; + // /** + // * 0x0301—DSA with SHA2-256 digest + // */ + // private static final int DSA_SHA_256 = 0x0301; +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.kt new file mode 100644 index 00000000..2c4137c6 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.kt @@ -0,0 +1,150 @@ +package net.dongliu.apk.parser.parser + +import net.dongliu.apk.parser.utils.Strings.join + +/** + * attribute value constant + * + * @author Liu Dong + */ +object AttributeValues { + /** + * Activity constants begin. see: + * http://developer.android.com/reference/android/content/pm/ActivityInfo.html + * http://developer.android.com/guide/topics/manifest/activity-element.html + */ + @JvmStatic + fun getScreenOrientation(value: Int): String { + return when (value) { + 0x00000003 -> "behind" + 0x0000000a -> "fullSensor" + 0x0000000d -> "fullUser" + 0x00000000 -> "landscape" + 0x0000000e -> "locked" + 0x00000005 -> "nosensor" + 0x00000001 -> "portrait" + 0x00000008 -> "reverseLandscape" + 0x00000009 -> "reversePortrait" + 0x00000004 -> "sensor" + 0x00000006 -> "sensorLandscape" + 0x00000007 -> "sensorPortrait" + -0x1 -> "unspecified" + 0x00000002 -> "user" + 0x0000000b -> "userLandscape" + 0x0000000c -> "userPortrait" + else -> "ScreenOrientation:" + Integer.toHexString(value) + } + } + + @JvmStatic + fun getLaunchMode(value: Int): String { + return when (value) { + 0x00000000 -> "standard" + 0x00000001 -> "singleTop" + 0x00000002 -> "singleTask" + 0x00000003 -> "singleInstance" + else -> "LaunchMode:" + Integer.toHexString(value) + } + } + + @JvmStatic + fun getConfigChanges(value: Int): String? { + val list: MutableList = ArrayList() + if (value and 0x00001000 != 0) { + list.add("density") + } else if (value and 0x40000000 != 0) { + list.add("fontScale") + } else if (value and 0x00000010 != 0) { + list.add("keyboard") + } else if (value and 0x00000020 != 0) { + list.add("keyboardHidden") + } else if (value and 0x00002000 != 0) { + list.add("direction") + } else if (value and 0x00000004 != 0) { + list.add("locale") + } else if (value and 0x00000001 != 0) { + list.add("mcc") + } else if (value and 0x00000002 != 0) { + list.add("mnc") + } else if (value and 0x00000040 != 0) { + list.add("navigation") + } else if (value and 0x00000080 != 0) { + list.add("orientation") + } else if (value and 0x00000100 != 0) { + list.add("screenLayout") + } else if (value and 0x00000400 != 0) { + list.add("screenSize") + } else if (value and 0x00000800 != 0) { + list.add("smallestScreenSize") + } else if (value and 0x00000008 != 0) { + list.add("touchscreen") + } else if (value and 0x00000200 != 0) { + list.add("uiMode") + } + return join(list, "|") + } + + @JvmStatic + fun getWindowSoftInputMode(value: Int): String? { + val adjust = value and 0x000000f0 + val state = value and 0x0000000f + val list: MutableList = ArrayList(2) + when (adjust) { + 0x00000030 -> list.add("adjustNothing") + 0x00000020 -> list.add("adjustPan") + 0x00000010 -> list.add("adjustResize") + 0x00000000 -> {} + else -> list.add("WindowInputModeAdjust:" + Integer.toHexString(adjust)) + } + when (state) { + 0x00000003 -> list.add("stateAlwaysHidden") + 0x00000005 -> list.add("stateAlwaysVisible") + 0x00000002 -> list.add("stateHidden") + 0x00000001 -> list.add("stateUnchanged") + 0x00000004 -> list.add("stateVisible") + 0x00000000 -> {} + else -> list.add("WindowInputModeState:" + Integer.toHexString(state)) + } + return join(list, "|") + //isForwardNavigation(0x00000100), + //mode_changed(0x00000200), + } + + /** + * http://developer.android.com/reference/android/content/pm/PermissionInfo.html + */ + @JvmStatic + fun getProtectionLevel(inputValue: Int): String? { + var value = inputValue + val levels: MutableList = ArrayList(3) + if (value and 0x10 != 0) { + value = value xor 0x10 + levels.add("system") + } + if (value and 0x20 != 0) { + value = value xor 0x20 + levels.add("development") + } + when (value) { + 0 -> levels.add("normal") + 1 -> levels.add("dangerous") + 2 -> levels.add("signature") + 3 -> levels.add("signatureOrSystem") + else -> levels.add("ProtectionLevel:" + Integer.toHexString(value)) + } + return join(levels, "|") + } + // Activity constants end + /** + * get Installation string values from int + */ + @JvmStatic + fun getInstallLocation(value: Int): String { + return when (value) { + 0 -> "auto" + 1 -> "internalOnly" + 2 -> "preferExternal" + else -> "installLocation:" + Integer.toHexString(value) + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java new file mode 100644 index 00000000..5f081548 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java @@ -0,0 +1,65 @@ +package net.dongliu.apk.parser.parser; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.bean.CertificateMeta; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerId; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.Store; + +import java.security.Provider; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Parser certificate info using BouncyCastle. + * + * @author dongliu + */ +class BCCertificateParser extends CertificateParser { + + private static final Provider provider = new BouncyCastleProvider(); + + public BCCertificateParser(@NonNull final byte[] data) { + super(data); + } + + /** + * get certificate info + */ + @Override + @SuppressWarnings("unchecked") + @NonNull + public List parse() throws CertificateException { + final CMSSignedData cmsSignedData; + try { + cmsSignedData = new CMSSignedData(this.data); + } catch (final CMSException e) { + throw new CertificateException(e); + } + final Store certStore = cmsSignedData.getCertificates(); + final SignerInformationStore signerInfos = cmsSignedData.getSignerInfos(); + final Collection signers = signerInfos.getSigners(); + final List certificates = new ArrayList<>(); + for (final SignerInformation signer : signers) { + final SignerId sid = signer.getSID(); + final Collection matches = certStore.getMatches(sid); + for (final X509CertificateHolder holder : matches) { + certificates.add(new JcaX509CertificateConverter().setProvider(BCCertificateParser.provider) + .getCertificate(holder)); + } + } + return CertificateMetas.from(certificates); + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java new file mode 100644 index 00000000..2bf21b62 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java @@ -0,0 +1,317 @@ +package net.dongliu.apk.parser.parser; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.exception.ParserException; +import net.dongliu.apk.parser.struct.ChunkHeader; +import net.dongliu.apk.parser.struct.ChunkType; +import net.dongliu.apk.parser.struct.ResourceValue; +import net.dongliu.apk.parser.struct.StringPool; +import net.dongliu.apk.parser.struct.StringPoolHeader; +import net.dongliu.apk.parser.struct.resource.ResourceTable; +import net.dongliu.apk.parser.struct.xml.Attribute; +import net.dongliu.apk.parser.struct.xml.Attributes; +import net.dongliu.apk.parser.struct.xml.NullHeader; +import net.dongliu.apk.parser.struct.xml.XmlCData; +import net.dongliu.apk.parser.struct.xml.XmlHeader; +import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag; +import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag; +import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag; +import net.dongliu.apk.parser.struct.xml.XmlNodeHeader; +import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag; +import net.dongliu.apk.parser.struct.xml.XmlResourceMapHeader; +import net.dongliu.apk.parser.utils.Buffers; +import net.dongliu.apk.parser.utils.Locales; +import net.dongliu.apk.parser.utils.ParseUtils; +import net.dongliu.apk.parser.utils.Strings; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** + * Android Binary XML format + * see http://justanapplication.wordpress.com/category/android/android-binary-xml/ + * + * @author dongliu + */ +public class BinaryXmlParser { + + /** + * By default the data buffer Chunks is buffer little-endian byte order both at runtime and when stored buffer + * files. + */ + private StringPool stringPool; + /** + * some attribute name stored by resource id + */ + private String[] resourceMap; + @NonNull + private final ByteBuffer buffer; + @NonNull + private final XmlStreamer xmlStreamer; + @NonNull + private final ResourceTable resourceTable; + /** + * default locale. + */ + @NonNull + private final Locale locale; + + public BinaryXmlParser(final @NonNull ByteBuffer buffer, final @NonNull ResourceTable resourceTable, final @NonNull XmlStreamer xmlStreamer + , final @Nullable Locale locale) { + this.buffer = buffer.duplicate(); + this.buffer.order(ByteOrder.LITTLE_ENDIAN); + this.resourceTable = resourceTable; + this.xmlStreamer = xmlStreamer; + this.locale = locale == null ? Locales.any : locale; + } + + /** + * Parse binary xml. + */ + public void parse() { + final ChunkHeader firstChunkHeader = this.readChunkHeader(); + if (firstChunkHeader == null) { + return; + } + switch ((int) firstChunkHeader.chunkType) { + case ChunkType.XML: + case ChunkType.NULL: + break; + case ChunkType.STRING_POOL: + default: + // strange chunk header type, just skip this chunk header? + } + // read string pool chunk + final ChunkHeader stringPoolChunkHeader = this.readChunkHeader(); + if (stringPoolChunkHeader == null) { + return; + } + ParseUtils.checkChunkType(ChunkType.STRING_POOL, stringPoolChunkHeader.chunkType); + this.stringPool = ParseUtils.readStringPool(this.buffer, (StringPoolHeader) stringPoolChunkHeader); + // read on chunk, check if it was an optional XMLResourceMap chunk + ChunkHeader chunkHeader = this.readChunkHeader(); + if (chunkHeader == null) { + return; + } + if ((int) chunkHeader.chunkType == ChunkType.XML_RESOURCE_MAP) { + final long[] resourceIds = this.readXmlResourceMap((XmlResourceMapHeader) chunkHeader); + this.resourceMap = new String[resourceIds.length]; + for (int i = 0; i < resourceIds.length; i++) { + this.resourceMap[i] = Attribute.getString(resourceIds[i]); + } + chunkHeader = this.readChunkHeader(); + } + while (chunkHeader != null) { + final long beginPos = this.buffer.position(); + switch ((int) chunkHeader.chunkType) { + case ChunkType.XML_END_NAMESPACE: + final XmlNamespaceEndTag xmlNamespaceEndTag = this.readXmlNamespaceEndTag(); + this.xmlStreamer.onNamespaceEnd(xmlNamespaceEndTag); + break; + case ChunkType.XML_START_NAMESPACE: + final XmlNamespaceStartTag namespaceStartTag = this.readXmlNamespaceStartTag(); + this.xmlStreamer.onNamespaceStart(namespaceStartTag); + break; + case ChunkType.XML_START_ELEMENT: + final XmlNodeStartTag xmlNodeStartTag = this.readXmlNodeStartTag(); + break; + case ChunkType.XML_END_ELEMENT: + final XmlNodeEndTag xmlNodeEndTag = this.readXmlNodeEndTag(); + break; + case ChunkType.XML_CDATA: + final XmlCData xmlCData = this.readXmlCData(); + break; + default: + if ((int) chunkHeader.chunkType >= ChunkType.XML_FIRST_CHUNK && + (int) chunkHeader.chunkType <= ChunkType.XML_LAST_CHUNK) { + Buffers.skip(this.buffer, chunkHeader.getBodySize()); + } else { + throw new ParserException("Unexpected chunk type:" + (int) chunkHeader.chunkType); + } + } + Buffers.position(this.buffer, beginPos + chunkHeader.getBodySize()); + chunkHeader = this.readChunkHeader(); + } + } + + private XmlCData readXmlCData() { + final XmlCData xmlCData = new XmlCData(); + final int dataRef = this.buffer.getInt(); + if (dataRef > 0) { + xmlCData.setData(this.stringPool.get(dataRef)); + } + xmlCData.setTypedData(ParseUtils.readResValue(this.buffer, this.stringPool)); + //TODO: to know more about cdata. some cdata appears buffer xml tags +// String value = xmlCData.toStringValue(resourceTable, locale); +// xmlCData.setValue(value); +// xmlStreamer.onCData(xmlCData); + return xmlCData; + } + + private XmlNodeEndTag readXmlNodeEndTag() { + final XmlNodeEndTag xmlNodeEndTag = new XmlNodeEndTag(); + final int nsRef = this.buffer.getInt(); + final int nameRef = this.buffer.getInt(); + if (nsRef > 0) { + xmlNodeEndTag.setNamespace(this.stringPool.get(nsRef)); + } + xmlNodeEndTag.setName(this.stringPool.get(nameRef)); + this.xmlStreamer.onEndTag(xmlNodeEndTag); + return xmlNodeEndTag; + } + + private XmlNodeStartTag readXmlNodeStartTag() { + final int nsRef = this.buffer.getInt(); + final int nameRef = this.buffer.getInt(); + final String namespace = nsRef > 0 ? this.stringPool.get(nsRef) : null; + final String name = this.stringPool.get(nameRef); + // read attributes. + // attributeStart and attributeSize are always 20 (0x14) + final int attributeStart = Buffers.readUShort(this.buffer); + final int attributeSize = Buffers.readUShort(this.buffer); + final int attributeCount = Buffers.readUShort(this.buffer); + final int idIndex = Buffers.readUShort(this.buffer); + final int classIndex = Buffers.readUShort(this.buffer); + final int styleIndex = Buffers.readUShort(this.buffer); + // read attributes + final Attributes attributes = new Attributes(attributeCount); + for (int count = 0; count < attributeCount; count++) { + final Attribute attribute = this.readAttribute(); + final String attributeName = attribute.name; + String value = attribute.toStringValue(this.resourceTable, this.locale); + if (value != null && BinaryXmlParser.intAttributes.contains(attributeName) && Strings.isNumeric(value)) { + try { + value = this.getFinalValueAsString(attributeName, value); + } catch (final Exception ignore) { + } + } + attribute.value = value; + attributes.set(count, attribute); + } + final XmlNodeStartTag xmlNodeStartTag = new XmlNodeStartTag(namespace, name, attributes); + this.xmlStreamer.onStartTag(xmlNodeStartTag); + return xmlNodeStartTag; + } + + private static final Set intAttributes = new HashSet<>( + Arrays.asList("screenOrientation", "configChanges", "windowSoftInputMode", + "launchMode", "installLocation", "protectionLevel")); + + /** + * trans int attr value to string + */ + private String getFinalValueAsString(final String attributeName, @NonNull final String str) { + final int value = Integer.parseInt(str); + switch (attributeName) { + case "screenOrientation": + return AttributeValues.getScreenOrientation(value); + case "configChanges": + return AttributeValues.getConfigChanges(value); + case "windowSoftInputMode": + return AttributeValues.getWindowSoftInputMode(value); + case "launchMode": + return AttributeValues.getLaunchMode(value); + case "installLocation": + return AttributeValues.getInstallLocation(value); + case "protectionLevel": + return AttributeValues.getProtectionLevel(value); + default: + return str; + } + } + + private Attribute readAttribute() { + final int namespaceRef = this.buffer.getInt(); + final int nameRef = this.buffer.getInt(); + String name = this.stringPool.get(nameRef); + if (name.isEmpty() && this.resourceMap != null && nameRef < this.resourceMap.length) { + // some processed apk file make the string pool value empty, if it is a xmlmap attr. + name = this.resourceMap[nameRef]; + } + String namespace = namespaceRef > 0 ? this.stringPool.get(namespaceRef) : null; + if (namespace == null || namespace.isEmpty() || "http://schemas.android.com/apk/res/android".equals(namespace)) { + //TODO parse namespaces better + //workaround for a weird case that there is no namespace found: https://github.com/hsiafan/apk-parser/issues/122 + // Log.d("AppLog", "Got a weird namespace, so setting as empty (namespace isn't supposed to be a URL): " + attribute.getName()); + namespace = "android"; + } + final int rawValueRef = this.buffer.getInt(); + final String rawValue = rawValueRef > 0 ? this.stringPool.get(rawValueRef) : null; + final ResourceValue resValue = ParseUtils.readResValue(this.buffer, this.stringPool); + return new Attribute(namespace, name, rawValue, resValue); + } + + @NonNull + private XmlNamespaceStartTag readXmlNamespaceStartTag() { + final int prefixRef = this.buffer.getInt(); + final int uriRef = this.buffer.getInt(); + final String prefix = prefixRef > 0 ? this.stringPool.get(prefixRef) : null; + final String uri = uriRef > 0 ? this.stringPool.get(uriRef) : null; + return new XmlNamespaceStartTag(prefix, uri); + } + + @NonNull + private XmlNamespaceEndTag readXmlNamespaceEndTag() { + final int prefixRef = this.buffer.getInt(); + final String prefix = prefixRef <= 0 ? null : this.stringPool.get(prefixRef); + final int uriRef = this.buffer.getInt(); + final String uri = uriRef <= 0 ? null : this.stringPool.get(uriRef); + return new XmlNamespaceEndTag(prefix, uri); + } + + private long[] readXmlResourceMap(final XmlResourceMapHeader chunkHeader) { + final int count = chunkHeader.getBodySize() / 4; + final long[] resourceIds = new long[count]; + for (int i = 0; i < count; i++) { + resourceIds[i] = Buffers.readUInt(this.buffer); + } + return resourceIds; + } + + @Nullable + private ChunkHeader readChunkHeader() { + // finished + if (!this.buffer.hasRemaining()) { + return null; + } + final long begin = this.buffer.position(); + final int chunkType = Buffers.readUShort(this.buffer); + final int headerSize = Buffers.readUShort(this.buffer); + final long chunkSize = Buffers.readUInt(this.buffer); + switch (chunkType) { + case ChunkType.XML: + return new XmlHeader(chunkType, headerSize, chunkSize); + case ChunkType.STRING_POOL: + final StringPoolHeader stringPoolHeader = new StringPoolHeader(headerSize, chunkSize, this.buffer); + Buffers.position(this.buffer, begin + headerSize); + return stringPoolHeader; + case ChunkType.XML_RESOURCE_MAP: + Buffers.position(this.buffer, begin + headerSize); + return new XmlResourceMapHeader(chunkType, headerSize, chunkSize); + case ChunkType.XML_START_NAMESPACE: + case ChunkType.XML_END_NAMESPACE: + case ChunkType.XML_START_ELEMENT: + case ChunkType.XML_END_ELEMENT: + case ChunkType.XML_CDATA: + final XmlNodeHeader header = new XmlNodeHeader(chunkType, headerSize, chunkSize, this.buffer); + Buffers.position(this.buffer, begin + headerSize); + return header; + case ChunkType.NULL: + return new NullHeader(chunkType, headerSize, chunkSize); + default: + throw new ParserException("Unexpected chunk type:" + chunkType); + } + } + + @NonNull + public Locale getLocale() { + return this.locale; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.kt new file mode 100644 index 00000000..6a87046f --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.kt @@ -0,0 +1,73 @@ +package net.dongliu.apk.parser.parser + +import net.dongliu.apk.parser.bean.CertificateMeta +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.* +import java.security.cert.* +import java.util.* + +object CertificateMetas { + @Throws(CertificateEncodingException::class) + @JvmStatic + fun from(certificates: List): List { + val certificateMetas: MutableList = ArrayList(certificates.size) + for (certificate in certificates) { + val certificateMeta = from(certificate) + certificateMetas.add(certificateMeta) + } + return certificateMetas + } + + @Throws(CertificateEncodingException::class) + fun from(certificate: X509Certificate): CertificateMeta { + val bytes = certificate.encoded + val certMd5 = md5Digest(bytes) + val publicKeyString = byteToHexString(bytes) + val certBase64Md5 = md5Digest(publicKeyString) + return CertificateMeta( + certificate.sigAlgName.uppercase(Locale.getDefault()), + certificate.sigAlgOID, + certificate.notBefore, + certificate.notAfter, + bytes, certBase64Md5, certMd5 + ) + } + + private fun md5Digest(input: ByteArray): String { + val digest = getDigest("md5") + digest.update(input) + return getHexString(digest.digest()) + } + + private fun md5Digest(input: String): String { + val digest = getDigest("md5") + digest.update(input.toByteArray(StandardCharsets.UTF_8)) + return getHexString(digest.digest()) + } + + private fun byteToHexString(bArray: ByteArray): String { + val sb = StringBuilder(bArray.size) + for (aBArray in bArray) { + val sTemp = Integer.toHexString(0xFF and Char(aBArray.toUShort()).code) + if (sTemp.length < 2) { + sb.append(0) + } + sb.append(sTemp.uppercase(Locale.getDefault())) + } + return sb.toString() + } + + private fun getHexString(digest: ByteArray): String { + val bi = BigInteger(1, digest) + return String.format("%032x", bi) + } + + private fun getDigest(algorithm: String): MessageDigest { + return try { + MessageDigest.getInstance(algorithm) + } catch (e: NoSuchAlgorithmException) { + throw RuntimeException(e.message) + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.kt new file mode 100644 index 00000000..4fdfd68c --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.kt @@ -0,0 +1,28 @@ +package net.dongliu.apk.parser.parser + +import net.dongliu.apk.parser.ApkParsers +import net.dongliu.apk.parser.bean.CertificateMeta +import java.security.cert.CertificateException + +/** + * Parser certificate info. + * One apk may have multi certificates(certificate chain). + * + * @author dongliu + */ +abstract class CertificateParser(@JvmField protected val data: ByteArray) { + /** + * get certificate info + */ + @Throws(CertificateException::class) + abstract fun parse(): List + + companion object { + @JvmStatic + fun getInstance(data: ByteArray): CertificateParser { + return if (ApkParsers.useBouncyCastle()) { + BCCertificateParser(data) + } else JSSECertificateParser(data) + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.kt new file mode 100644 index 00000000..c81c9628 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.kt @@ -0,0 +1,41 @@ +package net.dongliu.apk.parser.parser + +import net.dongliu.apk.parser.struct.xml.* + +/** + * @author dongliu + */ +class CompositeXmlStreamer(vararg xmlStreamers: XmlStreamer) : XmlStreamer { + @Suppress("UNCHECKED_CAST") + private val xmlStreamers: Array = xmlStreamers as Array + + override fun onStartTag(xmlNodeStartTag: XmlNodeStartTag) { + for (xmlStreamer in xmlStreamers) { + xmlStreamer.onStartTag(xmlNodeStartTag) + } + } + + override fun onEndTag(xmlNodeEndTag: XmlNodeEndTag) { + for (xmlStreamer in xmlStreamers) { + xmlStreamer.onEndTag(xmlNodeEndTag) + } + } + + override fun onCData(xmlCData: XmlCData) { + for (xmlStreamer in xmlStreamers) { + xmlStreamer.onCData(xmlCData) + } + } + + override fun onNamespaceStart(tag: XmlNamespaceStartTag) { + for (xmlStreamer in xmlStreamers) { + xmlStreamer.onNamespaceStart(tag) + } + } + + override fun onNamespaceEnd(tag: XmlNamespaceEndTag) { + for (xmlStreamer in xmlStreamers) { + xmlStreamer.onNamespaceEnd(tag) + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/DexParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/DexParser.java new file mode 100644 index 00000000..9960354a --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/DexParser.java @@ -0,0 +1,248 @@ +package net.dongliu.apk.parser.parser; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.bean.DexClass; +import net.dongliu.apk.parser.exception.ParserException; +import net.dongliu.apk.parser.struct.StringPool; +import net.dongliu.apk.parser.struct.dex.DexClassStruct; +import net.dongliu.apk.parser.struct.dex.DexHeader; +import net.dongliu.apk.parser.utils.Buffers; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * parse dex file. + * current we only get the class name. + * see: + * http://source.android.com/devices/tech/dalvik/dex-format.html + * http://dexandroid.googlecode.com/svn/trunk/dalvik/libdex/DexFile.h + * + * @author dongliu + */ +public class DexParser { + + private final ByteBuffer buffer; + + private static final int NO_INDEX = 0xffffffff; + + public DexParser(final @NonNull ByteBuffer buffer) { + this.buffer = buffer.duplicate(); + this.buffer.order(ByteOrder.LITTLE_ENDIAN); + } + + @NonNull + public DexClass[] parse() { + // read magic + final String magic = new String(Buffers.readBytes(this.buffer, 8)); + if (!magic.startsWith("dex\n")) { + return new DexClass[0]; + } + final int version = Integer.parseInt(magic.substring(4, 7)); + // now the version is 035 + if (version < 35) { + // version 009 was used for the M3 releases of the Android platform (November–December 2007), + // and version 013 was used for the M5 releases of the Android platform (February–March 2008) + throw new ParserException("Dex file version: " + version + " is not supported"); + } + // read header + final DexHeader header = this.readDexHeader(); + header.setVersion(version); + // read string pool + final long[] stringOffsets = this.readStringPool(header.getStringIdsOff(), header.getStringIdsSize()); + // read types + final int[] typeIds = this.readTypes(header.getTypeIdsOff(), header.getTypeIdsSize()); + // read classes + final DexClassStruct[] dexClassStructs = this.readClass(header.getClassDefsOff(), + header.getClassDefsSize()); + final StringPool stringpool = this.readStrings(stringOffsets); + final String[] types = new String[typeIds.length]; + for (int i = 0; i < typeIds.length; i++) { + types[i] = stringpool.get(typeIds[i]); + } + final DexClass[] dexClasses = new DexClass[dexClassStructs.length]; + for (int i = 0; i < dexClassStructs.length; i++) { + final DexClassStruct dexClassStruct = dexClassStructs[i]; + String superClass = null; + if (dexClassStruct.getSuperclassIdx() != DexParser.NO_INDEX) { + superClass = types[dexClassStruct.getSuperclassIdx()]; + } + dexClasses[i] = new DexClass( + types[dexClassStruct.getClassIdx()], + superClass, + dexClassStruct.getAccessFlags()); + } + return dexClasses; + } + + /** + * read class info. + */ + private DexClassStruct[] readClass(final long classDefsOff, final int classDefsSize) { + Buffers.position(this.buffer, classDefsOff); + final DexClassStruct[] dexClassStructs = new DexClassStruct[classDefsSize]; + for (int i = 0; i < classDefsSize; i++) { + final DexClassStruct dexClassStruct = new DexClassStruct(); + dexClassStruct.setClassIdx(this.buffer.getInt()); + dexClassStruct.setAccessFlags(this.buffer.getInt()); + dexClassStruct.setSuperclassIdx(this.buffer.getInt()); + dexClassStruct.setInterfacesOff(Buffers.readUInt(this.buffer)); + dexClassStruct.setSourceFileIdx(this.buffer.getInt()); + dexClassStruct.setAnnotationsOff(Buffers.readUInt(this.buffer)); + dexClassStruct.setClassDataOff(Buffers.readUInt(this.buffer)); + dexClassStruct.setStaticValuesOff(Buffers.readUInt(this.buffer)); + dexClassStructs[i] = dexClassStruct; + } + return dexClassStructs; + } + + /** + * read types. + */ + private int[] readTypes(final long typeIdsOff, final int typeIdsSize) { + Buffers.position(this.buffer, typeIdsOff); + final int[] typeIds = new int[typeIdsSize]; + for (int i = 0; i < typeIdsSize; i++) { + typeIds[i] = (int) Buffers.readUInt(this.buffer); + } + return typeIds; + } + + /** + * read string pool for dex file. + * dex file string pool diff a bit with binary xml file or resource table. + */ + private StringPool readStrings(final long[] offsets) { + // read strings. + // buffer some apk, the strings' offsets may not well ordered. we sort it first + final StringPoolEntry[] entries = new StringPoolEntry[offsets.length]; + for (int i = 0; i < offsets.length; i++) { + entries[i] = new StringPoolEntry(i, offsets[i]); + } + String lastStr = null; + long lastOffset = -1; + final StringPool stringpool = new StringPool(offsets.length); + for (final StringPoolEntry entry : entries) { + if (entry.offset == lastOffset) { + stringpool.set(entry.idx, lastStr); + continue; + } + Buffers.position(this.buffer, entry.offset); + lastOffset = entry.offset; + final String str = this.readString(); + lastStr = str; + stringpool.set(entry.idx, str); + } + return stringpool; + } + + /* + * read string identifiers list. + */ + private long[] readStringPool(final long stringIdsOff, final int stringIdsSize) { + Buffers.position(this.buffer, stringIdsOff); + final long[] offsets = new long[stringIdsSize]; + for (int i = 0; i < stringIdsSize; i++) { + offsets[i] = Buffers.readUInt(this.buffer); + } + return offsets; + } + + /** + * read dex encoding string. + */ + @NonNull + private String readString() { + // the length is char len, not byte len + final int strLen = this.readVarInts(); + return this.readString(strLen); + } + + /** + * read Modified UTF-8 encoding str. + * + * @param strLen the java-utf16-char len, not strLen nor bytes len. + */ + @NonNull + private String readString(final int strLen) { + final char[] chars = new char[strLen]; + for (int i = 0; i < strLen; i++) { + final short a = Buffers.readUByte(this.buffer); + if ((a & 0x80) == 0) { + // ascii char + chars[i] = (char) a; + } else if ((a & 0xe0) == 0xc0) { + // read one more + final short b = Buffers.readUByte(this.buffer); + chars[i] = (char) (((a & 0x1F) << 6) | (b & 0x3F)); + } else if ((a & 0xf0) == 0xe0) { + final short b = Buffers.readUByte(this.buffer); + final short c = Buffers.readUByte(this.buffer); + chars[i] = (char) (((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F)); + } else //noinspection StatementWithEmptyBody + if ((a & 0xf0) == 0xf0) { + //throw new UTFDataFormatException(); + } else { + //throw new UTFDataFormatException(); + } + //noinspection StatementWithEmptyBody + if (chars[i] == 0) { + // the end of string. + } + } + return new String(chars); + } + + /** + * read varints. + */ + private int readVarInts() { + int i = 0; + int count = 0; + short s; + do { + if (count > 4) { + throw new ParserException("read varints error."); + } + s = Buffers.readUByte(this.buffer); + i |= (s & 0x7f) << (count * 7); + count++; + } while ((s & 0x80) != 0); + return i; + } + + private DexHeader readDexHeader() { + // check sum. skip + this.buffer.getInt(); + // signature skip + Buffers.readBytes(this.buffer, DexHeader.kSHA1DigestLen); + final DexHeader header = new DexHeader(); + header.setFileSize(Buffers.readUInt(this.buffer)); + header.setHeaderSize(Buffers.readUInt(this.buffer)); + // skip? + Buffers.readUInt(this.buffer); + // static link data + header.setLinkSize(Buffers.readUInt(this.buffer)); + header.setLinkOff(Buffers.readUInt(this.buffer)); + // the map data is just the same as dex header. + header.setMapOff(Buffers.readUInt(this.buffer)); + header.setStringIdsSize(this.buffer.getInt()); + header.setStringIdsOff(Buffers.readUInt(this.buffer)); + header.setTypeIdsSize(this.buffer.getInt()); + header.setTypeIdsOff(Buffers.readUInt(this.buffer)); + header.setProtoIdsSize(this.buffer.getInt()); + header.setProtoIdsOff(Buffers.readUInt(this.buffer)); + header.setFieldIdsSize(this.buffer.getInt()); + header.setFieldIdsOff(Buffers.readUInt(this.buffer)); + header.setMethodIdsSize(this.buffer.getInt()); + header.setMethodIdsOff(Buffers.readUInt(this.buffer)); + header.setClassDefsSize(this.buffer.getInt()); + header.setClassDefsOff(Buffers.readUInt(this.buffer)); + header.setDataSize(this.buffer.getInt()); + header.setDataOff(Buffers.readUInt(this.buffer)); + Buffers.position(this.buffer, header.getHeaderSize()); + return header; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.kt new file mode 100644 index 00000000..aa0790f5 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.kt @@ -0,0 +1,44 @@ +package net.dongliu.apk.parser.parser + +import net.dongliu.apk.parser.bean.CertificateMeta +import net.dongliu.apk.parser.cert.asn1.* +import net.dongliu.apk.parser.cert.pkcs7.* +import net.dongliu.apk.parser.parser.CertificateMetas.from +import net.dongliu.apk.parser.utils.Buffers +import java.io.ByteArrayInputStream +import java.nio.ByteBuffer +import java.security.cert.* + +/** + * Parser certificate info using jsse. + * + * @author dongliu + */ +internal class JSSECertificateParser(data: ByteArray) : CertificateParser(data) { + @Throws(CertificateException::class) + override fun parse(): List { + val contentInfo: ContentInfo = try { + Asn1BerParser.parse(ByteBuffer.wrap(data), ContentInfo::class.java) + } catch (e: Asn1DecodingException) { + throw CertificateException(e) + } + if (Pkcs7Constants.OID_SIGNED_DATA != contentInfo.contentType) { + throw CertificateException("Unsupported ContentInfo.contentType: " + contentInfo.contentType) + } + val signedData: SignedData = try { + Asn1BerParser.parse(contentInfo.content.encoded, SignedData::class.java) + } catch (e: Asn1DecodingException) { + throw CertificateException(e) + } + val encodedCertificates = signedData.certificates + val certFactory = CertificateFactory.getInstance("X.509") + val result: MutableList = ArrayList(encodedCertificates.size) + for (i in encodedCertificates.indices) { + val encodedCertificate = encodedCertificates[i] + val encodedForm = Buffers.readBytes(encodedCertificate.encoded) + val certificate = certFactory.generateCertificate(ByteArrayInputStream(encodedForm)) + result.add(certificate as X509Certificate) + } + return from(result) + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java new file mode 100644 index 00000000..31c22378 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java @@ -0,0 +1,230 @@ +package net.dongliu.apk.parser.parser; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.exception.ParserException; +import net.dongliu.apk.parser.struct.ChunkHeader; +import net.dongliu.apk.parser.struct.ChunkType; +import net.dongliu.apk.parser.struct.StringPool; +import net.dongliu.apk.parser.struct.StringPoolHeader; +import net.dongliu.apk.parser.struct.resource.LibraryEntry; +import net.dongliu.apk.parser.struct.resource.LibraryHeader; +import net.dongliu.apk.parser.struct.resource.NullHeader; +import net.dongliu.apk.parser.struct.resource.PackageHeader; +import net.dongliu.apk.parser.struct.resource.ResourcePackage; +import net.dongliu.apk.parser.struct.resource.ResourceTable; +import net.dongliu.apk.parser.struct.resource.ResourceTableHeader; +import net.dongliu.apk.parser.struct.resource.Type; +import net.dongliu.apk.parser.struct.resource.TypeHeader; +import net.dongliu.apk.parser.struct.resource.TypeSpec; +import net.dongliu.apk.parser.struct.resource.TypeSpecHeader; +import net.dongliu.apk.parser.utils.Buffers; +import net.dongliu.apk.parser.utils.Pair; +import net.dongliu.apk.parser.utils.ParseUtils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** + * Parse android resource table file. + * + * @author dongliu + * @see ResourceTypes.h + * @see ResourceTypes.cpp + */ +public class ResourceTableParser { + + /** + * By default the data buffer Chunks is buffer little-endian byte order both at runtime and when stored buffer files. + */ + private final ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN; + private StringPool stringPool; + @NonNull + private final ByteBuffer buffer; + /** + * the resource table file size + */ + public ResourceTable resourceTable; + @NonNull + public final Set locales; + + public ResourceTableParser(final @NonNull ByteBuffer buffer) { + this.buffer = buffer.duplicate(); + this.buffer.order(this.byteOrder); + this.locales = new HashSet<>(); + } + + /** + * parse resource table file. + */ + public void parse() { + // read resource file header. + final ResourceTableHeader resourceTableHeader = (ResourceTableHeader) this.readChunkHeader(); + // read string pool chunk + final StringPool stringPool = ParseUtils.readStringPool(this.buffer, (StringPoolHeader) this.readChunkHeader()); + this.stringPool = stringPool; + this.resourceTable = new ResourceTable(stringPool); + final long packageCount = resourceTableHeader.getPackageCount(); + if (packageCount != 0) { + PackageHeader packageHeader = (PackageHeader) this.readChunkHeader(); + for (int i = 0; i < packageCount; i++) { + final Pair pair = this.readPackage(packageHeader); + this.resourceTable.addPackage(pair.getLeft()); + packageHeader = pair.getRight(); + } + } + } + + /** + * read one package + */ + private Pair readPackage(@NonNull final PackageHeader packageHeader) { + final Pair pair = new Pair<>(); + //read packageHeader + final ResourcePackage resourcePackage = new ResourcePackage(packageHeader); + pair.setLeft(resourcePackage); + final long beginPos = this.buffer.position(); + // read type string pool + if (packageHeader.getTypeStrings() > 0) { + Buffers.position(this.buffer, beginPos + packageHeader.getTypeStrings() - (int) packageHeader.headerSize); + resourcePackage.setTypeStringPool(ParseUtils.readStringPool(this.buffer, + (StringPoolHeader) this.readChunkHeader())); + } + //read key string pool + if (packageHeader.getKeyStrings() > 0) { + Buffers.position(this.buffer, beginPos + packageHeader.getKeyStrings() - (int) packageHeader.headerSize); + resourcePackage.setKeyStringPool(ParseUtils.readStringPool(this.buffer, + (StringPoolHeader) this.readChunkHeader())); + } + outer: + while (this.buffer.hasRemaining()) { + final ChunkHeader chunkHeader = this.readChunkHeader(); + final long chunkBegin = this.buffer.position(); + switch ((int) chunkHeader.chunkType) { + case ChunkType.TABLE_TYPE_SPEC: + final TypeSpecHeader typeSpecHeader = (TypeSpecHeader) chunkHeader; + final long[] entryFlags = new long[typeSpecHeader.getEntryCount()]; + for (int i = 0; i < typeSpecHeader.getEntryCount(); i++) { + entryFlags[i] = Buffers.readUInt(this.buffer); + } + //id start from 1 + final String typeSpecName = resourcePackage.getTypeStringPool() + .get(typeSpecHeader.getId() - 1); + final TypeSpec typeSpec = new TypeSpec(typeSpecHeader, entryFlags, typeSpecName); + resourcePackage.addTypeSpec(typeSpec); + Buffers.position(this.buffer, chunkBegin + typeSpecHeader.getBodySize()); + break; + case ChunkType.TABLE_TYPE: + final TypeHeader typeHeader = (TypeHeader) chunkHeader; + // read offsets table + final long[] offsets = new long[typeHeader.entryCount]; + for (int i = 0; i < typeHeader.entryCount; i++) { + if( (typeHeader.getFlags() & 0x01 ) == 0x01 ) /* FLAG_SPARSE */ { + throw new RuntimeException("FLAG_SPARSE unsupported at the moment"); + } else if( (typeHeader.getFlags() & 0x02 ) == 0x02 ) /* FLAG_OFFSET16 */ { + offsets[i] = Buffers.readUShort(buffer) * 4L; + } else { + offsets[i] = Buffers.readUInt(buffer); + } + } + final Type type = new Type(typeHeader); + type.setName(resourcePackage.getTypeStringPool().get(typeHeader.getId() - 1)); + final long entryPos = chunkBegin + typeHeader.entriesStart - (int) typeHeader.headerSize; + Buffers.position(this.buffer, entryPos); + final ByteBuffer b = this.buffer.slice(); + b.order(this.byteOrder); + type.setBuffer(b); + type.setKeyStringPool(resourcePackage.getKeyStringPool()); + type.setOffsets(offsets); + type.setStringPool(this.stringPool); + resourcePackage.addType(type); + this.locales.add(type.locale); + Buffers.position(this.buffer, chunkBegin + typeHeader.getBodySize()); + break; + case ChunkType.TABLE_PACKAGE: + // another package. we should read next package here + pair.setRight((PackageHeader) chunkHeader); + break outer; + case ChunkType.TABLE_LIBRARY: + // read entries + final LibraryHeader libraryHeader = (LibraryHeader) chunkHeader; + for (long i = 0; i < libraryHeader.getCount(); i++) { + final int packageId = this.buffer.getInt(); + final String name = Buffers.readZeroTerminatedString(this.buffer, 128); + final LibraryEntry entry = new LibraryEntry(packageId, name); + //TODO: now just skip it.. + } + Buffers.position(this.buffer, chunkBegin + chunkHeader.getBodySize()); + break; + case ChunkType.NULL: +// Buffers.position(buffer, chunkBegin + chunkHeader.getBodySize()); + Buffers.position(this.buffer, this.buffer.position() + this.buffer.remaining()); + break; + default: + throw new ParserException("unexpected chunk type: 0x" + (int) chunkHeader.chunkType); + } + } + return pair; + + } + + @NonNull + private ChunkHeader readChunkHeader() { + final long begin = this.buffer.position(); + final int chunkType = Buffers.readUShort(this.buffer); + final int headerSize = Buffers.readUShort(this.buffer); + final int chunkSize = (int) Buffers.readUInt(this.buffer); + switch (chunkType) { + case ChunkType.TABLE: { + final ResourceTableHeader resourceTableHeader = new ResourceTableHeader(headerSize, chunkSize, this.buffer); + Buffers.position(this.buffer, begin + headerSize); + return resourceTableHeader; + } + case ChunkType.STRING_POOL: { + final StringPoolHeader stringPoolHeader = new StringPoolHeader(headerSize, chunkSize, this.buffer); + Buffers.position(this.buffer, begin + headerSize); + return stringPoolHeader; + } + case ChunkType.TABLE_PACKAGE: { + final PackageHeader packageHeader = new PackageHeader(headerSize, chunkSize, this.buffer); + Buffers.position(this.buffer, begin + headerSize); + return packageHeader; + } + case ChunkType.TABLE_TYPE_SPEC: { + final TypeSpecHeader typeSpecHeader = new TypeSpecHeader(headerSize, chunkSize, this.buffer); + Buffers.position(this.buffer, begin + headerSize); + return typeSpecHeader; + } + case ChunkType.TABLE_TYPE: { + final TypeHeader typeHeader = new TypeHeader(headerSize, chunkSize, this.buffer); + Buffers.position(this.buffer, begin + headerSize); + return typeHeader; + } + case ChunkType.TABLE_LIBRARY: { + //DynamicRefTable + final LibraryHeader libraryHeader = new LibraryHeader(headerSize, chunkSize, this.buffer); + Buffers.position(this.buffer, begin + headerSize); + return libraryHeader; + } + case ChunkType.TABLE_OVERLAYABLE: + case ChunkType.NULL: { + Buffers.position(this.buffer, begin + headerSize); + return new NullHeader(headerSize, chunkSize); + } + case ChunkType.TABLE_STAGED_ALIAS: + //unknown how to handle this, and it causes a crash when being treated as NullHeader : https://github.com/AndroidDeveloperLB/apk-parser/issues/1#issuecomment-1152937896 + default: + throw new ParserException("Unexpected chunk Type: 0x" + Integer.toHexString(chunkType)); + } + } + + @Nullable + public ResourceTable getResourceTable() { + return this.resourceTable; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.kt new file mode 100644 index 00000000..3bd4f972 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.kt @@ -0,0 +1,6 @@ +package net.dongliu.apk.parser.parser + +/** + * class for sort string pool indexes + */ +class StringPoolEntry(@JvmField val idx: Int, @JvmField val offset: Long) diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.kt new file mode 100644 index 00000000..6e70c189 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.kt @@ -0,0 +1,64 @@ +package net.dongliu.apk.parser.parser + +import net.dongliu.apk.parser.struct.xml.* + +/** + * the xml file's namespaces. + * + * @author dongliu + */ +internal class XmlNamespaces { + private val namespaces: MutableList = ArrayList() + private val newNamespaces: MutableList = ArrayList() + fun addNamespace(tag: XmlNamespaceStartTag) { + val namespace = XmlNamespace(tag.prefix, tag.uri) + namespaces.add(namespace) + newNamespaces.add(namespace) + } + + fun removeNamespace(tag: XmlNamespaceEndTag) { + val namespace = XmlNamespace(tag.prefix, tag.uri) + namespaces.remove(namespace) + newNamespaces.remove(namespace) + } + + fun getPrefixViaUri(uri: String?): String? { + if (uri == null) { + return null + } + for (namespace in namespaces) { + if (uri == namespace.uri) { + return namespace.prefix + } + } + return null + } + + fun consumeNameSpaces(): List { + return if (newNamespaces.isNotEmpty()) { + val xmlNamespaces: List = + ArrayList(newNamespaces) + newNamespaces.clear() + xmlNamespaces + } else { + emptyList() + } + } + + /** + * one namespace + */ + class XmlNamespace constructor(@JvmField val prefix: String?, @JvmField val uri: String?) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is XmlNamespace) return false + return prefix == other.prefix && uri == other.uri + } + + override fun hashCode(): Int { + var result = prefix?.hashCode() ?: 0 + result = 31 * result + (uri?.hashCode() ?: 0) + return result + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.kt new file mode 100644 index 00000000..2f11e8c6 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.kt @@ -0,0 +1,20 @@ +package net.dongliu.apk.parser.parser + +import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag +import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag +import net.dongliu.apk.parser.struct.xml.XmlCData +import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag +import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag + +/** + * callback interface for parse binary xml file. + * + * @author dongliu + */ +interface XmlStreamer { + fun onStartTag(xmlNodeStartTag: XmlNodeStartTag) + fun onEndTag(xmlNodeEndTag: XmlNodeEndTag) + fun onCData(xmlCData: XmlCData) + fun onNamespaceStart(tag: XmlNamespaceStartTag) + fun onNamespaceEnd(tag: XmlNamespaceEndTag) +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java new file mode 100644 index 00000000..866e0423 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java @@ -0,0 +1,126 @@ +package net.dongliu.apk.parser.parser; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.struct.xml.Attribute; +import net.dongliu.apk.parser.struct.xml.XmlCData; +import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag; +import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag; +import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag; +import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag; +import net.dongliu.apk.parser.utils.xml.XmlEscaper; + +import java.util.List; + +/** + * trans to xml text when parse binary xml file. + * + * @author dongliu + */ +public class XmlTranslator implements XmlStreamer { + @NonNull + private final StringBuilder sb; + private int shift = 0; + @NonNull + private final XmlNamespaces namespaces; + private boolean isLastStartTag; + + public XmlTranslator() { + this.sb = new StringBuilder(); + this.sb.append("\n"); + this.namespaces = new XmlNamespaces(); + } + + @Override + public void onStartTag(final @NonNull XmlNodeStartTag xmlNodeStartTag) { + if (this.isLastStartTag) { + this.sb.append(">\n"); + } + this.appendShift(this.shift++); + this.sb.append('<'); + if (xmlNodeStartTag.namespace != null) { + final String prefix = this.namespaces.getPrefixViaUri(xmlNodeStartTag.namespace); + if (prefix != null) { + this.sb.append(prefix).append(":"); + } else { + this.sb.append(xmlNodeStartTag.namespace).append(":"); + } + } + this.sb.append(xmlNodeStartTag.name); + final List nps = this.namespaces.consumeNameSpaces(); + if (!nps.isEmpty()) { + for (final XmlNamespaces.XmlNamespace np : nps) { + this.sb.append(" xmlns:").append(np.prefix).append("=\"") + .append(np.uri) + .append("\""); + } + } + this.isLastStartTag = true; + for (final Attribute attribute : xmlNodeStartTag.attributes.attributes) { + this.onAttribute(attribute); + } + } + + private void onAttribute(final Attribute attribute) { + this.sb.append(" "); + String namespace = this.namespaces.getPrefixViaUri(attribute.namespace); + if (namespace == null) { + namespace = attribute.namespace; + } + if (!namespace.isEmpty()) { + this.sb.append(namespace).append(':'); + } + final String escapedFinalValue = XmlEscaper.escapeXml10(attribute.value); + this.sb.append(attribute.name).append('=').append('"') + .append(escapedFinalValue).append('"'); + } + + @Override + public void onEndTag(@NonNull final XmlNodeEndTag xmlNodeEndTag) { + --this.shift; + if (this.isLastStartTag) { + this.sb.append(" />\n"); + } else { + this.appendShift(this.shift); + this.sb.append("\n"); + } + this.isLastStartTag = false; + } + + @Override + public void onCData(@NonNull final XmlCData xmlCData) { + this.appendShift(this.shift); + this.sb.append(xmlCData.getValue()).append('\n'); + this.isLastStartTag = false; + } + + @Override + public void onNamespaceStart(@NonNull final XmlNamespaceStartTag tag) { + this.namespaces.addNamespace(tag); + } + + @Override + public void onNamespaceEnd(@NonNull final XmlNamespaceEndTag tag) { + this.namespaces.removeNamespace(tag); + } + + private void appendShift(final int shift) { + for (int i = 0; i < shift; i++) { + this.sb.append("\t"); + } + } + + @NonNull + public String getXml() { + return this.sb.toString(); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.kt new file mode 100644 index 00000000..89ebe98a --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.kt @@ -0,0 +1,33 @@ +package net.dongliu.apk.parser.struct + +/** + * android system file. + * + * @author dongiu + */ +object AndroidConstants { + const val RESOURCE_FILE = "resources.arsc" + const val MANIFEST_FILE = "AndroidManifest.xml" + const val DEX_FILE = "classes.dex" + const val DEX_ADDITIONAL = "classes%d.dex" +// const val RES_PREFIX = "res/" +// const val ASSETS_PREFIX = "assets/" +// const val LIB_PREFIX = "lib/" +// const val META_PREFIX = "META-INF/" +// const val ARCH_ARMEABI = "" + +// /** +// * the binary xml file used system attr id. +// */ +// const val ATTR_ID_START = 0x01010000 + + /** + * start offset for system android.R.style + */ + const val SYS_STYLE_ID_START = 0x01030000 + + /** + * end offset for system android.R.style + */ + const val SYS_STYLE_ID_END = 0x01031000 +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.kt new file mode 100644 index 00000000..a4ecbb16 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.kt @@ -0,0 +1,52 @@ +package net.dongliu.apk.parser.struct + +import net.dongliu.apk.parser.utils.Unsigned + +/** + * A Chunk is just a piece of memory split into two parts, a header and a body. + * The exact structure of the header and the body of a given Chunk is determined by its type. + *

+ * chunk header struct.
+ * struct ResChunk_header {
+ * uint16_t type;
+ * uint16_t headerSize;
+ * uint32_t size;
+ * }
+
* + * + * @author dongliu + */ +open class ChunkHeader(chunkType: Int, headerSize: Int, chunkSize: Long) { + /** + * Type identifier for this chunk. The meaning of this value depends + * on the containing chunk. + */ + @JvmField + val chunkType: Short + + /** + * Size of the chunk header (in bytes). Adding this value to + * the address of the chunk allows you to find its associated data + * (if any). + */ + @JvmField + val headerSize: Short + + /** + * Total size of this chunk (in bytes). This is the chunkSize plus + * the size of any data associated with the chunk. Adding this value + * to the chunk allows you to completely skip its contents (including + * any child chunks). If this value is the same as chunkSize, there is + * no data associated with the chunk. + */ + val chunkSize: Int + + init { + this.chunkType = Unsigned.toUShort(chunkType) + this.headerSize = Unsigned.toUShort(headerSize) + this.chunkSize = Unsigned.ensureUInt(chunkSize) + } + + val bodySize: Int + get() = chunkSize - headerSize +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkType.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkType.kt new file mode 100644 index 00000000..3c9e4d6b --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkType.kt @@ -0,0 +1,51 @@ +package net.dongliu.apk.parser.struct + +/** + * Resource type + * see https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h + * + * @author dongliu + */ +object ChunkType { + const val NULL = 0x0000 + const val STRING_POOL = 0x0001 + const val TABLE = 0x0002 + const val XML = 0x0003 + + /** + * Chunk types in XML + */ + const val XML_FIRST_CHUNK = 0x0100 + const val XML_START_NAMESPACE = 0x0100 + const val XML_END_NAMESPACE = 0x0101 + const val XML_START_ELEMENT = 0x0102 + const val XML_END_ELEMENT = 0x0103 + const val XML_CDATA = 0x0104 + const val XML_LAST_CHUNK = 0x017f + + /** + * This contains a uint32_t array mapping strings in the string + * pool back to resource identifiers. It is optional. + */ + const val XML_RESOURCE_MAP = 0x0180 + + /** + * Chunk types in RES_TABLE_TYPE + */ + const val TABLE_PACKAGE = 0x0200 + const val TABLE_TYPE = 0x0201 + const val TABLE_TYPE_SPEC = 0x0202 + + /** + * android5.0+ + * DynamicRefTable + */ + const val TABLE_LIBRARY = 0x0203 + + /** + * TODO: handle the chunks types below + * https://github.com/hsiafan/apk-parser/issues/96#issuecomment-500275300 https://github.com/AndroidDeveloperLB/apk-parser/issues/1 + */ + const val TABLE_OVERLAYABLE = 0x0204 + const val TABLE_STAGED_ALIAS = 0x0206 +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResValue.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResValue.kt new file mode 100644 index 00000000..903a0ffe --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResValue.kt @@ -0,0 +1,184 @@ +package net.dongliu.apk.parser.struct + +/** + * Apk res value struct. + * Only for description now, The value is hold in ResourceValue + * + * @author dongliu + */ +object ResValue { + object ResType { + /** + * Contains no data. + */ + const val NULL: Short = 0x00 + + /** + * The 'data' holds a ResTable_ref; a reference to another resource + * table entry. + */ + const val REFERENCE: Short = 0x01 + // /** + // * The 'data' holds an attribute resource identifier. + // */ + // public static final short ATTRIBUTE = 0x02; + /** + * The 'data' holds an index into the containing resource table's + * global value string pool. + */ + const val STRING: Short = 0x03 + // /** + // * The 'data' holds a single-precision floating point number. + // */ + // public static final short FLOAT = 0x04; + /** + * The 'data' holds a complex number encoding a dimension value; + * such as "100in". + */ + const val DIMENSION: Short = 0x05 + + /** + * The 'data' holds a complex number encoding a fraction of a + * container. + */ + const val FRACTION: Short = 0x06 + + /** + The 'data' holds a dynamic ResTable_ref, which needs to be + resolved before it can be used like a TYPE_REFERENCE. + */ + const val TYPE_DYNAMIC_REFERENCE: Short = 0x07 + + // /** + // * Beginning of integer flavors... + // */ + // public static final short FIRST_INT = 0x10; + /** + * The 'data' is a raw integer value of the form n..n. + */ + const val INT_DEC: Short = 0x10 + + /** + * The 'data' is a raw integer value of the form 0xn..n. + */ + const val INT_HEX: Short = 0x11 + + /** + * The 'data' is either 0 or 1; for input "false" or "true" respectively. + */ + const val INT_BOOLEAN: Short = 0x12 + // /** + // * Beginning of color integer flavors... + // */ + // public static final short FIRST_COLOR_INT = 0x1c; + /** + * The 'data' is a raw integer value of the form #aarrggbb. + */ + const val INT_COLOR_ARGB8: Short = 0x1c + + /** + * The 'data' is a raw integer value of the form #rrggbb. + */ + const val INT_COLOR_RGB8: Short = 0x1d + + /** + * The 'data' is a raw integer value of the form #argb. + */ + const val INT_COLOR_ARGB4: Short = 0x1e + + /** + * The 'data' is a raw integer value of the form #rgb. + */ + const val INT_COLOR_RGB4: Short = 0x1f // /** + // * ...end of integer flavors. + // */ + // public static final short LAST_COLOR_INT = 0x1f; + // + // /** + // * ...end of integer flavors. + // */ + // public static final short LAST_INT = 0x1f; + } + + /** + * A number of constants used when the data is interpreted as a composite value are defined + * by the following anonymous C++ enum + */ + object ResDataCOMPLEX { + // /** + // * Where the unit type information is. This gives us 16 possible + // * types; as defined below. + // */ + // public static final short UNIT_SHIFT = 0; + // public static final short UNIT_MASK = 0xf; + /** + * TYPE_DIMENSION: Value is raw pixels. + */ + const val UNIT_PX: Short = 0 + + /** + * TYPE_DIMENSION: Value is Device Independent Pixels. + */ + const val UNIT_DIP: Short = 1 + + /** + * TYPE_DIMENSION: Value is a Scaled device independent Pixels. + */ + const val UNIT_SP: Short = 2 + + /** + * TYPE_DIMENSION: Value is in points. + */ + const val UNIT_PT: Short = 3 + + /** + * TYPE_DIMENSION: Value is in inches. + */ + const val UNIT_IN: Short = 4 + + /** + * TYPE_DIMENSION: Value is in millimeters. + */ + const val UNIT_MM: Short = 5 + + /** + * TYPE_FRACTION: A basic fraction of the overall size. + */ + const val UNIT_FRACTION: Short = 0 + + /** + * TYPE_FRACTION: A fraction of the parent size. + */ + const val UNIT_FRACTION_PARENT: Short = 1 // /** + // * Where the radix information is; telling where the decimal place + // * appears in the mantissa. This give us 4 possible fixed point + // * representations as defined below. + // */ + // public static final short RADIX_SHIFT = 4; + // public static final short RADIX_MASK = 0x3; + // + // /** + // * The mantissa is an integral number -- i.e.; 0xnnnnnn.0 + // */ + // public static final short RADIX_23p0 = 0; + // /** + // * The mantissa magnitude is 16 bits -- i.e; 0xnnnn.nn + // */ + // public static final short RADIX_16p7 = 1; + // /** + // * The mantissa magnitude is 8 bits -- i.e; 0xnn.nnnn + // */ + // public static final short RADIX_8p15 = 2; + // /** + // * The mantissa magnitude is 0 bits -- i.e; 0x0.nnnnnn + // */ + // public static final short RADIX_0p23 = 3; + // + // /** + // * Where the actual value is. This gives us 23 bits of + // * precision. The top bit is the sign. + // */ + // public static final short MANTISSA_SHIFT = 8; + // public static final int MANTISSA_MASK = 0xffffff; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java new file mode 100644 index 00000000..9aefaa81 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java @@ -0,0 +1,316 @@ +package net.dongliu.apk.parser.struct; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.resource.Densities; +import net.dongliu.apk.parser.struct.resource.ResourceEntry; +import net.dongliu.apk.parser.struct.resource.ResourceTable; +import net.dongliu.apk.parser.struct.resource.Type; +import net.dongliu.apk.parser.struct.resource.TypeSpec; +import net.dongliu.apk.parser.utils.Locales; + +import java.util.List; +import java.util.Locale; + +/** + * Resource entity, contains the resource id, should retrieve the value from resource table, or string pool if it is a string resource. + * + * @author dongliu + */ +public abstract class ResourceValue { + protected final int value; + + protected ResourceValue(final int value) { + this.value = value; + } + + /** + * get value as string. + */ + @Nullable + public abstract String toStringValue(ResourceTable resourceTable, Locale locale); + + @NonNull + public static ResourceValue decimal(final int value) { + return new DecimalResourceValue(value); + } + + @NonNull + public static ResourceValue hexadecimal(final int value) { + return new HexadecimalResourceValue(value); + } + + @NonNull + public static ResourceValue bool(final int value) { + return new BooleanResourceValue(value); + } + + @NonNull + public static ResourceValue string(final int value, final StringPool stringPool) { + return new StringResourceValue(value, stringPool); + } + + @NonNull + public static ResourceValue reference(final int value) { + return new ReferenceResourceValue(value); + } + + @NonNull + public static ResourceValue nullValue() { + return NullResourceValue.instance; + } + + @NonNull + public static ResourceValue rgb(final int value, final int len) { + return new RGBResourceValue(value, len); + } + + @NonNull + public static ResourceValue dimension(final int value) { + return new DimensionValue(value); + } + + @NonNull + public static ResourceValue fraction(final int value) { + return new FractionValue(value); + } + + @NonNull + public static ResourceValue raw(final int value, final short type) { + return new RawValue(value, type); + } + + private static class DecimalResourceValue extends ResourceValue { + + private DecimalResourceValue(final int value) { + super(value); + } + + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + return String.valueOf(this.value); + } + } + + private static class HexadecimalResourceValue extends ResourceValue { + + private HexadecimalResourceValue(final int value) { + super(value); + } + + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + return "0x" + Integer.toHexString(this.value); + } + } + + private static class BooleanResourceValue extends ResourceValue { + + private BooleanResourceValue(final int value) { + super(value); + } + + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + return String.valueOf(this.value != 0); + } + } + + private static class StringResourceValue extends ResourceValue { + private final StringPool stringPool; + + private StringResourceValue(final int value, final StringPool stringPool) { + super(value); + this.stringPool = stringPool; + } + + @Nullable + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + if (this.value >= 0) { + return this.stringPool.get(this.value); + } else { + return null; + } + } + + @NonNull + @Override + public String toString() { + return this.value + ":" + this.stringPool.get(this.value); + } + } + + /** + * ReferenceResource ref one another resources, and may has different value for different resource config(locale, density, etc) + */ + public static class ReferenceResourceValue extends ResourceValue { + + private ReferenceResourceValue(final int value) { + super(value); + } + + @Override + @Nullable + public String toStringValue(final @Nullable ResourceTable resourceTable, final Locale locale) { + final long resourceId = this.getReferenceResourceId(); + // android system styles. + if (resourceId > AndroidConstants.SYS_STYLE_ID_START && resourceId < AndroidConstants.SYS_STYLE_ID_END) { + return "@android:style/" + ResourceTable.sysStyle.get((int) resourceId); + } + final String raw = "resourceId:0x" + Long.toHexString(resourceId); + if (resourceTable == null) { + return raw; + } + final List resources = resourceTable.getResourcesById(resourceId); + // read from type resource + ResourceEntry selected = null; + TypeSpec typeSpec = null; + int currentLocalMatchLevel = -1; + int currentDensityLevel = -1; + for (final ResourceTable.Resource resource : resources) { + final Type type = resource.type; + typeSpec = resource.typeSpec; + final ResourceEntry resourceEntry = resource.resourceEntry; + final int localMatchLevel = Locales.match(locale, type.locale); + final int densityLevel = ReferenceResourceValue.densityLevel(type.density); + if (localMatchLevel > currentLocalMatchLevel) { + selected = resourceEntry; + currentLocalMatchLevel = localMatchLevel; + currentDensityLevel = densityLevel; + } else if (densityLevel > currentDensityLevel) { + selected = resourceEntry; + currentDensityLevel = densityLevel; + } + } + final String result; + if (selected == null) { + result = raw; + } else if (locale == null) { + result = "@" + typeSpec.name + "/" + selected.key; + } else { + result = selected.toStringValue(resourceTable, locale); + } + return result; + } + + public long getReferenceResourceId() { + return this.value & 0xFFFFFFFFL; + } + + private static int densityLevel(final int density) { + if (density == Densities.ANY || density == Densities.NONE) { + return -1; + } + return density; + } + } + + private static class NullResourceValue extends ResourceValue { + private static final NullResourceValue instance = new NullResourceValue(); + + private NullResourceValue() { + super(-1); + } + + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + return ""; + } + } + + private static class RGBResourceValue extends ResourceValue { + private final int len; + + private RGBResourceValue(final int value, final int len) { + super(value); + this.len = len; + } + + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + final StringBuilder sb = new StringBuilder(); + for (int i = this.len / 2 - 1; i >= 0; i--) { + sb.append(Integer.toHexString((this.value >> i * 8) & 0xff)); + } + return sb.toString(); + } + } + + private static class DimensionValue extends ResourceValue { + + private DimensionValue(final int value) { + super(value); + } + + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + final short unit = (short) (this.value & 0xff); + final String unitStr; + switch (unit) { + case ResValue.ResDataCOMPLEX.UNIT_MM: + unitStr = "mm"; + break; + case ResValue.ResDataCOMPLEX.UNIT_PX: + unitStr = "px"; + break; + case ResValue.ResDataCOMPLEX.UNIT_DIP: + unitStr = "dp"; + break; + case ResValue.ResDataCOMPLEX.UNIT_SP: + unitStr = "sp"; + break; + case ResValue.ResDataCOMPLEX.UNIT_PT: + unitStr = "pt"; + break; + case ResValue.ResDataCOMPLEX.UNIT_IN: + unitStr = "in"; + break; + default: + unitStr = "unknown unit:0x" + Integer.toHexString(unit); + } + return (this.value >> 8) + unitStr; + } + } + + private static class FractionValue extends ResourceValue { + + private FractionValue(final int value) { + super(value); + } + + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + // The low-order 4 bits of the data value specify the type of the fraction + final short type = (short) (this.value & 0xf); + final String pstr; + switch (type) { + case ResValue.ResDataCOMPLEX.UNIT_FRACTION: + pstr = "%"; + break; + case ResValue.ResDataCOMPLEX.UNIT_FRACTION_PARENT: + pstr = "%p"; + break; + default: + pstr = "unknown type:0x" + Integer.toHexString(type); + } + final float f = Float.intBitsToFloat(this.value >> 4); + return f + pstr; + } + } + + private static class RawValue extends ResourceValue { + private final short dataType; + + private RawValue(final int value, final short dataType) { + super(value); + this.dataType = dataType; + } + + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + return "{" + this.dataType + ":" + (this.value & 0xFFFFFFFFL) + "}"; + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPool.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPool.java new file mode 100644 index 00000000..32f39507 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPool.java @@ -0,0 +1,25 @@ +package net.dongliu.apk.parser.struct; + +import androidx.annotation.NonNull; + +/** + * String pool. + * + * @author dongliu + */ +public class StringPool { + @NonNull + private final String[] pool; + + public StringPool(final int poolSize) { + this.pool = new String[poolSize]; + } + + public String get(final int idx) { + return this.pool[idx]; + } + + public void set(final int idx, final String value) { + this.pool[idx] = value; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.kt new file mode 100644 index 00000000..71cc9108 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.kt @@ -0,0 +1,56 @@ +package net.dongliu.apk.parser.struct + +import net.dongliu.apk.parser.utils.* +import java.nio.ByteBuffer + +/** + * String pool chunk header. + * + * @author dongliu + */ +@Suppress("MemberVisibilityCanBePrivate") +class StringPoolHeader(headerSize: Int, chunkSize: Long, buffer: ByteBuffer) : + ChunkHeader(ChunkType.STRING_POOL, headerSize, chunkSize) { + /** + * Number of style span arrays in the pool (number of uint32_t indices + * follow the string indices). + */ + val stringCount: Int + + /** + * Number of style span arrays in the pool (number of uint32_t indices + * follow the string indices). + */ + val styleCount: Int + val flags: Long + + /** + * Index from header of the string data. + */ + val stringsStart: Long + + /** + * Index from header of the style data. + */ + val stylesStart: Long + + init { + stringCount = Unsigned.ensureUInt(Buffers.readUInt(buffer)) + this.styleCount = Unsigned.ensureUInt(Buffers.readUInt(buffer)) + flags = Buffers.readUInt(buffer) + stringsStart = Buffers.readUInt(buffer) + stylesStart = Buffers.readUInt(buffer) + } + + companion object { + /** + * If set, the string index is sorted by the string values (based on strcmp16()). + */ + const val SORTED_FLAG = 1 + + /** + * String pool is encoded in UTF-8 + */ + const val UTF8_FLAG = 1 shl 8 + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java new file mode 100644 index 00000000..d9610100 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java @@ -0,0 +1,110 @@ +package net.dongliu.apk.parser.struct.dex; + +/** + * @author dongliu + */ +public class DexClassStruct { + /* index into typeIds for this class. u4 */ + private int classIdx; + + private int accessFlags; + /* index into typeIds for superclass. u4 */ + private int superclassIdx; + + /* file offset to DexTypeList. u4 */ + private long interfacesOff; + + /* index into stringIds for source file name. u4 */ + private int sourceFileIdx; + /* file offset to annotations_directory_item. u4 */ + private long annotationsOff; + /* file offset to class_data_item. u4 */ + private long classDataOff; + /* file offset to DexEncodedArray. u4 */ + private long staticValuesOff; + + public static final int ACC_PUBLIC = 0x1; + public static int ACC_PRIVATE = 0x2; + public static final int ACC_PROTECTED = 0x4; + public static final int ACC_STATIC = 0x8; + public static int ACC_FINAL = 0x10; + public static int ACC_SYNCHRONIZED = 0x20; + public static int ACC_VOLATILE = 0x40; + public static int ACC_BRIDGE = 0x40; + public static int ACC_TRANSIENT = 0x80; + public static int ACC_VARARGS = 0x80; + public static int ACC_NATIVE = 0x100; + public static final int ACC_INTERFACE = 0x200; + public static int ACC_ABSTRACT = 0x400; + public static int ACC_STRICT = 0x800; + public static int ACC_SYNTHETIC = 0x1000; + public static final int ACC_ANNOTATION = 0x2000; + public static final int ACC_ENUM = 0x4000; + public static int ACC_CONSTRUCTOR = 0x10000; + public static int ACC_DECLARED_SYNCHRONIZED = 0x20000; + + + public int getClassIdx() { + return this.classIdx; + } + + public void setClassIdx(final int classIdx) { + this.classIdx = classIdx; + } + + public int getAccessFlags() { + return this.accessFlags; + } + + public void setAccessFlags(final int accessFlags) { + this.accessFlags = accessFlags; + } + + public int getSuperclassIdx() { + return this.superclassIdx; + } + + public void setSuperclassIdx(final int superclassIdx) { + this.superclassIdx = superclassIdx; + } + + public long getInterfacesOff() { + return this.interfacesOff; + } + + public void setInterfacesOff(final long interfacesOff) { + this.interfacesOff = interfacesOff; + } + + public int getSourceFileIdx() { + return this.sourceFileIdx; + } + + public void setSourceFileIdx(final int sourceFileIdx) { + this.sourceFileIdx = sourceFileIdx; + } + + public long getAnnotationsOff() { + return this.annotationsOff; + } + + public void setAnnotationsOff(final long annotationsOff) { + this.annotationsOff = annotationsOff; + } + + public long getClassDataOff() { + return this.classDataOff; + } + + public void setClassDataOff(final long classDataOff) { + this.classDataOff = classDataOff; + } + + public long getStaticValuesOff() { + return this.staticValuesOff; + } + + public void setStaticValuesOff(final long staticValuesOff) { + this.staticValuesOff = staticValuesOff; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java new file mode 100644 index 00000000..1b73bec3 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java @@ -0,0 +1,271 @@ +package net.dongliu.apk.parser.struct.dex; + +/** + * dex file header. + * see http://dexandroid.googlecode.com/svn/trunk/dalvik/libdex/DexFile.h + * + * @author dongliu + */ +public class DexHeader { + + public static final int kSHA1DigestLen = 20; + public static final int kSHA1DigestOutputLen = DexHeader.kSHA1DigestLen * 2 + 1; + + /** + * includes version number. 8 bytes. + * public short magic; + */ + private int version; + /** + * adler32 checksum. u4 + * public long checksum; + * SHA-1 hash len = kSHA1DigestLen + */ + private byte[] signature; + /** + * length of entire file. u4 + */ + private long fileSize; + /** + * len of header.offset to start of next section. u4 + */ + private long headerSize; + /** + * u4 + * public long endianTag; + * u4 + */ + private long linkSize; + /** + * u4 + */ + private long linkOff; + /** + * u4 + */ + private long mapOff; + /** + * u4 + */ + private int stringIdsSize; + /** + * u4 + */ + private long stringIdsOff; + /** + * u4 + */ + private int typeIdsSize; + /** + * u4 + */ + private long typeIdsOff; + /** + * u4 + */ + private int protoIdsSize; + /** + * u4 + */ + private long protoIdsOff; + /** + * u4 + */ + private int fieldIdsSize; + /** + * u4 + */ + private long fieldIdsOff; + /** + * u4 + */ + private int methodIdsSize; + /** + * u4 + */ + private long methodIdsOff; + /** + * u4 + */ + private int classDefsSize; + /** + * u4 + */ + private long classDefsOff; + /** + * u4 + */ + private int dataSize; + /** + * u4 + */ + private long dataOff; + + public int getVersion() { + return this.version; + } + + public void setVersion(final int version) { + this.version = version; + } + + public byte[] getSignature() { + return this.signature; + } + + public void setSignature(final byte[] signature) { + this.signature = signature; + } + + public long getFileSize() { + return this.fileSize; + } + + public void setFileSize(final long fileSize) { + this.fileSize = fileSize; + } + + public long getHeaderSize() { + return this.headerSize; + } + + public void setHeaderSize(final long headerSize) { + this.headerSize = headerSize; + } + + public long getLinkSize() { + return this.linkSize; + } + + public void setLinkSize(final long linkSize) { + this.linkSize = linkSize; + } + + public long getLinkOff() { + return this.linkOff; + } + + public void setLinkOff(final long linkOff) { + this.linkOff = linkOff; + } + + public long getMapOff() { + return this.mapOff; + } + + public void setMapOff(final long mapOff) { + this.mapOff = mapOff; + } + + public int getStringIdsSize() { + return this.stringIdsSize; + } + + public void setStringIdsSize(final int stringIdsSize) { + this.stringIdsSize = stringIdsSize; + } + + public long getStringIdsOff() { + return this.stringIdsOff; + } + + public void setStringIdsOff(final long stringIdsOff) { + this.stringIdsOff = stringIdsOff; + } + + public int getTypeIdsSize() { + return this.typeIdsSize; + } + + public void setTypeIdsSize(final int typeIdsSize) { + this.typeIdsSize = typeIdsSize; + } + + public long getTypeIdsOff() { + return this.typeIdsOff; + } + + public void setTypeIdsOff(final long typeIdsOff) { + this.typeIdsOff = typeIdsOff; + } + + public int getProtoIdsSize() { + return this.protoIdsSize; + } + + public void setProtoIdsSize(final int protoIdsSize) { + this.protoIdsSize = protoIdsSize; + } + + public long getProtoIdsOff() { + return this.protoIdsOff; + } + + public void setProtoIdsOff(final long protoIdsOff) { + this.protoIdsOff = protoIdsOff; + } + + public int getFieldIdsSize() { + return this.fieldIdsSize; + } + + public void setFieldIdsSize(final int fieldIdsSize) { + this.fieldIdsSize = fieldIdsSize; + } + + public long getFieldIdsOff() { + return this.fieldIdsOff; + } + + public void setFieldIdsOff(final long fieldIdsOff) { + this.fieldIdsOff = fieldIdsOff; + } + + public int getMethodIdsSize() { + return this.methodIdsSize; + } + + public void setMethodIdsSize(final int methodIdsSize) { + this.methodIdsSize = methodIdsSize; + } + + public long getMethodIdsOff() { + return this.methodIdsOff; + } + + public void setMethodIdsOff(final long methodIdsOff) { + this.methodIdsOff = methodIdsOff; + } + + public int getClassDefsSize() { + return this.classDefsSize; + } + + public void setClassDefsSize(final int classDefsSize) { + this.classDefsSize = classDefsSize; + } + + public long getClassDefsOff() { + return this.classDefsOff; + } + + public void setClassDefsOff(final long classDefsOff) { + this.classDefsOff = classDefsOff; + } + + public int getDataSize() { + return this.dataSize; + } + + public void setDataSize(final int dataSize) { + this.dataSize = dataSize; + } + + public long getDataOff() { + return this.dataOff; + } + + public void setDataOff(final long dataOff) { + this.dataOff = dataOff; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.kt new file mode 100644 index 00000000..befc2304 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.kt @@ -0,0 +1,17 @@ +package net.dongliu.apk.parser.struct.resource + +/** + * Screen density values + */ +object Densities { + const val DEFAULT = 0 + const val LOW = 120 + const val MEDIUM = 160 + const val TV = 213 + const val HIGH = 240 + const val XHIGH = 320 + const val XXHIGH = 480 + const val XXXHIGH = 640 + const val ANY = 0xfffe + const val NONE = 0xffff +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java new file mode 100644 index 00000000..eaafdb26 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java @@ -0,0 +1,23 @@ +package net.dongliu.apk.parser.struct.resource; + +/** + * Library chunk entry + * + * @author Liu Dong + */ +public class LibraryEntry { + /** + * uint32. The package-id this shared library was assigned at build time. + */ + public final int packageId; + + /** + * The package name of the shared library. \0 terminated. max 128 + */ + public final String name; + + public LibraryEntry(final int packageId, final String name) { + this.packageId = packageId; + this.name = name; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.kt new file mode 100644 index 00000000..f9f123c9 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.kt @@ -0,0 +1,23 @@ +package net.dongliu.apk.parser.struct.resource + +import net.dongliu.apk.parser.struct.* +import net.dongliu.apk.parser.utils.Buffers +import net.dongliu.apk.parser.utils.Unsigned.ensureUInt +import java.nio.ByteBuffer + +/** + * Table library chunk header + * + * @author Liu Dong + */ +class LibraryHeader(headerSize: Int, chunkSize: Long, buffer: ByteBuffer) : + ChunkHeader(ChunkType.TABLE_LIBRARY, headerSize, chunkSize) { + /** + * uint32 value, The number of shared libraries linked in this resource table. + */ + val count: Int + + init { + count = ensureUInt(Buffers.readUInt(buffer)) + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.kt new file mode 100644 index 00000000..e93f6919 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.kt @@ -0,0 +1,6 @@ +package net.dongliu.apk.parser.struct.resource + +import net.dongliu.apk.parser.struct.* + +class NullHeader(headerSize: Int, chunkSize: Int) : + ChunkHeader(ChunkType.NULL, headerSize, chunkSize.toLong()) diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java new file mode 100644 index 00000000..50ff3c2f --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java @@ -0,0 +1,95 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.struct.ChunkHeader; +import net.dongliu.apk.parser.struct.ChunkType; +import net.dongliu.apk.parser.utils.Buffers; +import net.dongliu.apk.parser.utils.ParseUtils; +import net.dongliu.apk.parser.utils.Unsigned; + +import java.nio.ByteBuffer; + +/** + * @author dongliu + */ +public class PackageHeader extends ChunkHeader { + + /** + * ResourcePackage IDs start at 1 (corresponding to the value of the package bits in a resource identifier). + * 0 means this is not a base package. + * uint32_t + * 0 framework-res.apk + * 2-9 other framework files + * 127 application package + * Anroid 5.0+: Shared libraries will be assigned a package ID of 0x00 at build-time. + * At runtime, all loaded shared libraries will be assigned a new package ID. + */ + private int id; + + /** + * Actual name of this package, -terminated. + * char16_t name[128] + */ + @NonNull + private final String name; + + /** + * Offset to a ResStringPool_header defining the resource type symbol table. + * If zero, this package is inheriting from another base package (overriding specific values in it). + * uinit 32 + */ + private final int typeStrings; + + /** + * Last index into typeStrings that is for public use by others. + * uint32_t + */ + public final int lastPublicType; + + /** + * Offset to a ResStringPool_header defining the resource + * key symbol table. If zero, this package is inheriting from + * another base package (overriding specific values in it). + * uint32_t + */ + private final int keyStrings; + + /** + * Last index into keyStrings that is for public use by others. + * uint32_t + */ + public final int lastPublicKey; + + public PackageHeader(final int headerSize, final long chunkSize, final @NonNull ByteBuffer buffer) { + super(ChunkType.TABLE_PACKAGE, headerSize, chunkSize); + this.id = Unsigned.toUInt(Buffers.readUInt(buffer)); + this.name = ParseUtils.readStringUTF16(buffer, 128); + this.typeStrings = Unsigned.ensureUInt(Buffers.readUInt(buffer)); + this.lastPublicType = Unsigned.ensureUInt(Buffers.readUInt(buffer)); + this.keyStrings = Unsigned.ensureUInt(Buffers.readUInt(buffer)); + this.lastPublicKey = Unsigned.ensureUInt(Buffers.readUInt(buffer)); + } + + public long getId() { + return Unsigned.toLong(this.id); + } + + public void setId(final long id) { + this.id = Unsigned.toUInt(id); + } + + @NonNull + public String getName() { + return this.name; + } + + public int getTypeStrings() { + return this.typeStrings; + } + + public int getKeyStrings() { + return this.keyStrings; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java new file mode 100644 index 00000000..ad42f111 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java @@ -0,0 +1,262 @@ +package net.dongliu.apk.parser.struct.resource; + +import net.dongliu.apk.parser.utils.Unsigned; + +/** + * used by resource Type. + * + * @author dongliu + */ +public class ResTableConfig { + /** + * Number of bytes in this structure. uint32_t + */ + private int size; + + /** + * Mobile country code (from SIM). 0 means "any". uint16_t + */ + private short mcc; + /** + * Mobile network code (from SIM). 0 means "any". uint16_t + */ + private short mnc; + /** + * uint32_t imsi; + * 0 means "any". Otherwise, en, fr, etc. char[2] + */ + private String language; + /** + * 0 means "any". Otherwise, US, CA, etc. char[2] + */ + private String country; + /** + * uint32_t locale; + * uint8_t + */ + private byte orientation; + /** + * uint8_t + */ + private byte touchscreen; + /** + * uint16_t + */ + private short density; + /** + * uint32_t screenType; + * uint8_t + */ + private short keyboard; + /** + * uint8_t + */ + private short navigation; + /** + * uint8_t + */ + private short inputFlags; + /** + * uint8_t + */ + private short inputPad0; + /** + * uint32_t input; + * uint16_t + */ + private int screenWidth; + /** + * uint16_t + */ + private int screenHeight; + /** + * uint32_t screenSize; + * uint16_t + */ + private int sdkVersion; + /** + * For now minorVersion must always be 0!!! Its meaning is currently undefined. + * uint16_t + */ + private int minorVersion; + /** + * uint32_t version; + * uint8_t + */ + private short screenLayout; + /** + * uint8_t + */ + private short uiMode; + /** + * uint8_t + */ + private short screenConfigPad1; + /** + * uint8_t + */ + private short screenConfigPad2; + + /** + * uint32_t screenConfig; + */ + public int getSize() { + return this.size; + } + + public void setSize(final long size) { + this.size = Unsigned.ensureUInt(size); + } + + public short getMcc() { + return this.mcc; + } + + public void setMcc(final short mcc) { + this.mcc = mcc; + } + + public short getMnc() { + return this.mnc; + } + + public void setMnc(final short mnc) { + this.mnc = mnc; + } + + public String getLanguage() { + return this.language; + } + + public void setLanguage(final String language) { + this.language = language; + } + + public String getCountry() { + return this.country; + } + + public void setCountry(final String country) { + this.country = country; + } + + public short getOrientation() { + return (short) (this.orientation & 0xff); + } + + public void setOrientation(final short orientation) { + this.orientation = (byte) orientation; + } + + public short getTouchscreen() { + return (short) (this.touchscreen & 0xff); + } + + public void setTouchscreen(final short touchscreen) { + this.touchscreen = (byte) touchscreen; + } + + public int getDensity() { + return this.density & 0xffff; + } + + public void setDensity(final int density) { + this.density = (short) density; + } + + public short getKeyboard() { + return this.keyboard; + } + + public void setKeyboard(final short keyboard) { + this.keyboard = keyboard; + } + + public short getNavigation() { + return this.navigation; + } + + public void setNavigation(final short navigation) { + this.navigation = navigation; + } + + public short getInputFlags() { + return this.inputFlags; + } + + public void setInputFlags(final short inputFlags) { + this.inputFlags = inputFlags; + } + + public short getInputPad0() { + return this.inputPad0; + } + + public void setInputPad0(final short inputPad0) { + this.inputPad0 = inputPad0; + } + + public int getScreenWidth() { + return this.screenWidth; + } + + public void setScreenWidth(final int screenWidth) { + this.screenWidth = screenWidth; + } + + public int getScreenHeight() { + return this.screenHeight; + } + + public void setScreenHeight(final int screenHeight) { + this.screenHeight = screenHeight; + } + + public int getSdkVersion() { + return this.sdkVersion; + } + + public void setSdkVersion(final int sdkVersion) { + this.sdkVersion = sdkVersion; + } + + public int getMinorVersion() { + return this.minorVersion; + } + + public void setMinorVersion(final int minorVersion) { + this.minorVersion = minorVersion; + } + + public short getScreenLayout() { + return this.screenLayout; + } + + public void setScreenLayout(final short screenLayout) { + this.screenLayout = screenLayout; + } + + public short getUiMode() { + return this.uiMode; + } + + public void setUiMode(final short uiMode) { + this.uiMode = uiMode; + } + + public short getScreenConfigPad1() { + return this.screenConfigPad1; + } + + public void setScreenConfigPad1(final short screenConfigPad1) { + this.screenConfigPad1 = screenConfigPad1; + } + + public short getScreenConfigPad2() { + return this.screenConfigPad2; + } + + public void setScreenConfigPad2(final short screenConfigPad2) { + this.screenConfigPad2 = screenConfigPad2; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java new file mode 100644 index 00000000..5e172f79 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java @@ -0,0 +1,91 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.ResourceValue; + +import java.util.Locale; + +/** + * A Resource entry specifies the key (name) of the Resource. + * It is immediately followed by the value of that Resource. + * + * @author dongliu + */ +public class ResourceEntry { + /** + * Number of bytes in this structure. uint16_t + */ + public final int size; + + /** + * If set, this is a complex entry, holding a set of name/value + * mappings. It is followed by an array of ResTable_map structures. + */ + public static final int FLAG_COMPLEX = 0x0001; +// /** +// * If set, this resource has been declared public, so libraries +// * are allowed to reference it. +// */ +// public static final int FLAG_PUBLIC = 0x0002; + + /** + * If set, this is a weak resource and may be overriden by strong + * resources of the same name/type. This is only useful during + * linking with other resource tables. + */ + public static final int FLAG_WEAK = 0x0004; + /** + * If set, this is a compact entry with data type and value directly + * encoded in this entry, see ResTable_entry::compact + */ + public static final int FLAG_COMPACT = 0x0008; + /** + * uint16_t + */ + public final int flags; + + /** + * Reference into ResTable_package::keyStrings identifying this entry. + * public long keyRef; + */ + public final String key; + + /** + * the resvalue following this resource entry. + */ + @Nullable + public final ResourceValue value; + + public ResourceEntry(final int size, final int flags, final String key, @Nullable final ResourceValue value) { + this.size = size; + this.flags = flags; + this.key = key; + this.value = value; + } + + /** + * get value as string + */ + @Nullable + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + final ResourceValue value = this.value; + if (value != null) { + return value.toStringValue(resourceTable, locale); + } else { + return "null"; + } + } + + @NonNull + @Override + public String toString() { + return "ResourceEntry{" + + "size=" + this.size + + ", flags=" + this.flags + + ", key='" + this.key + '\'' + + ", value=" + this.value + + '}'; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java new file mode 100644 index 00000000..00caf736 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java @@ -0,0 +1,56 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Locale; + +/** + * @author dongliu. + */ +public class ResourceMapEntry extends ResourceEntry { + /** + * Resource identifier of the parent mapping, or 0 if there is none. + * ResTable_ref specifies the parent Resource, if any, of this Resource. + * struct ResTable_ref { uint32_t ident; }; + */ + public final long parent; + + /** + * Number of name/value pairs that follow for FLAG_COMPLEX. uint32_t + */ + public final long count; + @NonNull + public final ResourceTableMap[] resourceTableMaps; + + public ResourceMapEntry(final int size, final int flags, final String key, final long parent, final long count, final @NonNull ResourceTableMap[] resourceTableMaps) { + super(size, flags, key, null); + this.parent = parent; + this.count = count; + this.resourceTableMaps = resourceTableMaps; + } + + /** + * get value as string + */ + @Nullable + @Override + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + if (this.resourceTableMaps.length > 0) { + return this.resourceTableMaps[0].toString(); + } else { + return null; + } + } + + @NonNull + @Override + public String toString() { + return "ResourceMapEntry{" + + "parent=" + this.parent + + ", count=" + this.count + + ", resourceTableMaps=" + Arrays.toString(this.resourceTableMaps) + + '}'; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java new file mode 100644 index 00000000..a9a70e6b --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java @@ -0,0 +1,110 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.StringPool; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Resource packge. + * + * @author dongliu + */ +public class ResourcePackage { + // the packageName + private String name; + private short id; + /** + * contains the names of the types of the Resources defined in the ResourcePackage + */ + private StringPool typeStringPool; + /** + * contains the names (keys) of the Resources defined in the ResourcePackage. + */ + private StringPool keyStringPool; + + public ResourcePackage(final @NonNull PackageHeader header) { + this.name = header.getName(); + this.id = (short) header.getId(); + } + + private Map typeSpecMap = new HashMap<>(); + + private Map> typesMap = new HashMap<>(); + + public void addTypeSpec(final @NonNull TypeSpec typeSpec) { + this.typeSpecMap.put(typeSpec.id, typeSpec); + } + + @Nullable + public TypeSpec getTypeSpec(final short id) { + return this.typeSpecMap.get(id); + } + + public void addType(final Type type) { + List types = this.typesMap.get(type.id); + if (types == null) { + types = new ArrayList<>(); + this.typesMap.put(type.id, types); + } + types.add(type); + } + + @Nullable + public List getTypes(final short id) { + return this.typesMap.get(id); + } + + public String getName() { + return this.name; + } + + public void setName(final String name) { + this.name = name; + } + + public short getId() { + return this.id; + } + + public void setId(final short id) { + this.id = id; + } + + public StringPool getTypeStringPool() { + return this.typeStringPool; + } + + public void setTypeStringPool(final @NonNull StringPool typeStringPool) { + this.typeStringPool = typeStringPool; + } + + public StringPool getKeyStringPool() { + return this.keyStringPool; + } + + public void setKeyStringPool(final @NonNull StringPool keyStringPool) { + this.keyStringPool = keyStringPool; + } + + public Map getTypeSpecMap() { + return this.typeSpecMap; + } + + public void setTypeSpecMap(final Map typeSpecMap) { + this.typeSpecMap = typeSpecMap; + } + + public Map> getTypesMap() { + return this.typesMap; + } + + public void setTypesMap(final Map> typesMap) { + this.typesMap = typesMap; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java new file mode 100644 index 00000000..bcceacd7 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java @@ -0,0 +1,106 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.ResourceValue; +import net.dongliu.apk.parser.struct.StringPool; +import net.dongliu.apk.parser.utils.ResourceLoader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The apk resource table + * + * @author dongliu + */ +public class ResourceTable { + private final Map packageMap = new HashMap<>(); + @Nullable + public final StringPool stringPool; + @NonNull + public static final Map sysStyle = ResourceLoader.loadSystemStyles(); + + public ResourceTable(@Nullable final StringPool stringPool) { + this.stringPool = stringPool; + } + + public void addPackage(final @NonNull ResourcePackage resourcePackage) { + this.packageMap.put(resourcePackage.getId(), resourcePackage); + } + + @Nullable + public ResourcePackage getPackage(final short id) { + return this.packageMap.get(id); + } + + /** + * Get resources match the given resource id. + */ + @NonNull + public List getResourcesById(final long resourceId) { + // An Android Resource id is a 32-bit integer. It comprises + // an 8-bit Package id [bits 24-31] + // an 8-bit Type id [bits 16-23] + // a 16-bit Entry index [bits 0-15] + final short packageId = (short) (resourceId >> 24 & 0xff); + final short typeId = (short) ((resourceId >> 16) & 0xff); + final int entryIndex = (int) (resourceId & 0xffff); + final ResourcePackage resourcePackage = this.getPackage(packageId); + if (resourcePackage == null) { + return Collections.emptyList(); + } + final TypeSpec typeSpec = resourcePackage.getTypeSpec(typeId); + final List types = resourcePackage.getTypes(typeId); + if (typeSpec == null || types == null) { + return Collections.emptyList(); + } + if (!typeSpec.exists(entryIndex)) { + return Collections.emptyList(); + } + // read from type resource + final List result = new ArrayList<>(); + for (final Type type : types) { + final ResourceEntry resourceEntry = type.getResourceEntry(entryIndex); + if (resourceEntry == null) { + continue; + } + final ResourceValue currentResourceValue = resourceEntry.value; + if (currentResourceValue == null) { + continue; + } + // cyclic reference detect + if (currentResourceValue instanceof ResourceValue.ReferenceResourceValue) { + if (resourceId == ((ResourceValue.ReferenceResourceValue) currentResourceValue) + .getReferenceResourceId()) { + continue; + } + } + result.add(new Resource(typeSpec, type, resourceEntry)); + } + return result; + } + + /** + * contains all info for one resource + */ + public static class Resource { + @Nullable + public final TypeSpec typeSpec; + @NonNull + public final Type type; + @NonNull + public final ResourceEntry resourceEntry; + + public Resource(final @Nullable TypeSpec typeSpec, final @NonNull Type type, final @NonNull ResourceEntry resourceEntry) { + this.typeSpec = typeSpec; + this.type = type; + this.resourceEntry = resourceEntry; + } + + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java new file mode 100644 index 00000000..3519b513 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java @@ -0,0 +1,32 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.struct.ChunkHeader; +import net.dongliu.apk.parser.struct.ChunkType; +import net.dongliu.apk.parser.utils.Buffers; +import net.dongliu.apk.parser.utils.Unsigned; + +import java.nio.ByteBuffer; + +/** + * resource file header + * + * @author dongliu + */ +public class ResourceTableHeader extends ChunkHeader { + /** + * The number of ResTable_package structures. uint32 + */ + private final int packageCount; + + public ResourceTableHeader(final int headerSize, final int chunkSize, final @NonNull ByteBuffer buffer) { + super(ChunkType.TABLE, headerSize, chunkSize); + this.packageCount = Unsigned.toUInt(Buffers.readUInt(buffer)); + } + + public long getPackageCount() { + return Unsigned.toLong(this.packageCount); + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java new file mode 100644 index 00000000..a6da3e53 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java @@ -0,0 +1,149 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.ResourceValue; + +/** + * @author dongliu + */ +public class ResourceTableMap { + /** + * ...elided + * ResTable_ref; unit32 + */ + private long nameRef; + + private ResourceValue resValue; + private String data; + + public long getNameRef() { + return this.nameRef; + } + + public void setNameRef(final long nameRef) { + this.nameRef = nameRef; + } + + public ResourceValue getResValue() { + return this.resValue; + } + + public void setResValue(final @Nullable ResourceValue resValue) { + this.resValue = resValue; + } + + public String getData() { + return this.data; + } + + public void setData(final String data) { + this.data = data; + } + + @NonNull + @Override + public String toString() { + return this.data; + } + + public static class MapAttr { + @SuppressWarnings("PointlessBitwiseExpression") + public static final int TYPE = 0x01000000 | (0 & 0xFFFF); + + /** + * For integral attributes; this is the minimum value it can hold. + */ + public static final int MIN = 0x01000000 | (1 & 0xFFFF); + + /** + * For integral attributes; this is the maximum value it can hold. + */ + public static final int MAX = 0x01000000 | (2 & 0xFFFF); + + /** + * Localization of this resource is can be encouraged or required with + * an aapt flag if this is set + */ + public static final int L10N = 0x01000000 | (3 & 0xFFFF); + + /** + * for plural support; see android.content.res.PluralRules#attrForQuantity(int) + */ + public static final int OTHER = 0x01000000 | (4 & 0xFFFF); + public static final int ZERO = 0x01000000 | (5 & 0xFFFF); + public static final int ONE = 0x01000000 | (6 & 0xFFFF); + public static final int TWO = 0x01000000 | (7 & 0xFFFF); + public static final int FEW = 0x01000000 | (8 & 0xFFFF); + public static final int MANY = 0x01000000 | (9 & 0xFFFF); + + public static int makeArray(final int entry) { + return (0x02000000 | (entry & 0xFFFF)); + } + + } + + public static class AttributeType { + /** + * No type has been defined for this attribute; use generic + * type handling. The low 16 bits are for types that can be + * handled generically; the upper 16 require additional information + * in the bag so can not be handled generically for ANY. + */ + public static final int ANY = 0x0000FFFF; + + /** + * Attribute holds a references to another resource. + */ + public static final int REFERENCE = 1; + + /** + * Attribute holds a generic string. + */ + public static final int STRING = 1 << 1; + + /** + * Attribute holds an integer value. ATTR_MIN and ATTR_MIN can + * optionally specify a constrained range of possible integer values. + */ + public static final int INTEGER = 1 << 2; + + /** + * Attribute holds a boolean integer. + */ + public static final int BOOLEAN = 1 << 3; + + /** + * Attribute holds a color value. + */ + public static final int COLOR = 1 << 4; + + /** + * Attribute holds a floating point value. + */ + public static final int FLOAT = 1 << 5; + + /** + * Attribute holds a dimension value; such as "20px". + */ + public static final int DIMENSION = 1 << 6; + + /** + * Attribute holds a fraction value; such as "20%". + */ + public static final int FRACTION = 1 << 7; + + /** + * Attribute holds an enumeration. The enumeration values are + * supplied as additional entries in the map. + */ + public static final int ENUM = 1 << 16; + + /** + * Attribute holds a bitmaks of flags. The flag bit values are + * supplied as additional entries in the map. + */ + public static final int FLAGS = 1 << 17; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java new file mode 100644 index 00000000..fe2cb151 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java @@ -0,0 +1,150 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.ResourceValue; +import net.dongliu.apk.parser.struct.StringPool; +import net.dongliu.apk.parser.utils.Buffers; +import net.dongliu.apk.parser.utils.ParseUtils; + +import java.nio.ByteBuffer; +import java.util.Locale; + +/** + * @author dongliu + */ +public class Type { + private String name; + public final short id; + + @NonNull + public final Locale locale; + + private StringPool keyStringPool; + private ByteBuffer buffer; + private long[] offsets; + private StringPool stringPool; + + /** + * see Densities.java for values + */ + public final int density; + + public Type(final @NonNull TypeHeader header) { + this.id = header.getId(); + final ResTableConfig config = header.config; + this.locale = new Locale(config.getLanguage(), config.getCountry()); + this.density = config.getDensity(); + } + + @Nullable + public ResourceEntry getResourceEntry(final int resId) { + if (resId >= this.offsets.length) { + return null; + } + if (this.offsets[resId] == TypeHeader.NO_ENTRY) { + return null; + } + if( offsets[resId] >= buffer.limit() ) { + //System.out.println( "invalid offset: " + offsets[resId] ); + return null; + } + // read Resource Entries + Buffers.position(this.buffer, this.offsets[resId]); + return this.readResourceEntry(); + } + + private ResourceEntry readResourceEntry() { + long beginPos = buffer.position(); +// ResourceEntry resourceEntry = new ResourceEntry(); + // size is always 8(simple), or 16(complex) + final int size = Buffers.readUShort(buffer); + final int flags = Buffers.readUShort(buffer); + long keyRef = buffer.getInt(); + + if ((flags & ResourceEntry.FLAG_COMPLEX) != 0) { + String key = keyStringPool.get((int) keyRef); + + // Resource identifier of the parent mapping, or 0 if there is none. + final long parent = Buffers.readUInt(buffer); + final long count = Buffers.readUInt(buffer); + + Buffers.position(buffer, beginPos + size); + + //An individual complex Resource entry comprises an entry immediately followed by one or more fields. + ResourceTableMap[] resourceTableMaps = new ResourceTableMap[(int) count]; + for (int i = 0; i < count; i++) { + resourceTableMaps[i] = readResourceTableMap(); + } +// ResourceEntry resourceEntry = new ResourceEntry(size,flags,key,resourceTableMaps); + ResourceMapEntry resourceMapEntry = new ResourceMapEntry(size,flags,key,parent,count,resourceTableMaps); + return resourceMapEntry; + } else if ((flags & ResourceEntry.FLAG_COMPACT) != 0) { + final ResourceValue value = ResourceValue.string((int) keyRef, stringPool); + return new ResourceEntry(size,flags,null,value); + } else { + String key = keyStringPool.get((int) keyRef); + Buffers.position(buffer, beginPos + size); + final ResourceValue value = ParseUtils.readResValue(buffer, stringPool); + return new ResourceEntry(size,flags,key,value); + } + } + + private ResourceTableMap readResourceTableMap() { + final ResourceTableMap resourceTableMap = new ResourceTableMap(); + resourceTableMap.setNameRef(Buffers.readUInt(this.buffer)); + resourceTableMap.setResValue(ParseUtils.readResValue(this.buffer, this.stringPool)); + //noinspection StatementWithEmptyBody + if ((resourceTableMap.getNameRef() & 0x02000000) != 0) { + //read arrays + } else //noinspection StatementWithEmptyBody + if ((resourceTableMap.getNameRef() & 0x01000000) != 0) { + // read attrs + } else { + } + return resourceTableMap; + } + + public String getName() { + return this.name; + } + + public void setName(final String name) { + this.name = name; + } + + public StringPool getKeyStringPool() { + return this.keyStringPool; + } + + public void setKeyStringPool(final StringPool keyStringPool) { + this.keyStringPool = keyStringPool; + } + + public ByteBuffer getBuffer() { + return this.buffer; + } + + public void setBuffer(final ByteBuffer buffer) { + this.buffer = buffer; + } + + public void setOffsets(final long[] offsets) { + this.offsets = offsets; + } + + public void setStringPool(final StringPool stringPool) { + this.stringPool = stringPool; + } + + @NonNull + @Override + public String toString() { + return "Type{" + + "name='" + this.name + '\'' + + ", id=" + this.id + + ", locale=" + this.locale + + '}'; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java new file mode 100644 index 00000000..e444e57b --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java @@ -0,0 +1,93 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.struct.ChunkHeader; +import net.dongliu.apk.parser.struct.ChunkType; +import net.dongliu.apk.parser.utils.Buffers; +import net.dongliu.apk.parser.utils.Unsigned; + +import java.nio.ByteBuffer; + +/** + * @author dongliu + */ +public class TypeHeader extends ChunkHeader { + + public static final long NO_ENTRY = 0xFFFFFFFFL; + + /** + * The type identifier this chunk is holding. Type IDs start at 1 (corresponding to the value + * of the type bits in a resource identifier). 0 is invalid. + * uint8_t + */ + private final byte id; + + /** + * Must be 0. uint8_t + */ + private final byte flags; + /** + * Must be 0. uint16_t + */ + private final short res; + + /** + * Number of uint32_t entry indices that follow. uint32 + */ + public final int entryCount; + + /** + * Offset from header where ResTable_entry data starts.uint32_t + */ + public final int entriesStart; + + /** + * Configuration this collection of entries is designed for. + */ + @NonNull + public final ResTableConfig config; + + public TypeHeader(final int headerSize, final long chunkSize, @NonNull final ByteBuffer buffer) { + super(ChunkType.TABLE_TYPE, headerSize, chunkSize); + this.id = Unsigned.toUByte(Buffers.readUByte(buffer)); + this.flags = Unsigned.toUByte(Buffers.readUByte(buffer)); + this.res = Unsigned.toUShort(Buffers.readUShort(buffer)); + this.entryCount = Unsigned.ensureUInt(Buffers.readUInt(buffer)); + this.entriesStart = Unsigned.ensureUInt(Buffers.readUInt(buffer)); + this.config = this.readResTableConfig(buffer); + } + + public short getId() { + return Unsigned.toShort(this.id); + } + + public short getFlags() { + return Unsigned.toUShort(this.flags); + } + + public int getRes() { + return Unsigned.toInt(this.res); + } + + @NonNull + private ResTableConfig readResTableConfig(final ByteBuffer buffer) { + final long beginPos = buffer.position(); + final ResTableConfig config = new ResTableConfig(); + final long size = Buffers.readUInt(buffer); + // imsi + config.setMcc(buffer.getShort()); + config.setMnc(buffer.getShort()); + //read locale + config.setLanguage(new String(Buffers.readBytes(buffer, 2)).replace("\0", "")); + config.setCountry(new String(Buffers.readBytes(buffer, 2)).replace("\0", "")); + //screen type + config.setOrientation(Buffers.readUByte(buffer)); + config.setTouchscreen(Buffers.readUByte(buffer)); + config.setDensity(Buffers.readUShort(buffer)); + // now just skip the others... + final long endPos = buffer.position(); + Buffers.skip(buffer, (int) (size - (endPos - beginPos))); + return config; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java new file mode 100644 index 00000000..5facc8b7 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java @@ -0,0 +1,32 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; + +/** + * @author dongliu + */ +public class TypeSpec { + + public final long[] entryFlags; + public final String name; + public final short id; + + public TypeSpec(final @NonNull TypeSpecHeader header, @NonNull final long[] entryFlags, final String name) { + this.id = header.getId(); + this.entryFlags = entryFlags; + this.name = name; + } + + public boolean exists(final int id) { + return id < this.entryFlags.length; + } + + @NonNull + @Override + public String toString() { + return "TypeSpec{" + + "name='" + this.name + '\'' + + ", id=" + this.id + + '}'; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java new file mode 100644 index 00000000..c5e37ab4 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java @@ -0,0 +1,65 @@ +package net.dongliu.apk.parser.struct.resource; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.struct.ChunkHeader; +import net.dongliu.apk.parser.struct.ChunkType; +import net.dongliu.apk.parser.utils.Buffers; +import net.dongliu.apk.parser.utils.Unsigned; + +import java.nio.ByteBuffer; + +/** + * @author dongliu + */ +public class TypeSpecHeader extends ChunkHeader { + + /** + * The type identifier this chunk is holding. Type IDs start at 1 (corresponding to the value + * of the type bits in a resource identifier). 0 is invalid. + * The id also specifies the name of the Resource type. It is the string at index id - 1 in the + * typeStrings StringPool chunk in the containing Package chunk. + * uint8_t + */ + private final byte id; + + /** + * Must be 0. uint8_t + */ + private final byte res0; + + /** + * Must be 0.uint16_t + */ + private final short res1; + + /** + * Number of uint32_t entry configuration masks that follow. + */ + private final int entryCount; + + public TypeSpecHeader(final int headerSize, final long chunkSize, final @NonNull ByteBuffer buffer) { + super(ChunkType.TABLE_TYPE_SPEC, headerSize, chunkSize); + this.id = Unsigned.toUByte(Buffers.readUByte(buffer)); + this.res0 = Unsigned.toUByte(Buffers.readUByte(buffer)); + this.res1 = Unsigned.toUShort(Buffers.readUShort(buffer)); + this.entryCount = Unsigned.ensureUInt(Buffers.readUInt(buffer)); + } + + public short getId() { + return Unsigned.toShort(this.id); + } + + public short getRes0() { + return Unsigned.toShort(this.res0); + } + + public int getRes1() { + return Unsigned.toInt(this.res1); + } + + public int getEntryCount() { + return this.entryCount; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java new file mode 100644 index 00000000..ae232c9c --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java @@ -0,0 +1,23 @@ +package net.dongliu.apk.parser.struct.signingv2; + +import androidx.annotation.NonNull; + +import java.util.List; + +/** + * For read apk signing block + * + * @see apksigning v2 scheme + */ +public class ApkSigningBlock { + public static final int SIGNING_V2_ID = 0x7109871a; + + public static final String MAGIC = "APK Sig Block 42"; + @NonNull + public final List signerBlocks; + + public ApkSigningBlock(final @NonNull List signerBlocks) { + this.signerBlocks = signerBlocks; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java new file mode 100644 index 00000000..b518d3b2 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java @@ -0,0 +1,13 @@ +package net.dongliu.apk.parser.struct.signingv2; + +import androidx.annotation.NonNull; + +public class Digest { + public final int algorithmID; + public final byte[] value; + + public Digest(final int algorithmID, final @NonNull byte[] value) { + this.algorithmID = algorithmID; + this.value = value; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java new file mode 100644 index 00000000..b6a97421 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java @@ -0,0 +1,14 @@ +package net.dongliu.apk.parser.struct.signingv2; + +import androidx.annotation.NonNull; + +public class Signature { + public final int algorithmID; + public final byte[] data; + + public Signature(final int algorithmID, final @NonNull byte[] data) { + this.algorithmID = algorithmID; + this.data = data; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java new file mode 100644 index 00000000..3457acad --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java @@ -0,0 +1,19 @@ +package net.dongliu.apk.parser.struct.signingv2; + +import androidx.annotation.NonNull; + +import java.security.cert.X509Certificate; +import java.util.List; + +public class SignerBlock { + public final List digests; + public final List certificates; + public final List signatures; + + public SignerBlock(final @NonNull List digests, final @NonNull List certificates, final @NonNull List signatures) { + this.digests = digests; + this.certificates = certificates; + this.signatures = signatures; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.kt new file mode 100644 index 00000000..ccf44543 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.kt @@ -0,0 +1,65 @@ +package net.dongliu.apk.parser.struct.xml + +import net.dongliu.apk.parser.struct.ResourceValue +import net.dongliu.apk.parser.struct.resource.ResourceTable +import net.dongliu.apk.parser.utils.ResourceLoader +import java.util.Locale + +/** + * xml node attribute + * + * @author dongliu + */ +class Attribute( + @JvmField val namespace: String, @JvmField val name: String, + /** + * The original raw string value of Attribute + */ + @JvmField val rawValue: String?, + /** + * Processed typed value of Attribute + */ + @JvmField val typedValue: ResourceValue? +) { + /** + * the final value as string + */ + @JvmField + var value: String? = null + + fun toStringValue(resourceTable: ResourceTable, locale: Locale): String? { + val rawValue = rawValue + return if (rawValue != null) { + rawValue + } else { + val typedValue = typedValue + if (typedValue != null) { + typedValue.toStringValue(resourceTable, locale) + } else { + // something happen; + "" + } + } + } + + override fun toString(): String { + return "Attribute{" + + "name='" + name + '\'' + + ", namespace='" + namespace + '\'' + + '}' + } + + /** + * These are attribute resource constants for the platform; as found in android.R.attr + * + * @author dongliu + */ + companion object { + private val ids = ResourceLoader.loadSystemAttrIds() + + @JvmStatic + fun getString(id: Long): String { + return ids[id.toInt()] ?: "AttrId:0x${java.lang.Long.toHexString(id)}" + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.kt new file mode 100644 index 00000000..ebf85bb5 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.kt @@ -0,0 +1,70 @@ +package net.dongliu.apk.parser.struct.xml + +/** + * xml node attributes + * + * @author dongliu + */ +class Attributes(size: Int) { + /** + * return all attributes + */ + @JvmField + val attributes: Array + + init { + attributes = arrayOfNulls(size) + } + + operator fun set(i: Int, attribute: Attribute) { + attributes[i] = attribute + } + + operator fun get(name: String): Attribute? { + //TODO this is an inefficient search. Should probably be using HashMap + var result: Attribute? = null + for (attribute in attributes) { + if (attribute!!.name == name) { + val namespace = attribute.namespace + if (namespace.isEmpty() || namespace == "android" || namespace == "http://schemas.android.com/apk/res/android") { + //prefer default namespace of android. + result = attribute + break + } + if (result == null) + result = attribute + } + } + return result + } + + /** + * Get attribute with name, return value as string + */ + fun getString(name: String): String? { + return this[name]?.value + } + + fun size(): Int { + return attributes.size + } + + fun getBoolean(name: String, b: Boolean): Boolean { + val value = getString(name) + return if (value == null) b else java.lang.Boolean.parseBoolean(value) + } + + fun getInt(name: String): Int? { + val value = getString(name) ?: return null + return if (value.startsWith("0x")) { + Integer.valueOf(value.substring(2), 16) + } else Integer.valueOf(value) + } + + fun getLong(name: String): Long? { + val value = getString(name) ?: return null + return if (value.startsWith("0x")) { + java.lang.Long.valueOf(value.substring(2), 16) + } else java.lang.Long.valueOf(value) + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.kt new file mode 100644 index 00000000..020c3440 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.kt @@ -0,0 +1,11 @@ +package net.dongliu.apk.parser.struct.xml + +import net.dongliu.apk.parser.struct.ChunkHeader + +/** + * Null header. + * + * @author dongliu + */ +class NullHeader(chunkType: Int, headerSize: Int, chunkSize: Long) : + ChunkHeader(chunkType, headerSize, chunkSize) diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java new file mode 100644 index 00000000..8f4cacab --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java @@ -0,0 +1,78 @@ +package net.dongliu.apk.parser.struct.xml; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.struct.ResourceValue; +import net.dongliu.apk.parser.struct.resource.ResourceTable; + +import java.util.Locale; + +/** + * @author dongliu + */ +public class XmlCData { + + public static final String CDATA_START = ""; + + /** + * The raw CDATA character data. + */ + private String data; + + /** + * The typed value of the character data if this is a CDATA node. + */ + private ResourceValue typedData; + + /** + * the final value as string + */ + private String value; + + /** + * get value as string + */ + @NonNull + public String toStringValue(final ResourceTable resourceTable, final Locale locale) { + if (this.data != null) { + return XmlCData.CDATA_START + this.data + XmlCData.CDATA_END; + } else { + return XmlCData.CDATA_START + this.typedData.toStringValue(resourceTable, locale) + XmlCData.CDATA_END; + } + } + + public String getData() { + return this.data; + } + + public void setData(final String data) { + this.data = data; + } + + public ResourceValue getTypedData() { + return this.typedData; + } + + public void setTypedData(final @Nullable ResourceValue typedData) { + this.typedData = typedData; + } + + public String getValue() { + return this.value; + } + + public void setValue(final String value) { + this.value = value; + } + + @NonNull + @Override + public String toString() { + return "XmlCData{" + + "data='" + this.data + '\'' + + ", typedData=" + this.typedData + + '}'; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java new file mode 100644 index 00000000..8d8b32e0 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java @@ -0,0 +1,15 @@ +package net.dongliu.apk.parser.struct.xml; + +import net.dongliu.apk.parser.struct.ChunkHeader; + +/** + * Binary XML header. It is simply a struct ResChunk_header. + * The header.type is always 0×0003 (XML). + * + * @author dongliu + */ +public class XmlHeader extends ChunkHeader { + public XmlHeader(final int chunkType, final int headerSize, final long chunkSize) { + super(chunkType, headerSize, chunkSize); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java new file mode 100644 index 00000000..eb57fc94 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java @@ -0,0 +1,25 @@ +package net.dongliu.apk.parser.struct.xml; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * @author dongliu + */ +public class XmlNamespaceEndTag { + @Nullable + public final String prefix; + @Nullable + public final String uri; + + public XmlNamespaceEndTag(final @Nullable String prefix, final @Nullable String uri) { + this.prefix = prefix; + this.uri = uri; + } + + @NonNull + @Override + public String toString() { + return this.prefix + "=" + this.uri; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java new file mode 100644 index 00000000..43b21f4f --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java @@ -0,0 +1,26 @@ +package net.dongliu.apk.parser.struct.xml; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * @author dongliu + */ +public class XmlNamespaceStartTag { + @Nullable + public final String prefix; + @Nullable + public final String uri; + + public XmlNamespaceStartTag(final @Nullable String prefix, final @Nullable String uri) { + this.prefix = prefix; + this.uri = uri; + } + + + @NonNull + @Override + public String toString() { + return this.prefix + "=" + this.uri; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java new file mode 100644 index 00000000..60beafec --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java @@ -0,0 +1,39 @@ +package net.dongliu.apk.parser.struct.xml; + +import androidx.annotation.NonNull; + +/** + * @author dongliu + */ +public class XmlNodeEndTag { + private String namespace; + private String name; + + public String getNamespace() { + return this.namespace; + } + + public void setNamespace(final String namespace) { + this.namespace = namespace; + } + + public String getName() { + return this.name; + } + + public void setName(final String name) { + this.name = name; + } + + @NonNull + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("'); + return sb.toString(); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java new file mode 100644 index 00000000..f0b67c1f --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java @@ -0,0 +1,29 @@ +package net.dongliu.apk.parser.struct.xml; + +import androidx.annotation.NonNull; + +import net.dongliu.apk.parser.struct.ChunkHeader; +import net.dongliu.apk.parser.utils.Buffers; + +import java.nio.ByteBuffer; + +/** + * @author dongliu + */ +public class XmlNodeHeader extends ChunkHeader { + /** + * Line number in original source file at which this element appeared. + */ + public final int lineNum; + /** + * Optional XML comment string pool ref, -1 if none + */ + public final int commentRef; + + public XmlNodeHeader(final int chunkType, final int headerSize, final long chunkSize, final @NonNull ByteBuffer buffer) { + super(chunkType, headerSize, chunkSize); + this.lineNum = (int) Buffers.readUInt(buffer); + this.commentRef = (int) Buffers.readUInt(buffer); + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java new file mode 100644 index 00000000..3ee71c55 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java @@ -0,0 +1,57 @@ +package net.dongliu.apk.parser.struct.xml; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * @author dongliu + */ +public class XmlNodeStartTag { + @Nullable + public final String namespace; + @Nullable + public final String name; + + // /** +// * Byte offset from the start of this structure where the attributes start. uint16 +// */ +// public int attributeStart; +// /** +// * Size of the ResXMLTree_attribute structures that follow. unit16 +// */ +// public int attributeSize; +// /** +// * Number of attributes associated with an ELEMENT. uint 16 +// * These are available as an array of ResXMLTree_attribute structures immediately following this node. +// */ +// public int attributeCount; +// /** +// * Index (1-based) of the "id" attribute. 0 if none. uint16 +// */ +// public short idIndex; +// /** +// * Index (1-based) of the "style" attribute. 0 if none. uint16 +// */ +// public short styleIndex; + @NonNull + public final Attributes attributes; + + public XmlNodeStartTag(@Nullable final String namespace, final @Nullable String name, final @NonNull Attributes attributes) { + this.namespace = namespace; + this.name = name; + this.attributes = attributes; + } + + @NonNull + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append('<'); + if (this.namespace != null) { + sb.append(this.namespace).append(":"); + } + sb.append(this.name); + sb.append('>'); + return sb.toString(); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.kt new file mode 100644 index 00000000..870b84d9 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.kt @@ -0,0 +1,9 @@ +package net.dongliu.apk.parser.struct.xml + +import net.dongliu.apk.parser.struct.ChunkHeader + +/** + * @author dongliu + */ +class XmlResourceMapHeader(chunkType: Int, headerSize: Int, chunkSize: Long) : + ChunkHeader(chunkType, headerSize, chunkSize) diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java new file mode 100644 index 00000000..8bb6c097 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java @@ -0,0 +1,94 @@ +package net.dongliu.apk.parser.struct.zip; + +/** + * End of central directory record + */ +public class EOCD { + public static final int SIGNATURE = 0x06054b50; + /** + * private int signature; + * Number of this disk + */ + private short diskNum; + /** + * Disk where central directory starts + */ + private short cdStartDisk; + /** + * Number of central directory records on this disk + */ + private short cdRecordNum; + /** + * Total number of central directory records + */ + private short totalCDRecordNum; + /** + * Size of central directory (bytes) + */ + private int cdSize; + /** + * Offset of start of central directory, relative to start of archive + */ + private int cdStart; + /** + * Comment length (n) + */ + private short commentLen; + + public short getDiskNum() { + return this.diskNum; + } + + public void setDiskNum(final int diskNum) { + this.diskNum = (short) diskNum; + } + + public int getCdStartDisk() { + return this.cdStartDisk & 0xffff; + } + + public void setCdStartDisk(final int cdStartDisk) { + this.cdStartDisk = (short) cdStartDisk; + } + + public int getCdRecordNum() { + return this.cdRecordNum & 0xffff; + } + + public void setCdRecordNum(final int cdRecordNum) { + this.cdRecordNum = (short) cdRecordNum; + } + + public int getTotalCDRecordNum() { + return this.totalCDRecordNum & 0xffff; + } + + public void setTotalCDRecordNum(final int totalCDRecordNum) { + this.totalCDRecordNum = (short) totalCDRecordNum; + } + + public long getCdSize() { + return this.cdSize & 0xffffffffL; + } + + public void setCdSize(final long cdSize) { + this.cdSize = (int) cdSize; + } + + public long getCdStart() { + return this.cdStart & 0xffffffffL; + } + + public void setCdStart(final long cdStart) { + this.cdStart = (int) cdStart; + } + + public int getCommentLen() { + return this.commentLen & 0xffff; + } + + public void setCommentLen(final int commentLen) { + this.commentLen = (short) commentLen; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Buffers.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Buffers.java new file mode 100644 index 00000000..56b66b74 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Buffers.java @@ -0,0 +1,133 @@ +package net.dongliu.apk.parser.utils; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * utils method for byte buffer + *

+ * Cast java.nio.ByteBuffer instances where necessary to java.nio.Buffer to avoid NoSuchMethodError + * when running on Java 6 to Java 8. + * The Java 9 ByteBuffer classes introduces overloaded methods with covariant return types the following methods: + * position, limit, flip, clear, mark, reset, rewind, etc. + * + * @author Liu Dong dongliu@live.cn + */ +public class Buffers { + + /** + * get one unsigned byte as short type + */ + public static short readUByte(final @NonNull ByteBuffer buffer) { + final byte b = buffer.get(); + return (short) (b & 0xff); + } + + /** + * get one unsigned short as int type + */ + public static int readUShort(final @NonNull ByteBuffer buffer) { + final short s = buffer.getShort(); + return s & 0xffff; + } + + /** + * get one unsigned int as long type + */ + public static long readUInt(final @NonNull ByteBuffer buffer) { + final int i = buffer.getInt(); + return i & 0xffffffffL; + } + + /** + * get bytes + */ + @NonNull + public static byte[] readBytes(final @NonNull ByteBuffer buffer, final int size) { + final byte[] bytes = new byte[size]; + buffer.get(bytes); + return bytes; + } + + /** + * get all bytes remains + */ + @NonNull + public static byte[] readBytes(final @NonNull ByteBuffer buffer) { + return Buffers.readBytes(buffer, buffer.remaining()); + } + + /** + * Read ascii string ,by len + */ + @NonNull + public static String readAsciiString(final @NonNull ByteBuffer buffer, final int strLen) { + final byte[] bytes = new byte[strLen]; + buffer.get(bytes); + return new String(bytes); + } + + /** + * read utf16 strings, use strLen, not ending 0 char. + */ + @NonNull + public static String readString(final @NonNull ByteBuffer buffer, final int strLen) { + final StringBuilder sb = new StringBuilder(strLen); + for (int i = 0; i < strLen; i++) { + sb.append(buffer.getChar()); + } + return sb.toString(); + } + + /** + * read utf16 strings, ending with 0 char. + */ + @NonNull + public static String readZeroTerminatedString(final @NonNull ByteBuffer buffer, final int strLen) { + final StringBuilder sb = new StringBuilder(strLen); + for (int i = 0; i < strLen; i++) { + final char c = buffer.getChar(); + if (c == '\0') { + Buffers.skip(buffer, (strLen - i - 1) * 2); + break; + } + sb.append(c); + } + return sb.toString(); + } + + /** + * skip count bytes + */ + public static void skip(final @NonNull ByteBuffer buffer, final int count) { + Buffers.position(buffer, buffer.position() + count); + } + + /** + * set position + */ + public static void position(final @NonNull ByteBuffer buffer, final int position) { + buffer.position(position); + } + + /** + * set position + */ + public static void position(final @NonNull ByteBuffer buffer, final long position) { + Buffers.position(buffer, Unsigned.ensureUInt(position)); + } + + /** + * Return one new ByteBuffer from current position, with size, the byte order of new buffer will be set to little endian; + * And advance the original buffer with size. + */ + @NonNull + public static ByteBuffer sliceAndSkip(final @NonNull ByteBuffer buffer, final int size) { + final ByteBuffer buf = buffer.slice().order(ByteOrder.LITTLE_ENDIAN); + final ByteBuffer slice = (ByteBuffer) buf.limit(buf.position() + size); + Buffers.skip(buffer, size); + return slice; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Inputs.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Inputs.kt new file mode 100644 index 00000000..5705791e --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Inputs.kt @@ -0,0 +1,24 @@ +package net.dongliu.apk.parser.utils + +import java.io.* + +object Inputs { + @JvmStatic + @Throws(IOException::class) + fun readAll(inputStream: InputStream): ByteArray { + val buf = ByteArray(1024 * 8) + ByteArrayOutputStream().use { bos -> + var len: Int + while (inputStream.read(buf).also { len = it } != -1) { + bos.write(buf, 0, len) + } + return bos.toByteArray() + } + } + + @JvmStatic + @Throws(IOException::class) + fun readAllAndClose(inputStream: InputStream): ByteArray { + inputStream.use { return readAll(inputStream) } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Locales.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Locales.kt new file mode 100644 index 00000000..8f833d1c --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Locales.kt @@ -0,0 +1,49 @@ +package net.dongliu.apk.parser.utils + +import java.util.Locale + +/** + * @author dongliu + */ +object Locales { + /** + * when do localize, any locale will match this + */ + @JvmField + val any = Locale("", "") + + /** + * How much the given locale match the expected locale. + */ + @JvmStatic + fun match(locale: Locale?, targetLocale: Locale): Int { + if (locale == null) { + return -1 + } + return when { + locale.language == targetLocale.language -> { + when { + locale.country == targetLocale.country -> { + 3 + } + + targetLocale.country.isEmpty() -> { + 2 + } + + else -> { + 0 + } + } + } + + targetLocale.country.isEmpty() || targetLocale.language.isEmpty() -> { + 1 + } + + else -> { + 0 + } + } + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Pair.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Pair.java new file mode 100644 index 00000000..058f215d --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Pair.java @@ -0,0 +1,33 @@ +package net.dongliu.apk.parser.utils; + +/** + * @author Liu Dong {@literal } + */ +public class Pair { + private K left; + private V right; + + public Pair() { + } + + public Pair(final K left, final V right) { + this.left = left; + this.right = right; + } + + public K getLeft() { + return this.left; + } + + public void setLeft(final K left) { + this.left = left; + } + + public V getRight() { + return this.right; + } + + public void setRight(final V right) { + this.right = right; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java new file mode 100644 index 00000000..c354e344 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java @@ -0,0 +1,195 @@ +package net.dongliu.apk.parser.utils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.dongliu.apk.parser.exception.ParserException; +import net.dongliu.apk.parser.parser.StringPoolEntry; +import net.dongliu.apk.parser.struct.ResValue; +import net.dongliu.apk.parser.struct.ResourceValue; +import net.dongliu.apk.parser.struct.StringPool; +import net.dongliu.apk.parser.struct.StringPoolHeader; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * @author dongliu + */ +public class ParseUtils { + + public static final Charset charsetUTF8 = StandardCharsets.UTF_8; + + /** + * read string from input buffer. if get EOF before read enough data, throw IOException. + */ + @NonNull + public static String readString(@NonNull final ByteBuffer buffer, final boolean utf8) { + if (utf8) { + // The lengths are encoded in the same way as for the 16-bit format + // but using 8-bit rather than 16-bit integers. + final int strLen = ParseUtils.readLen(buffer); + final int bytesLen = ParseUtils.readLen(buffer); + final byte[] bytes = Buffers.readBytes(buffer, bytesLen); + final String str = new String(bytes, ParseUtils.charsetUTF8); + // zero + final int trailling = Buffers.readUByte(buffer); + return str; + } else { + // The length is encoded as either one or two 16-bit integers as per the commentRef... + final int strLen = ParseUtils.readLen16(buffer); + final String str = Buffers.readString(buffer, strLen); + // zero + final int trailling = Buffers.readUShort(buffer); + return str; + } + } + + /** + * read utf-16 encoding str, use zero char to end str. + */ + @NonNull + public static String readStringUTF16(@NonNull final ByteBuffer buffer, final int strLen) { + final String str = Buffers.readString(buffer, strLen); + for (int i = 0; i < str.length(); i++) { + final char c = str.charAt(i); + if (c == 0) { + return str.substring(0, i); + } + } + return str; + } + + /** + * read encoding len. + * see StringPool.cpp ENCODE_LENGTH + */ + private static int readLen(@NonNull final ByteBuffer buffer) { + int len = 0; + final int i = Buffers.readUByte(buffer); + if ((i & 0x80) != 0) { + //read one more byte. + len |= (i & 0x7f) << 8; + len += Buffers.readUByte(buffer); + } else { + len = i; + } + return len; + } + + /** + * read encoding len. + * see Stringpool.cpp ENCODE_LENGTH + */ + private static int readLen16(@NonNull final ByteBuffer buffer) { + int len = 0; + final int i = Buffers.readUShort(buffer); + if ((i & 0x8000) != 0) { + len |= (i & 0x7fff) << 16; + len += Buffers.readUShort(buffer); + } else { + len = i; + } + return len; + } + + /** + * read String pool, for apk binary xml file and resource table. + */ + @NonNull + public static StringPool readStringPool(final @NonNull ByteBuffer buffer, final @NonNull StringPoolHeader stringPoolHeader) { + final long beginPos = buffer.position(); + final int[] offsets = new int[stringPoolHeader.getStringCount()]; + // read strings offset + if (stringPoolHeader.getStringCount() > 0) { + for (int idx = 0; idx < stringPoolHeader.getStringCount(); idx++) { + offsets[idx] = Unsigned.toUInt(Buffers.readUInt(buffer)); + } + } + // read flag + // the string index is sorted by the string values if true + final boolean sorted = (stringPoolHeader.getFlags() & StringPoolHeader.SORTED_FLAG) != 0; + // string use utf-8 format if true, otherwise utf-16 + final boolean utf8 = (stringPoolHeader.getFlags() & StringPoolHeader.UTF8_FLAG) != 0; + // read strings. the head and metas have 28 bytes + final long stringPos = beginPos + stringPoolHeader.getStringsStart() - (int) stringPoolHeader.headerSize; + Buffers.position(buffer, stringPos); + final StringPoolEntry[] entries = new StringPoolEntry[offsets.length]; + for (int i = 0; i < offsets.length; i++) { + entries[i] = new StringPoolEntry(i, stringPos + Unsigned.toLong(offsets[i])); + } + String lastStr = null; + long lastOffset = -1; + final StringPool stringPool = new StringPool(stringPoolHeader.getStringCount()); + for (final StringPoolEntry entry : entries) { + if (entry.offset == lastOffset) { + stringPool.set(entry.idx, lastStr); + continue; + } + Buffers.position(buffer, entry.offset); + lastOffset = entry.offset; + final String str = ParseUtils.readString(buffer, utf8); + lastStr = str; + stringPool.set(entry.idx, str); + } + // read styles + //noinspection StatementWithEmptyBody + if (stringPoolHeader.getStyleCount() > 0) { + // now we just skip it + } + Buffers.position(buffer, beginPos + stringPoolHeader.getBodySize()); + return stringPool; + } + + /** + * read res value, convert from different types to string. + */ + @Nullable + public static ResourceValue readResValue(final @NonNull ByteBuffer buffer, final StringPool stringPool) { +// ResValue resValue = new ResValue(); + final int size = Buffers.readUShort(buffer); + final short res0 = Buffers.readUByte(buffer); + final short dataType = Buffers.readUByte(buffer); + switch (dataType) { + case ResValue.ResType.INT_DEC: + return ResourceValue.decimal(buffer.getInt()); + case ResValue.ResType.INT_HEX: + return ResourceValue.hexadecimal(buffer.getInt()); + case ResValue.ResType.STRING: + final int strRef = buffer.getInt(); + if (strRef >= 0) { + return ResourceValue.string(strRef, stringPool); + } else { + return null; + } + case ResValue.ResType.REFERENCE: + case ResValue.ResType.TYPE_DYNAMIC_REFERENCE: + return ResourceValue.reference(buffer.getInt()); + case ResValue.ResType.INT_BOOLEAN: + return ResourceValue.bool(buffer.getInt()); + case ResValue.ResType.NULL: + return ResourceValue.nullValue(); + case ResValue.ResType.INT_COLOR_RGB8: + case ResValue.ResType.INT_COLOR_RGB4: + return ResourceValue.rgb(buffer.getInt(), 6); + case ResValue.ResType.INT_COLOR_ARGB8: + case ResValue.ResType.INT_COLOR_ARGB4: + return ResourceValue.rgb(buffer.getInt(), 8); + case ResValue.ResType.DIMENSION: + return ResourceValue.dimension(buffer.getInt()); + case ResValue.ResType.FRACTION: + return ResourceValue.fraction(buffer.getInt()); + default: + return ResourceValue.raw(buffer.getInt(), dataType); + } + } + + public static void checkChunkType(final int expected, final int real) { + if (expected != real) { + throw new ParserException("Expect chunk type:" + Integer.toHexString(expected) + + ", but got:" + Integer.toHexString(real)); + } + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java new file mode 100644 index 00000000..bdcd2088 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java @@ -0,0 +1,142 @@ +package net.dongliu.apk.parser.utils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * fetch dependency resource file from android source + * + * @author Liu Dong dongliu@live.cn + */ +public class ResourceFetcher { + + /** + * from https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/public.xml + */ + private void fetchSystemAttrIds() + throws IOException, SAXException, ParserConfigurationException { + final String url = "https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/public.xml"; + final String html = this.getUrl(url); + final String xml = this.retrieveCode(html); + if (xml != null) { + this.parseAttributeXml(xml); + } + } + + private void parseAttributeXml(@NonNull final String xml) + throws IOException, ParserConfigurationException, SAXException { + final SAXParserFactory factory = SAXParserFactory.newInstance(); + final SAXParser parser = factory.newSAXParser(); + final List> attrIds = new ArrayList<>(); + final DefaultHandler dh = new DefaultHandler() { + @Override + public void startElement(final String uri, final String localName, final String qName, + final Attributes attributes) { + if (!qName.equals("public")) { + return; + } + final String type = attributes.getValue("type"); + if (type == null) { + return; + } + if (type.equals("attr")) { + //attr ids. + String idStr = attributes.getValue("id"); + if (idStr == null) { + return; + } + final String name = attributes.getValue("name"); + if (idStr.startsWith("0x")) { + idStr = idStr.substring(2); + } + final int id = Integer.parseInt(idStr, 16); + attrIds.add(new Pair<>(id, name)); + } + } + }; + parser.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), dh); + for (final Pair pair : attrIds) { + System.out.printf("%s=%d%n", pair.getRight(), pair.getLeft()); + } + } + + /** + * the android system r style. + * see http://developer.android.com/reference/android/R.style.html + * from https://android.googlesource.com/platform/frameworks/base/+/master/api/current.txt r.style section + */ + private void fetchSystemStyle() throws IOException { + final String url = "https://android.googlesource.com/platform/frameworks/base/+/master/api/current.txt"; + final String html = this.getUrl(url); + final String code = this.retrieveCode(html); + if (code == null) { + System.err.println("code area not found"); + return; + } + final int begin = code.indexOf("R.style"); + final int end = code.indexOf("}", begin); + final String styleCode = code.substring(begin, end); + final String[] lines = styleCode.split("\n"); + for (String line : lines) { + line = line.trim(); + if (line.startsWith("field public static final")) { + line = Strings.substringBefore(line, ";").replace("deprecated ", "") + .substring("field public static final int ".length()).replace("_", "."); + System.out.println(line); + } + } + } + + @NonNull + private String getUrl(final String url) throws IOException { + final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + try { + conn.setRequestMethod("GET"); + conn.setReadTimeout(10000); + conn.setConnectTimeout(10000); + final byte[] bytes = Inputs.readAllAndClose(conn.getInputStream()); + return new String(bytes, StandardCharsets.UTF_8); + } finally { + conn.disconnect(); + } + } + + @Nullable + private String retrieveCode(@NonNull final String html) { + final Matcher matcher = Pattern.compile("

    (.*?)
").matcher(html); + if (matcher.find()) { + final String codeHtml = matcher.group(1); + if (codeHtml == null) + return null; + return codeHtml.replace("", "\n").replaceAll("<[^>]+>", "").replace("<", "<") + .replace(""", "\"").replace(">", ">"); + } else { + return null; + } + } + + public static void main(final String[] args) + throws ParserConfigurationException, SAXException, IOException { + final ResourceFetcher fetcher = new ResourceFetcher(); + fetcher.fetchSystemAttrIds(); + //fetcher.fetchSystemStyle(); + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.kt new file mode 100644 index 00000000..359b222a --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.kt @@ -0,0 +1,64 @@ +package net.dongliu.apk.parser.utils + +import java.io.* + +/** + * methods for load resources. + * + * @author dongliu + */ +object ResourceLoader { + /** + * load system attr ids for parse binary xml. + */ + fun loadSystemAttrIds(): Map { + try { + toReader("/r_values.ini").use { reader -> + val map: MutableMap = HashMap() + while (true) { + val line: String = reader.readLine() ?: break + val items = line.trim().split("=") + if (items.size != 2) { + continue + } + val name = items[0].trim() + val id = Integer.valueOf(items[1].trim()) + map[id] = name + } + return map + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + @JvmStatic + fun loadSystemStyles(): Map { + val map: MutableMap = HashMap() + try { + toReader("/r_styles.ini").use { reader -> + while (true) { + val line = reader.readLine() ?: break + val items = line.trim().split("=") + if (items.size != 2) { + continue + } + val id = Integer.valueOf(items[1].trim()) + val name = items[0].trim() + map[id] = name + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + return map + } + + private fun toReader(path: String): BufferedReader { + return BufferedReader( + InputStreamReader( + ResourceLoader::class.java.getResourceAsStream(path) + ) + ) + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Strings.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Strings.kt new file mode 100644 index 00000000..40a33bc8 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Strings.kt @@ -0,0 +1,65 @@ +package net.dongliu.apk.parser.utils + +object Strings { + /** + * Copied fom commons StringUtils + * + * Joins the elements of the provided `Iterable` into + * a single String containing the provided elements. + */ + @JvmStatic + fun join(iterable: Iterable<*>, separator: String): String? { + return join(iterable.iterator(), separator) + } + + /** + * Copied fom commons StringUtils + */ + fun join(iterator: Iterator<*>, separator: String?): String? { + if (!iterator.hasNext()) { + return "" + } + val first = iterator.next() + if (!iterator.hasNext()) { + return first?.toString() + } + // two or more elements + val buf = StringBuilder(256) + // Java default is 16, probably too small + if (first != null) { + buf.append(first) + } + while (iterator.hasNext()) { + if (separator != null) { + buf.append(separator) + } + val obj = iterator.next() + if (obj != null) { + buf.append(obj) + } + } + return buf.toString() + } + + @JvmStatic + fun isNumeric(cs: CharSequence): Boolean { + if (cs.isEmpty()) { + return false + } + return cs.find { !it.isDigit() } == null + } + + @JvmStatic + fun substringBefore(str: String, separator: String): String { + if (str.isEmpty()) { + return str + } + if (separator.isEmpty()) { + return "" + } + val pos = str.indexOf(separator) + return if (pos == -1) { + str + } else str.substring(0, pos) + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Unsigned.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Unsigned.kt new file mode 100644 index 00000000..9ecd027d --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Unsigned.kt @@ -0,0 +1,52 @@ +package net.dongliu.apk.parser.utils + +/** + * Unsigned utils, for compatible with java6/java7. + */ +object Unsigned { + @JvmStatic + fun toLong(value: Int): Long { + return value.toLong() and 0xffffffffL + } + + @JvmStatic + fun toUInt(value: Long): Int { + return value.toInt() + } + + @JvmStatic + fun toInt(value: Short): Int { + return value.toInt() and 0xffff + } + + @JvmStatic + fun toUShort(value: Int): Short { + return value.toShort() + } + + @JvmStatic + fun ensureUInt(value: Long): Int { + if (value < 0 || value > Int.MAX_VALUE) { + throw ArithmeticException("unsigned integer overflow") + } + return value.toInt() + } + + @JvmStatic + fun ensureULong(value: Long): Long { + if (value < 0) { + throw ArithmeticException("unsigned long overflow") + } + return value + } + + @JvmStatic + fun toShort(value: Byte): Short { + return (value.toInt() and 0xff).toShort() + } + + @JvmStatic + fun toUByte(value: Short): Byte { + return value.toByte() + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java new file mode 100644 index 00000000..16aa135b --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.utils.xml; + +import java.io.IOException; +import java.io.Writer; + +/** + * Executes a sequence of translators one after the other. Execution ends whenever + * the first translator consumes codepoints from the input. + */ +class AggregateTranslator extends CharSequenceTranslator { + + private final CharSequenceTranslator[] translators; + + /** + * Specify the translators to be used at creation time. + * + * @param translators CharSequenceTranslator array to aggregate + */ + public AggregateTranslator(final CharSequenceTranslator... translators) { + this.translators = translators; + } + + /** + * The first translator to consume codepoints from the input is the 'winner'. + * Execution stops with the number of consumed codepoints being returned. + * {@inheritDoc} + */ + @Override + public int translate(final CharSequence input, final int index, final Writer out) throws IOException { + for (final CharSequenceTranslator translator : this.translators) { + final int consumed = translator.translate(input, index, out); + if (consumed != 0) { + return consumed; + } + } + return 0; + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java new file mode 100644 index 00000000..760ead8a --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.utils.xml; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Locale; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * An API for translating text. + * Its core use is to escape and unescape text. Because escaping and unescaping + * is completely contextual, the API does not present two separate signatures. + */ +abstract class CharSequenceTranslator { + + /** + * Translate a set of codepoints, represented by an int index into a CharSequence, + * into another set of codepoints. The number of codepoints consumed must be returned, + * and the only IOExceptions thrown must be from interacting with the Writer so that + * the top level API may reliably ignore StringWriter IOExceptions. + * + * @param input CharSequence that is being translated + * @param index int representing the current point of translation + * @param out Writer to translate the text to + * @return int count of codepoints consumed + * @throws IOException if and only if the Writer produces an IOException + */ + public abstract int translate(CharSequence input, int index, Writer out) throws IOException; + + /** + * Helper for non-Writer usage. + * + * @param input CharSequence to be translated + * @return String output of translation + */ + @Nullable + public final String translate(@Nullable final CharSequence input) { + if (input == null) { + return null; + } + try { + final StringWriter writer = new StringWriter(input.length() * 2); + this.translate(input, writer); + return writer.toString(); + } catch (final IOException ioe) { + // this should never ever happen while writing to a StringWriter + throw new RuntimeException(ioe); + } + } + + /** + * Translate an input onto a Writer. This is intentionally final as its algorithm is + * tightly coupled with the abstract method of this class. + * + * @param input CharSequence that is being translated + * @param out Writer to translate the text to + * @throws IOException if and only if the Writer produces an IOException + */ + public final void translate(final CharSequence input, final Writer out) throws IOException { + if (out == null) { + throw new IllegalArgumentException("The Writer must not be null"); + } + if (input == null) { + return; + } + int pos = 0; + final int len = input.length(); + while (pos < len) { + final int consumed = this.translate(input, pos, out); + if (consumed == 0) { + final char[] c = Character.toChars(Character.codePointAt(input, pos)); + out.write(c); + pos += c.length; + continue; + } + // contract with translators is that they have to understand codepoints + // and they just took care of a surrogate pair + for (int pt = 0; pt < consumed; pt++) { + pos += Character.charCount(Character.codePointAt(input, pos)); + } + } + } + + /** + * Helper method to create a merger of this translator with another set of + * translators. Useful in customizing the standard functionality. + * + * @param translators CharSequenceTranslator array of translators to merge with this one + * @return CharSequenceTranslator merging this translator with the others + */ + @NonNull + public final CharSequenceTranslator with(final CharSequenceTranslator... translators) { + final CharSequenceTranslator[] newArray = new CharSequenceTranslator[translators.length + 1]; + newArray[0] = this; + System.arraycopy(translators, 0, newArray, 1, translators.length); + return new AggregateTranslator(newArray); + } + + /** + *

Returns an upper case hexadecimal String for the given + * character.

+ * + * @param codepoint The codepoint to convert. + * @return An upper case hexadecimal String + */ + public static String hex(final int codepoint) { + return Integer.toHexString(codepoint).toUpperCase(Locale.ENGLISH); + } + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java new file mode 100644 index 00000000..897db93d --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.utils.xml; + +import java.io.IOException; +import java.io.Writer; + +/** + * Helper subclass to CharSequenceTranslator to allow for translations that + * will replace up to one character at a time. + */ +abstract class CodePointTranslator extends CharSequenceTranslator { + + /** + * Implementation of translate that maps onto the abstract translate(int, Writer) method. + * {@inheritDoc} + */ + @Override + public final int translate(final CharSequence input, final int index, final Writer out) throws IOException { + final int codepoint = Character.codePointAt(input, index); + final boolean consumed = this.translate(codepoint, out); + return consumed ? 1 : 0; + } + + /** + * Translate the specified codepoint into another. + * + * @param codepoint int character input to translate + * @param out Writer to optionally push the translated output to + * @return boolean as to whether translation occurred or not + * @throws IOException if and only if the Writer produces an IOException + */ + public abstract boolean translate(int codepoint, Writer out) throws IOException; + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java new file mode 100644 index 00000000..c7934914 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.utils.xml; + +import androidx.annotation.NonNull; + +/** + * Class holding various entity data for HTML and XML - generally for use with + * the LookupTranslator. + * All arrays are of length [*][2]. + */ +public class EntityArrays { + /** + * Mapping to escape the basic XML and HTML character entities. + *

+ * Namely: {@code " & < >} + * + * @return the mapping table + */ + @NonNull + public static String[][] BASIC_ESCAPE() { + return EntityArrays.BASIC_ESCAPE.clone(); + } + + private static final String[][] BASIC_ESCAPE = { + {"\"", """}, // " - double-quote + {"&", "&"}, // & - ampersand + {"<", "<"}, // < - less-than + {">", ">"}, // > - greater-than + }; + + /** + * Mapping to escape the apostrophe character to its XML character entity. + * + * @return the mapping table + */ + @NonNull + public static String[][] APOS_ESCAPE() { + return EntityArrays.APOS_ESCAPE.clone(); + } + + private static final String[][] APOS_ESCAPE = { + {"'", "'"}, // XML apostrophe + }; + +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java new file mode 100644 index 00000000..fed78524 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.utils.xml; + +import java.io.IOException; +import java.io.Writer; +import java.util.HashMap; + +/** + * Translates a value using a lookup table. + */ +class LookupTranslator extends CharSequenceTranslator { + + private final HashMap lookupMap; + private final int shortest; + private final int longest; + + /** + * Define the lookup table to be used in translation + *

+ * Note that, as of Lang 3.1, the key to the lookup table is converted to a + * java.lang.String, while the value remains as a java.lang.CharSequence. + * This is because we need the key to support hashCode and equals(Object), + * allowing it to be the key for a HashMap. See LANG-882. + * + * @param lookup CharSequence[][] table of size [*][2] + */ + public LookupTranslator(final CharSequence[]... lookup) { + this.lookupMap = new HashMap<>(); + int _shortest = Integer.MAX_VALUE; + int _longest = 0; + if (lookup != null) { + for (final CharSequence[] seq : lookup) { + this.lookupMap.put(seq[0].toString(), seq[1]); + final int sz = seq[0].length(); + if (sz < _shortest) { + _shortest = sz; + } + if (sz > _longest) { + _longest = sz; + } + } + } + this.shortest = _shortest; + this.longest = _longest; + } + + /** + * {@inheritDoc} + */ + @Override + public int translate(final CharSequence input, final int index, final Writer out) throws IOException { + int max = this.longest; + if (index + this.longest > input.length()) { + max = input.length() - index; + } + // descend so as to get a greedy algorithm + for (int i = max; i >= this.shortest; i--) { + final CharSequence subSeq = input.subSequence(index, index + i); + final CharSequence result = this.lookupMap.get(subSeq.toString()); + if (result != null) { + out.write(result.toString()); + return i; + } + } + return 0; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java new file mode 100644 index 00000000..9d6146cc --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.utils.xml; + +import java.io.IOException; +import java.io.Writer; + +import androidx.annotation.NonNull; + +/** + * Translates codepoints to their XML numeric entity escaped value. + */ +class NumericEntityEscaper extends CodePointTranslator { + + private final int below; + private final int above; + private final boolean between; + + /** + *

Constructs a NumericEntityEscaper for the specified range. This is + * the underlying method for the other constructors/builders. The below + * and above boundaries are inclusive when between is + * true and exclusive when it is false.

+ * + * @param below int value representing the lowest codepoint boundary + * @param above int value representing the highest codepoint boundary + * @param between whether to escape between the boundaries or outside them + */ + private NumericEntityEscaper(final int below, final int above, final boolean between) { + this.below = below; + this.above = above; + this.between = between; + } + + /** + *

Constructs a NumericEntityEscaper for all characters.

+ */ + public NumericEntityEscaper() { + this(0, Integer.MAX_VALUE, true); + } + + /** + *

Constructs a NumericEntityEscaper below the specified value (exclusive).

+ * + * @param codepoint below which to escape + * @return the newly created {@code NumericEntityEscaper} instance + */ + @NonNull + public static NumericEntityEscaper below(final int codepoint) { + return NumericEntityEscaper.outsideOf(codepoint, Integer.MAX_VALUE); + } + + /** + *

Constructs a NumericEntityEscaper above the specified value (exclusive).

+ * + * @param codepoint above which to escape + * @return the newly created {@code NumericEntityEscaper} instance + */ + @NonNull + public static NumericEntityEscaper above(final int codepoint) { + return NumericEntityEscaper.outsideOf(0, codepoint); + } + + /** + *

Constructs a NumericEntityEscaper between the specified values (inclusive).

+ * + * @param codepointLow above which to escape + * @param codepointHigh below which to escape + * @return the newly created {@code NumericEntityEscaper} instance + */ + @NonNull + public static NumericEntityEscaper between(final int codepointLow, final int codepointHigh) { + return new NumericEntityEscaper(codepointLow, codepointHigh, true); + } + + /** + *

Constructs a NumericEntityEscaper outside of the specified values (exclusive).

+ * + * @param codepointLow below which to escape + * @param codepointHigh above which to escape + * @return the newly created {@code NumericEntityEscaper} instance + */ + @NonNull + public static NumericEntityEscaper outsideOf(final int codepointLow, final int codepointHigh) { + return new NumericEntityEscaper(codepointLow, codepointHigh, false); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean translate(final int codepoint, final Writer out) throws IOException { + if (this.between) { + if (codepoint < this.below || codepoint > this.above) { + return false; + } + } else { + if (codepoint >= this.below && codepoint <= this.above) { + return false; + } + } + + out.write("&#"); + out.write(Integer.toString(codepoint, 10)); + out.write(';'); + return true; + } +} diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java new file mode 100644 index 00000000..8bcd7d4a --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.dongliu.apk.parser.utils.xml; + +import java.io.Writer; + +/** + * Helper subclass to CharSequenceTranslator to remove unpaired surrogates. + */ +class UnicodeUnpairedSurrogateRemover extends CodePointTranslator { + /** + * Implementation of translate that throws out unpaired surrogates. + * {@inheritDoc} + */ + @Override + public boolean translate(final int codepoint, final Writer out) { + // It's a surrogate. Write nothing and say we've translated. + // It's not a surrogate. Don't translate it. + return codepoint >= Character.MIN_SURROGATE && codepoint <= Character.MAX_SURROGATE; + } +} + diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java new file mode 100644 index 00000000..97c63619 --- /dev/null +++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java @@ -0,0 +1,62 @@ +package net.dongliu.apk.parser.utils.xml; + +import androidx.annotation.Nullable; + +/** + * Utils method to escape xml string, copied from apache commons lang3 + * + * @author Liu Dong {@literal } + */ +public class XmlEscaper { + + /** + *

Escapes the characters in a {@code String} using XML entities.

+ */ + @Nullable + public static String escapeXml10(@Nullable final String input) { + return XmlEscaper.ESCAPE_XML10.translate(input); + } + + public static final CharSequenceTranslator ESCAPE_XML10 = + new AggregateTranslator( + new LookupTranslator(EntityArrays.BASIC_ESCAPE()), + new LookupTranslator(EntityArrays.APOS_ESCAPE()), + new LookupTranslator( + new String[][]{ + {"\u0000", ""}, + {"\u0001", ""}, + {"\u0002", ""}, + {"\u0003", ""}, + {"\u0004", ""}, + {"\u0005", ""}, + {"\u0006", ""}, + {"\u0007", ""}, + {"\u0008", ""}, + {"\u000b", ""}, + {"\u000c", ""}, + {"\u000e", ""}, + {"\u000f", ""}, + {"\u0010", ""}, + {"\u0011", ""}, + {"\u0012", ""}, + {"\u0013", ""}, + {"\u0014", ""}, + {"\u0015", ""}, + {"\u0016", ""}, + {"\u0017", ""}, + {"\u0018", ""}, + {"\u0019", ""}, + {"\u001a", ""}, + {"\u001b", ""}, + {"\u001c", ""}, + {"\u001d", ""}, + {"\u001e", ""}, + {"\u001f", ""}, + {"\ufffe", ""}, + {"\uffff", ""} + }), + NumericEntityEscaper.between(0x7f, 0x84), + NumericEntityEscaper.between(0x86, 0x9f), + new UnicodeUnpairedSurrogateRemover() + ); +} diff --git a/modules/apk-parser/src/main/resources/r_styles.ini b/modules/apk-parser/src/main/resources/r_styles.ini new file mode 100644 index 00000000..38d71b6f --- /dev/null +++ b/modules/apk-parser/src/main/resources/r_styles.ini @@ -0,0 +1,720 @@ +[style] +Animation = 16973824 +Animation.Activity = 16973825 +Animation.Dialog = 16973826 +Animation.InputMethod = 16973910 +Animation.Toast = 16973828 +Animation.Translucent = 16973827 +DeviceDefault.ButtonBar = 16974287 +DeviceDefault.ButtonBar.AlertDialog = 16974288 +DeviceDefault.Light.ButtonBar = 16974290 +DeviceDefault.Light.ButtonBar.AlertDialog = 16974291 +DeviceDefault.Light.SegmentedButton = 16974292 +DeviceDefault.SegmentedButton = 16974289 +Holo.ButtonBar = 16974053 +Holo.ButtonBar.AlertDialog = 16974055 +Holo.Light.ButtonBar = 16974054 +Holo.Light.ButtonBar.AlertDialog = 16974056 +Holo.Light.SegmentedButton = 16974058 +Holo.SegmentedButton = 16974057 +MediaButton = 16973879 +MediaButton.Ffwd = 16973883 +MediaButton.Next = 16973881 +MediaButton.Pause = 16973885 +MediaButton.Play = 16973882 +MediaButton.Previous = 16973880 +MediaButton.Rew = 16973884 +TextAppearance = 16973886 +TextAppearance.DeviceDefault = 16974253 +TextAppearance.DeviceDefault.DialogWindowTitle = 16974264 +TextAppearance.DeviceDefault.Inverse = 16974254 +TextAppearance.DeviceDefault.Large = 16974255 +TextAppearance.DeviceDefault.Large.Inverse = 16974256 +TextAppearance.DeviceDefault.Medium = 16974257 +TextAppearance.DeviceDefault.Medium.Inverse = 16974258 +TextAppearance.DeviceDefault.SearchResult.Subtitle = 16974262 +TextAppearance.DeviceDefault.SearchResult.Title = 16974261 +TextAppearance.DeviceDefault.Small = 16974259 +TextAppearance.DeviceDefault.Small.Inverse = 16974260 +TextAppearance.DeviceDefault.Widget = 16974265 +TextAppearance.DeviceDefault.Widget.ActionBar.Menu = 16974286 +TextAppearance.DeviceDefault.Widget.ActionBar.Subtitle = 16974279 +TextAppearance.DeviceDefault.Widget.ActionBar.Subtitle.Inverse = 16974283 +TextAppearance.DeviceDefault.Widget.ActionBar.Title = 16974278 +TextAppearance.DeviceDefault.Widget.ActionBar.Title.Inverse = 16974282 +TextAppearance.DeviceDefault.Widget.ActionMode.Subtitle = 16974281 +TextAppearance.DeviceDefault.Widget.ActionMode.Subtitle.Inverse = 16974285 +TextAppearance.DeviceDefault.Widget.ActionMode.Title = 16974280 +TextAppearance.DeviceDefault.Widget.ActionMode.Title.Inverse = 16974284 +TextAppearance.DeviceDefault.Widget.Button = 16974266 +TextAppearance.DeviceDefault.Widget.DropDownHint = 16974271 +TextAppearance.DeviceDefault.Widget.DropDownItem = 16974272 +TextAppearance.DeviceDefault.Widget.EditText = 16974274 +TextAppearance.DeviceDefault.Widget.IconMenu.Item = 16974267 +TextAppearance.DeviceDefault.Widget.PopupMenu = 16974275 +TextAppearance.DeviceDefault.Widget.PopupMenu.Large = 16974276 +TextAppearance.DeviceDefault.Widget.PopupMenu.Small = 16974277 +TextAppearance.DeviceDefault.Widget.TabWidget = 16974268 +TextAppearance.DeviceDefault.Widget.TextView = 16974269 +TextAppearance.DeviceDefault.Widget.TextView.PopupMenu = 16974270 +TextAppearance.DeviceDefault.Widget.TextView.SpinnerItem = 16974273 +TextAppearance.DeviceDefault.WindowTitle = 16974263 +TextAppearance.DialogWindowTitle = 16973889 +TextAppearance.Holo = 16974075 +TextAppearance.Holo.DialogWindowTitle = 16974103 +TextAppearance.Holo.Inverse = 16974076 +TextAppearance.Holo.Large = 16974077 +TextAppearance.Holo.Large.Inverse = 16974078 +TextAppearance.Holo.Medium = 16974079 +TextAppearance.Holo.Medium.Inverse = 16974080 +TextAppearance.Holo.SearchResult.Subtitle = 16974084 +TextAppearance.Holo.SearchResult.Title = 16974083 +TextAppearance.Holo.Small = 16974081 +TextAppearance.Holo.Small.Inverse = 16974082 +TextAppearance.Holo.Widget = 16974085 +TextAppearance.Holo.Widget.ActionBar.Menu = 16974112 +TextAppearance.Holo.Widget.ActionBar.Subtitle = 16974099 +TextAppearance.Holo.Widget.ActionBar.Subtitle.Inverse = 16974109 +TextAppearance.Holo.Widget.ActionBar.Title = 16974098 +TextAppearance.Holo.Widget.ActionBar.Title.Inverse = 16974108 +TextAppearance.Holo.Widget.ActionMode.Subtitle = 16974101 +TextAppearance.Holo.Widget.ActionMode.Subtitle.Inverse = 16974111 +TextAppearance.Holo.Widget.ActionMode.Title = 16974100 +TextAppearance.Holo.Widget.ActionMode.Title.Inverse = 16974110 +TextAppearance.Holo.Widget.Button = 16974086 +TextAppearance.Holo.Widget.DropDownHint = 16974091 +TextAppearance.Holo.Widget.DropDownItem = 16974092 +TextAppearance.Holo.Widget.EditText = 16974094 +TextAppearance.Holo.Widget.IconMenu.Item = 16974087 +TextAppearance.Holo.Widget.PopupMenu = 16974095 +TextAppearance.Holo.Widget.PopupMenu.Large = 16974096 +TextAppearance.Holo.Widget.PopupMenu.Small = 16974097 +TextAppearance.Holo.Widget.TabWidget = 16974088 +TextAppearance.Holo.Widget.TextView = 16974089 +TextAppearance.Holo.Widget.TextView.PopupMenu = 16974090 +TextAppearance.Holo.Widget.TextView.SpinnerItem = 16974093 +TextAppearance.Holo.WindowTitle = 16974102 +TextAppearance.Inverse = 16973887 +TextAppearance.Large = 16973890 +TextAppearance.Large.Inverse = 16973891 +TextAppearance.Material = 16974317 +TextAppearance.Material.Body1 = 16974320 +TextAppearance.Material.Body2 = 16974319 +TextAppearance.Material.Button = 16974318 +TextAppearance.Material.Caption = 16974321 +TextAppearance.Material.DialogWindowTitle = 16974322 +TextAppearance.Material.Display1 = 16974326 +TextAppearance.Material.Display2 = 16974325 +TextAppearance.Material.Display3 = 16974324 +TextAppearance.Material.Display4 = 16974323 +TextAppearance.Material.Headline = 16974327 +TextAppearance.Material.Inverse = 16974328 +TextAppearance.Material.Large = 16974329 +TextAppearance.Material.Large.Inverse = 16974330 +TextAppearance.Material.Medium = 16974331 +TextAppearance.Material.Medium.Inverse = 16974332 +TextAppearance.Material.Menu = 16974333 +TextAppearance.Material.Notification = 16974334 +TextAppearance.Material.Notification.Emphasis = 16974335 +TextAppearance.Material.Notification.Info = 16974336 +TextAppearance.Material.Notification.Line2 = 16974337 +TextAppearance.Material.Notification.Time = 16974338 +TextAppearance.Material.Notification.Title = 16974339 +TextAppearance.Material.SearchResult.Subtitle = 16974340 +TextAppearance.Material.SearchResult.Title = 16974341 +TextAppearance.Material.Small = 16974342 +TextAppearance.Material.Small.Inverse = 16974343 +TextAppearance.Material.Subhead = 16974344 +TextAppearance.Material.Title = 16974345 +TextAppearance.Material.Widget = 16974347 +TextAppearance.Material.Widget.ActionBar.Menu = 16974348 +TextAppearance.Material.Widget.ActionBar.Subtitle = 16974349 +TextAppearance.Material.Widget.ActionBar.Subtitle.Inverse = 16974350 +TextAppearance.Material.Widget.ActionBar.Title = 16974351 +TextAppearance.Material.Widget.ActionBar.Title.Inverse = 16974352 +TextAppearance.Material.Widget.ActionMode.Subtitle = 16974353 +TextAppearance.Material.Widget.ActionMode.Subtitle.Inverse = 16974354 +TextAppearance.Material.Widget.ActionMode.Title = 16974355 +TextAppearance.Material.Widget.ActionMode.Title.Inverse = 16974356 +TextAppearance.Material.Widget.Button = 16974357 +TextAppearance.Material.Widget.DropDownHint = 16974358 +TextAppearance.Material.Widget.DropDownItem = 16974359 +TextAppearance.Material.Widget.EditText = 16974360 +TextAppearance.Material.Widget.IconMenu.Item = 16974361 +TextAppearance.Material.Widget.PopupMenu = 16974362 +TextAppearance.Material.Widget.PopupMenu.Large = 16974363 +TextAppearance.Material.Widget.PopupMenu.Small = 16974364 +TextAppearance.Material.Widget.TabWidget = 16974365 +TextAppearance.Material.Widget.TextView = 16974366 +TextAppearance.Material.Widget.TextView.PopupMenu = 16974367 +TextAppearance.Material.Widget.TextView.SpinnerItem = 16974368 +TextAppearance.Material.Widget.Toolbar.Subtitle = 16974369 +TextAppearance.Material.Widget.Toolbar.Title = 16974370 +TextAppearance.Material.WindowTitle = 16974346 +TextAppearance.Medium = 16973892 +TextAppearance.Medium.Inverse = 16973893 +TextAppearance.Small = 16973894 +TextAppearance.Small.Inverse = 16973895 +TextAppearance.StatusBar.EventContent = 16973927 +TextAppearance.StatusBar.EventContent.Title = 16973928 +TextAppearance.StatusBar.Icon = 16973926 +TextAppearance.StatusBar.Title = 16973925 +TextAppearance.SuggestionHighlight = 16974104 +TextAppearance.Theme = 16973888 +TextAppearance.Theme.Dialog = 16973896 +TextAppearance.Widget = 16973897 +TextAppearance.Widget.Button = 16973898 +TextAppearance.Widget.DropDownHint = 16973904 +TextAppearance.Widget.DropDownItem = 16973905 +TextAppearance.Widget.EditText = 16973900 +TextAppearance.Widget.IconMenu.Item = 16973899 +TextAppearance.Widget.PopupMenu.Large = 16973952 +TextAppearance.Widget.PopupMenu.Small = 16973953 +TextAppearance.Widget.TabWidget = 16973901 +TextAppearance.Widget.TextView = 16973902 +TextAppearance.Widget.TextView.PopupMenu = 16973903 +TextAppearance.Widget.TextView.SpinnerItem = 16973906 +TextAppearance.WindowTitle = 16973907 +Theme = 16973829 +ThemeOverlay = 16974407 +ThemeOverlay.Material = 16974408 +ThemeOverlay.Material.ActionBar = 16974409 +ThemeOverlay.Material.Dark = 16974411 +ThemeOverlay.Material.Dark.ActionBar = 16974412 +ThemeOverlay.Material.Light = 16974410 +Theme.Black = 16973832 +Theme.Black.NoTitleBar = 16973833 +Theme.Black.NoTitleBar.Fullscreen = 16973834 +Theme.DeviceDefault = 16974120 +Theme.DeviceDefault.Dialog = 16974126 +Theme.DeviceDefault.DialogWhenLarge = 16974134 +Theme.DeviceDefault.DialogWhenLarge.NoActionBar = 16974135 +Theme.DeviceDefault.Dialog.Alert = 16974545 +Theme.DeviceDefault.Dialog.MinWidth = 16974127 +Theme.DeviceDefault.Dialog.NoActionBar = 16974128 +Theme.DeviceDefault.Dialog.NoActionBar.MinWidth = 16974129 +Theme.DeviceDefault.InputMethod = 16974142 +Theme.DeviceDefault.Light = 16974123 +Theme.DeviceDefault.Light.DarkActionBar = 16974143 +Theme.DeviceDefault.Light.Dialog = 16974130 +Theme.DeviceDefault.Light.DialogWhenLarge = 16974136 +Theme.DeviceDefault.Light.DialogWhenLarge.NoActionBar = 16974137 +Theme.DeviceDefault.Light.Dialog.Alert = 16974546 +Theme.DeviceDefault.Light.Dialog.MinWidth = 16974131 +Theme.DeviceDefault.Light.Dialog.NoActionBar = 16974132 +Theme.DeviceDefault.Light.Dialog.NoActionBar.MinWidth = 16974133 +Theme.DeviceDefault.Light.NoActionBar = 16974124 +Theme.DeviceDefault.Light.NoActionBar.Fullscreen = 16974125 +Theme.DeviceDefault.Light.NoActionBar.Overscan = 16974304 +Theme.DeviceDefault.Light.NoActionBar.TranslucentDecor = 16974308 +Theme.DeviceDefault.Light.Panel = 16974139 +Theme.DeviceDefault.NoActionBar = 16974121 +Theme.DeviceDefault.NoActionBar.Fullscreen = 16974122 +Theme.DeviceDefault.NoActionBar.Overscan = 16974303 +Theme.DeviceDefault.NoActionBar.TranslucentDecor = 16974307 +Theme.DeviceDefault.Panel = 16974138 +Theme.DeviceDefault.Settings = 16974371 +Theme.DeviceDefault.Wallpaper = 16974140 +Theme.DeviceDefault.Wallpaper.NoTitleBar = 16974141 +Theme.Dialog = 16973835 +Theme.Holo = 16973931 +Theme.Holo.Dialog = 16973935 +Theme.Holo.DialogWhenLarge = 16973943 +Theme.Holo.DialogWhenLarge.NoActionBar = 16973944 +Theme.Holo.Dialog.MinWidth = 16973936 +Theme.Holo.Dialog.NoActionBar = 16973937 +Theme.Holo.Dialog.NoActionBar.MinWidth = 16973938 +Theme.Holo.InputMethod = 16973951 +Theme.Holo.Light = 16973934 +Theme.Holo.Light.DarkActionBar = 16974105 +Theme.Holo.Light.Dialog = 16973939 +Theme.Holo.Light.DialogWhenLarge = 16973945 +Theme.Holo.Light.DialogWhenLarge.NoActionBar = 16973946 +Theme.Holo.Light.Dialog.MinWidth = 16973940 +Theme.Holo.Light.Dialog.NoActionBar = 16973941 +Theme.Holo.Light.Dialog.NoActionBar.MinWidth = 16973942 +Theme.Holo.Light.NoActionBar = 16974064 +Theme.Holo.Light.NoActionBar.Fullscreen = 16974065 +Theme.Holo.Light.NoActionBar.Overscan = 16974302 +Theme.Holo.Light.NoActionBar.TranslucentDecor = 16974306 +Theme.Holo.Light.Panel = 16973948 +Theme.Holo.NoActionBar = 16973932 +Theme.Holo.NoActionBar.Fullscreen = 16973933 +Theme.Holo.NoActionBar.Overscan = 16974301 +Theme.Holo.NoActionBar.TranslucentDecor = 16974305 +Theme.Holo.Panel = 16973947 +Theme.Holo.Wallpaper = 16973949 +Theme.Holo.Wallpaper.NoTitleBar = 16973950 +Theme.InputMethod = 16973908 +Theme.Light = 16973836 +Theme.Light.NoTitleBar = 16973837 +Theme.Light.NoTitleBar.Fullscreen = 16973838 +Theme.Light.Panel = 16973914 +Theme.Light.WallpaperSettings = 16973922 +Theme.Material = 16974372 +Theme.Material.Dialog = 16974373 +Theme.Material.DialogWhenLarge = 16974379 +Theme.Material.DialogWhenLarge.NoActionBar = 16974380 +Theme.Material.Dialog.Alert = 16974374 +Theme.Material.Dialog.MinWidth = 16974375 +Theme.Material.Dialog.NoActionBar = 16974376 +Theme.Material.Dialog.NoActionBar.MinWidth = 16974377 +Theme.Material.Dialog.Presentation = 16974378 +Theme.Material.InputMethod = 16974381 +Theme.Material.Light = 16974391 +Theme.Material.Light.DarkActionBar = 16974392 +Theme.Material.Light.Dialog = 16974393 +Theme.Material.Light.DialogWhenLarge = 16974399 +Theme.Material.Light.DialogWhenLarge.NoActionBar = 16974400 +Theme.Material.Light.Dialog.Alert = 16974394 +Theme.Material.Light.Dialog.MinWidth = 16974395 +Theme.Material.Light.Dialog.NoActionBar = 16974396 +Theme.Material.Light.Dialog.NoActionBar.MinWidth = 16974397 +Theme.Material.Light.Dialog.Presentation = 16974398 +Theme.Material.Light.NoActionBar = 16974401 +Theme.Material.Light.NoActionBar.Fullscreen = 16974402 +Theme.Material.Light.NoActionBar.Overscan = 16974403 +Theme.Material.Light.NoActionBar.TranslucentDecor = 16974404 +Theme.Material.Light.Panel = 16974405 +Theme.Material.Light.Voice = 16974406 +Theme.Material.NoActionBar = 16974382 +Theme.Material.NoActionBar.Fullscreen = 16974383 +Theme.Material.NoActionBar.Overscan = 16974384 +Theme.Material.NoActionBar.TranslucentDecor = 16974385 +Theme.Material.Panel = 16974386 +Theme.Material.Settings = 16974387 +Theme.Material.Voice = 16974388 +Theme.Material.Wallpaper = 16974389 +Theme.Material.Wallpaper.NoTitleBar = 16974390 +Theme.NoDisplay = 16973909 +Theme.NoTitleBar = 16973830 +Theme.NoTitleBar.Fullscreen = 16973831 +Theme.NoTitleBar.OverlayActionModes = 16973930 +Theme.Panel = 16973913 +Theme.Translucent = 16973839 +Theme.Translucent.NoTitleBar = 16973840 +Theme.Translucent.NoTitleBar.Fullscreen = 16973841 +Theme.Wallpaper = 16973918 +Theme.WallpaperSettings = 16973921 +Theme.Wallpaper.NoTitleBar = 16973919 +Theme.Wallpaper.NoTitleBar.Fullscreen = 16973920 +Theme.WithActionBar = 16973929 +Widget = 16973842 +Widget.AbsListView = 16973843 +Widget.ActionBar = 16973954 +Widget.ActionBar.TabBar = 16974068 +Widget.ActionBar.TabText = 16974067 +Widget.ActionBar.TabView = 16974066 +Widget.ActionButton = 16973956 +Widget.ActionButton.CloseMode = 16973960 +Widget.ActionButton.Overflow = 16973959 +Widget.AutoCompleteTextView = 16973863 +Widget.Button = 16973844 +Widget.Button.Inset = 16973845 +Widget.Button.Small = 16973846 +Widget.Button.Toggle = 16973847 +Widget.CalendarView = 16974059 +Widget.CompoundButton = 16973848 +Widget.CompoundButton.CheckBox = 16973849 +Widget.CompoundButton.RadioButton = 16973850 +Widget.CompoundButton.Star = 16973851 +Widget.DatePicker = 16974062 +Widget.DeviceDefault = 16974144 +Widget.DeviceDefault.ActionBar = 16974187 +Widget.DeviceDefault.ActionBar.Solid = 16974195 +Widget.DeviceDefault.ActionBar.TabBar = 16974194 +Widget.DeviceDefault.ActionBar.TabText = 16974193 +Widget.DeviceDefault.ActionBar.TabView = 16974192 +Widget.DeviceDefault.ActionButton = 16974182 +Widget.DeviceDefault.ActionButton.CloseMode = 16974186 +Widget.DeviceDefault.ActionButton.Overflow = 16974183 +Widget.DeviceDefault.ActionButton.TextButton = 16974184 +Widget.DeviceDefault.ActionMode = 16974185 +Widget.DeviceDefault.AutoCompleteTextView = 16974151 +Widget.DeviceDefault.Button = 16974145 +Widget.DeviceDefault.Button.Borderless = 16974188 +Widget.DeviceDefault.Button.Borderless.Small = 16974149 +Widget.DeviceDefault.Button.Inset = 16974147 +Widget.DeviceDefault.Button.Small = 16974146 +Widget.DeviceDefault.Button.Toggle = 16974148 +Widget.DeviceDefault.CalendarView = 16974190 +Widget.DeviceDefault.CheckedTextView = 16974299 +Widget.DeviceDefault.CompoundButton.CheckBox = 16974152 +Widget.DeviceDefault.CompoundButton.RadioButton = 16974169 +Widget.DeviceDefault.CompoundButton.Star = 16974173 +Widget.DeviceDefault.DatePicker = 16974191 +Widget.DeviceDefault.DropDownItem = 16974177 +Widget.DeviceDefault.DropDownItem.Spinner = 16974178 +Widget.DeviceDefault.EditText = 16974154 +Widget.DeviceDefault.ExpandableListView = 16974155 +Widget.DeviceDefault.FastScroll = 16974313 +Widget.DeviceDefault.GridView = 16974156 +Widget.DeviceDefault.HorizontalScrollView = 16974171 +Widget.DeviceDefault.ImageButton = 16974157 +Widget.DeviceDefault.Light = 16974196 +Widget.DeviceDefault.Light.ActionBar = 16974243 +Widget.DeviceDefault.Light.ActionBar.Solid = 16974247 +Widget.DeviceDefault.Light.ActionBar.Solid.Inverse = 16974248 +Widget.DeviceDefault.Light.ActionBar.TabBar = 16974246 +Widget.DeviceDefault.Light.ActionBar.TabBar.Inverse = 16974249 +Widget.DeviceDefault.Light.ActionBar.TabText = 16974245 +Widget.DeviceDefault.Light.ActionBar.TabText.Inverse = 16974251 +Widget.DeviceDefault.Light.ActionBar.TabView = 16974244 +Widget.DeviceDefault.Light.ActionBar.TabView.Inverse = 16974250 +Widget.DeviceDefault.Light.ActionButton = 16974239 +Widget.DeviceDefault.Light.ActionButton.CloseMode = 16974242 +Widget.DeviceDefault.Light.ActionButton.Overflow = 16974240 +Widget.DeviceDefault.Light.ActionMode = 16974241 +Widget.DeviceDefault.Light.ActionMode.Inverse = 16974252 +Widget.DeviceDefault.Light.AutoCompleteTextView = 16974203 +Widget.DeviceDefault.Light.Button = 16974197 +Widget.DeviceDefault.Light.Button.Borderless.Small = 16974201 +Widget.DeviceDefault.Light.Button.Inset = 16974199 +Widget.DeviceDefault.Light.Button.Small = 16974198 +Widget.DeviceDefault.Light.Button.Toggle = 16974200 +Widget.DeviceDefault.Light.CalendarView = 16974238 +Widget.DeviceDefault.Light.CheckedTextView = 16974300 +Widget.DeviceDefault.Light.CompoundButton.CheckBox = 16974204 +Widget.DeviceDefault.Light.CompoundButton.RadioButton = 16974224 +Widget.DeviceDefault.Light.CompoundButton.Star = 16974228 +Widget.DeviceDefault.Light.DropDownItem = 16974232 +Widget.DeviceDefault.Light.DropDownItem.Spinner = 16974233 +Widget.DeviceDefault.Light.EditText = 16974206 +Widget.DeviceDefault.Light.ExpandableListView = 16974207 +Widget.DeviceDefault.Light.FastScroll = 16974315 +Widget.DeviceDefault.Light.GridView = 16974208 +Widget.DeviceDefault.Light.HorizontalScrollView = 16974226 +Widget.DeviceDefault.Light.ImageButton = 16974209 +Widget.DeviceDefault.Light.ListPopupWindow = 16974235 +Widget.DeviceDefault.Light.ListView = 16974210 +Widget.DeviceDefault.Light.ListView.DropDown = 16974205 +Widget.DeviceDefault.Light.MediaRouteButton = 16974296 +Widget.DeviceDefault.Light.PopupMenu = 16974236 +Widget.DeviceDefault.Light.PopupWindow = 16974211 +Widget.DeviceDefault.Light.ProgressBar = 16974212 +Widget.DeviceDefault.Light.ProgressBar.Horizontal = 16974213 +Widget.DeviceDefault.Light.ProgressBar.Inverse = 16974217 +Widget.DeviceDefault.Light.ProgressBar.Large = 16974216 +Widget.DeviceDefault.Light.ProgressBar.Large.Inverse = 16974219 +Widget.DeviceDefault.Light.ProgressBar.Small = 16974214 +Widget.DeviceDefault.Light.ProgressBar.Small.Inverse = 16974218 +Widget.DeviceDefault.Light.ProgressBar.Small.Title = 16974215 +Widget.DeviceDefault.Light.RatingBar = 16974221 +Widget.DeviceDefault.Light.RatingBar.Indicator = 16974222 +Widget.DeviceDefault.Light.RatingBar.Small = 16974223 +Widget.DeviceDefault.Light.ScrollView = 16974225 +Widget.DeviceDefault.Light.SeekBar = 16974220 +Widget.DeviceDefault.Light.Spinner = 16974227 +Widget.DeviceDefault.Light.StackView = 16974316 +Widget.DeviceDefault.Light.Tab = 16974237 +Widget.DeviceDefault.Light.TabWidget = 16974229 +Widget.DeviceDefault.Light.TextView = 16974202 +Widget.DeviceDefault.Light.TextView.SpinnerItem = 16974234 +Widget.DeviceDefault.Light.WebTextView = 16974230 +Widget.DeviceDefault.Light.WebView = 16974231 +Widget.DeviceDefault.ListPopupWindow = 16974180 +Widget.DeviceDefault.ListView = 16974158 +Widget.DeviceDefault.ListView.DropDown = 16974153 +Widget.DeviceDefault.MediaRouteButton = 16974295 +Widget.DeviceDefault.PopupMenu = 16974181 +Widget.DeviceDefault.PopupWindow = 16974159 +Widget.DeviceDefault.ProgressBar = 16974160 +Widget.DeviceDefault.ProgressBar.Horizontal = 16974161 +Widget.DeviceDefault.ProgressBar.Large = 16974164 +Widget.DeviceDefault.ProgressBar.Small = 16974162 +Widget.DeviceDefault.ProgressBar.Small.Title = 16974163 +Widget.DeviceDefault.RatingBar = 16974166 +Widget.DeviceDefault.RatingBar.Indicator = 16974167 +Widget.DeviceDefault.RatingBar.Small = 16974168 +Widget.DeviceDefault.ScrollView = 16974170 +Widget.DeviceDefault.SeekBar = 16974165 +Widget.DeviceDefault.Spinner = 16974172 +Widget.DeviceDefault.StackView = 16974314 +Widget.DeviceDefault.Tab = 16974189 +Widget.DeviceDefault.TabWidget = 16974174 +Widget.DeviceDefault.TextView = 16974150 +Widget.DeviceDefault.TextView.SpinnerItem = 16974179 +Widget.DeviceDefault.WebTextView = 16974175 +Widget.DeviceDefault.WebView = 16974176 +Widget.DropDownItem = 16973867 +Widget.DropDownItem.Spinner = 16973868 +Widget.EditText = 16973859 +Widget.ExpandableListView = 16973860 +Widget.FastScroll = 16974309 +Widget.FragmentBreadCrumbs = 16973961 +Widget.Gallery = 16973877 +Widget.GridView = 16973874 +Widget.Holo = 16973962 +Widget.Holo.ActionBar = 16974004 +Widget.Holo.ActionBar.Solid = 16974113 +Widget.Holo.ActionBar.TabBar = 16974071 +Widget.Holo.ActionBar.TabText = 16974070 +Widget.Holo.ActionBar.TabView = 16974069 +Widget.Holo.ActionButton = 16973999 +Widget.Holo.ActionButton.CloseMode = 16974003 +Widget.Holo.ActionButton.Overflow = 16974000 +Widget.Holo.ActionButton.TextButton = 16974001 +Widget.Holo.ActionMode = 16974002 +Widget.Holo.AutoCompleteTextView = 16973968 +Widget.Holo.Button = 16973963 +Widget.Holo.Button.Borderless = 16974050 +Widget.Holo.Button.Borderless.Small = 16974106 +Widget.Holo.Button.Inset = 16973965 +Widget.Holo.Button.Small = 16973964 +Widget.Holo.Button.Toggle = 16973966 +Widget.Holo.CalendarView = 16974060 +Widget.Holo.CheckedTextView = 16974297 +Widget.Holo.CompoundButton.CheckBox = 16973969 +Widget.Holo.CompoundButton.RadioButton = 16973986 +Widget.Holo.CompoundButton.Star = 16973990 +Widget.Holo.DatePicker = 16974063 +Widget.Holo.DropDownItem = 16973994 +Widget.Holo.DropDownItem.Spinner = 16973995 +Widget.Holo.EditText = 16973971 +Widget.Holo.ExpandableListView = 16973972 +Widget.Holo.GridView = 16973973 +Widget.Holo.HorizontalScrollView = 16973988 +Widget.Holo.ImageButton = 16973974 +Widget.Holo.Light = 16974005 +Widget.Holo.Light.ActionBar = 16974049 +Widget.Holo.Light.ActionBar.Solid = 16974114 +Widget.Holo.Light.ActionBar.Solid.Inverse = 16974115 +Widget.Holo.Light.ActionBar.TabBar = 16974074 +Widget.Holo.Light.ActionBar.TabBar.Inverse = 16974116 +Widget.Holo.Light.ActionBar.TabText = 16974073 +Widget.Holo.Light.ActionBar.TabText.Inverse = 16974118 +Widget.Holo.Light.ActionBar.TabView = 16974072 +Widget.Holo.Light.ActionBar.TabView.Inverse = 16974117 +Widget.Holo.Light.ActionButton = 16974045 +Widget.Holo.Light.ActionButton.CloseMode = 16974048 +Widget.Holo.Light.ActionButton.Overflow = 16974046 +Widget.Holo.Light.ActionMode = 16974047 +Widget.Holo.Light.ActionMode.Inverse = 16974119 +Widget.Holo.Light.AutoCompleteTextView = 16974011 +Widget.Holo.Light.Button = 16974006 +Widget.Holo.Light.Button.Borderless.Small = 16974107 +Widget.Holo.Light.Button.Inset = 16974008 +Widget.Holo.Light.Button.Small = 16974007 +Widget.Holo.Light.Button.Toggle = 16974009 +Widget.Holo.Light.CalendarView = 16974061 +Widget.Holo.Light.CheckedTextView = 16974298 +Widget.Holo.Light.CompoundButton.CheckBox = 16974012 +Widget.Holo.Light.CompoundButton.RadioButton = 16974032 +Widget.Holo.Light.CompoundButton.Star = 16974036 +Widget.Holo.Light.DropDownItem = 16974040 +Widget.Holo.Light.DropDownItem.Spinner = 16974041 +Widget.Holo.Light.EditText = 16974014 +Widget.Holo.Light.ExpandableListView = 16974015 +Widget.Holo.Light.GridView = 16974016 +Widget.Holo.Light.HorizontalScrollView = 16974034 +Widget.Holo.Light.ImageButton = 16974017 +Widget.Holo.Light.ListPopupWindow = 16974043 +Widget.Holo.Light.ListView = 16974018 +Widget.Holo.Light.ListView.DropDown = 16974013 +Widget.Holo.Light.MediaRouteButton = 16974294 +Widget.Holo.Light.PopupMenu = 16974044 +Widget.Holo.Light.PopupWindow = 16974019 +Widget.Holo.Light.ProgressBar = 16974020 +Widget.Holo.Light.ProgressBar.Horizontal = 16974021 +Widget.Holo.Light.ProgressBar.Inverse = 16974025 +Widget.Holo.Light.ProgressBar.Large = 16974024 +Widget.Holo.Light.ProgressBar.Large.Inverse = 16974027 +Widget.Holo.Light.ProgressBar.Small = 16974022 +Widget.Holo.Light.ProgressBar.Small.Inverse = 16974026 +Widget.Holo.Light.ProgressBar.Small.Title = 16974023 +Widget.Holo.Light.RatingBar = 16974029 +Widget.Holo.Light.RatingBar.Indicator = 16974030 +Widget.Holo.Light.RatingBar.Small = 16974031 +Widget.Holo.Light.ScrollView = 16974033 +Widget.Holo.Light.SeekBar = 16974028 +Widget.Holo.Light.Spinner = 16974035 +Widget.Holo.Light.Tab = 16974052 +Widget.Holo.Light.TabWidget = 16974037 +Widget.Holo.Light.TextView = 16974010 +Widget.Holo.Light.TextView.SpinnerItem = 16974042 +Widget.Holo.Light.WebTextView = 16974038 +Widget.Holo.Light.WebView = 16974039 +Widget.Holo.ListPopupWindow = 16973997 +Widget.Holo.ListView = 16973975 +Widget.Holo.ListView.DropDown = 16973970 +Widget.Holo.MediaRouteButton = 16974293 +Widget.Holo.PopupMenu = 16973998 +Widget.Holo.PopupWindow = 16973976 +Widget.Holo.ProgressBar = 16973977 +Widget.Holo.ProgressBar.Horizontal = 16973978 +Widget.Holo.ProgressBar.Large = 16973981 +Widget.Holo.ProgressBar.Small = 16973979 +Widget.Holo.ProgressBar.Small.Title = 16973980 +Widget.Holo.RatingBar = 16973983 +Widget.Holo.RatingBar.Indicator = 16973984 +Widget.Holo.RatingBar.Small = 16973985 +Widget.Holo.ScrollView = 16973987 +Widget.Holo.SeekBar = 16973982 +Widget.Holo.Spinner = 16973989 +Widget.Holo.Tab = 16974051 +Widget.Holo.TabWidget = 16973991 +Widget.Holo.TextView = 16973967 +Widget.Holo.TextView.SpinnerItem = 16973996 +Widget.Holo.WebTextView = 16973992 +Widget.Holo.WebView = 16973993 +Widget.ImageButton = 16973862 +Widget.ImageWell = 16973861 +Widget.KeyboardView = 16973911 +Widget.ListPopupWindow = 16973957 +Widget.ListView = 16973870 +Widget.ListView.DropDown = 16973872 +Widget.ListView.Menu = 16973873 +Widget.ListView.White = 16973871 +Widget.Material = 16974413 +Widget.Material.ActionBar = 16974414 +Widget.Material.ActionBar.Solid = 16974415 +Widget.Material.ActionBar.TabBar = 16974416 +Widget.Material.ActionBar.TabText = 16974417 +Widget.Material.ActionBar.TabView = 16974418 +Widget.Material.ActionButton = 16974419 +Widget.Material.ActionButton.CloseMode = 16974420 +Widget.Material.ActionButton.Overflow = 16974421 +Widget.Material.ActionMode = 16974422 +Widget.Material.AutoCompleteTextView = 16974423 +Widget.Material.Button = 16974424 +Widget.Material.ButtonBar = 16974431 +Widget.Material.ButtonBar.AlertDialog = 16974432 +Widget.Material.Button.Borderless = 16974425 +Widget.Material.Button.Borderless.Colored = 16974426 +Widget.Material.Button.Borderless.Small = 16974427 +Widget.Material.Button.Inset = 16974428 +Widget.Material.Button.Small = 16974429 +Widget.Material.Button.Toggle = 16974430 +Widget.Material.CalendarView = 16974433 +Widget.Material.CheckedTextView = 16974434 +Widget.Material.CompoundButton.CheckBox = 16974435 +Widget.Material.CompoundButton.RadioButton = 16974436 +Widget.Material.CompoundButton.Star = 16974437 +Widget.Material.DatePicker = 16974438 +Widget.Material.DropDownItem = 16974439 +Widget.Material.DropDownItem.Spinner = 16974440 +Widget.Material.EditText = 16974441 +Widget.Material.ExpandableListView = 16974442 +Widget.Material.FastScroll = 16974443 +Widget.Material.GridView = 16974444 +Widget.Material.HorizontalScrollView = 16974445 +Widget.Material.ImageButton = 16974446 +Widget.Material.Light = 16974478 +Widget.Material.Light.ActionBar = 16974479 +Widget.Material.Light.ActionBar.Solid = 16974480 +Widget.Material.Light.ActionBar.TabBar = 16974481 +Widget.Material.Light.ActionBar.TabText = 16974482 +Widget.Material.Light.ActionBar.TabView = 16974483 +Widget.Material.Light.ActionButton = 16974484 +Widget.Material.Light.ActionButton.CloseMode = 16974485 +Widget.Material.Light.ActionButton.Overflow = 16974486 +Widget.Material.Light.ActionMode = 16974487 +Widget.Material.Light.AutoCompleteTextView = 16974488 +Widget.Material.Light.Button = 16974489 +Widget.Material.Light.ButtonBar = 16974496 +Widget.Material.Light.ButtonBar.AlertDialog = 16974497 +Widget.Material.Light.Button.Borderless = 16974490 +Widget.Material.Light.Button.Borderless.Colored = 16974491 +Widget.Material.Light.Button.Borderless.Small = 16974492 +Widget.Material.Light.Button.Inset = 16974493 +Widget.Material.Light.Button.Small = 16974494 +Widget.Material.Light.Button.Toggle = 16974495 +Widget.Material.Light.CalendarView = 16974498 +Widget.Material.Light.CheckedTextView = 16974499 +Widget.Material.Light.CompoundButton.CheckBox = 16974500 +Widget.Material.Light.CompoundButton.RadioButton = 16974501 +Widget.Material.Light.CompoundButton.Star = 16974502 +Widget.Material.Light.DatePicker = 16974503 +Widget.Material.Light.DropDownItem = 16974504 +Widget.Material.Light.DropDownItem.Spinner = 16974505 +Widget.Material.Light.EditText = 16974506 +Widget.Material.Light.ExpandableListView = 16974507 +Widget.Material.Light.FastScroll = 16974508 +Widget.Material.Light.GridView = 16974509 +Widget.Material.Light.HorizontalScrollView = 16974510 +Widget.Material.Light.ImageButton = 16974511 +Widget.Material.Light.ListPopupWindow = 16974512 +Widget.Material.Light.ListView = 16974513 +Widget.Material.Light.ListView.DropDown = 16974514 +Widget.Material.Light.MediaRouteButton = 16974515 +Widget.Material.Light.PopupMenu = 16974516 +Widget.Material.Light.PopupMenu.Overflow = 16974517 +Widget.Material.Light.PopupWindow = 16974518 +Widget.Material.Light.ProgressBar = 16974519 +Widget.Material.Light.ProgressBar.Horizontal = 16974520 +Widget.Material.Light.ProgressBar.Inverse = 16974521 +Widget.Material.Light.ProgressBar.Large = 16974522 +Widget.Material.Light.ProgressBar.Large.Inverse = 16974523 +Widget.Material.Light.ProgressBar.Small = 16974524 +Widget.Material.Light.ProgressBar.Small.Inverse = 16974525 +Widget.Material.Light.ProgressBar.Small.Title = 16974526 +Widget.Material.Light.RatingBar = 16974527 +Widget.Material.Light.RatingBar.Indicator = 16974528 +Widget.Material.Light.RatingBar.Small = 16974529 +Widget.Material.Light.ScrollView = 16974530 +Widget.Material.Light.SearchView = 16974531 +Widget.Material.Light.SeekBar = 16974532 +Widget.Material.Light.SegmentedButton = 16974533 +Widget.Material.Light.Spinner = 16974535 +Widget.Material.Light.Spinner.Underlined = 16974536 +Widget.Material.Light.StackView = 16974534 +Widget.Material.Light.Tab = 16974537 +Widget.Material.Light.TabWidget = 16974538 +Widget.Material.Light.TextView = 16974539 +Widget.Material.Light.TextView.SpinnerItem = 16974540 +Widget.Material.Light.TimePicker = 16974541 +Widget.Material.Light.WebTextView = 16974542 +Widget.Material.Light.WebView = 16974543 +Widget.Material.ListPopupWindow = 16974447 +Widget.Material.ListView = 16974448 +Widget.Material.ListView.DropDown = 16974449 +Widget.Material.MediaRouteButton = 16974450 +Widget.Material.PopupMenu = 16974451 +Widget.Material.PopupMenu.Overflow = 16974452 +Widget.Material.PopupWindow = 16974453 +Widget.Material.ProgressBar = 16974454 +Widget.Material.ProgressBar.Horizontal = 16974455 +Widget.Material.ProgressBar.Large = 16974456 +Widget.Material.ProgressBar.Small = 16974457 +Widget.Material.ProgressBar.Small.Title = 16974458 +Widget.Material.RatingBar = 16974459 +Widget.Material.RatingBar.Indicator = 16974460 +Widget.Material.RatingBar.Small = 16974461 +Widget.Material.ScrollView = 16974462 +Widget.Material.SearchView = 16974463 +Widget.Material.SeekBar = 16974464 +Widget.Material.SegmentedButton = 16974465 +Widget.Material.Spinner = 16974467 +Widget.Material.Spinner.Underlined = 16974468 +Widget.Material.StackView = 16974466 +Widget.Material.Tab = 16974469 +Widget.Material.TabWidget = 16974470 +Widget.Material.TextView = 16974471 +Widget.Material.TextView.SpinnerItem = 16974472 +Widget.Material.TimePicker = 16974473 +Widget.Material.Toolbar = 16974474 +Widget.Material.Toolbar.Button.Navigation = 16974475 +Widget.Material.WebTextView = 16974476 +Widget.Material.WebView = 16974477 +Widget.PopupMenu = 16973958 +Widget.PopupWindow = 16973878 +Widget.ProgressBar = 16973852 +Widget.ProgressBar.Horizontal = 16973855 +Widget.ProgressBar.Inverse = 16973915 +Widget.ProgressBar.Large = 16973853 +Widget.ProgressBar.Large.Inverse = 16973916 +Widget.ProgressBar.Small = 16973854 +Widget.ProgressBar.Small.Inverse = 16973917 +Widget.RatingBar = 16973857 +Widget.ScrollView = 16973869 +Widget.SeekBar = 16973856 +Widget.Spinner = 16973864 +Widget.Spinner.DropDown = 16973955 +Widget.StackView = 16974310 +Widget.TabWidget = 16973876 +Widget.TextView = 16973858 +Widget.TextView.PopupMenu = 16973865 +Widget.TextView.SpinnerItem = 16973866 +Widget.Toolbar = 16974311 +Widget.Toolbar.Button.Navigation = 16974312 +Widget.WebView = 16973875 \ No newline at end of file diff --git a/modules/apk-parser/src/main/resources/r_values.ini b/modules/apk-parser/src/main/resources/r_values.ini new file mode 100644 index 00000000..b0a50936 --- /dev/null +++ b/modules/apk-parser/src/main/resources/r_values.ini @@ -0,0 +1,1207 @@ +theme=16842752 +label=16842753 +icon=16842754 +name=16842755 +manageSpaceActivity=16842756 +allowClearUserData=16842757 +permission=16842758 +readPermission=16842759 +writePermission=16842760 +protectionLevel=16842761 +permissionGroup=16842762 +sharedUserId=16842763 +hasCode=16842764 +persistent=16842765 +enabled=16842766 +debuggable=16842767 +exported=16842768 +process=16842769 +taskAffinity=16842770 +multiprocess=16842771 +finishOnTaskLaunch=16842772 +clearTaskOnLaunch=16842773 +stateNotNeeded=16842774 +excludeFromRecents=16842775 +authorities=16842776 +syncable=16842777 +initOrder=16842778 +grantUriPermissions=16842779 +priority=16842780 +launchMode=16842781 +screenOrientation=16842782 +configChanges=16842783 +description=16842784 +targetPackage=16842785 +handleProfiling=16842786 +functionalTest=16842787 +value=16842788 +resource=16842789 +mimeType=16842790 +scheme=16842791 +host=16842792 +port=16842793 +path=16842794 +pathPrefix=16842795 +pathPattern=16842796 +action=16842797 +data=16842798 +targetClass=16842799 +colorForeground=16842800 +colorBackground=16842801 +backgroundDimAmount=16842802 +disabledAlpha=16842803 +textAppearance=16842804 +textAppearanceInverse=16842805 +textColorPrimary=16842806 +textColorPrimaryDisableOnly=16842807 +textColorSecondary=16842808 +textColorPrimaryInverse=16842809 +textColorSecondaryInverse=16842810 +textColorPrimaryNoDisable=16842811 +textColorSecondaryNoDisable=16842812 +textColorPrimaryInverseNoDisable=16842813 +textColorSecondaryInverseNoDisable=16842814 +textColorHintInverse=16842815 +textAppearanceLarge=16842816 +textAppearanceMedium=16842817 +textAppearanceSmall=16842818 +textAppearanceLargeInverse=16842819 +textAppearanceMediumInverse=16842820 +textAppearanceSmallInverse=16842821 +textCheckMark=16842822 +textCheckMarkInverse=16842823 +buttonStyle=16842824 +buttonStyleSmall=16842825 +buttonStyleInset=16842826 +buttonStyleToggle=16842827 +galleryItemBackground=16842828 +listPreferredItemHeight=16842829 +expandableListPreferredItemPaddingLeft=16842830 +expandableListPreferredChildPaddingLeft=16842831 +expandableListPreferredItemIndicatorLeft=16842832 +expandableListPreferredItemIndicatorRight=16842833 +expandableListPreferredChildIndicatorLeft=16842834 +expandableListPreferredChildIndicatorRight=16842835 +windowBackground=16842836 +windowFrame=16842837 +windowNoTitle=16842838 +windowIsFloating=16842839 +windowIsTranslucent=16842840 +windowContentOverlay=16842841 +windowTitleSize=16842842 +windowTitleStyle=16842843 +windowTitleBackgroundStyle=16842844 +alertDialogStyle=16842845 +panelBackground=16842846 +panelFullBackground=16842847 +panelColorForeground=16842848 +panelColorBackground=16842849 +panelTextAppearance=16842850 +scrollbarSize=16842851 +scrollbarThumbHorizontal=16842852 +scrollbarThumbVertical=16842853 +scrollbarTrackHorizontal=16842854 +scrollbarTrackVertical=16842855 +scrollbarAlwaysDrawHorizontalTrack=16842856 +scrollbarAlwaysDrawVerticalTrack=16842857 +absListViewStyle=16842858 +autoCompleteTextViewStyle=16842859 +checkboxStyle=16842860 +dropDownListViewStyle=16842861 +editTextStyle=16842862 +expandableListViewStyle=16842863 +galleryStyle=16842864 +gridViewStyle=16842865 +imageButtonStyle=16842866 +imageWellStyle=16842867 +listViewStyle=16842868 +listViewWhiteStyle=16842869 +popupWindowStyle=16842870 +progressBarStyle=16842871 +progressBarStyleHorizontal=16842872 +progressBarStyleSmall=16842873 +progressBarStyleLarge=16842874 +seekBarStyle=16842875 +ratingBarStyle=16842876 +ratingBarStyleSmall=16842877 +radioButtonStyle=16842878 +scrollbarStyle=16842879 +scrollViewStyle=16842880 +spinnerStyle=16842881 +starStyle=16842882 +tabWidgetStyle=16842883 +textViewStyle=16842884 +webViewStyle=16842885 +dropDownItemStyle=16842886 +spinnerDropDownItemStyle=16842887 +dropDownHintAppearance=16842888 +spinnerItemStyle=16842889 +mapViewStyle=16842890 +preferenceScreenStyle=16842891 +preferenceCategoryStyle=16842892 +preferenceInformationStyle=16842893 +preferenceStyle=16842894 +checkBoxPreferenceStyle=16842895 +yesNoPreferenceStyle=16842896 +dialogPreferenceStyle=16842897 +editTextPreferenceStyle=16842898 +ringtonePreferenceStyle=16842899 +preferenceLayoutChild=16842900 +textSize=16842901 +typeface=16842902 +textStyle=16842903 +textColor=16842904 +textColorHighlight=16842905 +textColorHint=16842906 +textColorLink=16842907 +state_focused=16842908 +state_window_focused=16842909 +state_enabled=16842910 +state_checkable=16842911 +state_checked=16842912 +state_selected=16842913 +state_active=16842914 +state_single=16842915 +state_first=16842916 +state_middle=16842917 +state_last=16842918 +state_pressed=16842919 +state_expanded=16842920 +state_empty=16842921 +state_above_anchor=16842922 +ellipsize=16842923 +x=16842924 +y=16842925 +windowAnimationStyle=16842926 +gravity=16842927 +autoLink=16842928 +linksClickable=16842929 +entries=16842930 +layout_gravity=16842931 +windowEnterAnimation=16842932 +windowExitAnimation=16842933 +windowShowAnimation=16842934 +windowHideAnimation=16842935 +activityOpenEnterAnimation=16842936 +activityOpenExitAnimation=16842937 +activityCloseEnterAnimation=16842938 +activityCloseExitAnimation=16842939 +taskOpenEnterAnimation=16842940 +taskOpenExitAnimation=16842941 +taskCloseEnterAnimation=16842942 +taskCloseExitAnimation=16842943 +taskToFrontEnterAnimation=16842944 +taskToFrontExitAnimation=16842945 +taskToBackEnterAnimation=16842946 +taskToBackExitAnimation=16842947 +orientation=16842948 +keycode=16842949 +fullDark=16842950 +topDark=16842951 +centerDark=16842952 +bottomDark=16842953 +fullBright=16842954 +topBright=16842955 +centerBright=16842956 +bottomBright=16842957 +bottomMedium=16842958 +centerMedium=16842959 +id=16842960 +tag=16842961 +scrollX=16842962 +scrollY=16842963 +background=16842964 +padding=16842965 +paddingLeft=16842966 +paddingTop=16842967 +paddingRight=16842968 +paddingBottom=16842969 +focusable=16842970 +focusableInTouchMode=16842971 +visibility=16842972 +fitsSystemWindows=16842973 +scrollbars=16842974 +fadingEdge=16842975 +fadingEdgeLength=16842976 +nextFocusLeft=16842977 +nextFocusRight=16842978 +nextFocusUp=16842979 +nextFocusDown=16842980 +clickable=16842981 +longClickable=16842982 +saveEnabled=16842983 +drawingCacheQuality=16842984 +duplicateParentState=16842985 +clipChildren=16842986 +clipToPadding=16842987 +layoutAnimation=16842988 +animationCache=16842989 +persistentDrawingCache=16842990 +alwaysDrawnWithCache=16842991 +addStatesFromChildren=16842992 +descendantFocusability=16842993 +layout=16842994 +inflatedId=16842995 +layout_width=16842996 +layout_height=16842997 +layout_margin=16842998 +layout_marginLeft=16842999 +layout_marginTop=16843000 +layout_marginRight=16843001 +layout_marginBottom=16843002 +listSelector=16843003 +drawSelectorOnTop=16843004 +stackFromBottom=16843005 +scrollingCache=16843006 +textFilterEnabled=16843007 +transcriptMode=16843008 +cacheColorHint=16843009 +dial=16843010 +hand_hour=16843011 +hand_minute=16843012 +format=16843013 +checked=16843014 +button=16843015 +checkMark=16843016 +foreground=16843017 +measureAllChildren=16843018 +groupIndicator=16843019 +childIndicator=16843020 +indicatorLeft=16843021 +indicatorRight=16843022 +childIndicatorLeft=16843023 +childIndicatorRight=16843024 +childDivider=16843025 +animationDuration=16843026 +spacing=16843027 +horizontalSpacing=16843028 +verticalSpacing=16843029 +stretchMode=16843030 +columnWidth=16843031 +numColumns=16843032 +src=16843033 +antialias=16843034 +filter=16843035 +dither=16843036 +scaleType=16843037 +adjustViewBounds=16843038 +maxWidth=16843039 +maxHeight=16843040 +tint=16843041 +baselineAlignBottom=16843042 +cropToPadding=16843043 +textOn=16843044 +textOff=16843045 +baselineAligned=16843046 +baselineAlignedChildIndex=16843047 +weightSum=16843048 +divider=16843049 +dividerHeight=16843050 +choiceMode=16843051 +itemTextAppearance=16843052 +horizontalDivider=16843053 +verticalDivider=16843054 +headerBackground=16843055 +itemBackground=16843056 +itemIconDisabledAlpha=16843057 +rowHeight=16843058 +maxRows=16843059 +maxItemsPerRow=16843060 +moreIcon=16843061 +max=16843062 +progress=16843063 +secondaryProgress=16843064 +indeterminate=16843065 +indeterminateOnly=16843066 +indeterminateDrawable=16843067 +progressDrawable=16843068 +indeterminateDuration=16843069 +indeterminateBehavior=16843070 +minWidth=16843071 +minHeight=16843072 +interpolator=16843073 +thumb=16843074 +thumbOffset=16843075 +numStars=16843076 +rating=16843077 +stepSize=16843078 +isIndicator=16843079 +checkedButton=16843080 +stretchColumns=16843081 +shrinkColumns=16843082 +collapseColumns=16843083 +layout_column=16843084 +layout_span=16843085 +bufferType=16843086 +text=16843087 +hint=16843088 +textScaleX=16843089 +cursorVisible=16843090 +maxLines=16843091 +lines=16843092 +height=16843093 +minLines=16843094 +maxEms=16843095 +ems=16843096 +width=16843097 +minEms=16843098 +scrollHorizontally=16843099 +password=16843100 +singleLine=16843101 +selectAllOnFocus=16843102 +includeFontPadding=16843103 +maxLength=16843104 +shadowColor=16843105 +shadowDx=16843106 +shadowDy=16843107 +shadowRadius=16843108 +numeric=16843109 +digits=16843110 +phoneNumber=16843111 +inputMethod=16843112 +capitalize=16843113 +autoText=16843114 +editable=16843115 +freezesText=16843116 +drawableTop=16843117 +drawableBottom=16843118 +drawableLeft=16843119 +drawableRight=16843120 +drawablePadding=16843121 +completionHint=16843122 +completionHintView=16843123 +completionThreshold=16843124 +dropDownSelector=16843125 +popupBackground=16843126 +inAnimation=16843127 +outAnimation=16843128 +flipInterval=16843129 +fillViewport=16843130 +prompt=16843131 +startYear=16843132 +endYear=16843133 +mode=16843134 +layout_x=16843135 +layout_y=16843136 +layout_weight=16843137 +layout_toLeftOf=16843138 +layout_toRightOf=16843139 +layout_above=16843140 +layout_below=16843141 +layout_alignBaseline=16843142 +layout_alignLeft=16843143 +layout_alignTop=16843144 +layout_alignRight=16843145 +layout_alignBottom=16843146 +layout_alignParentLeft=16843147 +layout_alignParentTop=16843148 +layout_alignParentRight=16843149 +layout_alignParentBottom=16843150 +layout_centerInParent=16843151 +layout_centerHorizontal=16843152 +layout_centerVertical=16843153 +layout_alignWithParentIfMissing=16843154 +layout_scale=16843155 +visible=16843156 +variablePadding=16843157 +constantSize=16843158 +oneshot=16843159 +duration=16843160 +drawable=16843161 +shape=16843162 +innerRadiusRatio=16843163 +thicknessRatio=16843164 +startColor=16843165 +endColor=16843166 +useLevel=16843167 +angle=16843168 +type=16843169 +centerX=16843170 +centerY=16843171 +gradientRadius=16843172 +color=16843173 +dashWidth=16843174 +dashGap=16843175 +radius=16843176 +topLeftRadius=16843177 +topRightRadius=16843178 +bottomLeftRadius=16843179 +bottomRightRadius=16843180 +left=16843181 +top=16843182 +right=16843183 +bottom=16843184 +minLevel=16843185 +maxLevel=16843186 +fromDegrees=16843187 +toDegrees=16843188 +pivotX=16843189 +pivotY=16843190 +insetLeft=16843191 +insetRight=16843192 +insetTop=16843193 +insetBottom=16843194 +shareInterpolator=16843195 +fillBefore=16843196 +fillAfter=16843197 +startOffset=16843198 +repeatCount=16843199 +repeatMode=16843200 +zAdjustment=16843201 +fromXScale=16843202 +toXScale=16843203 +fromYScale=16843204 +toYScale=16843205 +fromXDelta=16843206 +toXDelta=16843207 +fromYDelta=16843208 +toYDelta=16843209 +fromAlpha=16843210 +toAlpha=16843211 +delay=16843212 +animation=16843213 +animationOrder=16843214 +columnDelay=16843215 +rowDelay=16843216 +direction=16843217 +directionPriority=16843218 +factor=16843219 +cycles=16843220 +searchMode=16843221 +searchSuggestAuthority=16843222 +searchSuggestPath=16843223 +searchSuggestSelection=16843224 +searchSuggestIntentAction=16843225 +searchSuggestIntentData=16843226 +queryActionMsg=16843227 +suggestActionMsg=16843228 +suggestActionMsgColumn=16843229 +menuCategory=16843230 +orderInCategory=16843231 +checkableBehavior=16843232 +title=16843233 +titleCondensed=16843234 +alphabeticShortcut=16843235 +numericShortcut=16843236 +checkable=16843237 +selectable=16843238 +orderingFromXml=16843239 +key=16843240 +summary=16843241 +order=16843242 +widgetLayout=16843243 +dependency=16843244 +defaultValue=16843245 +shouldDisableView=16843246 +summaryOn=16843247 +summaryOff=16843248 +disableDependentsState=16843249 +dialogTitle=16843250 +dialogMessage=16843251 +dialogIcon=16843252 +positiveButtonText=16843253 +negativeButtonText=16843254 +dialogLayout=16843255 +entryValues=16843256 +ringtoneType=16843257 +showDefault=16843258 +showSilent=16843259 +scaleWidth=16843260 +scaleHeight=16843261 +scaleGravity=16843262 +ignoreGravity=16843263 +foregroundGravity=16843264 +tileMode=16843265 +targetActivity=16843266 +alwaysRetainTaskState=16843267 +allowTaskReparenting=16843268 +searchButtonText=16843269 +colorForegroundInverse=16843270 +textAppearanceButton=16843271 +listSeparatorTextViewStyle=16843272 +streamType=16843273 +clipOrientation=16843274 +centerColor=16843275 +minSdkVersion=16843276 +windowFullscreen=16843277 +unselectedAlpha=16843278 +progressBarStyleSmallTitle=16843279 +ratingBarStyleIndicator=16843280 +apiKey=16843281 +textColorTertiary=16843282 +textColorTertiaryInverse=16843283 +listDivider=16843284 +soundEffectsEnabled=16843285 +keepScreenOn=16843286 +lineSpacingExtra=16843287 +lineSpacingMultiplier=16843288 +listChoiceIndicatorSingle=16843289 +listChoiceIndicatorMultiple=16843290 +versionCode=16843291 +versionName=16843292 +marqueeRepeatLimit=16843293 +windowNoDisplay=16843294 +backgroundDimEnabled=16843295 +inputType=16843296 +isDefault=16843297 +windowDisablePreview=16843298 +privateImeOptions=16843299 +editorExtras=16843300 +settingsActivity=16843301 +fastScrollEnabled=16843302 +reqTouchScreen=16843303 +reqKeyboardType=16843304 +reqHardKeyboard=16843305 +reqNavigation=16843306 +windowSoftInputMode=16843307 +imeFullscreenBackground=16843308 +noHistory=16843309 +headerDividersEnabled=16843310 +footerDividersEnabled=16843311 +candidatesTextStyleSpans=16843312 +smoothScrollbar=16843313 +reqFiveWayNav=16843314 +keyBackground=16843315 +keyTextSize=16843316 +labelTextSize=16843317 +keyTextColor=16843318 +keyPreviewLayout=16843319 +keyPreviewOffset=16843320 +keyPreviewHeight=16843321 +verticalCorrection=16843322 +popupLayout=16843323 +state_long_pressable=16843324 +keyWidth=16843325 +keyHeight=16843326 +horizontalGap=16843327 +verticalGap=16843328 +rowEdgeFlags=16843329 +codes=16843330 +popupKeyboard=16843331 +popupCharacters=16843332 +keyEdgeFlags=16843333 +isModifier=16843334 +isSticky=16843335 +isRepeatable=16843336 +iconPreview=16843337 +keyOutputText=16843338 +keyLabel=16843339 +keyIcon=16843340 +keyboardMode=16843341 +isScrollContainer=16843342 +fillEnabled=16843343 +updatePeriodMillis=16843344 +initialLayout=16843345 +voiceSearchMode=16843346 +voiceLanguageModel=16843347 +voicePromptText=16843348 +voiceLanguage=16843349 +voiceMaxResults=16843350 +bottomOffset=16843351 +topOffset=16843352 +allowSingleTap=16843353 +handle=16843354 +content=16843355 +animateOnClick=16843356 +configure=16843357 +hapticFeedbackEnabled=16843358 +innerRadius=16843359 +thickness=16843360 +sharedUserLabel=16843361 +dropDownWidth=16843362 +dropDownAnchor=16843363 +imeOptions=16843364 +imeActionLabel=16843365 +imeActionId=16843366 +imeExtractEnterAnimation=16843368 +imeExtractExitAnimation=16843369 +tension=16843370 +extraTension=16843371 +anyDensity=16843372 +searchSuggestThreshold=16843373 +includeInGlobalSearch=16843374 +onClick=16843375 +targetSdkVersion=16843376 +maxSdkVersion=16843377 +testOnly=16843378 +contentDescription=16843379 +gestureStrokeWidth=16843380 +gestureColor=16843381 +uncertainGestureColor=16843382 +fadeOffset=16843383 +fadeDuration=16843384 +gestureStrokeType=16843385 +gestureStrokeLengthThreshold=16843386 +gestureStrokeSquarenessThreshold=16843387 +gestureStrokeAngleThreshold=16843388 +eventsInterceptionEnabled=16843389 +fadeEnabled=16843390 +backupAgent=16843391 +allowBackup=16843392 +glEsVersion=16843393 +queryAfterZeroResults=16843394 +dropDownHeight=16843395 +smallScreens=16843396 +normalScreens=16843397 +largeScreens=16843398 +progressBarStyleInverse=16843399 +progressBarStyleSmallInverse=16843400 +progressBarStyleLargeInverse=16843401 +searchSettingsDescription=16843402 +textColorPrimaryInverseDisableOnly=16843403 +autoUrlDetect=16843404 +resizeable=16843405 +required=16843406 +accountType=16843407 +contentAuthority=16843408 +userVisible=16843409 +windowShowWallpaper=16843410 +wallpaperOpenEnterAnimation=16843411 +wallpaperOpenExitAnimation=16843412 +wallpaperCloseEnterAnimation=16843413 +wallpaperCloseExitAnimation=16843414 +wallpaperIntraOpenEnterAnimation=16843415 +wallpaperIntraOpenExitAnimation=16843416 +wallpaperIntraCloseEnterAnimation=16843417 +wallpaperIntraCloseExitAnimation=16843418 +supportsUploading=16843419 +killAfterRestore=16843420 +restoreNeedsApplication=16843421 +smallIcon=16843422 +accountPreferences=16843423 +textAppearanceSearchResultSubtitle=16843424 +textAppearanceSearchResultTitle=16843425 +summaryColumn=16843426 +detailColumn=16843427 +detailSocialSummary=16843428 +thumbnail=16843429 +detachWallpaper=16843430 +finishOnCloseSystemDialogs=16843431 +scrollbarFadeDuration=16843432 +scrollbarDefaultDelayBeforeFade=16843433 +fadeScrollbars=16843434 +colorBackgroundCacheHint=16843435 +dropDownHorizontalOffset=16843436 +dropDownVerticalOffset=16843437 +quickContactBadgeStyleWindowSmall=16843438 +quickContactBadgeStyleWindowMedium=16843439 +quickContactBadgeStyleWindowLarge=16843440 +quickContactBadgeStyleSmallWindowSmall=16843441 +quickContactBadgeStyleSmallWindowMedium=16843442 +quickContactBadgeStyleSmallWindowLarge=16843443 +author=16843444 +autoStart=16843445 +expandableListViewWhiteStyle=16843446 +installLocation=16843447 +vmSafeMode=16843448 +webTextViewStyle=16843449 +restoreAnyVersion=16843450 +tabStripLeft=16843451 +tabStripRight=16843452 +tabStripEnabled=16843453 +logo=16843454 +xlargeScreens=16843455 +immersive=16843456 +overScrollMode=16843457 +overScrollHeader=16843458 +overScrollFooter=16843459 +filterTouchesWhenObscured=16843460 +textSelectHandleLeft=16843461 +textSelectHandleRight=16843462 +textSelectHandle=16843463 +textSelectHandleWindowStyle=16843464 +popupAnimationStyle=16843465 +screenSize=16843466 +screenDensity=16843467 +allContactsName=16843468 +windowActionBar=16843469 +actionBarStyle=16843470 +navigationMode=16843471 +displayOptions=16843472 +subtitle=16843473 +customNavigationLayout=16843474 +hardwareAccelerated=16843475 +measureWithLargestChild=16843476 +animateFirstView=16843477 +dropDownSpinnerStyle=16843478 +actionDropDownStyle=16843479 +actionButtonStyle=16843480 +showAsAction=16843481 +previewImage=16843482 +actionModeBackground=16843483 +actionModeCloseDrawable=16843484 +windowActionModeOverlay=16843485 +valueFrom=16843486 +valueTo=16843487 +valueType=16843488 +propertyName=16843489 +ordering=16843490 +fragment=16843491 +windowActionBarOverlay=16843492 +fragmentOpenEnterAnimation=16843493 +fragmentOpenExitAnimation=16843494 +fragmentCloseEnterAnimation=16843495 +fragmentCloseExitAnimation=16843496 +fragmentFadeEnterAnimation=16843497 +fragmentFadeExitAnimation=16843498 +actionBarSize=16843499 +imeSubtypeLocale=16843500 +imeSubtypeMode=16843501 +imeSubtypeExtraValue=16843502 +splitMotionEvents=16843503 +listChoiceBackgroundIndicator=16843504 +spinnerMode=16843505 +animateLayoutChanges=16843506 +actionBarTabStyle=16843507 +actionBarTabBarStyle=16843508 +actionBarTabTextStyle=16843509 +actionOverflowButtonStyle=16843510 +actionModeCloseButtonStyle=16843511 +titleTextStyle=16843512 +subtitleTextStyle=16843513 +iconifiedByDefault=16843514 +actionLayout=16843515 +actionViewClass=16843516 +activatedBackgroundIndicator=16843517 +state_activated=16843518 +listPopupWindowStyle=16843519 +popupMenuStyle=16843520 +textAppearanceLargePopupMenu=16843521 +textAppearanceSmallPopupMenu=16843522 +breadCrumbTitle=16843523 +breadCrumbShortTitle=16843524 +listDividerAlertDialog=16843525 +textColorAlertDialogListItem=16843526 +loopViews=16843527 +dialogTheme=16843528 +alertDialogTheme=16843529 +dividerVertical=16843530 +homeAsUpIndicator=16843531 +enterFadeDuration=16843532 +exitFadeDuration=16843533 +selectableItemBackground=16843534 +autoAdvanceViewId=16843535 +useIntrinsicSizeAsMinimum=16843536 +actionModeCutDrawable=16843537 +actionModeCopyDrawable=16843538 +actionModePasteDrawable=16843539 +textEditPasteWindowLayout=16843540 +textEditNoPasteWindowLayout=16843541 +textIsSelectable=16843542 +windowEnableSplitTouch=16843543 +indeterminateProgressStyle=16843544 +progressBarPadding=16843545 +animationResolution=16843546 +state_accelerated=16843547 +baseline=16843548 +homeLayout=16843549 +opacity=16843550 +alpha=16843551 +transformPivotX=16843552 +transformPivotY=16843553 +translationX=16843554 +translationY=16843555 +scaleX=16843556 +scaleY=16843557 +rotation=16843558 +rotationX=16843559 +rotationY=16843560 +showDividers=16843561 +dividerPadding=16843562 +borderlessButtonStyle=16843563 +dividerHorizontal=16843564 +itemPadding=16843565 +buttonBarStyle=16843566 +buttonBarButtonStyle=16843567 +segmentedButtonStyle=16843568 +staticWallpaperPreview=16843569 +allowParallelSyncs=16843570 +isAlwaysSyncable=16843571 +verticalScrollbarPosition=16843572 +fastScrollAlwaysVisible=16843573 +fastScrollThumbDrawable=16843574 +fastScrollPreviewBackgroundLeft=16843575 +fastScrollPreviewBackgroundRight=16843576 +fastScrollTrackDrawable=16843577 +fastScrollOverlayPosition=16843578 +customTokens=16843579 +nextFocusForward=16843580 +firstDayOfWeek=16843581 +showWeekNumber=16843582 +minDate=16843583 +maxDate=16843584 +shownWeekCount=16843585 +selectedWeekBackgroundColor=16843586 +focusedMonthDateColor=16843587 +unfocusedMonthDateColor=16843588 +weekNumberColor=16843589 +weekSeparatorLineColor=16843590 +selectedDateVerticalBar=16843591 +weekDayTextAppearance=16843592 +dateTextAppearance=16843593 +solidColor=16843594 +spinnersShown=16843595 +calendarViewShown=16843596 +state_multiline=16843597 +detailsElementBackground=16843598 +textColorHighlightInverse=16843599 +textColorLinkInverse=16843600 +editTextColor=16843601 +editTextBackground=16843602 +horizontalScrollViewStyle=16843603 +layerType=16843604 +alertDialogIcon=16843605 +windowMinWidthMajor=16843606 +windowMinWidthMinor=16843607 +queryHint=16843608 +fastScrollTextColor=16843609 +largeHeap=16843610 +windowCloseOnTouchOutside=16843611 +datePickerStyle=16843612 +calendarViewStyle=16843613 +textEditSidePasteWindowLayout=16843614 +textEditSideNoPasteWindowLayout=16843615 +actionMenuTextAppearance=16843616 +actionMenuTextColor=16843617 +textCursorDrawable=16843618 +resizeMode=16843619 +requiresSmallestWidthDp=16843620 +compatibleWidthLimitDp=16843621 +largestWidthLimitDp=16843622 +state_hovered=16843623 +state_drag_can_accept=16843624 +state_drag_hovered=16843625 +stopWithTask=16843626 +switchTextOn=16843627 +switchTextOff=16843628 +switchPreferenceStyle=16843629 +switchTextAppearance=16843630 +track=16843631 +switchMinWidth=16843632 +switchPadding=16843633 +thumbTextPadding=16843634 +textSuggestionsWindowStyle=16843635 +textEditSuggestionItemLayout=16843636 +rowCount=16843637 +rowOrderPreserved=16843638 +columnCount=16843639 +columnOrderPreserved=16843640 +useDefaultMargins=16843641 +alignmentMode=16843642 +layout_row=16843643 +layout_rowSpan=16843644 +layout_columnSpan=16843645 +actionModeSelectAllDrawable=16843646 +isAuxiliary=16843647 +accessibilityEventTypes=16843648 +packageNames=16843649 +accessibilityFeedbackType=16843650 +notificationTimeout=16843651 +accessibilityFlags=16843652 +canRetrieveWindowContent=16843653 +listPreferredItemHeightLarge=16843654 +listPreferredItemHeightSmall=16843655 +actionBarSplitStyle=16843656 +actionProviderClass=16843657 +backgroundStacked=16843658 +backgroundSplit=16843659 +textAllCaps=16843660 +colorPressedHighlight=16843661 +colorLongPressedHighlight=16843662 +colorFocusedHighlight=16843663 +colorActivatedHighlight=16843664 +colorMultiSelectHighlight=16843665 +drawableStart=16843666 +drawableEnd=16843667 +actionModeStyle=16843668 +minResizeWidth=16843669 +minResizeHeight=16843670 +actionBarWidgetTheme=16843671 +uiOptions=16843672 +subtypeLocale=16843673 +subtypeExtraValue=16843674 +actionBarDivider=16843675 +actionBarItemBackground=16843676 +actionModeSplitBackground=16843677 +textAppearanceListItem=16843678 +textAppearanceListItemSmall=16843679 +targetDescriptions=16843680 +directionDescriptions=16843681 +overridesImplicitlyEnabledSubtype=16843682 +listPreferredItemPaddingLeft=16843683 +listPreferredItemPaddingRight=16843684 +requiresFadingEdge=16843685 +publicKey=16843686 +parentActivityName=16843687 +isolatedProcess=16843689 +importantForAccessibility=16843690 +keyboardLayout=16843691 +fontFamily=16843692 +mediaRouteButtonStyle=16843693 +mediaRouteTypes=16843694 +supportsRtl=16843695 +textDirection=16843696 +textAlignment=16843697 +layoutDirection=16843698 +paddingStart=16843699 +paddingEnd=16843700 +layout_marginStart=16843701 +layout_marginEnd=16843702 +layout_toStartOf=16843703 +layout_toEndOf=16843704 +layout_alignStart=16843705 +layout_alignEnd=16843706 +layout_alignParentStart=16843707 +layout_alignParentEnd=16843708 +listPreferredItemPaddingStart=16843709 +listPreferredItemPaddingEnd=16843710 +singleUser=16843711 +presentationTheme=16843712 +subtypeId=16843713 +initialKeyguardLayout=16843714 +widgetCategory=16843716 +permissionGroupFlags=16843717 +labelFor=16843718 +permissionFlags=16843719 +checkedTextViewStyle=16843720 +showOnLockScreen=16843721 +format12Hour=16843722 +format24Hour=16843723 +timeZone=16843724 +mipMap=16843725 +mirrorForRtl=16843726 +windowOverscan=16843727 +requiredForAllUsers=16843728 +indicatorStart=16843729 +indicatorEnd=16843730 +childIndicatorStart=16843731 +childIndicatorEnd=16843732 +restrictedAccountType=16843733 +requiredAccountType=16843734 +canRequestTouchExplorationMode=16843735 +canRequestEnhancedWebAccessibility=16843736 +canRequestFilterKeyEvents=16843737 +layoutMode=16843738 +keySet=16843739 +targetId=16843740 +fromScene=16843741 +toScene=16843742 +transition=16843743 +transitionOrdering=16843744 +fadingMode=16843745 +startDelay=16843746 +ssp=16843747 +sspPrefix=16843748 +sspPattern=16843749 +addPrintersActivity=16843750 +vendor=16843751 +category=16843752 +isAsciiCapable=16843753 +autoMirrored=16843754 +supportsSwitchingToNextInputMethod=16843755 +requireDeviceUnlock=16843756 +apduServiceBanner=16843757 +accessibilityLiveRegion=16843758 +windowTranslucentStatus=16843759 +windowTranslucentNavigation=16843760 +advancedPrintOptionsActivity=16843761 +banner=16843762 +windowSwipeToDismiss=16843763 +isGame=16843764 +allowEmbedded=16843765 +setupActivity=16843766 +fastScrollStyle=16843767 +windowContentTransitions=16843768 +windowContentTransitionManager=16843769 +translationZ=16843770 +tintMode=16843771 +controlX1=16843772 +controlY1=16843773 +controlX2=16843774 +controlY2=16843775 +transitionName=16843776 +transitionGroup=16843777 +viewportWidth=16843778 +viewportHeight=16843779 +fillColor=16843780 +pathData=16843781 +strokeColor=16843782 +strokeWidth=16843783 +trimPathStart=16843784 +trimPathEnd=16843785 +trimPathOffset=16843786 +strokeLineCap=16843787 +strokeLineJoin=16843788 +strokeMiterLimit=16843789 +colorControlNormal=16843817 +colorControlActivated=16843818 +colorButtonNormal=16843819 +colorControlHighlight=16843820 +persistableMode=16843821 +titleTextAppearance=16843822 +subtitleTextAppearance=16843823 +slideEdge=16843824 +actionBarTheme=16843825 +textAppearanceListItemSecondary=16843826 +colorPrimary=16843827 +colorPrimaryDark=16843828 +colorAccent=16843829 +nestedScrollingEnabled=16843830 +windowEnterTransition=16843831 +windowExitTransition=16843832 +windowSharedElementEnterTransition=16843833 +windowSharedElementExitTransition=16843834 +windowAllowReturnTransitionOverlap=16843835 +windowAllowEnterTransitionOverlap=16843836 +sessionService=16843837 +stackViewStyle=16843838 +switchStyle=16843839 +elevation=16843840 +excludeId=16843841 +excludeClass=16843842 +hideOnContentScroll=16843843 +actionOverflowMenuStyle=16843844 +documentLaunchMode=16843845 +maxRecents=16843846 +autoRemoveFromRecents=16843847 +stateListAnimator=16843848 +toId=16843849 +fromId=16843850 +reversible=16843851 +splitTrack=16843852 +targetName=16843853 +excludeName=16843854 +matchOrder=16843855 +windowDrawsSystemBarBackgrounds=16843856 +statusBarColor=16843857 +navigationBarColor=16843858 +contentInsetStart=16843859 +contentInsetEnd=16843860 +contentInsetLeft=16843861 +contentInsetRight=16843862 +paddingMode=16843863 +layout_rowWeight=16843864 +layout_columnWeight=16843865 +translateX=16843866 +translateY=16843867 +selectableItemBackgroundBorderless=16843868 +elegantTextHeight=16843869 +searchKeyphraseId=16843870 +searchKeyphrase=16843871 +searchKeyphraseSupportedLocales=16843872 +windowTransitionBackgroundFadeDuration=16843873 +overlapAnchor=16843874 +progressTint=16843875 +progressTintMode=16843876 +progressBackgroundTint=16843877 +progressBackgroundTintMode=16843878 +secondaryProgressTint=16843879 +secondaryProgressTintMode=16843880 +indeterminateTint=16843881 +indeterminateTintMode=16843882 +backgroundTint=16843883 +backgroundTintMode=16843884 +foregroundTint=16843885 +foregroundTintMode=16843886 +buttonTint=16843887 +buttonTintMode=16843888 +thumbTint=16843889 +thumbTintMode=16843890 +fullBackupOnly=16843891 +propertyXName=16843892 +propertyYName=16843893 +relinquishTaskIdentity=16843894 +tileModeX=16843895 +tileModeY=16843896 +actionModeShareDrawable=16843897 +actionModeFindDrawable=16843898 +actionModeWebSearchDrawable=16843899 +transitionVisibilityMode=16843900 +minimumHorizontalAngle=16843901 +minimumVerticalAngle=16843902 +maximumAngle=16843903 +searchViewStyle=16843904 +closeIcon=16843905 +goIcon=16843906 +searchIcon=16843907 +voiceIcon=16843908 +commitIcon=16843909 +suggestionRowLayout=16843910 +queryBackground=16843911 +submitBackground=16843912 +buttonBarPositiveButtonStyle=16843913 +buttonBarNeutralButtonStyle=16843914 +buttonBarNegativeButtonStyle=16843915 +popupElevation=16843916 +actionBarPopupTheme=16843917 +multiArch=16843918 +touchscreenBlocksFocus=16843919 +windowElevation=16843920 +launchTaskBehindTargetAnimation=16843921 +launchTaskBehindSourceAnimation=16843922 +restrictionType=16843923 +dayOfWeekBackground=16843924 +dayOfWeekTextAppearance=16843925 +headerMonthTextAppearance=16843926 +headerDayOfMonthTextAppearance=16843927 +headerYearTextAppearance=16843928 +yearListItemTextAppearance=16843929 +yearListSelectorColor=16843930 +calendarTextColor=16843931 +recognitionService=16843932 +timePickerStyle=16843933 +timePickerDialogTheme=16843934 +headerTimeTextAppearance=16843935 +headerAmPmTextAppearance=16843936 +numbersTextColor=16843937 +numbersBackgroundColor=16843938 +numbersSelectorColor=16843939 +amPmTextColor=16843940 +amPmBackgroundColor=16843941 +searchKeyphraseRecognitionFlags=16843942 +checkMarkTint=16843943 +checkMarkTintMode=16843944 +popupTheme=16843945 +toolbarStyle=16843946 +windowClipToOutline=16843947 +datePickerDialogTheme=16843948 +showText=16843949 +windowReturnTransition=16843950 +windowReenterTransition=16843951 +windowSharedElementReturnTransition=16843952 +windowSharedElementReenterTransition=16843953 +resumeWhilePausing=16843954 +datePickerMode=16843955 +timePickerMode=16843956 +inset=16843957 +letterSpacing=16843958 +fontFeatureSettings=16843959 +outlineProvider=16843960 +contentAgeHint=16843961 +country=16843962 +windowSharedElementsUseOverlay=16843963 +reparent=16843964 +reparentWithOverlay=16843965 +ambientShadowAlpha=16843966 +spotShadowAlpha=16843967 +navigationIcon=16843968 +navigationContentDescription=16843969 +fragmentExitTransition=16843970 +fragmentEnterTransition=16843971 +fragmentSharedElementEnterTransition=16843972 +fragmentReturnTransition=16843973 +fragmentSharedElementReturnTransition=16843974 +fragmentReenterTransition=16843975 +fragmentAllowEnterTransitionOverlap=16843976 +fragmentAllowReturnTransitionOverlap=16843977 +patternPathData=16843978 +strokeAlpha=16843979 +fillAlpha=16843980 +windowActivityTransitions=16843981 +colorEdgeEffect=16843982 +resizeClip=16843983 +collapseContentDescription=16843984 +accessibilityTraversalBefore=16843985 +accessibilityTraversalAfter=16843986 +dialogPreferredPadding=16843987 +searchHintIcon=16843988 \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 7ed9b4d7..7277762e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ private val modules = listOf( "jieba-analysis", "apk-signer", + "apk-parser", "color-picker", ) diff --git a/version.properties b/version.properties index 4e0460ff..f9046bd9 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Sat Apr 19 00:16:16 CST 2025 -BUILD_TIME=1744992976260 +#Sat Apr 19 01:36:17 CST 2025 +BUILD_TIME=1744997777676 COMPILE_SDK_VERSION=35 JAVA_VERSION=23 JAVA_VERSION_MIN_RADICAL=0 @@ -17,6 +17,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=35 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3149 +VERSION_BUILD=3150 VERSION_NAME=6.6.3 Alpha VSCODE_EXT_REQUIRED_VERSION=1.0.8