Merge "Make apks depend on their certificates"
diff --git a/core/binary.mk b/core/binary.mk
index 4736f06..ca589bf 100644
--- a/core/binary.mk
+++ b/core/binary.mk
@@ -410,7 +410,7 @@
ifeq ($(my_clang),true)
my_coverage_lib := $($(LOCAL_2ND_ARCH_VAR_PREFIX)TARGET_LIBPROFILE_RT)
else
- my_coverage_lib := $($(LOCAL_2ND_ARCH_VAR_PREFIX)TARGET_LIBGCOV)
+ my_coverage_lib := $(call intermediates-dir-for,STATIC_LIBRARIES,libgcov,,,$(LOCAL_2ND_ARCH_VAR_PREFIX))/libgcov.a
endif
$(LOCAL_INTERMEDIATE_TARGETS): PRIVATE_TARGET_COVERAGE_LIB := $(my_coverage_lib)
diff --git a/core/combo/TARGET_linux-arm.mk b/core/combo/TARGET_linux-arm.mk
index 9be6c73..e2d42cc 100644
--- a/core/combo/TARGET_linux-arm.mk
+++ b/core/combo/TARGET_linux-arm.mk
@@ -146,20 +146,6 @@
libc_root := bionic/libc
-
-## on some hosts, the target cross-compiler is not available so do not run this command
-ifneq ($(wildcard $($(combo_2nd_arch_prefix)TARGET_CC)),)
-# We compile with the global cflags to ensure that
-# any flags which affect libgcc are correctly taken
-# into account.
-$(combo_2nd_arch_prefix)TARGET_LIBGCC := $(shell $($(combo_2nd_arch_prefix)TARGET_CC) \
- $($(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS) -print-libgcc-file-name)
-$(combo_2nd_arch_prefix)TARGET_LIBATOMIC := $(shell $($(combo_2nd_arch_prefix)TARGET_CC) \
- $($(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS) -print-file-name=libatomic.a)
-$(combo_2nd_arch_prefix)TARGET_LIBGCOV := $(shell $($(combo_2nd_arch_prefix)TARGET_CC) \
- $($(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS) -print-file-name=libgcov.a)
-endif
-
KERNEL_HEADERS_COMMON := $(libc_root)/kernel/uapi
KERNEL_HEADERS_COMMON += $(libc_root)/kernel/common
KERNEL_HEADERS_ARCH := $(libc_root)/kernel/uapi/asm-$(TARGET_$(combo_2nd_arch_prefix)ARCH)
diff --git a/core/combo/TARGET_linux-arm64.mk b/core/combo/TARGET_linux-arm64.mk
index 61028c4..c027113 100644
--- a/core/combo/TARGET_linux-arm64.mk
+++ b/core/combo/TARGET_linux-arm64.mk
@@ -126,13 +126,6 @@
libc_root := bionic/libc
-TARGET_LIBGCC := $(shell $(TARGET_CC) $(TARGET_GLOBAL_CFLAGS) \
- -print-libgcc-file-name)
-TARGET_LIBATOMIC := $(shell $(TARGET_CC) $(TARGET_GLOBAL_CFLAGS) \
- -print-file-name=libatomic.a)
-TARGET_LIBGCOV := $(shell $(TARGET_CC) $(TARGET_GLOBAL_CFLAGS) \
- -print-file-name=libgcov.a)
-
KERNEL_HEADERS_COMMON := $(libc_root)/kernel/uapi
KERNEL_HEADERS_COMMON += $(libc_root)/kernel/common
KERNEL_HEADERS_ARCH := $(libc_root)/kernel/uapi/asm-$(TARGET_ARCH)
diff --git a/core/combo/TARGET_linux-mips.mk b/core/combo/TARGET_linux-mips.mk
index fcf4c9d..d872a4a 100644
--- a/core/combo/TARGET_linux-mips.mk
+++ b/core/combo/TARGET_linux-mips.mk
@@ -127,24 +127,6 @@
libc_root := bionic/libc
-
-## on some hosts, the target cross-compiler is not available so do not run this command
-ifneq ($(wildcard $($(combo_2nd_arch_prefix)TARGET_CC)),)
-# We compile with the global cflags to ensure that
-# any flags which affect libgcc are correctly taken
-# into account.
-$(combo_2nd_arch_prefix)TARGET_LIBGCC := \
- $(shell $($(combo_2nd_arch_prefix)TARGET_CC) $($(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS) -print-file-name=libgcc.a)
-$(combo_2nd_arch_prefix)TARGET_LIBATOMIC := \
- $(shell $($(combo_2nd_arch_prefix)TARGET_CC) $($(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS) -print-file-name=libatomic.a)
-LIBGCC_EH := $(shell $($(combo_2nd_arch_prefix)TARGET_CC) $($(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS) -print-file-name=libgcc_eh.a)
-ifneq ($(LIBGCC_EH),libgcc_eh.a)
- $(combo_2nd_arch_prefix)TARGET_LIBGCC += $(LIBGCC_EH)
-endif
-$(combo_2nd_arch_prefix)TARGET_LIBGCOV := $(shell $($(combo_2nd_arch_prefix)TARGET_CC) $($(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS) \
- --print-file-name=libgcov.a)
-endif
-
KERNEL_HEADERS_COMMON := $(libc_root)/kernel/uapi
KERNEL_HEADERS_COMMON += $(libc_root)/kernel/common
KERNEL_HEADERS_ARCH := $(libc_root)/kernel/uapi/asm-mips # mips covers both mips and mips64.
diff --git a/core/combo/TARGET_linux-mips64.mk b/core/combo/TARGET_linux-mips64.mk
index f869317..32128b2 100644
--- a/core/combo/TARGET_linux-mips64.mk
+++ b/core/combo/TARGET_linux-mips64.mk
@@ -133,24 +133,6 @@
libc_root := bionic/libc
-
-## on some hosts, the target cross-compiler is not available so do not run this command
-ifneq ($(wildcard $(TARGET_CC)),)
-# We compile with the global cflags to ensure that
-# any flags which affect libgcc are correctly taken
-# into account.
-TARGET_LIBGCC := \
- $(shell $(TARGET_CC) $(TARGET_GLOBAL_CFLAGS) -print-file-name=libgcc.a)
-TARGET_LIBATOMIC := \
- $(shell $(TARGET_CC) $(TARGET_GLOBAL_CFLAGS) -print-file-name=libatomic.a)
-LIBGCC_EH := $(shell $(TARGET_CC) $(TARGET_GLOBAL_CFLAGS) -print-file-name=libgcc_eh.a)
-ifneq ($(LIBGCC_EH),libgcc_eh.a)
- TARGET_LIBGCC += $(LIBGCC_EH)
-endif
-TARGET_LIBGCOV := $(shell $(TARGET_CC) $(TARGET_GLOBAL_CFLAGS) \
- --print-file-name=libgcov.a)
-endif
-
KERNEL_HEADERS_COMMON := $(libc_root)/kernel/uapi
KERNEL_HEADERS_COMMON += $(libc_root)/kernel/common
KERNEL_HEADERS_ARCH := $(libc_root)/kernel/uapi/asm-mips
diff --git a/core/combo/TARGET_linux-x86.mk b/core/combo/TARGET_linux-x86.mk
index 800a1dd..1f27502 100644
--- a/core/combo/TARGET_linux-x86.mk
+++ b/core/combo/TARGET_linux-x86.mk
@@ -62,15 +62,6 @@
$(call _gen_toc_command_for_elf,$(1),$(2))
endef
-ifneq ($(wildcard $($(combo_2nd_arch_prefix)TARGET_CC)),)
-$(combo_2nd_arch_prefix)TARGET_LIBGCC := \
- $(shell $($(combo_2nd_arch_prefix)TARGET_CC) -m32 -print-file-name=libgcc.a)
-$(combo_2nd_arch_prefix)TARGET_LIBATOMIC := \
- $(shell $($(combo_2nd_arch_prefix)TARGET_CC) -m32 -print-file-name=libatomic.a)
-$(combo_2nd_arch_prefix)TARGET_LIBGCOV := \
- $(shell $($(combo_2nd_arch_prefix)TARGET_CC) -m32 -print-file-name=libgcov.a)
-endif
-
$(combo_2nd_arch_prefix)TARGET_NO_UNDEFINED_LDFLAGS := -Wl,--no-undefined
libc_root := bionic/libc
diff --git a/core/combo/TARGET_linux-x86_64.mk b/core/combo/TARGET_linux-x86_64.mk
index bf31334..ea6550b 100644
--- a/core/combo/TARGET_linux-x86_64.mk
+++ b/core/combo/TARGET_linux-x86_64.mk
@@ -62,15 +62,6 @@
$(call _gen_toc_command_for_elf,$(1),$(2))
endef
-ifneq ($(wildcard $(TARGET_CC)),)
-TARGET_LIBGCC := \
- $(shell $(TARGET_CC) -m64 -print-file-name=libgcc.a)
-TARGET_LIBATOMIC := \
- $(shell $(TARGET_CC) -m64 -print-file-name=libatomic.a)
-TARGET_LIBGCOV := \
- $(shell $(TARGET_CC) -m64 -print-file-name=libgcov.a)
-endif
-
TARGET_NO_UNDEFINED_LDFLAGS := -Wl,--no-undefined
libc_root := bionic/libc
diff --git a/core/definitions.mk b/core/definitions.mk
index 420691d..abc6209 100644
--- a/core/definitions.mk
+++ b/core/definitions.mk
@@ -144,7 +144,10 @@
define filter-soong-makefiles
$(foreach mk,$(1),\
$(if $(wildcard $(patsubst %/Android.mk,%/Android.bp,$(mk))),\
- $(info skipping $(mk) ...),\
+ $(if $(wildcard $(patsubst %/Android.mk,%/Android.soong.mk,$(mk))),\
+ $(info skipping $(mk), but including Android.soong.mk ...)\
+ $(patsubst %/Android.mk,%/Android.soong.mk,$(mk)),\
+ $(info skipping $(mk) ...)),\
$(mk)))
endef
else
@@ -402,7 +405,7 @@
define find-subdir-assets
$(sort $(if $(1),$(patsubst ./%,%, \
- $(shell if [ -d $(1) ] ; then cd $(1) ; find ./ -not -name '.*' -and -type f -and -not -type l ; fi)), \
+ $(shell if [ -d $(1) ] ; then cd $(1) ; find -L ./ -not -name '.*' -and -type f ; fi)), \
$(warning Empty argument supplied to find-subdir-assets) \
))
endef
@@ -2599,6 +2602,35 @@
###########################################################
+## Commands to copy toolchain libraries
+###########################################################
+ifneq ($(USE_SOONG),true)
+# Used when Soong isn't defining our toolchain libraries
+# $(1): Name of library (libgcc, etc)
+define copy-toolchain-library
+$(call copy-toolchain-library-internal,\
+ $(call intermediates-dir-for,STATIC_LIBRARIES,$(1))/$(1).a,,$(1))
+ifdef TARGET_2ND_ARCH
+$(call copy-toolchain-library-internal,\
+ $(call intermediates-dir-for,STATIC_LIBRARIES,$(1),,,2ND_)/$(1).a,2ND_,$(1))
+endif
+endef
+
+# $(1): the intermediates library path
+# $(2): whether this is the 2nd target architecture
+# $(3): the name of the library without the extension
+define copy-toolchain-library-internal
+$(1): build/soong/scripts/copygcclib.sh $($(2)TARGET_CC)
+ @echo "Toolchain library: $(3)"
+ @mkdir -p $$(dir $$@)
+ $$(hide) rm -f $$@
+ $$(hide) build/soong/scripts/copygcclib.sh $$@ $($(2)TARGET_CC) $($(2)TARGET_GLOBAL_CFLAGS) -print-file-name=$(3).a
+
+$(call include-depfile,$(1).d,$(1))
+endef
+endif
+
+###########################################################
## Commands to call Proguard
###########################################################
define transform-jar-to-proguard
diff --git a/core/executable_internal.mk b/core/executable_internal.mk
index febea98..3808412 100644
--- a/core/executable_internal.mk
+++ b/core/executable_internal.mk
@@ -38,9 +38,9 @@
ifeq ($(LOCAL_NO_LIBGCC),true)
my_target_libgcc :=
else
-my_target_libgcc := $($(LOCAL_2ND_ARCH_VAR_PREFIX)TARGET_LIBGCC)
+my_target_libgcc := $(call intermediates-dir-for,STATIC_LIBRARIES,libgcc,,,$(LOCAL_2ND_ARCH_VAR_PREFIX))/libgcc.a
endif
-my_target_libatomic := $($(LOCAL_2ND_ARCH_VAR_PREFIX)TARGET_LIBATOMIC)
+my_target_libatomic := $(call intermediates-dir-for,STATIC_LIBRARIES,libatomic,,,$(LOCAL_2ND_ARCH_VAR_PREFIX))/libatomic.a
ifeq ($(LOCAL_NO_CRT),true)
my_target_crtbegin_dynamic_o :=
my_target_crtbegin_static_o :=
@@ -73,11 +73,11 @@
$(linked_module): PRIVATE_POST_LINK_CMD := $(LOCAL_POST_LINK_CMD)
ifeq ($(LOCAL_FORCE_STATIC_EXECUTABLE),true)
-$(linked_module): $(my_target_crtbegin_static_o) $(all_objects) $(all_libraries) $(my_target_crtend_o)
+$(linked_module): $(my_target_crtbegin_static_o) $(all_objects) $(all_libraries) $(my_target_crtend_o) $(my_target_libgcc) $(my_target_libatomic)
$(transform-o-to-static-executable)
$(PRIVATE_POST_LINK_CMD)
else
-$(linked_module): $(my_target_crtbegin_dynamic_o) $(all_objects) $(all_libraries) $(my_target_crtend_o)
+$(linked_module): $(my_target_crtbegin_dynamic_o) $(all_objects) $(all_libraries) $(my_target_crtend_o) $(my_target_libgcc) $(my_target_libatomic)
$(transform-o-to-executable)
$(PRIVATE_POST_LINK_CMD)
endif
diff --git a/core/host_test_internal.mk b/core/host_test_internal.mk
index e4fa4c5..6c52e64 100644
--- a/core/host_test_internal.mk
+++ b/core/host_test_internal.mk
@@ -5,7 +5,7 @@
LOCAL_CFLAGS_windows += -DGTEST_OS_WINDOWS
LOCAL_CFLAGS_linux += -DGTEST_OS_LINUX
LOCAL_LDLIBS_linux += -lpthread
-LOCAL_CFLAGS_darwin += -DGTEST_OS_LINUX
+LOCAL_CFLAGS_darwin += -DGTEST_OS_MAC
LOCAL_LDLIBS_darwin += -lpthread
LOCAL_CFLAGS += -DGTEST_HAS_STD_STRING -O0 -g
diff --git a/core/main.mk b/core/main.mk
index c455320..83c60e5 100644
--- a/core/main.mk
+++ b/core/main.mk
@@ -97,6 +97,9 @@
# and host information.
include $(BUILD_SYSTEM)/config.mk
+# Default soong to on
+USE_SOONG ?= true
+
ifndef KATI
ifdef USE_NINJA
$(warning USE_NINJA is ignored. Ninja is always used.)
@@ -107,6 +110,8 @@
include build/core/ninja.mk
else # KATI
+include $(SOONG_MAKEVARS_MK)
+
# Write the build number to a file so it can be read back in
# without changing the command line every time. Avoids rebuilds
# when using ninja.
@@ -286,6 +291,12 @@
# Bring in standard build system definitions.
include $(BUILD_SYSTEM)/definitions.mk
+ifneq ($(USE_SOONG),true)
+$(eval $(call copy-toolchain-library,libgcc))
+$(eval $(call copy-toolchain-library,libatomic))
+$(eval $(call copy-toolchain-library,libgcov))
+endif
+
# Bring in dex_preopt.mk
include $(BUILD_SYSTEM)/dex_preopt.mk
diff --git a/core/ninja.mk b/core/ninja.mk
index 9e78c46..5136f4e 100644
--- a/core/ninja.mk
+++ b/core/ninja.mk
@@ -159,7 +159,7 @@
endif
$(KATI_BUILD_NINJA): $(CKATI) $(MAKEPARALLEL) $(DUMMY_OUT_MKS) run_soong FORCE
@echo Running kati to generate build$(KATI_NINJA_SUFFIX).ninja...
- +$(hide) $(KATI_MAKEPARALLEL) $(CKATI) --ninja --ninja_dir=$(OUT_DIR) --ninja_suffix=$(KATI_NINJA_SUFFIX) --regen --ignore_dirty=$(OUT_DIR)/% --no_ignore_dirty=$(SOONG_ANDROID_MK) --ignore_optional_include=$(OUT_DIR)/%.P --detect_android_echo $(KATI_FIND_EMULATOR) -f build/core/main.mk $(KATI_GOALS) --gen_all_targets BUILDING_WITH_NINJA=true SOONG_ANDROID_MK=$(SOONG_ANDROID_MK)
+ +$(hide) $(KATI_MAKEPARALLEL) $(CKATI) --ninja --ninja_dir=$(OUT_DIR) --ninja_suffix=$(KATI_NINJA_SUFFIX) --regen --ignore_dirty=$(OUT_DIR)/% --no_ignore_dirty=$(SOONG_ANDROID_MK) --no_ignore_dirty=$(SOONG_MAKEVARS_MK) --ignore_optional_include=$(OUT_DIR)/%.P --detect_android_echo $(KATI_FIND_EMULATOR) -f build/core/main.mk $(KATI_GOALS) --gen_all_targets BUILDING_WITH_NINJA=true SOONG_ANDROID_MK=$(SOONG_ANDROID_MK) SOONG_MAKEVARS_MK=$(SOONG_MAKEVARS_MK)
.PHONY: FORCE
FORCE:
diff --git a/core/shared_library_internal.mk b/core/shared_library_internal.mk
index 6fec460..cf35b5e 100644
--- a/core/shared_library_internal.mk
+++ b/core/shared_library_internal.mk
@@ -43,9 +43,9 @@
ifeq ($(LOCAL_NO_LIBGCC),true)
my_target_libgcc :=
else
-my_target_libgcc := $($(LOCAL_2ND_ARCH_VAR_PREFIX)TARGET_LIBGCC)
+my_target_libgcc := $(call intermediates-dir-for,STATIC_LIBRARIES,libgcc,,,$(LOCAL_2ND_ARCH_VAR_PREFIX))/libgcc.a
endif
-my_target_libatomic := $($(LOCAL_2ND_ARCH_VAR_PREFIX)TARGET_LIBATOMIC)
+my_target_libatomic := $(call intermediates-dir-for,STATIC_LIBRARIES,libatomic,,,$(LOCAL_2ND_ARCH_VAR_PREFIX))/libatomic.a
ifeq ($(LOCAL_NO_CRT),true)
my_target_crtbegin_so_o :=
my_target_crtend_so_o :=
@@ -76,6 +76,8 @@
$(all_libraries) \
$(my_target_crtbegin_so_o) \
$(my_target_crtend_so_o) \
+ $(my_target_libgcc) \
+ $(my_target_libatomic) \
$(LOCAL_ADDITIONAL_DEPENDENCIES)
$(transform-o-to-shared-lib)
diff --git a/core/soong.mk b/core/soong.mk
index 990a861..ebcccd3 100644
--- a/core/soong.mk
+++ b/core/soong.mk
@@ -3,12 +3,13 @@
SOONG_BOOTSTRAP := $(SOONG_OUT_DIR)/.soong.bootstrap
SOONG_BUILD_NINJA := $(SOONG_OUT_DIR)/build.ninja
SOONG_IN_MAKE := $(SOONG_OUT_DIR)/.soong.in_make
+SOONG_MAKEVARS_MK := $(SOONG_OUT_DIR)/make_vars-$(TARGET_PRODUCT).mk
SOONG_VARIABLES := $(SOONG_OUT_DIR)/soong.variables
# Only include the Soong-generated Android.mk if we're merging the
# Soong-defined binaries with Kati-defined binaries.
ifeq ($(USE_SOONG),true)
-SOONG_ANDROID_MK := $(SOONG_OUT_DIR)/Android.mk
+SOONG_ANDROID_MK := $(SOONG_OUT_DIR)/Android-$(TARGET_PRODUCT).mk
endif
# We need to rebootstrap soong if SOONG_OUT_DIR or the reverse path from
@@ -37,6 +38,8 @@
$(hide) mkdir -p $(dir $@)
$(hide) (\
echo '{'; \
+ echo ' "Make_suffix": "-$(TARGET_PRODUCT)",'; \
+ echo ''; \
echo ' "Platform_sdk_version": $(PLATFORM_SDK_VERSION),'; \
echo ' "Unbundled_build": $(if $(TARGET_BUILD_APPS),true,false),'; \
echo ' "Brillo": $(if $(BRILLO),true,false),'; \
@@ -62,7 +65,8 @@
echo ''; \
echo ' "CrossHost": "$(HOST_CROSS_OS)",'; \
echo ' "CrossHostArch": "$(HOST_CROSS_ARCH)",'; \
- echo ' "CrossHostSecondaryArch": "$(HOST_CROSS_2ND_ARCH)"'; \
+ echo ' "CrossHostSecondaryArch": "$(HOST_CROSS_2ND_ARCH)",'; \
+ echo ' "Safestack": $(if $(filter true,$(USE_SAFESTACK)),true,false)'; \
echo '}') > $(SOONG_VARIABLES_TMP); \
if ! cmp -s $(SOONG_VARIABLES_TMP) $(SOONG_VARIABLES); then \
mv $(SOONG_VARIABLES_TMP) $(SOONG_VARIABLES); \
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java b/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java
new file mode 100644
index 0000000..30d4011
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java
@@ -0,0 +1,870 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core;
+
+import com.android.apksigner.core.internal.apk.v1.DigestAlgorithm;
+import com.android.apksigner.core.internal.apk.v1.V1SchemeSigner;
+import com.android.apksigner.core.internal.apk.v2.MessageDigestSink;
+import com.android.apksigner.core.internal.apk.v2.V2SchemeSigner;
+import com.android.apksigner.core.internal.util.ByteArrayOutputStreamSink;
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSource;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Default implementation of {@link ApkSignerEngine}.
+ *
+ * <p>Use {@link Builder} to obtain instances of this engine.
+ */
+public class DefaultApkSignerEngine implements ApkSignerEngine {
+
+ // IMPLEMENTATION NOTE: This engine generates a signed APK as follows:
+ // 1. The engine asks its client to output input JAR entries which are not part of JAR
+ // signature.
+ // 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to
+ // compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects
+ // the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the
+ // file. It does not care about individual (i.e., JAR entry-specific) sections. It then
+ // emits the v1 signature (a set of JAR entries) and asks the client to output them.
+ // 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block
+ // from outputZipSections() and asks its client to insert this block into the output.
+
+ private final boolean mV1SigningEnabled;
+ private final boolean mV2SigningEnabled;
+ private final boolean mOtherSignersSignaturesPreserved;
+ private final List<V1SchemeSigner.SignerConfig> mV1SignerConfigs;
+ private final DigestAlgorithm mV1ContentDigestAlgorithm;
+ private final List<V2SchemeSigner.SignerConfig> mV2SignerConfigs;
+
+ private boolean mClosed;
+
+ private boolean mV1SignaturePending;
+
+ /**
+ * Names of JAR entries which this engine is expected to output as part of v1 signing.
+ */
+ private final Set<String> mSignatureExpectedOutputJarEntryNames;
+
+ /** Requests for digests of output JAR entries. */
+ private final Map<String, GetJarEntryDataDigestRequest> mOutputJarEntryDigestRequests =
+ new HashMap<>();
+
+ /** Digests of output JAR entries. */
+ private final Map<String, byte[]> mOutputJarEntryDigests = new HashMap<>();
+
+ /** Data of JAR entries emitted by this engine as v1 signature. */
+ private final Map<String, byte[]> mEmittedSignatureJarEntryData = new HashMap<>();
+
+ /** Requests for data of output JAR entries which comprise the v1 signature. */
+ private final Map<String, GetJarEntryDataRequest> mOutputSignatureJarEntryDataRequests =
+ new HashMap<>();
+ /**
+ * Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued.
+ */
+ private GetJarEntryDataRequest mInputJarManifestEntryDataRequest;
+
+ /**
+ * Request to output the emitted v1 signature or {@code null} if the request hasn't been issued.
+ */
+ private OutputJarSignatureRequestImpl mAddV1SignatureRequest;
+
+ private boolean mV2SignaturePending;
+
+ /**
+ * Request to output the emitted v2 signature or {@code null} if the request hasn't been issued.
+ */
+ private OutputApkSigningBlockRequestImpl mAddV2SignatureRequest;
+
+ private DefaultApkSignerEngine(
+ List<SignerConfig> signerConfigs,
+ int minSdkVersion,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled,
+ boolean otherSignersSignaturesPreserved) throws InvalidKeyException {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+ if (otherSignersSignaturesPreserved) {
+ throw new UnsupportedOperationException(
+ "Preserving other signer's signatures is not yet implemented");
+ }
+
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
+ mV1SignerConfigs =
+ (v1SigningEnabled)
+ ? new ArrayList<>(signerConfigs.size()) : Collections.emptyList();
+ mV2SignerConfigs =
+ (v2SigningEnabled)
+ ? new ArrayList<>(signerConfigs.size()) : Collections.emptyList();
+ mV1ContentDigestAlgorithm =
+ (v1SigningEnabled)
+ ? V1SchemeSigner.getSuggestedContentDigestAlgorithm(minSdkVersion) : null;
+ for (SignerConfig signerConfig : signerConfigs) {
+ List<X509Certificate> certificates = signerConfig.getCertificates();
+ PublicKey publicKey = certificates.get(0).getPublicKey();
+
+ if (v1SigningEnabled) {
+ DigestAlgorithm v1SignatureDigestAlgorithm =
+ V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(
+ publicKey, minSdkVersion);
+ V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig();
+ v1SignerConfig.name = signerConfig.getName();
+ v1SignerConfig.privateKey = signerConfig.getPrivateKey();
+ v1SignerConfig.certificates = certificates;
+ v1SignerConfig.contentDigestAlgorithm = mV1ContentDigestAlgorithm;
+ v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm;
+ mV1SignerConfigs.add(v1SignerConfig);
+ }
+
+ if (v2SigningEnabled) {
+ V2SchemeSigner.SignerConfig v2SignerConfig = new V2SchemeSigner.SignerConfig();
+ v2SignerConfig.privateKey = signerConfig.getPrivateKey();
+ v2SignerConfig.certificates = certificates;
+ v2SignerConfig.signatureAlgorithms =
+ V2SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, minSdkVersion);
+ mV2SignerConfigs.add(v2SignerConfig);
+ }
+ }
+ mSignatureExpectedOutputJarEntryNames =
+ (v1SigningEnabled)
+ ? V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs)
+ : Collections.emptySet();
+ }
+
+ @Override
+ public void inputApkSigningBlock(DataSource apkSigningBlock) {
+ checkNotClosed();
+
+ if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) {
+ return;
+ }
+
+ if (mOtherSignersSignaturesPreserved) {
+ // TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured
+ // in this engine.
+ return;
+ }
+ // TODO: Preserve blocks other than APK Signature Scheme v2 blocks.
+ }
+
+ @Override
+ public InputJarEntryInstructions inputJarEntry(String entryName) {
+ checkNotClosed();
+
+ InputJarEntryInstructions.OutputPolicy outputPolicy =
+ getInputJarEntryOutputPolicy(entryName);
+ switch (outputPolicy) {
+ case SKIP:
+ return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP);
+ case OUTPUT:
+ return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT);
+ case OUTPUT_BY_ENGINE:
+ if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
+ // We copy the main section of the JAR manifest from input to output. Thus, this
+ // invalidates v1 signature and we need to see the entry's data.
+ mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+ return new InputJarEntryInstructions(
+ InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE,
+ mInputJarManifestEntryDataRequest);
+ }
+ return new InputJarEntryInstructions(
+ InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE);
+ default:
+ throw new RuntimeException("Unsupported output policy: " + outputPolicy);
+ }
+ }
+
+ @Override
+ public InspectJarEntryRequest outputJarEntry(String entryName) {
+ checkNotClosed();
+ invalidateV2Signature();
+ if (!mV1SigningEnabled) {
+ // No need to inspect JAR entries when v1 signing is not enabled.
+ return null;
+ }
+ // v1 signing is enabled
+
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
+ // This entry is covered by v1 signature. We thus need to inspect the entry's data to
+ // compute its digest(s) for v1 signature.
+
+ // TODO: Handle the case where other signer's v1 signatures are present and need to be
+ // preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries
+ // covered by v1 signature.
+ invalidateV1Signature();
+ GetJarEntryDataDigestRequest dataDigestRequest =
+ new GetJarEntryDataDigestRequest(
+ entryName,
+ V1SchemeSigner.getMessageDigestInstance(mV1ContentDigestAlgorithm));
+ mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest);
+ mOutputJarEntryDigests.remove(entryName);
+ return dataDigestRequest;
+ }
+
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ // This entry is part of v1 signature generated by this engine. We need to check whether
+ // the entry's data is as output by the engine.
+ invalidateV1Signature();
+ GetJarEntryDataRequest dataRequest;
+ if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
+ dataRequest = new GetJarEntryDataRequest(entryName);
+ mInputJarManifestEntryDataRequest = dataRequest;
+ } else {
+ // If this entry is part of v1 signature which has been emitted by this engine,
+ // check whether the output entry's data matches what the engine emitted.
+ dataRequest =
+ (mEmittedSignatureJarEntryData.containsKey(entryName))
+ ? new GetJarEntryDataRequest(entryName) : null;
+ }
+
+ if (dataRequest != null) {
+ mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest);
+ }
+ return dataRequest;
+ }
+
+ // This entry is not covered by v1 signature and isn't part of v1 signature.
+ return null;
+ }
+
+ @Override
+ public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) {
+ checkNotClosed();
+ return getInputJarEntryOutputPolicy(entryName);
+ }
+
+ @Override
+ public void outputJarEntryRemoved(String entryName) {
+ checkNotClosed();
+ invalidateV2Signature();
+ if (!mV1SigningEnabled) {
+ return;
+ }
+
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
+ // This entry is covered by v1 signature.
+ invalidateV1Signature();
+ mOutputJarEntryDigests.remove(entryName);
+ mOutputJarEntryDigestRequests.remove(entryName);
+ mOutputSignatureJarEntryDataRequests.remove(entryName);
+ return;
+ }
+
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ // This entry is part of the v1 signature generated by this engine.
+ invalidateV1Signature();
+ return;
+ }
+ }
+
+ @Override
+ public OutputJarSignatureRequest outputJarEntries()
+ throws InvalidKeyException, SignatureException {
+ checkNotClosed();
+
+ if (!mV1SignaturePending) {
+ return null;
+ }
+
+ if ((mInputJarManifestEntryDataRequest != null)
+ && (!mInputJarManifestEntryDataRequest.isDone())) {
+ throw new IllegalStateException(
+ "Still waiting to inspect input APK's "
+ + mInputJarManifestEntryDataRequest.getEntryName());
+ }
+
+ for (GetJarEntryDataDigestRequest digestRequest
+ : mOutputJarEntryDigestRequests.values()) {
+ String entryName = digestRequest.getEntryName();
+ if (!digestRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + entryName);
+ }
+ mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
+ }
+ mOutputJarEntryDigestRequests.clear();
+
+ for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
+ if (!dataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + dataRequest.getEntryName());
+ }
+ }
+
+ List<Integer> apkSigningSchemeIds =
+ (mV2SigningEnabled) ? Collections.singletonList(2) : Collections.emptyList();
+ byte[] inputJarManifest =
+ (mInputJarManifestEntryDataRequest != null)
+ ? mInputJarManifestEntryDataRequest.getData() : null;
+
+ // Check whether the most recently used signature (if present) is still fine.
+ List<Pair<String, byte[]>> signatureZipEntries;
+ if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) {
+ try {
+ signatureZipEntries =
+ V1SchemeSigner.sign(
+ mV1SignerConfigs,
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ apkSigningSchemeIds,
+ inputJarManifest);
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException("Failed to generate v1 signature", e);
+ }
+ } else {
+ V1SchemeSigner.OutputManifestFile newManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
+ byte[] emittedSignatureManifest =
+ mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME);
+ if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
+ // Emitted v1 signature is no longer valid.
+ try {
+ signatureZipEntries =
+ V1SchemeSigner.signManifest(
+ mV1SignerConfigs,
+ mV1ContentDigestAlgorithm,
+ apkSigningSchemeIds,
+ newManifest);
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException("Failed to generate v1 signature", e);
+ }
+ } else {
+ // Emitted v1 signature is still valid. Check whether the signature is there in the
+ // output.
+ signatureZipEntries = new ArrayList<>();
+ for (Map.Entry<String, byte[]> expectedOutputEntry
+ : mEmittedSignatureJarEntryData.entrySet()) {
+ String entryName = expectedOutputEntry.getKey();
+ byte[] expectedData = expectedOutputEntry.getValue();
+ GetJarEntryDataRequest actualDataRequest =
+ mOutputSignatureJarEntryDataRequests.get(entryName);
+ if (actualDataRequest == null) {
+ // This signature entry hasn't been output.
+ signatureZipEntries.add(Pair.of(entryName, expectedData));
+ continue;
+ }
+ byte[] actualData = actualDataRequest.getData();
+ if (!Arrays.equals(expectedData, actualData)) {
+ signatureZipEntries.add(Pair.of(entryName, expectedData));
+ }
+ }
+ if (signatureZipEntries.isEmpty()) {
+ // v1 signature in the output is valid
+ return null;
+ }
+ // v1 signature in the output is not valid.
+ }
+ }
+
+ if (signatureZipEntries.isEmpty()) {
+ // v1 signature in the output is valid
+ mV1SignaturePending = false;
+ return null;
+ }
+
+ List<OutputJarSignatureRequest.JarEntry> sigEntries =
+ new ArrayList<>(signatureZipEntries.size());
+ for (Pair<String, byte[]> entry : signatureZipEntries) {
+ String entryName = entry.getFirst();
+ byte[] entryData = entry.getSecond();
+ sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData));
+ mEmittedSignatureJarEntryData.put(entryName, entryData);
+ }
+ mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries);
+ return mAddV1SignatureRequest;
+ }
+
+ @Override
+ public OutputApkSigningBlockRequest outputZipSections(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd) throws IOException, InvalidKeyException, SignatureException {
+ checkNotClosed();
+ checkV1SigningDoneIfEnabled();
+ if (!mV2SigningEnabled) {
+ return null;
+ }
+ invalidateV2Signature();
+
+ byte[] apkSigningBlock =
+ V2SchemeSigner.generateApkSigningBlock(
+ zipEntries, zipCentralDirectory, zipEocd, mV2SignerConfigs);
+
+ mAddV2SignatureRequest = new OutputApkSigningBlockRequestImpl(apkSigningBlock);
+ return mAddV2SignatureRequest;
+ }
+
+ @Override
+ public void outputDone() {
+ checkNotClosed();
+ checkV1SigningDoneIfEnabled();
+ checkV2SigningDoneIfEnabled();
+ }
+
+ @Override
+ public void close() {
+ mClosed = true;
+
+ mAddV1SignatureRequest = null;
+ mInputJarManifestEntryDataRequest = null;
+ mOutputJarEntryDigestRequests.clear();
+ mOutputJarEntryDigests.clear();
+ mEmittedSignatureJarEntryData.clear();
+ mOutputSignatureJarEntryDataRequests.clear();
+
+ mAddV2SignatureRequest = null;
+ }
+
+ private void invalidateV1Signature() {
+ if (mV1SigningEnabled) {
+ mV1SignaturePending = true;
+ }
+ invalidateV2Signature();
+ }
+
+ private void invalidateV2Signature() {
+ if (mV2SigningEnabled) {
+ mV2SignaturePending = true;
+ mAddV2SignatureRequest = null;
+ }
+ }
+
+ private void checkNotClosed() {
+ if (mClosed) {
+ throw new IllegalStateException("Engine closed");
+ }
+ }
+
+ private void checkV1SigningDoneIfEnabled() {
+ if (!mV1SignaturePending) {
+ return;
+ }
+
+ if (mAddV1SignatureRequest == null) {
+ throw new IllegalStateException(
+ "v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?");
+ }
+ if (!mAddV1SignatureRequest.isDone()) {
+ throw new IllegalStateException(
+ "v1 signature (JAR signature) addition requested by outputJarEntries() hasn't"
+ + " been fulfilled");
+ }
+ for (Map.Entry<String, byte[]> expectedOutputEntry
+ : mEmittedSignatureJarEntryData.entrySet()) {
+ String entryName = expectedOutputEntry.getKey();
+ byte[] expectedData = expectedOutputEntry.getValue();
+ GetJarEntryDataRequest actualDataRequest =
+ mOutputSignatureJarEntryDataRequests.get(entryName);
+ if (actualDataRequest == null) {
+ throw new IllegalStateException(
+ "APK entry " + entryName + " not yet output despite this having been"
+ + " requested");
+ } else if (!actualDataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + entryName);
+ }
+ byte[] actualData = actualDataRequest.getData();
+ if (!Arrays.equals(expectedData, actualData)) {
+ throw new IllegalStateException(
+ "Output APK entry " + entryName + " data differs from what was requested");
+ }
+ }
+ mV1SignaturePending = false;
+ }
+
+ private void checkV2SigningDoneIfEnabled() {
+ if (!mV2SignaturePending) {
+ return;
+ }
+ if (mAddV2SignatureRequest == null) {
+ throw new IllegalStateException(
+ "v2 signature (APK Signature Scheme v2 signature) not yet generated."
+ + " Skipped outputZipSections()?");
+ }
+ if (!mAddV2SignatureRequest.isDone()) {
+ throw new IllegalStateException(
+ "v2 signature (APK Signature Scheme v2 signature) addition requested by"
+ + " outputZipSections() hasn't been fulfilled yet");
+ }
+ mAddV2SignatureRequest = null;
+ mV2SignaturePending = false;
+ }
+
+ /**
+ * Returns the output policy for the provided input JAR entry.
+ */
+ private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) {
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE;
+ }
+ if ((mOtherSignersSignaturesPreserved)
+ || (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) {
+ return InputJarEntryInstructions.OutputPolicy.OUTPUT;
+ }
+ return InputJarEntryInstructions.OutputPolicy.SKIP;
+ }
+
+ private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
+ private final List<JarEntry> mAdditionalJarEntries;
+ private volatile boolean mDone;
+
+ private OutputJarSignatureRequestImpl(List<JarEntry> additionalZipEntries) {
+ mAdditionalJarEntries =
+ Collections.unmodifiableList(new ArrayList<>(additionalZipEntries));
+ }
+
+ @Override
+ public List<JarEntry> getAdditionalJarEntries() {
+ return mAdditionalJarEntries;
+ }
+
+ @Override
+ public void done() {
+ mDone = true;
+ }
+
+ private boolean isDone() {
+ return mDone;
+ }
+ }
+
+ private static class OutputApkSigningBlockRequestImpl implements OutputApkSigningBlockRequest {
+ private final byte[] mApkSigningBlock;
+ private volatile boolean mDone;
+
+ private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock) {
+ mApkSigningBlock = apkSigingBlock.clone();
+ }
+
+ @Override
+ public byte[] getApkSigningBlock() {
+ return mApkSigningBlock.clone();
+ }
+
+ @Override
+ public void done() {
+ mDone = true;
+ }
+
+ private boolean isDone() {
+ return mDone;
+ }
+ }
+
+ /**
+ * JAR entry inspection request which obtain the entry's uncompressed data.
+ */
+ private static class GetJarEntryDataRequest implements InspectJarEntryRequest {
+ private final String mEntryName;
+ private final Object mLock = new Object();
+ private final ByteArrayOutputStreamSink mBuf = new ByteArrayOutputStreamSink();
+
+ private boolean mDone;
+
+ private GetJarEntryDataRequest(String entryName) {
+ mEntryName = entryName;
+ }
+
+ @Override
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ @Override
+ public DataSink getDataSink() {
+ synchronized (mLock) {
+ checkNotDone();
+ return mBuf;
+ }
+ }
+
+ @Override
+ public void done() {
+ synchronized (mLock) {
+ if (mDone) {
+ return;
+ }
+ mDone = true;
+ }
+ }
+
+ private boolean isDone() {
+ synchronized (mLock) {
+ return mDone;
+ }
+ }
+
+ private void checkNotDone() throws IllegalStateException {
+ synchronized (mLock) {
+ if (mDone) {
+ throw new IllegalStateException("Already done");
+ }
+ }
+ }
+
+ private byte[] getData() {
+ synchronized (mLock) {
+ if (!mDone) {
+ throw new IllegalStateException("Not yet done");
+ }
+ return mBuf.getData();
+ }
+ }
+ }
+
+ /**
+ * JAR entry inspection request which obtains the digest of the entry's uncompressed data.
+ */
+ private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest {
+ private final String mEntryName;
+ private final MessageDigest mMessageDigest;
+ private final DataSink mDataSink;
+ private final Object mLock = new Object();
+
+ private boolean mDone;
+ private byte[] mDigest;
+
+ private GetJarEntryDataDigestRequest(String entryName, MessageDigest digest) {
+ mEntryName = entryName;
+ mMessageDigest = digest;
+ mDataSink = new MessageDigestSink(new MessageDigest[] {mMessageDigest});
+ }
+
+ @Override
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ @Override
+ public DataSink getDataSink() {
+ synchronized (mLock) {
+ checkNotDone();
+ return mDataSink;
+ }
+ }
+
+ @Override
+ public void done() {
+ synchronized (mLock) {
+ if (mDone) {
+ return;
+ }
+ mDone = true;
+ mDigest = mMessageDigest.digest();
+ }
+ }
+
+ private boolean isDone() {
+ synchronized (mLock) {
+ return mDone;
+ }
+ }
+
+ private void checkNotDone() throws IllegalStateException {
+ synchronized (mLock) {
+ if (mDone) {
+ throw new IllegalStateException("Already done");
+ }
+ }
+ }
+
+ private byte[] getDigest() {
+ synchronized (mLock) {
+ if (!mDone) {
+ throw new IllegalStateException("Not yet done");
+ }
+ return mDigest.clone();
+ }
+ }
+ }
+
+ /**
+ * Configuration of a signer.
+ *
+ * <p>Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List<X509Certificate> mCertificates;
+
+ private SignerConfig(
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates) {
+ mName = name;
+ mPrivateKey = privateKey;
+ mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
+ }
+
+ /**
+ * Returns the name of this signer.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the signing key of this signer.
+ */
+ public PrivateKey getPrivateKey() {
+ return mPrivateKey;
+ }
+
+ /**
+ * Returns the certificate(s) of this signer. The first certificate's public key corresponds
+ * to this signer's private key.
+ */
+ public List<X509Certificate> getCertificates() {
+ return mCertificates;
+ }
+
+ /**
+ * Builder of {@link SignerConfig} instances.
+ */
+ public static class Builder {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List<X509Certificate> mCertificates;
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param name signer's name. The name is reflected in the name of files comprising the
+ * JAR signature of the APK.
+ * @param privateKey signing key
+ * @param certificates list of one or more X.509 certificates. The subject public key of
+ * the first certificate must correspond to the {@code privateKey}.
+ */
+ public Builder(
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates) {
+ mName = name;
+ mPrivateKey = privateKey;
+ mCertificates = new ArrayList<>(certificates);
+ }
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerConfig build() {
+ return new SignerConfig(
+ mName,
+ mPrivateKey,
+ mCertificates);
+ }
+ }
+ }
+
+ /**
+ * Builder of {@link DefaultApkSignerEngine} instances.
+ */
+ public static class Builder {
+ private final List<SignerConfig> mSignerConfigs;
+ private final int mMinSdkVersion;
+
+ private boolean mV1SigningEnabled = true;
+ private boolean mV2SigningEnabled = true;
+ private boolean mOtherSignersSignaturesPreserved;
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param signerConfigs information about signers with which the APK will be signed. At
+ * least one signer configuration must be provided.
+ * @param minSdkVersion API Level of the oldest Android platform on which the APK is
+ * supposed to be installed. See {@code minSdkVersion} attribute in the APK's
+ * {@code AndroidManifest.xml}. The higher the version, the stronger signing features
+ * will be enabled.
+ */
+ public Builder(
+ List<SignerConfig> signerConfigs,
+ int minSdkVersion) {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+ mSignerConfigs = new ArrayList<>(signerConfigs);
+ mMinSdkVersion = minSdkVersion;
+ }
+
+ /**
+ * Returns a new {@code DefaultApkSignerEngine} instance configured based on the
+ * configuration of this builder.
+ */
+ public DefaultApkSignerEngine build() throws InvalidKeyException {
+ return new DefaultApkSignerEngine(
+ mSignerConfigs,
+ mMinSdkVersion,
+ mV1SigningEnabled,
+ mV2SigningEnabled,
+ mOtherSignersSignaturesPreserved);
+ }
+
+ /**
+ * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
+ *
+ * <p>By default, the APK will be signed using this scheme.
+ */
+ public Builder setV1SigningEnabled(boolean enabled) {
+ mV1SigningEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
+ * scheme).
+ *
+ * <p>By default, the APK will be signed using this scheme.
+ */
+ public Builder setV2SigningEnabled(boolean enabled) {
+ mV2SigningEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether signatures produced by signers other than the ones configured in this engine
+ * should be copied from the input APK to the output APK.
+ *
+ * <p>By default, signatures of other signers are omitted from the output APK.
+ */
+ public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
+ mOtherSignersSignaturesPreserved = preserved;
+ return this;
+ }
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
index 182b4ed..9ef04bf 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
@@ -24,11 +24,11 @@
* Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
* {@code MessageDigest} instance receives the same data.
*/
-class MessageDigestSink implements DataSink {
+public class MessageDigestSink implements DataSink {
private final MessageDigest[] mMessageDigests;
- MessageDigestSink(MessageDigest[] digests) {
+ public MessageDigestSink(MessageDigest[] digests) {
mMessageDigests = digests;
}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteArrayOutputStreamSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteArrayOutputStreamSink.java
new file mode 100644
index 0000000..ca79df7
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteArrayOutputStreamSink.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.util;
+
+import com.android.apksigner.core.util.DataSink;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Data sink which stores all input data into an internal {@link ByteArrayOutputStream}, thus
+ * accepting an arbitrary amount of data.
+ */
+public class ByteArrayOutputStreamSink implements DataSink {
+
+ private final ByteArrayOutputStream mBuf = new ByteArrayOutputStream();
+
+ @Override
+ public void consume(byte[] buf, int offset, int length) {
+ mBuf.write(buf, offset, length);
+ }
+
+ @Override
+ public void consume(ByteBuffer buf) {
+ if (!buf.hasRemaining()) {
+ return;
+ }
+
+ if (buf.hasArray()) {
+ mBuf.write(
+ buf.array(),
+ buf.arrayOffset() + buf.position(),
+ buf.remaining());
+ buf.position(buf.limit());
+ } else {
+ byte[] tmp = new byte[buf.remaining()];
+ buf.get(tmp);
+ mBuf.write(tmp, 0, tmp.length);
+ }
+ }
+
+ /**
+ * Returns the data received so far.
+ */
+ public byte[] getData() {
+ return mBuf.toByteArray();
+ }
+}
diff --git a/tools/releasetools/build_image.py b/tools/releasetools/build_image.py
index 4ff8c43..abb23d1 100755
--- a/tools/releasetools/build_image.py
+++ b/tools/releasetools/build_image.py
@@ -378,6 +378,8 @@
build_command.extend(["-m", prop_dict["mount_point"]])
if target_out:
build_command.extend(["-d", target_out])
+ if fs_config:
+ build_command.extend(["-C", fs_config])
if "selinux_fc" in prop_dict:
build_command.extend(["-c", prop_dict["selinux_fc"]])
if "squashfs_compressor" in prop_dict:
diff --git a/tools/signapk/src/com/android/signapk/SignApk.java b/tools/signapk/src/com/android/signapk/SignApk.java
index c80d93c..1df6b80 100644
--- a/tools/signapk/src/com/android/signapk/SignApk.java
+++ b/tools/signapk/src/com/android/signapk/SignApk.java
@@ -200,10 +200,10 @@
}
}
- // Files matching this pattern are not copied to the output.
- private static Pattern stripPattern =
- Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
- Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
+ /* Files matching this pattern are not copied to the output. */
+ private static final Pattern STRIP_PATTERN =
+ Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
+ + Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
private static X509Certificate readPublicKey(File file)
throws IOException, GeneralSecurityException {
@@ -313,8 +313,9 @@
* Add the hash(es) of every file to the manifest, creating it if
* necessary.
*/
- private static Manifest addDigestsToManifest(JarFile jar, int hashes)
- throws IOException, GeneralSecurityException {
+ private static Manifest addDigestsToManifest(
+ JarFile jar, Pattern ignoredFilenamePattern, int hashes)
+ throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
Attributes main = output.getMainAttributes();
@@ -350,8 +351,9 @@
for (JarEntry entry: byName.values()) {
String name = entry.getName();
- if (!entry.isDirectory() &&
- (stripPattern == null || !stripPattern.matcher(name).matches())) {
+ if (!entry.isDirectory()
+ && (ignoredFilenamePattern == null
+ || !ignoredFilenamePattern.matcher(name).matches())) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
@@ -394,16 +396,13 @@
* Add a copy of the public key to the archive; this should
* exactly match one of the files in
* /system/etc/security/otacerts.zip on the device. (The same
- * cert can be extracted from the CERT.RSA file but this is much
- * easier to get at.)
+ * cert can be extracted from the OTA update package's signature
+ * block but this is much easier to get at.)
*/
private static void addOtacert(JarOutputStream outputJar,
File publicKeyFile,
- long timestamp,
- Manifest manifest,
- int hash)
+ long timestamp)
throws IOException, GeneralSecurityException {
- MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
JarEntry je = new JarEntry(OTACERT_NAME);
je.setTime(timestamp);
@@ -413,14 +412,8 @@
int read;
while ((read = input.read(b)) != -1) {
outputJar.write(b, 0, read);
- md.update(b, 0, read);
}
input.close();
-
- Attributes attr = new Attributes();
- attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
- new String(Base64.encode(md.digest()), "ASCII"));
- manifest.getEntries().put(OTACERT_NAME, attr);
}
@@ -545,18 +538,31 @@
}
/**
- * Copy all the files in a manifest from input to output. We set
- * the modification times in the output to a fixed time, so as to
- * reduce variation in the output file and make incremental OTAs
- * more efficient.
+ * Copy all JAR entries from input to output. We set the modification times in the output to a
+ * fixed time, so as to reduce variation in the output file and make incremental OTAs more
+ * efficient.
*/
- private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
- long timestamp, int defaultAlignment) throws IOException {
+ private static void copyFiles(JarFile in,
+ Pattern ignoredFilenamePattern,
+ JarOutputStream out,
+ long timestamp,
+ int defaultAlignment) throws IOException {
byte[] buffer = new byte[4096];
int num;
- Map<String, Attributes> entries = manifest.getEntries();
- ArrayList<String> names = new ArrayList<String>(entries.keySet());
+ ArrayList<String> names = new ArrayList<String>();
+ for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) {
+ JarEntry entry = e.nextElement();
+ if (entry.isDirectory()) {
+ continue;
+ }
+ String entryName = entry.getName();
+ if ((ignoredFilenamePattern != null)
+ && (ignoredFilenamePattern.matcher(entryName).matches())) {
+ continue;
+ }
+ names.add(entryName);
+ }
Collections.sort(names);
boolean firstEntry = true;
@@ -757,17 +763,8 @@
signer = new WholeFileSignerOutputStream(out, outputStream);
JarOutputStream outputJar = new JarOutputStream(signer);
- Manifest manifest = addDigestsToManifest(inputJar, hash);
- copyFiles(manifest, inputJar, outputJar, timestamp, 0);
- addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
-
- signFile(manifest,
- new X509Certificate[]{ publicKey },
- new PrivateKey[]{ privateKey },
- new int[] { hash },
- timestamp,
- false, // Don't sign using APK Signature Scheme v2
- outputJar);
+ copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, 0);
+ addOtacert(outputJar, publicKeyFile, timestamp);
signer.notifyClosing();
outputJar.close();
@@ -1156,8 +1153,9 @@
v1DigestAlgorithm[i] = getV1DigestAlgorithmForApk(publicKey[i], minSdkVersion);
v1DigestAlgorithmBitSet |= v1DigestAlgorithm[i];
}
- Manifest manifest = addDigestsToManifest(inputJar, v1DigestAlgorithmBitSet);
- copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
+ Manifest manifest =
+ addDigestsToManifest(inputJar, STRIP_PATTERN, v1DigestAlgorithmBitSet);
+ copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, alignment);
signFile(
manifest,
publicKey, privateKey, v1DigestAlgorithm,
diff --git a/tools/warn.py b/tools/warn.py
index 135cd51..a4a9e16 100755
--- a/tools/warn.py
+++ b/tools/warn.py
@@ -312,87 +312,1001 @@
'description':'Java: Unchecked conversion',
'patterns':[r".*: warning: \[unchecked\] unchecked conversion"] },
- # Warnings from error prone.
- { 'category':'java', 'severity':severity.LOW, 'members':[], 'option':'',
- 'description':'Java: Long literal suffix',
- 'patterns':[r".*: warning: \[LongLiteralLowerCaseSuffix\] Prefer 'L' to 'l' for the suffix to long literal"] },
- { 'category':'java', 'severity':severity.LOW, 'members':[], 'option':'',
- 'description':'Java: Missing @Deprecated',
- 'patterns':[r".*: warning: \[DepAnn\] Deprecated item is not annotated with @Deprecated"] },
- { 'category':'java', 'severity':severity.LOW, 'members':[], 'option':'',
- 'description':'Java: Use of deprecated member',
- 'patterns':[r".*: warning: \[deprecation\] .+ in .+ has been deprecated"] },
- { 'category':'java', 'severity':severity.LOW, 'members':[], 'option':'',
- 'description':'Java: Missing hashCode method',
- 'patterns':[r".*: warning: \[EqualsHashCode\] Classes that override equals should also override hashCode."] },
- { 'category':'java', 'severity':severity.LOW, 'members':[], 'option':'',
- 'description':'Java: Hashtable contains is a legacy method',
- 'patterns':[r".*: warning: \[HashtableContains\] contains\(\) is a legacy method that is equivalent to containsValue\(\)"] },
- { 'category':'java', 'severity':severity.LOW, 'members':[], 'option':'',
- 'description':'Java: Type parameter used only for return type',
- 'patterns':[r".*: warning: \[TypeParameterUnusedInFormals\] Declaring a type parameter that is only used in the return type is a misuse of generics: operations on the type parameter are unchecked, it hides unsafe casts at invocations of the method, and it interacts badly with method overload resolution."] },
+ # Warnings from Error Prone.
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description': 'Java: Use of deprecated member',
+ 'patterns': [r'.*: warning: \[deprecation\] .+']},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description': 'Java: Unchecked conversion',
+ 'patterns': [r'.*: warning: \[unchecked\] .+']},
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: reference equality used on arrays',
- 'patterns':[r".*: warning: \[ArrayEquals\] Reference equality used to compare arrays"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: hashcode used on array',
- 'patterns':[r".*: warning: \[ArrayHashCode\] hashcode method on array does not hash array contents"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: toString used on an array',
- 'patterns':[r".*: warning: \[ArrayToStringConcatenation\] Implicit toString used on an array \(String \+ Array\)",
- r".*: warning: \[ArrayToString\] Calling toString on an array does not provide useful information"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Exception created but not thrown',
- 'patterns':[r".*: warning: \[DeadException\] Exception created but not thrown"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Return or throw from a finally',
- 'patterns':[r".*: warning: \[Finally\] If you return or throw from a finally, then values returned or thrown from the try-catch block will be ignored. Consider using try-with-resources instead."] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Erroneous use of @GuardedBy',
- 'patterns':[r".*: warning: \[GuardedByChecker\] This access should be guarded by '.+'; instead found: '.+'",
- r".*: warning: \[GuardedByChecker\] This access should be guarded by '.+', which is not currently held"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Mislabeled Android string',
- 'patterns':[r".*: warning: \[MislabeledAndroidString\] .+ is not \".+\" but \".+\"; prefer .+ for clarity"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Missing cases in enum switch',
- 'patterns':[r".*: warning: \[MissingCasesInEnumSwitch\] Non-exhaustive switch, expected cases for: .+"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Multiple top-level classes (inhibits bug analysis)',
- 'patterns':[r".*: warning: \[MultipleTopLevelClasses\] Expected at most one top-level class declaration, instead found: .+"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: equals method doesn\'t override Object.equals',
- 'patterns':[r".*: warning: \[NonOverridingEquals\] equals method doesn't override Object\.equals.*"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Update of a volatile variable is non-atomic',
- 'patterns':[r".*: warning: \[NonAtomicVolatileUpdate\] This update of a volatile variable is non-atomic"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Return value ignored',
- 'patterns':[r".*: warning: \[ReturnValueIgnored\] Return value of this method must be used",
- r".*: warning: \[RectIntersectReturnValueIgnored\] Return value of android.graphics.Rect.intersect\(\) must be checked"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Static variable accessed from an object instance',
- 'patterns':[r".*: warning: \[StaticAccessedFromInstance\] Static (method|variable) .+ should not be accessed from an object instance; instead use .+"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Static guarded by instance',
- 'patterns':[r".*: warning: \[StaticGuardedByInstance\] Write to static variable should not be guarded by instance lock '.+'"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: String reference equality',
- 'patterns':[r".*: warning: \[StringEquality\] String comparison using reference equality instead of value equality"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Synchronization on non-final field',
- 'patterns':[r".*: warning: \[SynchronizeOnNonFinalField\] Synchronizing on non-final fields is not safe: if the field is ever updated, different threads may end up locking on different objects."] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Catch masks fail or assert',
- 'patterns':[r".*: warning: \[TryFailThrowable\] Catching Throwable/Error masks failures from fail\(\) or assert\*\(\) in the try block"] },
- { 'category':'java', 'severity':severity.MEDIUM, 'members':[], 'option':'',
- 'description':'Java: Wait not in a loop',
- 'patterns':[r".*: warning: \[WaitNotInLoop\] Because of spurious wakeups, a?wait.*\(.*\) must always be called in a loop"] },
+ # Warnings from Error Prone (auto generated list).
+ {'category': 'java',
+ 'severity': severity.LOW,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Deprecated item is not annotated with @Deprecated',
+ 'patterns': [r".*: warning: \[DepAnn\] .+"]},
+ {'category': 'java',
+ 'severity': severity.LOW,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Fallthrough warning suppression has no effect if warning is suppressed',
+ 'patterns': [r".*: warning: \[FallthroughSuppression\] .+"]},
+ {'category': 'java',
+ 'severity': severity.LOW,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Prefer \'L\' to \'l\' for the suffix to long literals',
+ 'patterns': [r".*: warning: \[LongLiteralLowerCaseSuffix\] .+"]},
+ {'category': 'java',
+ 'severity': severity.LOW,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: @Binds is a more efficient and declaritive mechanism for delegating a binding.',
+ 'patterns': [r".*: warning: \[UseBinds\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Assertions may be disabled at runtime and do not guarantee that execution will halt here; consider throwing an exception instead',
+ 'patterns': [r".*: warning: \[AssertFalse\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Classes that implement Annotation must override equals and hashCode. Consider using AutoAnnotation instead of implementing Annotation by hand.',
+ 'patterns': [r".*: warning: \[BadAnnotationImplementation\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: BigDecimal(double) and BigDecimal.valueOf(double) may lose precision, prefer BigDecimal(String) or BigDecimal(long)',
+ 'patterns': [r".*: warning: \[BigDecimalLiteralDouble\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Mockito cannot mock final classes',
+ 'patterns': [r".*: warning: \[CannotMockFinalClass\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This code, which counts elements using a loop, can be replaced by a simpler library method',
+ 'patterns': [r".*: warning: \[ElementsCountedInLoop\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Empty top-level type declaration',
+ 'patterns': [r".*: warning: \[EmptyTopLevelDeclaration\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Classes that override equals should also override hashCode.',
+ 'patterns': [r".*: warning: \[EqualsHashCode\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: An equality test between objects with incompatible types always returns false',
+ 'patterns': [r".*: warning: \[EqualsIncompatibleType\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: If you return or throw from a finally, then values returned or thrown from the try-catch block will be ignored. Consider using try-with-resources instead.',
+ 'patterns': [r".*: warning: \[Finally\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This annotation has incompatible modifiers as specified by its @IncompatibleModifiers annotation',
+ 'patterns': [r".*: warning: \[IncompatibleModifiers\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Class should not implement both `Iterable` and `Iterator`',
+ 'patterns': [r".*: warning: \[IterableAndIterator\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Floating-point comparison without error tolerance',
+ 'patterns': [r".*: warning: \[JUnit3FloatingPointComparisonWithoutDelta\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Test class inherits from JUnit 3\'s TestCase but has JUnit 4 @Test annotations.',
+ 'patterns': [r".*: warning: \[JUnitAmbiguousTestClass\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Enum switch statement is missing cases',
+ 'patterns': [r".*: warning: \[MissingCasesInEnumSwitch\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Not calling fail() when expecting an exception masks bugs',
+ 'patterns': [r".*: warning: \[MissingFail\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: method overrides method in supertype; expected @Override',
+ 'patterns': [r".*: warning: \[MissingOverride\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Source files should not contain multiple top-level class declarations',
+ 'patterns': [r".*: warning: \[MultipleTopLevelClasses\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This update of a volatile variable is non-atomic',
+ 'patterns': [r".*: warning: \[NonAtomicVolatileUpdate\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Static import of member uses non-canonical name',
+ 'patterns': [r".*: warning: \[NonCanonicalStaticMemberImport\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: equals method doesn\'t override Object.equals',
+ 'patterns': [r".*: warning: \[NonOverridingEquals\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Constructors should not be annotated with @Nullable since they cannot return null',
+ 'patterns': [r".*: warning: \[NullableConstructor\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: @Nullable should not be used for primitive types since they cannot be null',
+ 'patterns': [r".*: warning: \[NullablePrimitive\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: void-returning methods should not be annotated with @Nullable, since they cannot return null',
+ 'patterns': [r".*: warning: \[NullableVoid\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Package names should match the directory they are declared in',
+ 'patterns': [r".*: warning: \[PackageLocation\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Second argument to Preconditions.* is a call to String.format(), which can be unwrapped',
+ 'patterns': [r".*: warning: \[PreconditionsErrorMessageEagerEvaluation\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Preconditions only accepts the %s placeholder in error message strings',
+ 'patterns': [r".*: warning: \[PreconditionsInvalidPlaceholder\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Passing a primitive array to a varargs method is usually wrong',
+ 'patterns': [r".*: warning: \[PrimitiveArrayPassedToVarargsMethod\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Protobuf fields cannot be null, so this check is redundant',
+ 'patterns': [r".*: warning: \[ProtoFieldPreconditionsCheckNotNull\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This annotation is missing required modifiers as specified by its @RequiredModifiers annotation',
+ 'patterns': [r".*: warning: \[RequiredModifiers\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: A static variable or method should not be accessed from an object instance',
+ 'patterns': [r".*: warning: \[StaticAccessedFromInstance\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: String comparison using reference equality instead of value equality',
+ 'patterns': [r".*: warning: \[StringEquality\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Declaring a type parameter that is only used in the return type is a misuse of generics: operations on the type parameter are unchecked, it hides unsafe casts at invocations of the method, and it interacts badly with method overload resolution.',
+ 'patterns': [r".*: warning: \[TypeParameterUnusedInFormals\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Using static imports for types is unnecessary',
+ 'patterns': [r".*: warning: \[UnnecessaryStaticImport\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Unsynchronized method overrides a synchronized method.',
+ 'patterns': [r".*: warning: \[UnsynchronizedOverridesSynchronized\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Non-constant variable missing @Var annotation',
+ 'patterns': [r".*: warning: \[Var\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Because of spurious wakeups, Object.wait() and Condition.await() must always be called in a loop',
+ 'patterns': [r".*: warning: \[WaitNotInLoop\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Subclasses of Fragment must be instantiable via Class#newInstance(): the class must be public, static and have a public nullary constructor',
+ 'patterns': [r".*: warning: \[FragmentNotInstantiable\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Hardcoded reference to /sdcard',
+ 'patterns': [r".*: warning: \[HardCodedSdCardPath\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Incompatible type as argument to Object-accepting Java collections method',
+ 'patterns': [r".*: warning: \[CollectionIncompatibleType\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: @AssistedInject and @Inject should not be used on different constructors in the same class.',
+ 'patterns': [r".*: warning: \[AssistedInjectAndInjectOnConstructors\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Although Guice allows injecting final fields, doing so is not recommended because the injected value may not be visible to other threads.',
+ 'patterns': [r".*: warning: \[GuiceInjectOnFinalField\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This method is not annotated with @Inject, but it overrides a method that is annotated with @com.google.inject.Inject. Guice will inject this method, and it is recommended to annotate it explicitly.',
+ 'patterns': [r".*: warning: \[OverridesGuiceInjectableMethod\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Double-checked locking on non-volatile fields is unsafe',
+ 'patterns': [r".*: warning: \[DoubleCheckedLocking\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Writes to static fields should not be guarded by instance locks',
+ 'patterns': [r".*: warning: \[StaticGuardedByInstance\] .+"]},
+ {'category': 'java',
+ 'severity': severity.MEDIUM,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Synchronizing on non-final fields is not safe: if the field is ever updated, different threads may end up locking on different objects.',
+ 'patterns': [r".*: warning: \[SynchronizeOnNonFinalField\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Reference equality used to compare arrays',
+ 'patterns': [r".*: warning: \[ArrayEquals\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: hashcode method on array does not hash array contents',
+ 'patterns': [r".*: warning: \[ArrayHashCode\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Calling toString on an array does not provide useful information',
+ 'patterns': [r".*: warning: \[ArrayToString.*\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Arrays.asList does not autobox primitive arrays, as one might expect.',
+ 'patterns': [r".*: warning: \[ArraysAsListPrimitiveArray\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: AsyncCallable should not return a null Future, only a Future whose result is null.',
+ 'patterns': [r".*: warning: \[AsyncCallableReturnsNull\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: AsyncFunction should not return a null Future, only a Future whose result is null.',
+ 'patterns': [r".*: warning: \[AsyncFunctionReturnsNull\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Possible sign flip from narrowing conversion',
+ 'patterns': [r".*: warning: \[BadComparable\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Shift by an amount that is out of range',
+ 'patterns': [r".*: warning: \[BadShiftAmount\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: valueOf provides better time and space performance',
+ 'patterns': [r".*: warning: \[BoxedPrimitiveConstructor\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: The called constructor accepts a parameter with the same name and type as one of its caller\'s parameters, but its caller doesn\'t pass that parameter to it. It\'s likely that it was intended to.',
+ 'patterns': [r".*: warning: \[ChainingConstructorIgnoresParameter\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Ignored return value of method that is annotated with @CheckReturnValue',
+ 'patterns': [r".*: warning: \[CheckReturnValue\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Inner class is non-static but does not reference enclosing class',
+ 'patterns': [r".*: warning: \[ClassCanBeStatic\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: The source file name should match the name of the top-level class it contains',
+ 'patterns': [r".*: warning: \[ClassName\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This comparison method violates the contract',
+ 'patterns': [r".*: warning: \[ComparisonContractViolated\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Comparison to value that is out of range for the compared type',
+ 'patterns': [r".*: warning: \[ComparisonOutOfRange\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Non-compile-time constant expression passed to parameter with @CompileTimeConstant type annotation.',
+ 'patterns': [r".*: warning: \[CompileTimeConstant\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Exception created but not thrown',
+ 'patterns': [r".*: warning: \[DeadException\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Division by integer literal zero',
+ 'patterns': [r".*: warning: \[DivZero\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Empty statement after if',
+ 'patterns': [r".*: warning: \[EmptyIf\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: == NaN always returns false; use the isNaN methods instead',
+ 'patterns': [r".*: warning: \[EqualsNaN\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Method annotated @ForOverride must be protected or package-private and only invoked from declaring class',
+ 'patterns': [r".*: warning: \[ForOverride\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Futures.getChecked requires a checked exception type with a standard constructor.',
+ 'patterns': [r".*: warning: \[FuturesGetCheckedIllegalExceptionType\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Calling getClass() on an object of type Class returns the Class object for java.lang.Class; you probably meant to operate on the object directly',
+ 'patterns': [r".*: warning: \[GetClassOnClass\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: An object is tested for equality to itself using Guava Libraries',
+ 'patterns': [r".*: warning: \[GuavaSelfEquals\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: contains() is a legacy method that is equivalent to containsValue()',
+ 'patterns': [r".*: warning: \[HashtableContains\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Cipher.getInstance() is invoked using either the default settings or ECB mode',
+ 'patterns': [r".*: warning: \[InsecureCipherMode\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Invalid syntax used for a regular expression',
+ 'patterns': [r".*: warning: \[InvalidPatternSyntax\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: The argument to Class#isInstance(Object) should not be a Class',
+ 'patterns': [r".*: warning: \[IsInstanceOfClass\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: jMock tests must have a @RunWith(JMock.class) annotation, or the Mockery field must have a @Rule JUnit annotation',
+ 'patterns': [r".*: warning: \[JMockTestWithoutRunWithOrRuleAnnotation\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Test method will not be run; please prefix name with "test"',
+ 'patterns': [r".*: warning: \[JUnit3TestNotRun\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: setUp() method will not be run; Please add a @Before annotation',
+ 'patterns': [r".*: warning: \[JUnit4SetUpNotRun\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: tearDown() method will not be run; Please add an @After annotation',
+ 'patterns': [r".*: warning: \[JUnit4TearDownNotRun\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Test method will not be run; please add @Test annotation',
+ 'patterns': [r".*: warning: \[JUnit4TestNotRun\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Printf-like format string does not match its arguments',
+ 'patterns': [r".*: warning: \[MalformedFormatString\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Use of "YYYY" (week year) in a date pattern without "ww" (week in year). You probably meant to use "yyyy" (year) instead.',
+ 'patterns': [r".*: warning: \[MisusedWeekYear\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: A bug in Mockito will cause this test to fail at runtime with a ClassCastException',
+ 'patterns': [r".*: warning: \[MockitoCast\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Missing method call for verify(mock) here',
+ 'patterns': [r".*: warning: \[MockitoUsage\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Modifying a collection with itself',
+ 'patterns': [r".*: warning: \[ModifyingCollectionWithItself\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Compound assignments to bytes, shorts, chars, and floats hide dangerous casts',
+ 'patterns': [r".*: warning: \[NarrowingCompoundAssignment\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: @NoAllocation was specified on this method, but something was found that would trigger an allocation',
+ 'patterns': [r".*: warning: \[NoAllocation\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Static import of type uses non-canonical name',
+ 'patterns': [r".*: warning: \[NonCanonicalStaticImport\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: @CompileTimeConstant parameters should be final',
+ 'patterns': [r".*: warning: \[NonFinalCompileTimeConstant\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Calling getAnnotation on an annotation that is not retained at runtime.',
+ 'patterns': [r".*: warning: \[NonRuntimeAnnotation\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Numeric comparison using reference equality instead of value equality',
+ 'patterns': [r".*: warning: \[NumericEquality\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Comparison using reference equality instead of value equality',
+ 'patterns': [r".*: warning: \[OptionalEquality\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Varargs doesn\'t agree for overridden method',
+ 'patterns': [r".*: warning: \[Overrides\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Literal passed as first argument to Preconditions.checkNotNull() can never be null',
+ 'patterns': [r".*: warning: \[PreconditionsCheckNotNull\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: First argument to `Preconditions.checkNotNull()` is a primitive rather than an object reference',
+ 'patterns': [r".*: warning: \[PreconditionsCheckNotNullPrimitive\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Protobuf fields cannot be null',
+ 'patterns': [r".*: warning: \[ProtoFieldNullComparison\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Comparing protobuf fields of type String using reference equality',
+ 'patterns': [r".*: warning: \[ProtoStringFieldReferenceEquality\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Check for non-whitelisted callers to RestrictedApiChecker.',
+ 'patterns': [r".*: warning: \[RestrictedApiChecker\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Return value of this method must be used',
+ 'patterns': [r".*: warning: \[ReturnValueIgnored\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Variable assigned to itself',
+ 'patterns': [r".*: warning: \[SelfAssignment\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: An object is compared to itself',
+ 'patterns': [r".*: warning: \[SelfComparision\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Variable compared to itself',
+ 'patterns': [r".*: warning: \[SelfEquality\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: An object is tested for equality to itself',
+ 'patterns': [r".*: warning: \[SelfEquals\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Comparison of a size >= 0 is always true, did you intend to check for non-emptiness?',
+ 'patterns': [r".*: warning: \[SizeGreaterThanOrEqualsZero\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Calling toString on a Stream does not provide useful information',
+ 'patterns': [r".*: warning: \[StreamToString\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: StringBuilder does not have a char constructor; this invokes the int constructor.',
+ 'patterns': [r".*: warning: \[StringBuilderInitWithChar\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Suppressing "deprecated" is probably a typo for "deprecation"',
+ 'patterns': [r".*: warning: \[SuppressWarningsDeprecated\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: throwIfUnchecked(knownCheckedException) is a no-op.',
+ 'patterns': [r".*: warning: \[ThrowIfUncheckedKnownChecked\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Catching Throwable/Error masks failures from fail() or assert*() in the try block',
+ 'patterns': [r".*: warning: \[TryFailThrowable\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Type parameter used as type qualifier',
+ 'patterns': [r".*: warning: \[TypeParameterQualifier\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Non-generic methods should not be invoked with type arguments',
+ 'patterns': [r".*: warning: \[UnnecessaryTypeArgument\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Instance created but never used',
+ 'patterns': [r".*: warning: \[UnusedAnonymousClass\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Use of wildcard imports is forbidden',
+ 'patterns': [r".*: warning: \[WildcardImport\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Method parameter has wrong package',
+ 'patterns': [r".*: warning: \[ParameterPackage\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Certain resources in `android.R.string` have names that do not match their content',
+ 'patterns': [r".*: warning: \[MislabeledAndroidString\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Return value of android.graphics.Rect.intersect() must be checked',
+ 'patterns': [r".*: warning: \[RectIntersectReturnValueIgnored\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Invalid printf-style format string',
+ 'patterns': [r".*: warning: \[FormatString\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: @AssistedInject and @Inject cannot be used on the same constructor.',
+ 'patterns': [r".*: warning: \[AssistedInjectAndInjectOnSameConstructor\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Injected constructors cannot be optional nor have binding annotations',
+ 'patterns': [r".*: warning: \[InjectedConstructorAnnotations\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: The target of a scoping annotation must be set to METHOD and/or TYPE.',
+ 'patterns': [r".*: warning: \[InjectInvalidTargetingOnScopingAnnotation\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Abstract methods are not injectable with javax.inject.Inject.',
+ 'patterns': [r".*: warning: \[JavaxInjectOnAbstractMethod\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: @javax.inject.Inject cannot be put on a final field.',
+ 'patterns': [r".*: warning: \[JavaxInjectOnFinalField\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: A class may not have more than one injectable constructor.',
+ 'patterns': [r".*: warning: \[MoreThanOneInjectableConstructor\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Using more than one qualifier annotation on the same element is not allowed.',
+ 'patterns': [r".*: warning: \[InjectMoreThanOneQualifier\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: A class can be annotated with at most one scope annotation',
+ 'patterns': [r".*: warning: \[InjectMoreThanOneScopeAnnotationOnClass\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Annotations cannot be both Qualifiers/BindingAnnotations and Scopes',
+ 'patterns': [r".*: warning: \[OverlappingQualifierAndScopeAnnotation\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Scope annotation on an interface or abstact class is not allowed',
+ 'patterns': [r".*: warning: \[InjectScopeAnnotationOnInterfaceOrAbstractClass\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Scoping and qualifier annotations must have runtime retention.',
+ 'patterns': [r".*: warning: \[InjectScopeOrQualifierAnnotationRetention\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Dagger @Provides methods may not return null unless annotated with @Nullable',
+ 'patterns': [r".*: warning: \[DaggerProvidesNull\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Scope annotation on implementation class of AssistedInject factory is not allowed',
+ 'patterns': [r".*: warning: \[GuiceAssistedInjectScoping\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: A constructor cannot have two @Assisted parameters of the same type unless they are disambiguated with named @Assisted annotations. ',
+ 'patterns': [r".*: warning: \[GuiceAssistedParameters\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This method is not annotated with @Inject, but it overrides a method that is annotated with @javax.inject.Inject.',
+ 'patterns': [r".*: warning: \[OverridesJavaxInjectableMethod\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Checks for unguarded accesses to fields and methods with @GuardedBy annotations',
+ 'patterns': [r".*: warning: \[GuardedByChecker\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Invalid @GuardedBy expression',
+ 'patterns': [r".*: warning: \[GuardedByValidator\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: Type declaration annotated with @Immutable is not immutable',
+ 'patterns': [r".*: warning: \[Immutable\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This method does not acquire the locks specified by its @LockMethod annotation',
+ 'patterns': [r".*: warning: \[LockMethodChecker\] .+"]},
+ {'category': 'java',
+ 'severity': severity.HIGH,
+ 'members': [],
+ 'option': '',
+ 'description':
+ 'Java: This method does not acquire the locks specified by its @UnlockMethod annotation',
+ 'patterns': [r".*: warning: \[UnlockMethod\] .+"]},
- { 'category':'java', 'severity':severity.UNKNOWN, 'members':[], 'option':'',
- 'description':'Java: Unclassified/unrecognized warnings',
- 'patterns':[r".*: warning: \[.+\] .+"] },
+ {'category': 'java',
+ 'severity': severity.UNKNOWN,
+ 'members': [],
+ 'option': '',
+ 'description': 'Java: Unclassified/unrecognized warnings',
+ 'patterns': [r".*: warning: \[.+\] .+"]},
{ 'category':'aapt', 'severity':severity.MEDIUM, 'members':[], 'option':'',
'description':'aapt: No default translation',