Merge "Add the option to reserve headroom for partition images. This is useful for devices with low disk space with different build variants."
diff --git a/core/Makefile b/core/Makefile
index 955b406..244f9cd 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -191,11 +191,16 @@
 
 BUILDINFO_SH := build/tools/buildinfo.sh
 
-# TARGET_BUILD_FLAVOR and ro.build.flavor are used only by the test harness to distinguish builds.
+# TARGET_BUILD_FLAVOR and ro.build.flavor are used only by the test
+# harness to distinguish builds. Only add _asan for a sanitized build
+# if it isn't already a part of the flavor (via a dedicated lunch
+# config for example).
 TARGET_BUILD_FLAVOR := $(TARGET_PRODUCT)-$(TARGET_BUILD_VARIANT)
 ifdef SANITIZE_TARGET
+ifeq (,$(findstring _asan,$(TARGET_BUILD_FLAVOR)))
 TARGET_BUILD_FLAVOR := $(TARGET_BUILD_FLAVOR)_asan
 endif
+endif
 
 ifdef TARGET_SYSTEM_PROP
 system_prop_file := $(TARGET_SYSTEM_PROP)
@@ -1155,6 +1160,9 @@
 ifeq (true,$(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VBOOT))
 $(INSTALLED_BOOTIMAGE_TARGET) : $(VBOOT_SIGNER)
 endif
+ifeq (true,$(BOARD_AVB_ENABLE))
+$(INSTALLED_BOOTIMAGE_TARGET) : $(AVBTOOL)
+endif
 $(INSTALLED_BOOTIMAGE_TARGET): $(MKBOOTFS) $(MKBOOTIMG) $(MINIGZIP) \
 		$(INSTALLED_RAMDISK_TARGET) \
 		$(INTERNAL_RECOVERYIMAGE_FILES) \
diff --git a/core/binary.mk b/core/binary.mk
index 8a238ba..b2b8186 100644
--- a/core/binary.mk
+++ b/core/binary.mk
@@ -1684,6 +1684,12 @@
     ifeq ($(my_tidy_flags),)
       my_tidy_flags := $(call default_tidy_header_filter,$(LOCAL_PATH))
     endif
+
+    # We might be using the static analyzer through clang-tidy.
+    # https://bugs.llvm.org/show_bug.cgi?id=32914
+    ifneq ($(my_tidy_checks),)
+      my_tidy_flags += "-extra-arg-before=-D__clang_analyzer__"
+    endif
   endif
 endif
 
diff --git a/core/definitions.mk b/core/definitions.mk
index 3e99606..64bdfe6 100644
--- a/core/definitions.mk
+++ b/core/definitions.mk
@@ -1017,12 +1017,15 @@
 $(hide) echo >> $2
 endef
 
+# b/37755219
+RS_CC_ASAN_OPTIONS := ASAN_OPTIONS=detect_leaks=0:detect_container_overflow=0
+
 define transform-renderscripts-to-java-and-bc
 @echo "RenderScript: $(PRIVATE_MODULE) <= $(PRIVATE_RS_SOURCE_FILES)"
 $(hide) rm -rf $(PRIVATE_RS_OUTPUT_DIR)
 $(hide) mkdir -p $(PRIVATE_RS_OUTPUT_DIR)/res/raw
 $(hide) mkdir -p $(PRIVATE_RS_OUTPUT_DIR)/src
-$(hide) $(PRIVATE_RS_CC) \
+$(hide) $(RS_CC_ASAN_OPTIONS) $(PRIVATE_RS_CC) \
   -o $(PRIVATE_RS_OUTPUT_DIR)/res/raw \
   -p $(PRIVATE_RS_OUTPUT_DIR)/src \
   -d $(PRIVATE_RS_OUTPUT_DIR) \
@@ -1058,7 +1061,7 @@
 @echo "RenderScript: $(PRIVATE_MODULE) <= $(PRIVATE_RS_SOURCE_FILES)"
 $(hide) rm -rf $(PRIVATE_RS_OUTPUT_DIR)
 $(hide) mkdir -p $(PRIVATE_RS_OUTPUT_DIR)/
-$(hide) $(PRIVATE_RS_CC) \
+$(hide) $(RS_CC_ASAN_OPTIONS) $(PRIVATE_RS_CC) \
   -o $(PRIVATE_RS_OUTPUT_DIR)/ \
   -d $(PRIVATE_RS_OUTPUT_DIR) \
   -a $@ -MD \
@@ -2000,6 +2003,9 @@
 APPS_DEFAULT_VERSION_NAME := $(PLATFORM_VERSION)
 endif
 
+# b/37750224
+AAPT_ASAN_OPTIONS := ASAN_OPTIONS=detect_leaks=0
+
 # TODO: Right now we generate the asset resources twice, first as part
 # of generating the Java classes, then at the end when packaging the final
 # assets.  This should be changed to do one of two things: (1) Don't generate
@@ -2014,7 +2020,7 @@
 define create-resource-java-files
 @mkdir -p $(PRIVATE_SOURCE_INTERMEDIATES_DIR)
 @mkdir -p $(dir $(PRIVATE_RESOURCE_PUBLICS_OUTPUT))
-$(hide) $(AAPT) package $(PRIVATE_AAPT_FLAGS) -m \
+$(hide) $(AAPT_ASAN_OPTIONS) $(AAPT) package $(PRIVATE_AAPT_FLAGS) -m \
     $(eval # PRIVATE_PRODUCT_AAPT_CONFIG is intentionally missing-- see comment.) \
     $(addprefix -J , $(PRIVATE_SOURCE_INTERMEDIATES_DIR)) \
     $(addprefix -M , $(PRIVATE_ANDROID_MANIFEST)) \
@@ -2394,13 +2400,16 @@
 fi
 endef
 
+# b/37756495
+IJAR_ASAN_OPTIONS := ASAN_OPTIONS=detect_leaks=0
+
 ## Rule to create a table of contents from a .jar file.
 ## Must be called with $(eval).
 # $(1): A .jar file
 define _transform-jar-to-toc
 $1.toc: $1 | $(IJAR)
 	@echo Generating TOC: $$@
-	$(hide) $(IJAR) $$< $$@.tmp
+	$(hide) $(IJAR_ASAN_OPTIONS) $(IJAR) $$< $$@.tmp
 	$$(call commit-change-for-toc,$$@)
 endef
 
@@ -2597,7 +2606,7 @@
 #values; applications can override these by explicitly stating
 #them in their manifest.
 define add-assets-to-package
-$(hide) $(AAPT) package -u $(PRIVATE_AAPT_FLAGS) \
+$(hide) $(AAPT_ASAN_OPTIONS) $(AAPT) package -u $(PRIVATE_AAPT_FLAGS) \
     $(addprefix -c , $(PRIVATE_PRODUCT_AAPT_CONFIG)) \
     $(addprefix --preferred-density , $(PRIVATE_PRODUCT_AAPT_PREF_CONFIG)) \
     $(addprefix -M , $(PRIVATE_ANDROID_MANIFEST)) \
@@ -3229,6 +3238,141 @@
 endef
 
 ###########################################################
+## Path Cleaning
+###########################################################
+
+# Remove "dir .." combinations (but keep ".. ..")
+#
+# $(1): The expanded path, where / is converted to ' ' to work with $(word)
+define _clean-path-strip-dotdot
+$(strip \
+  $(if $(word 2,$(1)),
+    $(if $(call streq,$(word 2,$(1)),..),
+      $(if $(call streq,$(word 1,$(1)),..),
+        $(word 1,$(1)) $(call _clean-path-strip-dotdot,$(wordlist 2,$(words $(1)),$(1)))
+      ,
+        $(call _clean-path-strip-dotdot,$(wordlist 3,$(words $(1)),$(1)))
+      )
+    ,
+      $(word 1,$(1)) $(call _clean-path-strip-dotdot,$(wordlist 2,$(words $(1)),$(1)))
+    )
+  ,
+    $(1)
+  )
+)
+endef
+
+# Remove any leading .. from the path (in case of /..)
+#
+# Should only be called if the original path started with /
+# $(1): The expanded path, where / is converted to ' ' to work with $(word)
+define _clean-path-strip-root-dotdots
+$(strip $(if $(call streq,$(firstword $(1)),..),
+  $(call _clean-path-strip-root-dotdots,$(wordlist 2,$(words $(1)),$(1))),
+  $(1)))
+endef
+
+# Call _clean-path-strip-dotdot until the path stops changing
+# $(1): Non-empty if this path started with a /
+# $(2): The expanded path, where / is converted to ' ' to work with $(word)
+define _clean-path-expanded
+$(strip \
+  $(eval _ep := $(call _clean-path-strip-dotdot,$(2)))
+  $(if $(1),$(eval _ep := $(call _clean-path-strip-root-dotdots,$(_ep))))
+  $(if $(call streq,$(2),$(_ep)),
+    $(_ep),
+    $(call _clean-path-expanded,$(1),$(_ep))))
+endef
+
+# Clean the file path -- remove //, dir/.., extra .
+#
+# This should be the same semantics as golang's filepath.Clean
+#
+# $(1): The file path to clean
+define clean-path
+$(strip \
+  $(if $(call streq,$(words $(1)),1),
+    $(eval _rooted := $(filter /%,$(1)))
+    $(eval _expanded_path := $(filter-out .,$(subst /,$(space),$(1))))
+    $(eval _path := $(if $(_rooted),/)$(subst $(space),/,$(call _clean-path-expanded,$(_rooted),$(_expanded_path))))
+    $(if $(_path),
+      $(_path),
+      .
+     )
+  ,
+    $(if $(call streq,$(words $(1)),0),
+      .,
+      $(error Call clean-path with only one path (without spaces))
+    )
+  )
+)
+endef
+
+ifeq ($(TEST_MAKE_clean_path),true)
+  define my_test
+    $(if $(call streq,$(call clean-path,$(1)),$(2)),,
+      $(eval my_failed := true)
+      $(warning clean-path test '$(1)': expected '$(2)', got '$(call clean-path,$(1))'))
+  endef
+  my_failed :=
+
+  # Already clean
+  $(call my_test,abc,abc)
+  $(call my_test,abc/def,abc/def)
+  $(call my_test,a/b/c,a/b/c)
+  $(call my_test,.,.)
+  $(call my_test,..,..)
+  $(call my_test,../..,../..)
+  $(call my_test,../../abc,../../abc)
+  $(call my_test,/abc,/abc)
+  $(call my_test,/,/)
+
+  # Empty is current dir
+  $(call my_test,,.)
+
+  # Remove trailing slash
+  $(call my_test,abc/,abc)
+  $(call my_test,abc/def/,abc/def)
+  $(call my_test,a/b/c/,a/b/c)
+  $(call my_test,./,.)
+  $(call my_test,../,..)
+  $(call my_test,../../,../..)
+  $(call my_test,/abc/,/abc)
+
+  # Remove doubled slash
+  $(call my_test,abc//def//ghi,abc/def/ghi)
+  $(call my_test,//abc,/abc)
+  $(call my_test,///abc,/abc)
+  $(call my_test,//abc//,/abc)
+  $(call my_test,abc//,abc)
+
+  # Remove . elements
+  $(call my_test,abc/./def,abc/def)
+  $(call my_test,/./abc/def,/abc/def)
+  $(call my_test,abc/.,abc)
+
+  # Remove .. elements
+  $(call my_test,abc/def/ghi/../jkl,abc/def/jkl)
+  $(call my_test,abc/def/../ghi/../jkl,abc/jkl)
+  $(call my_test,abc/def/..,abc)
+  $(call my_test,abc/def/../..,.)
+  $(call my_test,/abc/def/../..,/)
+  $(call my_test,abc/def/../../..,..)
+  $(call my_test,/abc/def/../../..,/)
+  $(call my_test,abc/def/../../../ghi/jkl/../../../mno,../../mno)
+  $(call my_test,/../abc,/abc)
+
+  # Combinations
+  $(call my_test,abc/./../def,def)
+  $(call my_test,abc//./../def,def)
+  $(call my_test,abc/../../././../def,../../def)
+
+  ifdef my_failed
+    $(error failed clean-path test)
+  endif
+endif
+
+###########################################################
 ## Other includes
 ###########################################################
 
diff --git a/core/dex_preopt_libart.mk b/core/dex_preopt_libart.mk
index 8ee800b..9db5dbf 100644
--- a/core/dex_preopt_libart.mk
+++ b/core/dex_preopt_libart.mk
@@ -122,6 +122,7 @@
 	--runtime-arg -Xnorelocate --compile-pic \
 	--no-generate-debug-info --generate-build-id \
 	--abort-on-hard-verifier-error \
+	--force-determinism \
 	--no-inline-from=core-oj.jar \
 	$(PRIVATE_DEX_PREOPT_FLAGS) \
 	$(PRIVATE_ART_FILE_PREOPT_FLAGS) \
diff --git a/core/package_internal.mk b/core/package_internal.mk
index f9f8e35..6b07be6 100644
--- a/core/package_internal.mk
+++ b/core/package_internal.mk
@@ -92,6 +92,7 @@
   LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 else
   need_compile_res := true
+  LOCAL_RESOURCE_DIR := $(foreach d,$(LOCAL_RESOURCE_DIR),$(call clean-path,$(d)))
 endif
 
 package_resource_overlays := $(strip \
diff --git a/core/soong_config.mk b/core/soong_config.mk
index 9cda3b1..0a2208b 100644
--- a/core/soong_config.mk
+++ b/core/soong_config.mk
@@ -79,6 +79,7 @@
 	echo '    "Safestack": $(if $(filter true,$(USE_SAFESTACK)),true,false),'; \
 	echo '    "EnableCFI": $(if $(filter false,$(ENABLE_CFI)),false,true),'; \
 	echo '    "Device_uses_hwc2": $(if $(filter true,$(TARGET_USES_HWC2)),true,false),'; \
+	echo '    "Override_rs_driver": "$(OVERRIDE_RS_DRIVER)",'; \
 	echo ''; \
 	echo '    "ArtUseReadBarrier": $(if $(filter false,$(PRODUCT_ART_USE_READ_BARRIER)),false,true),'; \
 	echo ''; \
diff --git a/core/static_java_library.mk b/core/static_java_library.mk
index a8b0103..343b949 100644
--- a/core/static_java_library.mk
+++ b/core/static_java_library.mk
@@ -39,6 +39,7 @@
 # A static Java library needs to explicily set LOCAL_RESOURCE_DIR.
 ifdef LOCAL_RESOURCE_DIR
 need_compile_res := true
+LOCAL_RESOURCE_DIR := $(foreach d,$(LOCAL_RESOURCE_DIR),$(call clean-path,$(d)))
 endif
 ifdef LOCAL_USE_AAPT2
 ifneq ($(LOCAL_STATIC_ANDROID_LIBRARIES),)
diff --git a/tools/releasetools/blockimgdiff.py b/tools/releasetools/blockimgdiff.py
index e385866..b8123c0 100644
--- a/tools/releasetools/blockimgdiff.py
+++ b/tools/releasetools/blockimgdiff.py
@@ -41,10 +41,10 @@
   cmd = ['imgdiff', '-z'] if imgdiff else ['bsdiff']
   cmd.extend([srcfile, tgtfile, patchfile])
 
-  # Not using common.Run(), which would otherwise dump all the bsdiff/imgdiff
-  # commands when OPTIONS.verbose is True - not useful for the case here, since
-  # they contain temp filenames only.
-  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+  # Don't dump the bsdiff/imgdiff commands, which are not useful for the case
+  # here, since they contain temp filenames only.
+  p = common.Run(cmd, verbose=False, stdout=subprocess.PIPE,
+                 stderr=subprocess.STDOUT)
   output, _ = p.communicate()
 
   if p.returncode != 0:
diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py
index e200f9f..925a523 100644
--- a/tools/releasetools/common.py
+++ b/tools/releasetools/common.py
@@ -107,10 +107,15 @@
   pass
 
 
-def Run(args, **kwargs):
-  """Create and return a subprocess.Popen object, printing the command
-  line on the terminal if -v was specified."""
-  if OPTIONS.verbose:
+def Run(args, verbose=None, **kwargs):
+  """Create and return a subprocess.Popen object.
+
+  Caller can specify if the command line should be printed. The global
+  OPTIONS.verbose will be used if not specified.
+  """
+  if verbose is None:
+    verbose = OPTIONS.verbose
+  if verbose:
     print("  running: ", " ".join(args))
   return subprocess.Popen(args, **kwargs)
 
diff --git a/tools/releasetools/ota_from_target_files.py b/tools/releasetools/ota_from_target_files.py
index 1a7e10e..2090400 100755
--- a/tools/releasetools/ota_from_target_files.py
+++ b/tools/releasetools/ota_from_target_files.py
@@ -181,14 +181,14 @@
 OPTIONS.payload_signer = None
 OPTIONS.payload_signer_args = []
 OPTIONS.extracted_input = None
+OPTIONS.key_passwords = []
 
 METADATA_NAME = 'META-INF/com/android/metadata'
 UNZIP_PATTERN = ['IMAGES/*', 'META/*']
 
 
 def SignOutput(temp_zip_name, output_zip_name):
-  key_passwords = common.GetKeyPasswords([OPTIONS.package_key])
-  pw = key_passwords[OPTIONS.package_key]
+  pw = OPTIONS.key_passwords[OPTIONS.package_key]
 
   common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
                   whole_file=True)
@@ -1021,21 +1021,17 @@
   # The place where the output from the subprocess should go.
   log_file = sys.stdout if OPTIONS.verbose else subprocess.PIPE
 
-  # Setup signing keys.
-  if OPTIONS.package_key is None:
-    OPTIONS.package_key = OPTIONS.info_dict.get(
-        "default_system_dev_certificate",
-        "build/target/product/security/testkey")
-
   # A/B updater expects a signing key in RSA format. Gets the key ready for
   # later use in step 3, unless a payload_signer has been specified.
   if OPTIONS.payload_signer is None:
     cmd = ["openssl", "pkcs8",
            "-in", OPTIONS.package_key + OPTIONS.private_key_suffix,
-           "-inform", "DER", "-nocrypt"]
+           "-inform", "DER"]
+    pw = OPTIONS.key_passwords[OPTIONS.package_key]
+    cmd.extend(["-passin", "pass:" + pw] if pw else ["-nocrypt"])
     rsa_key = common.MakeTempFile(prefix="key-", suffix=".key")
     cmd.extend(["-out", rsa_key])
-    p1 = common.Run(cmd, stdout=log_file, stderr=subprocess.STDOUT)
+    p1 = common.Run(cmd, verbose=False, stdout=log_file, stderr=subprocess.STDOUT)
     p1.communicate()
     assert p1.returncode == 0, "openssl pkcs8 failed"
 
@@ -1383,6 +1379,17 @@
 
   ab_update = OPTIONS.info_dict.get("ab_update") == "true"
 
+  # Use the default key to sign the package if not specified with package_key.
+  # package_keys are needed on ab_updates, so always define them if an
+  # ab_update is getting created.
+  if not OPTIONS.no_signing or ab_update:
+    if OPTIONS.package_key is None:
+      OPTIONS.package_key = OPTIONS.info_dict.get(
+          "default_system_dev_certificate",
+          "build/target/product/security/testkey")
+    # Get signing keys
+    OPTIONS.key_passwords = common.GetKeyPasswords([OPTIONS.package_key])
+
   if ab_update:
     if OPTIONS.incremental_source is not None:
       OPTIONS.target_info_dict = OPTIONS.info_dict
@@ -1448,13 +1455,6 @@
     raise common.ExternalError(
         "--- target build has specified no recovery ---")
 
-  # Use the default key to sign the package if not specified with package_key.
-  if not OPTIONS.no_signing:
-    if OPTIONS.package_key is None:
-      OPTIONS.package_key = OPTIONS.info_dict.get(
-          "default_system_dev_certificate",
-          "build/target/product/security/testkey")
-
   # Set up the output zip. Create a temporary zip file if signing is needed.
   if OPTIONS.no_signing:
     if os.path.exists(args[1]):
diff --git a/tools/releasetools/ota_package_parser.py b/tools/releasetools/ota_package_parser.py
new file mode 100755
index 0000000..331122b
--- /dev/null
+++ b/tools/releasetools/ota_package_parser.py
@@ -0,0 +1,228 @@
+#!/usr/bin/env python
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import logging
+import sys
+import traceback
+import zipfile
+
+from rangelib import RangeSet
+
+class Stash(object):
+  """Build a map to track stashed blocks during update simulation."""
+
+  def __init__(self):
+    self.blocks_stashed = 0
+    self.overlap_blocks_stashed = 0
+    self.max_stash_needed = 0
+    self.current_stash_size = 0
+    self.stash_map = {}
+
+  def StashBlocks(self, SHA1, blocks):
+    if SHA1 in self.stash_map:
+      logging.info("already stashed {}: {}".format(SHA1, blocks))
+      return
+    self.blocks_stashed += blocks.size()
+    self.current_stash_size += blocks.size()
+    self.max_stash_needed = max(self.current_stash_size, self.max_stash_needed)
+    self.stash_map[SHA1] = blocks
+
+  def FreeBlocks(self, SHA1):
+    assert self.stash_map.has_key(SHA1), "stash {} not found".format(SHA1)
+    self.current_stash_size -= self.stash_map[SHA1].size()
+    del self.stash_map[SHA1]
+
+  def HandleOverlapBlocks(self, SHA1, blocks):
+    self.StashBlocks(SHA1, blocks)
+    self.overlap_blocks_stashed += blocks.size()
+    self.FreeBlocks(SHA1)
+
+
+class OtaPackageParser(object):
+  """Parse a block-based OTA package."""
+
+  def __init__(self, package):
+    self.package = package
+    self.new_data_size = 0
+    self.patch_data_size = 0
+    self.block_written = 0
+    self.block_stashed = 0
+
+  @staticmethod
+  def GetSizeString(size):
+    assert size >= 0
+    base = 1024.0
+    if size <= base:
+      return "{} bytes".format(size)
+    for units in ['K', 'M', 'G']:
+      if size <= base * 1024 or units == 'G':
+        return "{:.1f}{}".format(size / base, units)
+      base *= 1024
+
+  def ParseTransferList(self, name):
+    """Simulate the transfer commands and calculate the amout of I/O."""
+
+    logging.info("\nSimulating commands in '{}':".format(name))
+    lines = self.package.read(name).strip().splitlines()
+    assert len(lines) >= 4, "{} is too short; Transfer list expects at least" \
+        "4 lines, it has {}".format(name, len(lines))
+    assert int(lines[0]) >= 3
+    logging.info("(version: {})".format(lines[0]))
+
+    blocks_written = 0
+    my_stash = Stash()
+    for line in lines[4:]:
+      cmd_list = line.strip().split(" ")
+      cmd_name = cmd_list[0]
+      try:
+        if cmd_name == "new" or cmd_name == "zero":
+          assert len(cmd_list) == 2, "command format error: {}".format(line)
+          target_range = RangeSet.parse_raw(cmd_list[1])
+          blocks_written += target_range.size()
+        elif cmd_name == "move":
+          # Example:  move <onehash> <tgt_range> <src_blk_count> <src_range>
+          # [<loc_range> <stashed_blocks>]
+          assert len(cmd_list) >= 5, "command format error: {}".format(line)
+          target_range = RangeSet.parse_raw(cmd_list[2])
+          blocks_written += target_range.size()
+          if cmd_list[4] == '-':
+            continue
+          SHA1 = cmd_list[1]
+          source_range = RangeSet.parse_raw(cmd_list[4])
+          if target_range.overlaps(source_range):
+            my_stash.HandleOverlapBlocks(SHA1, source_range)
+        elif cmd_name == "bsdiff" or cmd_name == "imgdiff":
+          # Example:  bsdiff <offset> <len> <src_hash> <tgt_hash> <tgt_range>
+          # <src_blk_count> <src_range> [<loc_range> <stashed_blocks>]
+          assert len(cmd_list) >= 8, "command format error: {}".format(line)
+          target_range = RangeSet.parse_raw(cmd_list[5])
+          blocks_written += target_range.size()
+          if cmd_list[7] == '-':
+            continue
+          source_SHA1 = cmd_list[3]
+          source_range = RangeSet.parse_raw(cmd_list[7])
+          if target_range.overlaps(source_range):
+            my_stash.HandleOverlapBlocks(source_SHA1, source_range)
+        elif cmd_name == "stash":
+          assert len(cmd_list) == 3, "command format error: {}".format(line)
+          SHA1 = cmd_list[1]
+          source_range = RangeSet.parse_raw(cmd_list[2])
+          my_stash.StashBlocks(SHA1, source_range)
+        elif cmd_name == "free":
+          assert len(cmd_list) == 2, "command format error: {}".format(line)
+          SHA1 = cmd_list[1]
+          my_stash.FreeBlocks(SHA1)
+      except:
+        logging.error("failed to parse command in: " + line)
+        raise
+
+    self.block_written += blocks_written
+    self.block_stashed += my_stash.blocks_stashed
+
+    logging.info("blocks written: {}  (expected: {})".format(
+        blocks_written, lines[1]))
+    logging.info("max blocks stashed simultaneously: {}  (expected: {})".
+        format(my_stash.max_stash_needed, lines[3]))
+    logging.info("total blocks stashed: {}".format(my_stash.blocks_stashed))
+    logging.info("blocks stashed implicitly: {}".format(
+        my_stash.overlap_blocks_stashed))
+
+  def PrintDataInfo(self, partition):
+    logging.info("\nReading data info for {} partition:".format(partition))
+    new_data = self.package.getinfo(partition + ".new.dat")
+    patch_data = self.package.getinfo(partition + ".patch.dat")
+    logging.info("{:<40}{:<40}".format(new_data.filename, patch_data.filename))
+    logging.info("{:<40}{:<40}".format(
+          "compress_type: " + str(new_data.compress_type),
+          "compress_type: " + str(patch_data.compress_type)))
+    logging.info("{:<40}{:<40}".format(
+          "compressed_size: " + OtaPackageParser.GetSizeString(
+              new_data.compress_size),
+          "compressed_size: " + OtaPackageParser.GetSizeString(
+              patch_data.compress_size)))
+    logging.info("{:<40}{:<40}".format(
+        "file_size: " + OtaPackageParser.GetSizeString(new_data.file_size),
+        "file_size: " + OtaPackageParser.GetSizeString(patch_data.file_size)))
+
+    self.new_data_size += new_data.file_size
+    self.patch_data_size += patch_data.file_size
+
+  def AnalyzePartition(self, partition):
+    assert partition in ("system", "vendor")
+    assert partition + ".new.dat" in self.package.namelist()
+    assert partition + ".patch.dat" in self.package.namelist()
+    assert partition + ".transfer.list" in self.package.namelist()
+
+    self.PrintDataInfo(partition)
+    self.ParseTransferList(partition + ".transfer.list")
+
+  def PrintMetadata(self):
+    metadata_path = "META-INF/com/android/metadata"
+    logging.info("\nMetadata info:")
+    metadata_info = {}
+    for line in self.package.read(metadata_path).strip().splitlines():
+      index = line.find("=")
+      metadata_info[line[0 : index].strip()] = line[index + 1:].strip()
+    assert metadata_info.get("ota-type") == "BLOCK"
+    assert "pre-device" in metadata_info
+    logging.info("device: {}".format(metadata_info["pre-device"]))
+    if "pre-build" in metadata_info:
+      logging.info("pre-build: {}".format(metadata_info["pre-build"]))
+    assert "post-build" in metadata_info
+    logging.info("post-build: {}".format(metadata_info["post-build"]))
+
+  def Analyze(self):
+    logging.info("Analyzing ota package: " + self.package.filename)
+    self.PrintMetadata()
+    assert "system.new.dat" in self.package.namelist()
+    self.AnalyzePartition("system")
+    if "vendor.new.dat" in self.package.namelist():
+      self.AnalyzePartition("vendor")
+
+    #TODO Add analysis of other partitions(e.g. bootloader, boot, radio)
+
+    BLOCK_SIZE = 4096
+    logging.info("\nOTA package analyzed:")
+    logging.info("new data size (uncompressed): " +
+        OtaPackageParser.GetSizeString(self.new_data_size))
+    logging.info("patch data size (uncompressed): " +
+        OtaPackageParser.GetSizeString(self.patch_data_size))
+    logging.info("total data written: " +
+        OtaPackageParser.GetSizeString(self.block_written * BLOCK_SIZE))
+    logging.info("total data stashed: " +
+        OtaPackageParser.GetSizeString(self.block_stashed * BLOCK_SIZE))
+
+
+def main(argv):
+  parser = argparse.ArgumentParser(description='Analyze an OTA package.')
+  parser.add_argument("ota_package", help='Path of the OTA package.')
+  args = parser.parse_args(argv)
+
+  logging_format = '%(message)s'
+  logging.basicConfig(level=logging.INFO, format=logging_format)
+
+  try:
+    with zipfile.ZipFile(args.ota_package, 'r') as package:
+      package_parser = OtaPackageParser(package)
+      package_parser.Analyze()
+  except:
+    logging.error("Failed to read " + args.ota_package)
+    traceback.print_exc()
+    sys.exit(1)
+
+
+if __name__ == '__main__':
+  main(sys.argv[1:])