Snap for 7483611 from 3a0c86e2eb47ab09cf1befbed8febe39ea2e27d4 to mainline-neuralnetworks-release

Change-Id: I3e3e9d64beb066df5210e7e89bf4f3c24458e03e
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bee8a64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/Android.bp b/Android.bp
index c3cf746..23c55b8 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,5 +1,9 @@
 // Copyright 2012 The Android Open Source Project
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 cc_library_headers {
     name: "libmkbootimg_abi_headers",
     vendor_available: true,
@@ -17,6 +21,10 @@
             enabled: true,
         },
     },
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.virt",
+    ],
 }
 
 cc_library {
@@ -34,15 +42,13 @@
 
 python_defaults {
     name: "mkbootimg_defaults",
-
     version: {
         py2: {
-            enabled: true,
-            embedded_launcher: true,
+            enabled: false,
         },
         py3: {
-            enabled: false,
-            embedded_launcher: false,
+            enabled: true,
+            embedded_launcher: true,
         },
     },
 }
@@ -53,6 +59,9 @@
     srcs: [
         "mkbootimg.py",
     ],
+    required: [
+        "avbtool",
+    ],
 }
 
 python_binary_host {
@@ -62,3 +71,38 @@
         "unpack_bootimg.py",
     ],
 }
+
+
+python_binary_host {
+    name: "repack_bootimg",
+    defaults: ["mkbootimg_defaults"],
+    srcs: [
+        "repack_bootimg.py",
+    ],
+    required: [
+        "lz4",
+        "minigzip",
+        "mkbootfs",
+        "mkbootimg",
+        "toybox",
+        "unpack_bootimg",
+    ],
+}
+
+python_test_host {
+    name: "mkbootimg_test",
+    defaults: ["mkbootimg_defaults"],
+    main: "tests/mkbootimg_test.py",
+    srcs: [
+        "tests/mkbootimg_test.py",
+    ],
+    data: [
+        ":avbtool",
+        ":mkbootimg",
+        ":unpack_bootimg",
+        "tests/data/*",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+}
diff --git a/OWNERS b/OWNERS
index de2d568..51e09a2 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,2 +1,3 @@
 hridya@google.com
 smuckle@google.com
+yochiang@google.com
\ No newline at end of file
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index d937da1..734a94d 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,3 +1,2 @@
 [Builtin Hooks]
-pylint2 = true
-pylint3 = true
+pylint = true
diff --git a/include/bootimg/bootimg.h b/include/bootimg/bootimg.h
index 8c9f6ee..8ad95a8 100644
--- a/include/bootimg/bootimg.h
+++ b/include/bootimg/bootimg.h
@@ -29,8 +29,41 @@
 #define VENDOR_BOOT_ARGS_SIZE 2048
 #define VENDOR_BOOT_NAME_SIZE 16
 
-// The bootloader expects the structure of boot_img_hdr with header
-// version 0 to be as follows:
+#define VENDOR_RAMDISK_TYPE_NONE 0
+#define VENDOR_RAMDISK_TYPE_PLATFORM 1
+#define VENDOR_RAMDISK_TYPE_RECOVERY 2
+#define VENDOR_RAMDISK_TYPE_DLKM 3
+#define VENDOR_RAMDISK_NAME_SIZE 32
+#define VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE 16
+
+/* When a boot header is of version 0, the structure of boot image is as
+ * follows:
+ *
+ * +-----------------+
+ * | boot header     | 1 page
+ * +-----------------+
+ * | kernel          | n pages
+ * +-----------------+
+ * | ramdisk         | m pages
+ * +-----------------+
+ * | second stage    | o pages
+ * +-----------------+
+ *
+ * n = (kernel_size + page_size - 1) / page_size
+ * m = (ramdisk_size + page_size - 1) / page_size
+ * o = (second_size + page_size - 1) / page_size
+ *
+ * 0. all entities are page_size aligned in flash
+ * 1. kernel and ramdisk are required (size != 0)
+ * 2. second is optional (second_size == 0 -> no second)
+ * 3. load each element (kernel, ramdisk, second) at
+ *    the specified physical address (kernel_addr, etc)
+ * 4. prepare tags at tag_addr.  kernel_args[] is
+ *    appended to the kernel commandline in the tags.
+ * 5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr
+ * 6. if second_size != 0: jump to second_addr
+ *    else: jump to kernel_addr
+ */
 struct boot_img_hdr_v0 {
     // Must be BOOT_MAGIC.
     uint8_t magic[BOOT_MAGIC_SIZE];
@@ -70,12 +103,13 @@
 
     uint8_t name[BOOT_NAME_SIZE]; /* asciiz product name */
 
-    uint8_t cmdline[BOOT_ARGS_SIZE];
+    uint8_t cmdline[BOOT_ARGS_SIZE]; /* asciiz kernel commandline */
 
     uint32_t id[8]; /* timestamp / checksum / sha1 / etc */
 
     // Supplemental command line data; kept here to maintain
     // binary compatibility with older versions of mkbootimg.
+    // Asciiz.
     uint8_t extra_cmdline[BOOT_EXTRA_ARGS_SIZE];
 } __attribute__((packed));
 
@@ -85,35 +119,40 @@
  */
 typedef struct boot_img_hdr_v0 boot_img_hdr;
 
-/* When a boot header is of version 0, the structure of boot image is as
+/* When a boot header is of version 1, the structure of boot image is as
  * follows:
  *
- * +-----------------+
- * | boot header     | 1 page
- * +-----------------+
- * | kernel          | n pages
- * +-----------------+
- * | ramdisk         | m pages
- * +-----------------+
- * | second stage    | o pages
- * +-----------------+
+ * +---------------------+
+ * | boot header         | 1 page
+ * +---------------------+
+ * | kernel              | n pages
+ * +---------------------+
+ * | ramdisk             | m pages
+ * +---------------------+
+ * | second stage        | o pages
+ * +---------------------+
+ * | recovery dtbo/acpio | p pages
+ * +---------------------+
  *
  * n = (kernel_size + page_size - 1) / page_size
  * m = (ramdisk_size + page_size - 1) / page_size
  * o = (second_size + page_size - 1) / page_size
+ * p = (recovery_dtbo_size + page_size - 1) / page_size
  *
  * 0. all entities are page_size aligned in flash
  * 1. kernel and ramdisk are required (size != 0)
- * 2. second is optional (second_size == 0 -> no second)
- * 3. load each element (kernel, ramdisk, second) at
+ * 2. recovery_dtbo/recovery_acpio is required for recovery.img in non-A/B
+ *    devices(recovery_dtbo_size != 0)
+ * 3. second is optional (second_size == 0 -> no second)
+ * 4. load each element (kernel, ramdisk, second) at
  *    the specified physical address (kernel_addr, etc)
- * 4. prepare tags at tag_addr.  kernel_args[] is
- *    appended to the kernel commandline in the tags.
- * 5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr
- * 6. if second_size != 0: jump to second_addr
+ * 5. If booting to recovery mode in a non-A/B device, extract recovery
+ *    dtbo/acpio and apply the correct set of overlays on the base device tree
+ *    depending on the hardware/product revision.
+ * 6. set up registers for kernel entry as required by your architecture
+ * 7. if second_size != 0: jump to second_addr
  *    else: jump to kernel_addr
  */
-
 struct boot_img_hdr_v1 : public boot_img_hdr_v0 {
     uint32_t recovery_dtbo_size;   /* size in bytes for recovery DTBO/ACPIO image */
     uint64_t recovery_dtbo_offset; /* offset to recovery dtbo/acpio in boot image */
@@ -240,6 +279,7 @@
     // Version of the boot image header.
     uint32_t header_version;
 
+    // Asciiz kernel commandline.
     uint8_t cmdline[BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE];
 } __attribute__((packed));
 
@@ -257,7 +297,7 @@
 
     uint32_t vendor_ramdisk_size; /* size in bytes */
 
-    uint8_t cmdline[VENDOR_BOOT_ARGS_SIZE];
+    uint8_t cmdline[VENDOR_BOOT_ARGS_SIZE]; /* asciiz kernel commandline */
 
     uint32_t tags_addr; /* physical addr for kernel tags (if required) */
     uint8_t name[VENDOR_BOOT_NAME_SIZE]; /* asciiz product name */
@@ -267,3 +307,114 @@
     uint32_t dtb_size; /* size in bytes for DTB image */
     uint64_t dtb_addr; /* physical load address for DTB image */
 } __attribute__((packed));
+
+/* When the boot image header has a version of 4, the structure of the boot
+ * image is as follows:
+ *
+ * +---------------------+
+ * | boot header         | 4096 bytes
+ * +---------------------+
+ * | kernel              | m pages
+ * +---------------------+
+ * | ramdisk             | n pages
+ * +---------------------+
+ * | boot signature      | g pages
+ * +---------------------+
+ *
+ * m = (kernel_size + 4096 - 1) / 4096
+ * n = (ramdisk_size + 4096 - 1) / 4096
+ * g = (signature_size + 4096 - 1) / 4096
+ *
+ * Note that in version 4 of the boot image header, page size is fixed at 4096
+ * bytes.
+ *
+ * The structure of the vendor boot image version 4, which is required to be
+ * present when a version 4 boot image is used, is as follows:
+ *
+ * +------------------------+
+ * | vendor boot header     | o pages
+ * +------------------------+
+ * | vendor ramdisk section | p pages
+ * +------------------------+
+ * | dtb                    | q pages
+ * +------------------------+
+ * | vendor ramdisk table   | r pages
+ * +------------------------+
+ * | bootconfig             | s pages
+ * +------------------------+
+ *
+ * o = (2128 + page_size - 1) / page_size
+ * p = (vendor_ramdisk_size + page_size - 1) / page_size
+ * q = (dtb_size + page_size - 1) / page_size
+ * r = (vendor_ramdisk_table_size + page_size - 1) / page_size
+ * s = (vendor_bootconfig_size + page_size - 1) / page_size
+ *
+ * Note that in version 4 of the vendor boot image, multiple vendor ramdisks can
+ * be included in the vendor boot image. The bootloader can select a subset of
+ * ramdisks to load at runtime. To help the bootloader select the ramdisks, each
+ * ramdisk is tagged with a type tag and a set of hardware identifiers
+ * describing the board, soc or platform that this ramdisk is intended for.
+ *
+ * The vendor ramdisk section is consist of multiple ramdisk images concatenated
+ * one after another, and vendor_ramdisk_size is the size of the section, which
+ * is the total size of all the ramdisks included in the vendor boot image.
+ *
+ * The vendor ramdisk table holds the size, offset, type, name and hardware
+ * identifiers of each ramdisk. The type field denotes the type of its content.
+ * The vendor ramdisk names are unique. The hardware identifiers are specified
+ * in the board_id field in each table entry. The board_id field is consist of a
+ * vector of unsigned integer words, and the encoding scheme is defined by the
+ * hardware vendor.
+ *
+ * For the different type of ramdisks, there are:
+ *    - VENDOR_RAMDISK_TYPE_NONE indicates the value is unspecified.
+ *    - VENDOR_RAMDISK_TYPE_PLATFORM ramdisks contain platform specific bits, so
+ *      the bootloader should always load these into memory.
+ *    - VENDOR_RAMDISK_TYPE_RECOVERY ramdisks contain recovery resources, so
+ *      the bootloader should load these when booting into recovery.
+ *    - VENDOR_RAMDISK_TYPE_DLKM ramdisks contain dynamic loadable kernel
+ *      modules.
+ *
+ * Version 4 of the vendor boot image also adds a bootconfig section to the end
+ * of the image. This section contains Boot Configuration parameters known at
+ * build time. The bootloader is responsible for placing this section directly
+ * after the generic ramdisk, followed by the bootconfig trailer, before
+ * entering the kernel.
+ *
+ * 0. all entities in the boot image are 4096-byte aligned in flash, all
+ *    entities in the vendor boot image are page_size (determined by the vendor
+ *    and specified in the vendor boot image header) aligned in flash
+ * 1. kernel, ramdisk, and DTB are required (size != 0)
+ * 2. load the kernel and DTB at the specified physical address (kernel_addr,
+ *    dtb_addr)
+ * 3. load the vendor ramdisks at ramdisk_addr
+ * 4. load the generic ramdisk immediately following the vendor ramdisk in
+ *    memory
+ * 5. load the bootconfig immediately following the generic ramdisk. Add
+ *    additional bootconfig parameters followed by the bootconfig trailer.
+ * 6. set up registers for kernel entry as required by your architecture
+ * 7. if the platform has a second stage bootloader jump to it (must be
+ *    contained outside boot and vendor boot partitions), otherwise
+ *    jump to kernel_addr
+ */
+struct boot_img_hdr_v4 : public boot_img_hdr_v3 {
+    uint32_t signature_size; /* size in bytes */
+} __attribute__((packed));
+
+struct vendor_boot_img_hdr_v4 : public vendor_boot_img_hdr_v3 {
+    uint32_t vendor_ramdisk_table_size; /* size in bytes for the vendor ramdisk table */
+    uint32_t vendor_ramdisk_table_entry_num; /* number of entries in the vendor ramdisk table */
+    uint32_t vendor_ramdisk_table_entry_size; /* size in bytes for a vendor ramdisk table entry */
+    uint32_t bootconfig_size; /* size in bytes for the bootconfig section */
+} __attribute__((packed));
+
+struct vendor_ramdisk_table_entry_v4 {
+    uint32_t ramdisk_size; /* size in bytes for the ramdisk image */
+    uint32_t ramdisk_offset; /* offset to the ramdisk image in vendor ramdisk section */
+    uint32_t ramdisk_type; /* type of the ramdisk */
+    uint8_t ramdisk_name[VENDOR_RAMDISK_NAME_SIZE]; /* asciiz ramdisk name */
+
+    // Hardware identifiers describing the board, soc or platform which this
+    // ramdisk is intended to be loaded on.
+    uint32_t board_id[VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE];
+} __attribute__((packed));
diff --git a/mkbootimg.py b/mkbootimg.py
old mode 100644
new mode 100755
index 00a4623..e0b0839
--- a/mkbootimg.py
+++ b/mkbootimg.py
@@ -1,4 +1,5 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
+#
 # Copyright 2015, The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,16 +14,55 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from __future__ import print_function
+"""Creates the boot image."""
 
-from argparse import ArgumentParser, FileType, Action
+from argparse import (ArgumentParser, ArgumentTypeError,
+                      FileType, RawDescriptionHelpFormatter)
 from hashlib import sha1
 from os import fstat
-import re
 from struct import pack
 
+import array
+import collections
+import os
+import re
+import subprocess
+import tempfile
 
+# Constant and structure definition is in
+# system/tools/mkbootimg/include/bootimg/bootimg.h
+BOOT_MAGIC = 'ANDROID!'
+BOOT_MAGIC_SIZE = 8
+BOOT_NAME_SIZE = 16
+BOOT_ARGS_SIZE = 512
+BOOT_EXTRA_ARGS_SIZE = 1024
+BOOT_IMAGE_HEADER_V1_SIZE = 1648
+BOOT_IMAGE_HEADER_V2_SIZE = 1660
+BOOT_IMAGE_HEADER_V3_SIZE = 1580
 BOOT_IMAGE_HEADER_V3_PAGESIZE = 4096
+BOOT_IMAGE_HEADER_V4_SIZE = 1584
+BOOT_IMAGE_V4_SIGNATURE_SIZE = 4096
+
+VENDOR_BOOT_MAGIC = 'VNDRBOOT'
+VENDOR_BOOT_MAGIC_SIZE = 8
+VENDOR_BOOT_NAME_SIZE = BOOT_NAME_SIZE
+VENDOR_BOOT_ARGS_SIZE = 2048
+VENDOR_BOOT_IMAGE_HEADER_V3_SIZE = 2112
+VENDOR_BOOT_IMAGE_HEADER_V4_SIZE = 2128
+
+VENDOR_RAMDISK_TYPE_NONE = 0
+VENDOR_RAMDISK_TYPE_PLATFORM = 1
+VENDOR_RAMDISK_TYPE_RECOVERY = 2
+VENDOR_RAMDISK_TYPE_DLKM = 3
+VENDOR_RAMDISK_NAME_SIZE = 32
+VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE = 16
+VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE = 108
+
+# Names with special meaning, mustn't be specified in --ramdisk_name.
+VENDOR_RAMDISK_NAME_BLOCKLIST = {b'default'}
+
+PARSER_ARGUMENT_VENDOR_RAMDISK_FRAGMENT = '--vendor_ramdisk_fragment'
+
 
 def filesize(f):
     if f is None:
@@ -49,87 +89,135 @@
 
 def get_number_of_pages(image_size, page_size):
     """calculates the number of pages required for the image"""
-    return (image_size + page_size - 1) / page_size
+    return (image_size + page_size - 1) // page_size
 
 
 def get_recovery_dtbo_offset(args):
     """calculates the offset of recovery_dtbo image in the boot image"""
     num_header_pages = 1 # header occupies a page
     num_kernel_pages = get_number_of_pages(filesize(args.kernel), args.pagesize)
-    num_ramdisk_pages = get_number_of_pages(filesize(args.ramdisk), args.pagesize)
+    num_ramdisk_pages = get_number_of_pages(filesize(args.ramdisk),
+                                            args.pagesize)
     num_second_pages = get_number_of_pages(filesize(args.second), args.pagesize)
     dtbo_offset = args.pagesize * (num_header_pages + num_kernel_pages +
                                    num_ramdisk_pages + num_second_pages)
     return dtbo_offset
 
 
-def write_header_v3(args):
-    BOOT_IMAGE_HEADER_V3_SIZE = 1580
-    BOOT_MAGIC = 'ANDROID!'.encode()
+def write_header_v3_and_above(args):
+    if args.header_version > 3:
+        boot_header_size = BOOT_IMAGE_HEADER_V4_SIZE
+    else:
+        boot_header_size = BOOT_IMAGE_HEADER_V3_SIZE
 
-    args.output.write(pack('8s', BOOT_MAGIC))
-    args.output.write(pack(
-        '4I',
-        filesize(args.kernel),                          # kernel size in bytes
-        filesize(args.ramdisk),                         # ramdisk size in bytes
-        (args.os_version << 11) | args.os_patch_level,  # os version and patch level
-        BOOT_IMAGE_HEADER_V3_SIZE))
-
-    args.output.write(pack('4I', 0, 0, 0, 0))           # reserved
-
-    args.output.write(pack('I', args.header_version))   # version of bootimage header
-    args.output.write(pack('1536s', args.cmdline.encode()))
+    args.output.write(pack(f'{BOOT_MAGIC_SIZE}s', BOOT_MAGIC.encode()))
+    # kernel size in bytes
+    args.output.write(pack('I', filesize(args.kernel)))
+    # ramdisk size in bytes
+    args.output.write(pack('I', filesize(args.ramdisk)))
+    # os version and patch level
+    args.output.write(pack('I', (args.os_version << 11) | args.os_patch_level))
+    args.output.write(pack('I', boot_header_size))
+    # reserved
+    args.output.write(pack('4I', 0, 0, 0, 0))
+    # version of boot image header
+    args.output.write(pack('I', args.header_version))
+    args.output.write(pack(f'{BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE}s',
+                           args.cmdline))
+    if args.header_version >= 4:
+        # The signature used to verify boot image v4.
+        args.output.write(pack('I', BOOT_IMAGE_V4_SIGNATURE_SIZE))
     pad_file(args.output, BOOT_IMAGE_HEADER_V3_PAGESIZE)
 
+
 def write_vendor_boot_header(args):
-    VENDOR_BOOT_IMAGE_HEADER_V3_SIZE = 2112
-    BOOT_MAGIC = 'VNDRBOOT'.encode()
-
-    args.vendor_boot.write(pack('8s', BOOT_MAGIC))
-    args.vendor_boot.write(pack(
-        '5I',
-        args.header_version,                            # version of header
-        args.pagesize,                                  # flash page size we assume
-        args.base + args.kernel_offset,                 # kernel physical load addr
-        args.base + args.ramdisk_offset,                # ramdisk physical load addr
-        filesize(args.vendor_ramdisk)))                 # vendor ramdisk size in bytes
-    args.vendor_boot.write(pack('2048s', args.vendor_cmdline.encode()))
-    args.vendor_boot.write(pack('I', args.base + args.tags_offset)) # physical addr for kernel tags
-    args.vendor_boot.write(pack('16s', args.board.encode())) # asciiz product name
-    args.vendor_boot.write(pack('I', VENDOR_BOOT_IMAGE_HEADER_V3_SIZE)) # header size in bytes
     if filesize(args.dtb) == 0:
-        raise ValueError("DTB image must not be empty.")
-    args.vendor_boot.write(pack('I', filesize(args.dtb)))   # size in bytes
-    args.vendor_boot.write(pack('Q', args.base + args.dtb_offset)) # dtb physical load address
-    pad_file(args.vendor_boot, args.pagesize)
-
-def write_header(args):
-    BOOT_IMAGE_HEADER_V1_SIZE = 1648
-    BOOT_IMAGE_HEADER_V2_SIZE = 1660
-    BOOT_MAGIC = 'ANDROID!'.encode()
+        raise ValueError('DTB image must not be empty.')
 
     if args.header_version > 3:
-        raise ValueError('Boot header version %d not supported' % args.header_version)
-    elif args.header_version == 3:
-        return write_header_v3(args)
+        vendor_ramdisk_size = args.vendor_ramdisk_total_size
+        vendor_boot_header_size = VENDOR_BOOT_IMAGE_HEADER_V4_SIZE
+    else:
+        vendor_ramdisk_size = filesize(args.vendor_ramdisk)
+        vendor_boot_header_size = VENDOR_BOOT_IMAGE_HEADER_V3_SIZE
 
-    args.output.write(pack('8s', BOOT_MAGIC))
-    final_ramdisk_offset = (args.base + args.ramdisk_offset) if filesize(args.ramdisk) > 0 else 0
-    final_second_offset = (args.base + args.second_offset) if filesize(args.second) > 0 else 0
-    args.output.write(pack(
-        '10I',
-        filesize(args.kernel),                          # size in bytes
-        args.base + args.kernel_offset,                 # physical load addr
-        filesize(args.ramdisk),                         # size in bytes
-        final_ramdisk_offset,                           # physical load addr
-        filesize(args.second),                          # size in bytes
-        final_second_offset,                            # physical load addr
-        args.base + args.tags_offset,                   # physical addr for kernel tags
-        args.pagesize,                                  # flash page size we assume
-        args.header_version,                            # version of bootimage header
-        (args.os_version << 11) | args.os_patch_level)) # os version and patch level
-    args.output.write(pack('16s', args.board.encode())) # asciiz product name
-    args.output.write(pack('512s', args.cmdline[:512].encode()))
+    args.vendor_boot.write(pack(f'{VENDOR_BOOT_MAGIC_SIZE}s',
+                                VENDOR_BOOT_MAGIC.encode()))
+    # version of boot image header
+    args.vendor_boot.write(pack('I', args.header_version))
+    # flash page size
+    args.vendor_boot.write(pack('I', args.pagesize))
+    # kernel physical load address
+    args.vendor_boot.write(pack('I', args.base + args.kernel_offset))
+    # ramdisk physical load address
+    args.vendor_boot.write(pack('I', args.base + args.ramdisk_offset))
+    # ramdisk size in bytes
+    args.vendor_boot.write(pack('I', vendor_ramdisk_size))
+    args.vendor_boot.write(pack(f'{VENDOR_BOOT_ARGS_SIZE}s',
+                                args.vendor_cmdline))
+    # kernel tags physical load address
+    args.vendor_boot.write(pack('I', args.base + args.tags_offset))
+    # asciiz product name
+    args.vendor_boot.write(pack(f'{VENDOR_BOOT_NAME_SIZE}s', args.board))
+
+    # header size in bytes
+    args.vendor_boot.write(pack('I', vendor_boot_header_size))
+
+    # dtb size in bytes
+    args.vendor_boot.write(pack('I', filesize(args.dtb)))
+    # dtb physical load address
+    args.vendor_boot.write(pack('Q', args.base + args.dtb_offset))
+
+    if args.header_version > 3:
+        vendor_ramdisk_table_size = (args.vendor_ramdisk_table_entry_num *
+                                     VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE)
+        # vendor ramdisk table size in bytes
+        args.vendor_boot.write(pack('I', vendor_ramdisk_table_size))
+        # number of vendor ramdisk table entries
+        args.vendor_boot.write(pack('I', args.vendor_ramdisk_table_entry_num))
+        # vendor ramdisk table entry size in bytes
+        args.vendor_boot.write(pack('I', VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE))
+        # bootconfig section size in bytes
+        args.vendor_boot.write(pack('I', filesize(args.vendor_bootconfig)))
+    pad_file(args.vendor_boot, args.pagesize)
+
+
+def write_header(args):
+    if args.header_version > 4:
+        raise ValueError(
+            f'Boot header version {args.header_version} not supported')
+    if args.header_version in {3, 4}:
+        return write_header_v3_and_above(args)
+
+    ramdisk_load_address = ((args.base + args.ramdisk_offset)
+                            if filesize(args.ramdisk) > 0 else 0)
+    second_load_address = ((args.base + args.second_offset)
+                           if filesize(args.second) > 0 else 0)
+
+    args.output.write(pack(f'{BOOT_MAGIC_SIZE}s', BOOT_MAGIC.encode()))
+    # kernel size in bytes
+    args.output.write(pack('I', filesize(args.kernel)))
+    # kernel physical load address
+    args.output.write(pack('I', args.base + args.kernel_offset))
+    # ramdisk size in bytes
+    args.output.write(pack('I', filesize(args.ramdisk)))
+    # ramdisk physical load address
+    args.output.write(pack('I', ramdisk_load_address))
+    # second bootloader size in bytes
+    args.output.write(pack('I', filesize(args.second)))
+    # second bootloader physical load address
+    args.output.write(pack('I', second_load_address))
+    # kernel tags physical load address
+    args.output.write(pack('I', args.base + args.tags_offset))
+    # flash page size
+    args.output.write(pack('I', args.pagesize))
+    # version of boot image header
+    args.output.write(pack('I', args.header_version))
+    # os version and patch level
+    args.output.write(pack('I', (args.os_version << 11) | args.os_patch_level))
+    # asciiz product name
+    args.output.write(pack(f'{BOOT_NAME_SIZE}s', args.board))
+    args.output.write(pack(f'{BOOT_ARGS_SIZE}s', args.cmdline))
 
     sha = sha1()
     update_sha(sha, args.kernel)
@@ -144,14 +232,18 @@
     img_id = pack('32s', sha.digest())
 
     args.output.write(img_id)
-    args.output.write(pack('1024s', args.cmdline[512:].encode()))
+    args.output.write(pack(f'{BOOT_EXTRA_ARGS_SIZE}s', args.extra_cmdline))
 
     if args.header_version > 0:
-        args.output.write(pack('I', filesize(args.recovery_dtbo)))   # size in bytes
         if args.recovery_dtbo:
-            args.output.write(pack('Q', get_recovery_dtbo_offset(args))) # recovery dtbo offset
+            # recovery dtbo size in bytes
+            args.output.write(pack('I', filesize(args.recovery_dtbo)))
+            # recovert dtbo offset in the boot image
+            args.output.write(pack('Q', get_recovery_dtbo_offset(args)))
         else:
-            args.output.write(pack('Q', 0)) # Will be set to 0 for devices without a recovery dtbo
+            # Set to zero if no recovery dtbo
+            args.output.write(pack('I', 0))
+            args.output.write(pack('Q', 0))
 
     # Populate boot image header size for header versions 1 and 2.
     if args.header_version == 1:
@@ -160,29 +252,101 @@
         args.output.write(pack('I', BOOT_IMAGE_HEADER_V2_SIZE))
 
     if args.header_version > 1:
-
         if filesize(args.dtb) == 0:
-            raise ValueError("DTB image must not be empty.")
+            raise ValueError('DTB image must not be empty.')
 
-        args.output.write(pack('I', filesize(args.dtb)))   # size in bytes
-        args.output.write(pack('Q', args.base + args.dtb_offset)) # dtb physical load address
+        # dtb size in bytes
+        args.output.write(pack('I', filesize(args.dtb)))
+        # dtb physical load address
+        args.output.write(pack('Q', args.base + args.dtb_offset))
+
     pad_file(args.output, args.pagesize)
     return img_id
 
 
-class ValidateStrLenAction(Action):
-    def __init__(self, option_strings, dest, nargs=None, **kwargs):
-        if 'maxlen' not in kwargs:
-            raise ValueError('maxlen must be set')
-        self.maxlen = int(kwargs['maxlen'])
-        del kwargs['maxlen']
-        super(ValidateStrLenAction, self).__init__(option_strings, dest, **kwargs)
+class AsciizBytes:
+    """Parses a string and encodes it as an asciiz bytes object.
 
-    def __call__(self, parser, namespace, values, option_string=None):
-        if len(values) > self.maxlen:
+    >>> AsciizBytes(bufsize=4)('foo')
+    b'foo\\x00'
+    >>> AsciizBytes(bufsize=4)('foob')
+    Traceback (most recent call last):
+        ...
+    argparse.ArgumentTypeError: Encoded asciiz length exceeded: max 4, got 5
+    """
+
+    def __init__(self, bufsize):
+        self.bufsize = bufsize
+
+    def __call__(self, arg):
+        arg_bytes = arg.encode() + b'\x00'
+        if len(arg_bytes) > self.bufsize:
+            raise ArgumentTypeError(
+                'Encoded asciiz length exceeded: '
+                f'max {self.bufsize}, got {len(arg_bytes)}')
+        return arg_bytes
+
+
+class VendorRamdiskTableBuilder:
+    """Vendor ramdisk table builder.
+
+    Attributes:
+        entries: A list of VendorRamdiskTableEntry namedtuple.
+        ramdisk_total_size: Total size in bytes of all ramdisks in the table.
+    """
+
+    VendorRamdiskTableEntry = collections.namedtuple(  # pylint: disable=invalid-name
+        'VendorRamdiskTableEntry',
+        ['ramdisk_path', 'ramdisk_size', 'ramdisk_offset', 'ramdisk_type',
+         'ramdisk_name', 'board_id'])
+
+    def __init__(self):
+        self.entries = []
+        self.ramdisk_total_size = 0
+        self.ramdisk_names = set()
+
+    def add_entry(self, ramdisk_path, ramdisk_type, ramdisk_name, board_id):
+        # Strip any trailing null for simple comparison.
+        stripped_ramdisk_name = ramdisk_name.rstrip(b'\x00')
+        if stripped_ramdisk_name in VENDOR_RAMDISK_NAME_BLOCKLIST:
             raise ValueError(
-                'String argument too long: max {0:d}, got {1:d}'.format(self.maxlen, len(values)))
-        setattr(namespace, self.dest, values)
+                f'Banned vendor ramdisk name: {stripped_ramdisk_name}')
+        if stripped_ramdisk_name in self.ramdisk_names:
+            raise ValueError(
+                f'Duplicated vendor ramdisk name: {stripped_ramdisk_name}')
+        self.ramdisk_names.add(stripped_ramdisk_name)
+
+        if board_id is None:
+            board_id = array.array(
+                'I', [0] * VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE)
+        else:
+            board_id = array.array('I', board_id)
+        if len(board_id) != VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE:
+            raise ValueError('board_id size must be '
+                             f'{VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE}')
+
+        with open(ramdisk_path, 'rb') as f:
+            ramdisk_size = filesize(f)
+        self.entries.append(self.VendorRamdiskTableEntry(
+            ramdisk_path, ramdisk_size, self.ramdisk_total_size, ramdisk_type,
+            ramdisk_name, board_id))
+        self.ramdisk_total_size += ramdisk_size
+
+    def write_ramdisks_padded(self, fout, alignment):
+        for entry in self.entries:
+            with open(entry.ramdisk_path, 'rb') as f:
+                fout.write(f.read())
+        pad_file(fout, alignment)
+
+    def write_entries_padded(self, fout, alignment):
+        for entry in self.entries:
+            fout.write(pack('I', entry.ramdisk_size))
+            fout.write(pack('I', entry.ramdisk_offset))
+            fout.write(pack('I', entry.ramdisk_type))
+            fout.write(pack(f'{VENDOR_RAMDISK_NAME_SIZE}s',
+                            entry.ramdisk_name))
+            fout.write(entry.board_id)
+        pad_file(fout, alignment)
 
 
 def write_padded_file(f_out, f_in, padding):
@@ -225,49 +389,236 @@
     return 0
 
 
+def parse_vendor_ramdisk_type(x):
+    type_dict = {
+        'none': VENDOR_RAMDISK_TYPE_NONE,
+        'platform': VENDOR_RAMDISK_TYPE_PLATFORM,
+        'recovery': VENDOR_RAMDISK_TYPE_RECOVERY,
+        'dlkm': VENDOR_RAMDISK_TYPE_DLKM,
+    }
+    if x.lower() in type_dict:
+        return type_dict[x.lower()]
+    return parse_int(x)
+
+
+def get_vendor_boot_v4_usage():
+    return """vendor boot version 4 arguments:
+  --ramdisk_type {none,platform,recovery,dlkm}
+                        specify the type of the ramdisk
+  --ramdisk_name NAME
+                        specify the name of the ramdisk
+  --board_id{0..15} NUMBER
+                        specify the value of the board_id vector, defaults to 0
+  --vendor_ramdisk_fragment VENDOR_RAMDISK_FILE
+                        path to the vendor ramdisk file
+
+  These options can be specified multiple times, where each vendor ramdisk
+  option group ends with a --vendor_ramdisk_fragment option.
+  Each option group appends an additional ramdisk to the vendor boot image.
+"""
+
+
+def parse_vendor_ramdisk_args(args, args_list):
+    """Parses vendor ramdisk specific arguments.
+
+    Args:
+        args: An argparse.Namespace object. Parsed results are stored into this
+            object.
+        args_list: A list of argument strings to be parsed.
+
+    Returns:
+        A list argument strings that are not parsed by this method.
+    """
+    parser = ArgumentParser(add_help=False)
+    parser.add_argument('--ramdisk_type', type=parse_vendor_ramdisk_type,
+                        default=VENDOR_RAMDISK_TYPE_NONE)
+    parser.add_argument('--ramdisk_name',
+                        type=AsciizBytes(bufsize=VENDOR_RAMDISK_NAME_SIZE),
+                        required=True)
+    for i in range(VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE):
+        parser.add_argument(f'--board_id{i}', type=parse_int, default=0)
+    parser.add_argument(PARSER_ARGUMENT_VENDOR_RAMDISK_FRAGMENT, required=True)
+
+    unknown_args = []
+
+    vendor_ramdisk_table_builder = VendorRamdiskTableBuilder()
+    if args.vendor_ramdisk is not None:
+        vendor_ramdisk_table_builder.add_entry(
+            args.vendor_ramdisk.name, VENDOR_RAMDISK_TYPE_PLATFORM, b'', None)
+
+    while PARSER_ARGUMENT_VENDOR_RAMDISK_FRAGMENT in args_list:
+        idx = args_list.index(PARSER_ARGUMENT_VENDOR_RAMDISK_FRAGMENT) + 2
+        vendor_ramdisk_args = args_list[:idx]
+        args_list = args_list[idx:]
+
+        ramdisk_args, extra_args = parser.parse_known_args(vendor_ramdisk_args)
+        ramdisk_args_dict = vars(ramdisk_args)
+        unknown_args.extend(extra_args)
+
+        ramdisk_path = ramdisk_args.vendor_ramdisk_fragment
+        ramdisk_type = ramdisk_args.ramdisk_type
+        ramdisk_name = ramdisk_args.ramdisk_name
+        board_id = [ramdisk_args_dict[f'board_id{i}']
+                    for i in range(VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE)]
+        vendor_ramdisk_table_builder.add_entry(ramdisk_path, ramdisk_type,
+                                               ramdisk_name, board_id)
+
+    if len(args_list) > 0:
+        unknown_args.extend(args_list)
+
+    args.vendor_ramdisk_total_size = (vendor_ramdisk_table_builder
+                                      .ramdisk_total_size)
+    args.vendor_ramdisk_table_entry_num = len(vendor_ramdisk_table_builder
+                                              .entries)
+    args.vendor_ramdisk_table_builder = vendor_ramdisk_table_builder
+    return unknown_args
+
+
 def parse_cmdline():
-    parser = ArgumentParser()
-    parser.add_argument('--kernel', help='path to the kernel', type=FileType('rb'))
-    parser.add_argument('--ramdisk', help='path to the ramdisk', type=FileType('rb'))
-    parser.add_argument('--second', help='path to the 2nd bootloader', type=FileType('rb'))
-    parser.add_argument('--dtb', help='path to dtb', type=FileType('rb'))
-    recovery_dtbo_group = parser.add_mutually_exclusive_group()
-    recovery_dtbo_group.add_argument('--recovery_dtbo', help='path to the recovery DTBO',
-                                     type=FileType('rb'))
-    recovery_dtbo_group.add_argument('--recovery_acpio', help='path to the recovery ACPIO',
-                                     type=FileType('rb'), metavar='RECOVERY_ACPIO',
-                                     dest='recovery_dtbo')
-    parser.add_argument('--cmdline', help='extra arguments to be passed on the '
-                        'kernel command line', default='', action=ValidateStrLenAction, maxlen=1536)
+    version_parser = ArgumentParser(add_help=False)
+    version_parser.add_argument('--header_version', type=parse_int, default=0)
+    if version_parser.parse_known_args()[0].header_version < 3:
+        # For boot header v0 to v2, the kernel commandline field is split into
+        # two fields, cmdline and extra_cmdline. Both fields are asciiz strings,
+        # so we minus one here to ensure the encoded string plus the
+        # null-terminator can fit in the buffer size.
+        cmdline_size = BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE - 1
+    else:
+        cmdline_size = BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE
+
+    parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
+                            epilog=get_vendor_boot_v4_usage())
+    parser.add_argument('--kernel', type=FileType('rb'),
+                        help='path to the kernel')
+    parser.add_argument('--ramdisk', type=FileType('rb'),
+                        help='path to the ramdisk')
+    parser.add_argument('--second', type=FileType('rb'),
+                        help='path to the second bootloader')
+    parser.add_argument('--dtb', type=FileType('rb'), help='path to the dtb')
+    dtbo_group = parser.add_mutually_exclusive_group()
+    dtbo_group.add_argument('--recovery_dtbo', type=FileType('rb'),
+                            help='path to the recovery DTBO')
+    dtbo_group.add_argument('--recovery_acpio', type=FileType('rb'),
+                            metavar='RECOVERY_ACPIO', dest='recovery_dtbo',
+                            help='path to the recovery ACPIO')
+    parser.add_argument('--cmdline', type=AsciizBytes(bufsize=cmdline_size),
+                        default='', help='kernel command line arguments')
     parser.add_argument('--vendor_cmdline',
-                        help='kernel command line arguments contained in vendor boot',
-                        default='', action=ValidateStrLenAction, maxlen=2048)
-    parser.add_argument('--base', help='base address', type=parse_int, default=0x10000000)
-    parser.add_argument('--kernel_offset', help='kernel offset', type=parse_int, default=0x00008000)
-    parser.add_argument('--ramdisk_offset', help='ramdisk offset', type=parse_int,
-                        default=0x01000000)
-    parser.add_argument('--second_offset', help='2nd bootloader offset', type=parse_int,
-                        default=0x00f00000)
-    parser.add_argument('--dtb_offset', help='dtb offset', type=parse_int, default=0x01f00000)
+                        type=AsciizBytes(bufsize=VENDOR_BOOT_ARGS_SIZE),
+                        default='',
+                        help='vendor boot kernel command line arguments')
+    parser.add_argument('--base', type=parse_int, default=0x10000000,
+                        help='base address')
+    parser.add_argument('--kernel_offset', type=parse_int, default=0x00008000,
+                        help='kernel offset')
+    parser.add_argument('--ramdisk_offset', type=parse_int, default=0x01000000,
+                        help='ramdisk offset')
+    parser.add_argument('--second_offset', type=parse_int, default=0x00f00000,
+                        help='second bootloader offset')
+    parser.add_argument('--dtb_offset', type=parse_int, default=0x01f00000,
+                        help='dtb offset')
 
-    parser.add_argument('--os_version', help='operating system version', type=parse_os_version,
-                        default=0)
-    parser.add_argument('--os_patch_level', help='operating system patch level',
-                        type=parse_os_patch_level, default=0)
-    parser.add_argument('--tags_offset', help='tags offset', type=parse_int, default=0x00000100)
-    parser.add_argument('--board', help='board name', default='', action=ValidateStrLenAction,
-                        maxlen=16)
-    parser.add_argument('--pagesize', help='page size', type=parse_int,
-                        choices=[2**i for i in range(11, 15)], default=2048)
-    parser.add_argument('--id', help='print the image ID on standard output',
-                        action='store_true')
-    parser.add_argument('--header_version', help='boot image header version', type=parse_int,
-                        default=0)
-    parser.add_argument('-o', '--output', help='output file name', type=FileType('wb'))
-    parser.add_argument('--vendor_boot', help='vendor boot output file name', type=FileType('wb'))
-    parser.add_argument('--vendor_ramdisk', help='path to the vendor ramdisk', type=FileType('rb'))
+    parser.add_argument('--os_version', type=parse_os_version, default=0,
+                        help='operating system version')
+    parser.add_argument('--os_patch_level', type=parse_os_patch_level,
+                        default=0, help='operating system patch level')
+    parser.add_argument('--tags_offset', type=parse_int, default=0x00000100,
+                        help='tags offset')
+    parser.add_argument('--board', type=AsciizBytes(bufsize=BOOT_NAME_SIZE),
+                        default='', help='board name')
+    parser.add_argument('--pagesize', type=parse_int,
+                        choices=[2**i for i in range(11, 15)], default=2048,
+                        help='page size')
+    parser.add_argument('--id', action='store_true',
+                        help='print the image ID on standard output')
+    parser.add_argument('--header_version', type=parse_int, default=0,
+                        help='boot image header version')
+    parser.add_argument('-o', '--output', type=FileType('wb'),
+                        help='output file name')
+    parser.add_argument('--gki_signing_algorithm',
+                        help='GKI signing algorithm to use')
+    parser.add_argument('--gki_signing_key',
+                        help='path to RSA private key file')
+    parser.add_argument('--gki_signing_signature_args',
+                        help='other hash arguments passed to avbtool')
+    parser.add_argument('--gki_signing_avbtool_path',
+                        help='path to avbtool for boot signature generation')
+    parser.add_argument('--vendor_boot', type=FileType('wb'),
+                        help='vendor boot output file name')
+    parser.add_argument('--vendor_ramdisk', type=FileType('rb'),
+                        help='path to the vendor ramdisk')
+    parser.add_argument('--vendor_bootconfig', type=FileType('rb'),
+                        help='path to the vendor bootconfig file')
 
-    return parser.parse_args()
+    args, extra_args = parser.parse_known_args()
+    if args.vendor_boot is not None and args.header_version > 3:
+        extra_args = parse_vendor_ramdisk_args(args, extra_args)
+    if len(extra_args) > 0:
+        raise ValueError(f'Unrecognized arguments: {extra_args}')
+
+    if args.header_version < 3:
+        args.extra_cmdline = args.cmdline[BOOT_ARGS_SIZE-1:]
+        args.cmdline = args.cmdline[:BOOT_ARGS_SIZE-1] + b'\x00'
+        assert len(args.cmdline) <= BOOT_ARGS_SIZE
+        assert len(args.extra_cmdline) <= BOOT_EXTRA_ARGS_SIZE
+
+    return args
+
+
+def add_boot_image_signature(args, pagesize):
+    """Adds the boot image signature.
+
+    Note that the signature will only be verified in VTS to ensure a
+    generic boot.img is used. It will not be used by the device
+    bootloader at boot time. The bootloader should only verify
+    the boot vbmeta at the end of the boot partition (or in the top-level
+    vbmeta partition) via the Android Verified Boot process, when the
+    device boots.
+    """
+    args.output.flush()  # Flush the buffer for signature calculation.
+
+    # Appends zeros if the signing key is not specified.
+    if not args.gki_signing_key or not args.gki_signing_algorithm:
+        zeros = b'\x00' * BOOT_IMAGE_V4_SIGNATURE_SIZE
+        args.output.write(zeros)
+        pad_file(args.output, pagesize)
+        return
+
+    avbtool = 'avbtool'  # Used from otatools.zip or Android build env.
+
+    # We need to specify the path of avbtool in build/core/Makefile.
+    # Because avbtool is not guaranteed to be in $PATH there.
+    if args.gki_signing_avbtool_path:
+        avbtool = args.gki_signing_avbtool_path
+
+    # Need to specify a value of --partition_size for avbtool to work.
+    # We use 64 MB below, but avbtool will not resize the boot image to
+    # this size because --do_not_append_vbmeta_image is also specified.
+    avbtool_cmd = [
+        avbtool, 'add_hash_footer',
+        '--partition_name', 'boot',
+        '--partition_size', str(64 * 1024 * 1024),
+        '--image', args.output.name,
+        '--algorithm', args.gki_signing_algorithm,
+        '--key', args.gki_signing_key,
+        '--salt', 'd00df00d']  # TODO: use a hash of kernel/ramdisk as the salt.
+
+    # Additional arguments passed to avbtool.
+    if args.gki_signing_signature_args:
+        avbtool_cmd += args.gki_signing_signature_args.split()
+
+    # Outputs the signed vbmeta to a separate file, then append to boot.img
+    # as the boot signature.
+    with tempfile.TemporaryDirectory() as temp_out_dir:
+        boot_signature_output = os.path.join(temp_out_dir, 'boot_signature')
+        avbtool_cmd += ['--do_not_append_vbmeta_image',
+                        '--output_vbmeta_image', boot_signature_output]
+        subprocess.check_call(avbtool_cmd)
+        with open(boot_signature_output, 'rb') as boot_signature:
+            if filesize(boot_signature) > BOOT_IMAGE_V4_SIGNATURE_SIZE:
+                raise ValueError(
+                    f'boot sigature size is > {BOOT_IMAGE_V4_SIGNATURE_SIZE}')
+            write_padded_file(args.output, boot_signature, pagesize)
 
 
 def write_data(args, pagesize):
@@ -279,37 +630,44 @@
         write_padded_file(args.output, args.recovery_dtbo, pagesize)
     if args.header_version == 2:
         write_padded_file(args.output, args.dtb, pagesize)
+    if args.header_version >= 4:
+        add_boot_image_signature(args, pagesize)
 
 
 def write_vendor_boot_data(args):
-    write_padded_file(args.vendor_boot, args.vendor_ramdisk, args.pagesize)
-    write_padded_file(args.vendor_boot, args.dtb, args.pagesize)
+    if args.header_version > 3:
+        builder = args.vendor_ramdisk_table_builder
+        builder.write_ramdisks_padded(args.vendor_boot, args.pagesize)
+        write_padded_file(args.vendor_boot, args.dtb, args.pagesize)
+        builder.write_entries_padded(args.vendor_boot, args.pagesize)
+        write_padded_file(args.vendor_boot, args.vendor_bootconfig,
+            args.pagesize)
+    else:
+        write_padded_file(args.vendor_boot, args.vendor_ramdisk, args.pagesize)
+        write_padded_file(args.vendor_boot, args.dtb, args.pagesize)
 
 
 def main():
     args = parse_cmdline()
     if args.vendor_boot is not None:
-        if args.header_version < 3:
-            raise ValueError('--vendor_boot not compatible with given header version')
-        if args.vendor_ramdisk is None:
+        if args.header_version not in {3, 4}:
+            raise ValueError(
+                '--vendor_boot not compatible with given header version')
+        if args.header_version == 3 and args.vendor_ramdisk is None:
             raise ValueError('--vendor_ramdisk missing or invalid')
         write_vendor_boot_header(args)
         write_vendor_boot_data(args)
     if args.output is not None:
-        if args.kernel is None:
-            raise ValueError('kernel must be supplied when creating a boot image')
         if args.second is not None and args.header_version > 2:
-            raise ValueError('--second not compatible with given header version')
+            raise ValueError(
+                '--second not compatible with given header version')
         img_id = write_header(args)
         if args.header_version > 2:
             write_data(args, BOOT_IMAGE_HEADER_V3_PAGESIZE)
         else:
             write_data(args, args.pagesize)
         if args.id and img_id is not None:
-            # Python 2's struct.pack returns a string, but py3 returns bytes.
-            if isinstance(img_id, str):
-                img_id = [ord(x) for x in img_id]
-            print('0x' + ''.join('{:02x}'.format(c) for c in img_id))
+            print('0x' + ''.join(f'{octet:02x}' for octet in img_id))
 
 
 if __name__ == '__main__':
diff --git a/pylintrc b/pylintrc
index 1990be7..b65d218 100644
--- a/pylintrc
+++ b/pylintrc
@@ -1,46 +1,33 @@
+# This Pylint rcfile contains a best-effort configuration to uphold the
+# best-practices and style described in the Google Python style guide:
+#   https://google.github.io/styleguide/pyguide.html
+#
+# Its canonical open-source location is:
+#   https://google.github.io/styleguide/pylintrc
+
 [MASTER]
 
-# Specify a configuration file.
-#rcfile=
+# Files or directories to be skipped. They should be base names, not paths.
+ignore=third_party
 
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Profiled execution.
-profile=no
-
-# Add files or directories to the blacklist. They should be base names, not
-# paths.
-ignore=CVS
+# Files or directories matching the regex patterns are skipped. The regex
+# matches against base names, not paths.
+ignore-patterns=
 
 # Pickle collected data for later comparisons.
-persistent=yes
+persistent=no
 
 # List of plugins (as comma separated values of python modules names) to load,
 # usually to register additional checkers.
 load-plugins=
 
 # Use multiple processes to speed up Pylint.
-jobs=1
+jobs=4
 
 # Allow loading of arbitrary C extensions. Extensions are imported into the
 # active Python interpreter and may run arbitrary code.
 unsafe-load-any-extension=no
 
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code
-extension-pkg-whitelist=
-
-# Allow optimization of some AST trees. This will activate a peephole AST
-# optimizer, which will apply various small optimizations. For instance, it can
-# be used to obtain the result of joining multiple strings with the addition
-# operator. Joining a lot of strings can lead to a maximum recursion error in
-# Pylint and this flag can prevent that. It has one side effect, the resulting
-# AST will be different than the one from reality.
-optimize-ast=no
-
 
 [MESSAGES CONTROL]
 
@@ -50,7 +37,8 @@
 
 # Enable the message, report, category or checker with the given id(s). You can
 # either give multiple identifier separated by comma (,) or put this option
-# multiple time. See also the "--disable" option for examples.
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
 #enable=
 
 # Disable the message, report, category or checker with the given id(s). You
@@ -62,7 +50,102 @@
 # --enable=similarities". If you want to run only the classes checker, but have
 # no Warning level messages displayed, use"--disable=all --enable=classes
 # --disable=W"
-disable=invalid-name,missing-docstring,too-many-branches,too-many-locals,too-many-arguments,too-many-statements,duplicate-code,too-few-public-methods,too-many-instance-attributes,too-many-lines,too-many-public-methods,locally-disabled,fixme,not-callable
+disable=abstract-method,
+        apply-builtin,
+        arguments-differ,
+        attribute-defined-outside-init,
+        backtick,
+        bad-option-value,
+        basestring-builtin,
+        buffer-builtin,
+        c-extension-no-member,
+        consider-using-enumerate,
+        cmp-builtin,
+        cmp-method,
+        coerce-builtin,
+        coerce-method,
+        delslice-method,
+        div-method,
+        duplicate-code,
+        eq-without-hash,
+        execfile-builtin,
+        file-builtin,
+        filter-builtin-not-iterating,
+        fixme,
+        getslice-method,
+        global-statement,
+        hex-method,
+        idiv-method,
+        implicit-str-concat-in-sequence,
+        import-error,
+        import-self,
+        import-star-module-level,
+        inconsistent-return-statements,
+        input-builtin,
+        intern-builtin,
+        invalid-str-codec,
+        locally-disabled,
+        long-builtin,
+        long-suffix,
+        map-builtin-not-iterating,
+        misplaced-comparison-constant,
+        missing-function-docstring,
+        metaclass-assignment,
+        next-method-called,
+        next-method-defined,
+        no-absolute-import,
+        no-else-break,
+        no-else-continue,
+        no-else-raise,
+        no-else-return,
+        no-init,  # added
+        no-member,
+        no-name-in-module,
+        no-self-use,
+        nonzero-method,
+        oct-method,
+        old-division,
+        old-ne-operator,
+        old-octal-literal,
+        old-raise-syntax,
+        parameter-unpacking,
+        print-statement,
+        raising-string,
+        range-builtin-not-iterating,
+        raw_input-builtin,
+        rdiv-method,
+        reduce-builtin,
+        relative-import,
+        reload-builtin,
+        round-builtin,
+        setslice-method,
+        signature-differs,
+        standarderror-builtin,
+        suppressed-message,
+        sys-max-int,
+        too-few-public-methods,
+        too-many-ancestors,
+        too-many-arguments,
+        too-many-boolean-expressions,
+        too-many-branches,
+        too-many-instance-attributes,
+        too-many-locals,
+        too-many-nested-blocks,
+        too-many-public-methods,
+        too-many-return-statements,
+        too-many-statements,
+        trailing-newlines,
+        unichr-builtin,
+        unicode-builtin,
+        unnecessary-pass,
+        unpacking-in-except,
+        useless-else-on-loop,
+        useless-object-inheritance,
+        useless-suppression,
+        using-cmp-argument,
+        wrong-import-order,
+        xrange-builtin,
+        zip-builtin-not-iterating,
 
 
 [REPORTS]
@@ -74,11 +157,12 @@
 
 # Put messages in a separate file for each module / package specified on the
 # command line instead of printing them on stdout. Reports (if any) will be
-# written in a file name "pylint_global.[txt|html]".
+# written in a file name "pylint_global.[txt|html]". This option is deprecated
+# and it will be removed in Pylint 2.0.
 files-output=no
 
 # Tells whether to display a full report or only the messages
-reports=yes
+reports=no
 
 # Python expression which should return a note less than 10 (10 is the highest
 # note). You have access to the variables errors warning, statement which
@@ -87,15 +171,177 @@
 # (RP0004).
 evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
 
-# Add a comment according to your evaluation note. This is used by the global
-# evaluation report (RP0004).
-comment=no
-
 # Template used to display messages. This is a python new-style format string
 # used to format the message information. See doc for all details
 #msg-template=
 
 
+[BASIC]
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=main,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
+
+# Regular expression matching correct function names
+function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct constant names
+const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression matching correct attribute names
+attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
+
+# Regular expression matching correct argument names
+argument-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct class names
+class-rgx=^_?[A-Z][a-zA-Z0-9]*$
+
+# Regular expression matching correct module names
+module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
+
+# Regular expression matching correct method names
+method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=10
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
+# lines made too long by directives to pytype.
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=(?x)(
+  ^\s*(\#\ )?<?https?://\S+>?$|
+  ^\s*(from\s+\S+\s+)?import\s+.+$)
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=yes
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=
+
+# Maximum number of lines in a module
+max-module-lines=99999
+
+# String used as indentation unit.  The internal Google style guide mandates 2
+# spaces.  Google's externaly-published style guide says 4, consistent with
+# PEP 8.
+indent-string='    '
+
+# Number of spaces of indent required inside a hanging  or continued line.
+indent-after-paren=4
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=LF
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=TODO
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=yes
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging,absl.logging,tensorflow.io.logging
+
+
 [SIMILARITIES]
 
 # Minimum lines number of a similarity.
@@ -111,124 +357,6 @@
 ignore-imports=no
 
 
-[TYPECHECK]
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis
-ignored-modules=
-
-# List of classes names for which member attributes should not be checked
-# (useful for classes with attributes dynamically set).
-ignored-classes=SQLObject
-
-# When zope mode is activated, add a predefined set of Zope acquired attributes
-# to generated-members.
-zope=no
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E0201 when accessed. Python regular
-# expressions are accepted.
-generated-members=REQUEST,acl_users,aq_parent
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,XXX,TODO
-
-
-[BASIC]
-
-# List of builtins function names that should not be used, separated by a comma
-bad-functions=map,filter,input
-
-# Good variable names which should always be accepted, separated by a comma
-good-names=i,j,k,ex,Run,_
-
-# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Include a hint for the correct naming format with invalid-name
-include-naming-hint=no
-
-# Regular expression matching correct function names
-function-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for function names
-function-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression matching correct variable names
-variable-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for variable names
-variable-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression matching correct constant names
-const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-
-# Naming hint for constant names
-const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-
-# Regular expression matching correct attribute names
-attr-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for attribute names
-attr-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression matching correct argument names
-argument-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for argument names
-argument-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression matching correct class attribute names
-class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-
-# Naming hint for class attribute names
-class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-
-# Regular expression matching correct inline iteration names
-inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
-
-# Naming hint for inline iteration names
-inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
-
-# Regular expression matching correct class names
-class-rgx=[A-Z_][a-zA-Z0-9]+$
-
-# Naming hint for class names
-class-name-hint=[A-Z_][a-zA-Z0-9]+$
-
-# Regular expression matching correct module names
-module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Naming hint for module names
-module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Regular expression matching correct method names
-method-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for method names
-method-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=__.*__
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-
 [SPELLING]
 
 # Spelling dictionary name. Available dictionaries: none. To make it working
@@ -246,98 +374,14 @@
 spelling-store-unknown-words=no
 
 
-[FORMAT]
-
-# Maximum number of characters on a single line.
-max-line-length=100
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )?<?https?://\S+>?$
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=no
-
-# List of optional constructs for which whitespace checking is disabled
-no-space-check=trailing-comma,dict-separator
-
-# Maximum number of lines in a module
-max-module-lines=1000
-
-# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
-# tab).
-indent-string='    '
-
-# Number of spaces of indent required inside a hanging or continued line.
-indent-after-paren=4
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-expected-line-ending-format=LF
-
-
-[LOGGING]
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format
-logging-modules=logging
-
-
-[VARIABLES]
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# A regular expression matching the name of dummy variables (i.e. expectedly
-# not used).
-dummy-variables-rgx=_$|dummy
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-# List of strings which can identify a callback function by name. A callback
-# name must start or end with one of those strings.
-callbacks=cb_,_cb
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method
-max-args=5
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore
-ignored-argument-names=_.*
-
-# Maximum number of locals for function / method body
-max-locals=15
-
-# Maximum number of return / yield for function / method body
-max-returns=6
-
-# Maximum number of branch for function / method body
-max-branches=12
-
-# Maximum number of statements in function / method body
-max-statements=50
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-
 [IMPORTS]
 
 # Deprecated modules which should not be used, separated by a comma
-deprecated-modules=regsub,TERMIOS,Bastion,rexec
+deprecated-modules=regsub,
+                   TERMIOS,
+                   Bastion,
+                   rexec,
+                   sets
 
 # Create a graph of every (i.e. internal and external) dependencies in the
 # given file (report RP0402 must not be disabled)
@@ -351,25 +395,46 @@
 # not be disabled)
 int-import-graph=
 
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant, absl
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
 
 [CLASSES]
 
 # List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,__new__,setUp
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=mcs
+defining-attr-methods=__init__,
+                      __new__,
+                      setUp
 
 # List of member names, which should be excluded from the protected access
 # warning.
-exclude-protected=_asdict,_fields,_replace,_source,_make
+exclude-protected=_asdict,
+                  _fields,
+                  _replace,
+                  _source,
+                  _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls,
+                            class_
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
 
 
 [EXCEPTIONS]
 
 # Exceptions that will emit a warning when being caught. Defaults to
 # "Exception"
-overgeneral-exceptions=Exception
+overgeneral-exceptions=StandardError,
+                       Exception,
+                       BaseException
diff --git a/repack_bootimg.py b/repack_bootimg.py
new file mode 100755
index 0000000..c320018
--- /dev/null
+++ b/repack_bootimg.py
@@ -0,0 +1,362 @@
+#!/usr/bin/env python3
+#
+# Copyright 2021, 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.
+
+"""Repacks the boot image.
+
+Unpacks the boot image and the ramdisk inside, then add files into
+the ramdisk to repack the boot image.
+"""
+
+import argparse
+import datetime
+import enum
+import glob
+import os
+import shlex
+import shutil
+import subprocess
+import tempfile
+
+
+class TempFileManager:
+    """Manages temporary files and dirs."""
+
+    def __init__(self):
+        self._temp_files = []
+
+    def __del__(self):
+        """Removes temp dirs and files."""
+        for f in self._temp_files:
+            if os.path.isdir(f):
+                shutil.rmtree(f, ignore_errors=True)
+            else:
+                os.remove(f)
+
+    def make_temp_dir(self, prefix='tmp', suffix=''):
+        """Makes a temporary dir that will be cleaned up in the destructor.
+
+        Returns:
+            The absolute pathname of the new directory.
+        """
+        dir_name = tempfile.mkdtemp(prefix=prefix, suffix=suffix)
+        self._temp_files.append(dir_name)
+        return dir_name
+
+    def make_temp_file(self, prefix='tmp', suffix=''):
+        """Make a temp file that will be deleted in the destructor.
+
+        Returns:
+            The absolute pathname of the new file.
+        """
+        fd, file_name = tempfile.mkstemp(prefix=prefix, suffix=suffix)
+        os.close(fd)
+        self._temp_files.append(file_name)
+        return file_name
+
+
+class RamdiskFormat(enum.Enum):
+    """Enum class for different ramdisk compression formats."""
+    LZ4 = 1
+    GZIP = 2
+
+
+class BootImageType(enum.Enum):
+    """Enum class for different boot image types."""
+    BOOT_IMAGE = 1
+    VENDOR_BOOT_IMAGE = 2
+    SINGLE_RAMDISK_FRAGMENT = 3
+    MULTIPLE_RAMDISK_FRAGMENTS = 4
+
+
+class RamdiskImage:
+    """A class that supports packing/unpacking a ramdisk."""
+    def __init__(self, ramdisk_img, unpack=True):
+        self._ramdisk_img = ramdisk_img
+        self._ramdisk_format = None
+        self._ramdisk_dir = None
+        self._temp_file_manager = TempFileManager()
+
+        if unpack:
+            self._unpack_ramdisk()
+        else:
+            self._ramdisk_dir = self._temp_file_manager.make_temp_dir(
+                suffix='_new_ramdisk')
+
+    def _unpack_ramdisk(self):
+        """Unpacks the ramdisk."""
+        self._ramdisk_dir = self._temp_file_manager.make_temp_dir(
+            suffix='_' + os.path.basename(self._ramdisk_img))
+
+        # The compression format might be in 'lz4' or 'gzip' format,
+        # trying lz4 first.
+        for compression_type, compression_util in [
+            (RamdiskFormat.LZ4, 'lz4'),
+            (RamdiskFormat.GZIP, 'minigzip')]:
+
+            # Command arguments:
+            #   -d: decompression
+            #   -c: write to stdout
+            decompression_cmd = [
+                compression_util, '-d', '-c', self._ramdisk_img]
+
+            decompressed_result = subprocess.run(
+                decompression_cmd, check=False, capture_output=True)
+
+            if decompressed_result.returncode == 0:
+                self._ramdisk_format = compression_type
+                break
+
+        if self._ramdisk_format is not None:
+            # toybox cpio arguments:
+            #   -i: extract files from stdin
+            #   -d: create directories if needed
+            #   -u: override existing files
+            subprocess.run(
+                ['toybox', 'cpio', '-idu'], check=True,
+                input=decompressed_result.stdout, cwd=self._ramdisk_dir)
+
+            print("=== Unpacked ramdisk: '{}' ===".format(
+                self._ramdisk_img))
+        else:
+            raise RuntimeError('Failed to decompress ramdisk.')
+
+    def repack_ramdisk(self, out_ramdisk_file):
+        """Repacks a ramdisk from self._ramdisk_dir.
+
+        Args:
+            out_ramdisk_file: the output ramdisk file to save.
+        """
+        compression_cmd = ['lz4', '-l', '-12', '--favor-decSpeed']
+        if self._ramdisk_format == RamdiskFormat.GZIP:
+            compression_cmd = ['minigzip']
+
+        print('Repacking ramdisk, which might take a few seconds ...')
+
+        mkbootfs_result = subprocess.run(
+            ['mkbootfs', self._ramdisk_dir], check=True, capture_output=True)
+
+        with open(out_ramdisk_file, 'w') as output_fd:
+            subprocess.run(compression_cmd, check=True,
+                           input=mkbootfs_result.stdout, stdout=output_fd)
+
+        print("=== Repacked ramdisk: '{}' ===".format(out_ramdisk_file))
+
+    @property
+    def ramdisk_dir(self):
+        """Returns the internal ramdisk dir."""
+        return self._ramdisk_dir
+
+
+class BootImage:
+    """A class that supports packing/unpacking a boot.img and ramdisk."""
+
+    def __init__(self, bootimg):
+        self._bootimg = bootimg
+        self._bootimg_dir = None
+        self._bootimg_type = None
+        self._ramdisk = None
+        self._previous_mkbootimg_args = []
+        self._temp_file_manager = TempFileManager()
+
+        self._unpack_bootimg()
+
+    def _get_vendor_ramdisks(self):
+        """Returns a list of vendor ramdisks after unpack."""
+        return sorted(glob.glob(
+            os.path.join(self._bootimg_dir, 'vendor_ramdisk*')))
+
+    def _unpack_bootimg(self):
+        """Unpacks the boot.img and the ramdisk inside."""
+        self._bootimg_dir = self._temp_file_manager.make_temp_dir(
+            suffix='_' + os.path.basename(self._bootimg))
+
+        # Unpacks the boot.img first.
+        unpack_bootimg_cmds = [
+            'unpack_bootimg',
+            '--boot_img', self._bootimg,
+            '--out', self._bootimg_dir,
+            '--format=mkbootimg',
+        ]
+        result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                capture_output=True, encoding='utf-8')
+        self._previous_mkbootimg_args = shlex.split(result.stdout)
+        print("=== Unpacked boot image: '{}' ===".format(self._bootimg))
+
+        # From the output dir, checks there is 'ramdisk' or 'vendor_ramdisk'.
+        ramdisk = os.path.join(self._bootimg_dir, 'ramdisk')
+        vendor_ramdisk = os.path.join(self._bootimg_dir, 'vendor_ramdisk')
+        vendor_ramdisks = self._get_vendor_ramdisks()
+        if os.path.exists(ramdisk):
+            self._ramdisk = RamdiskImage(ramdisk)
+            self._bootimg_type = BootImageType.BOOT_IMAGE
+        elif os.path.exists(vendor_ramdisk):
+            self._ramdisk = RamdiskImage(vendor_ramdisk)
+            self._bootimg_type = BootImageType.VENDOR_BOOT_IMAGE
+        elif len(vendor_ramdisks) == 1:
+            self._ramdisk = RamdiskImage(vendor_ramdisks[0])
+            self._bootimg_type = BootImageType.SINGLE_RAMDISK_FRAGMENT
+        elif len(vendor_ramdisks) > 1:
+            # Creates an empty RamdiskImage() below, without unpack.
+            # We'll then add files into this newly created ramdisk, then pack
+            # it with other vendor ramdisks together.
+            self._ramdisk = RamdiskImage(ramdisk_img=None, unpack=False)
+            self._bootimg_type = BootImageType.MULTIPLE_RAMDISK_FRAGMENTS
+        else:
+            raise RuntimeError('Both ramdisk and vendor_ramdisk do not exist.')
+
+    def repack_bootimg(self):
+        """Repacks the ramdisk and rebuild the boot.img"""
+
+        new_ramdisk = self._temp_file_manager.make_temp_file(
+            prefix='ramdisk-patched')
+        self._ramdisk.repack_ramdisk(new_ramdisk)
+
+        mkbootimg_cmd = ['mkbootimg']
+
+        # Uses previous mkbootimg args, e.g., --vendor_cmdline, --dtb_offset.
+        mkbootimg_cmd.extend(self._previous_mkbootimg_args)
+
+        ramdisk_option = ''
+        if self._bootimg_type == BootImageType.BOOT_IMAGE:
+            ramdisk_option = '--ramdisk'
+            mkbootimg_cmd.extend(['--output', self._bootimg])
+        elif self._bootimg_type == BootImageType.VENDOR_BOOT_IMAGE:
+            ramdisk_option = '--vendor_ramdisk'
+            mkbootimg_cmd.extend(['--vendor_boot', self._bootimg])
+        elif self._bootimg_type == BootImageType.SINGLE_RAMDISK_FRAGMENT:
+            ramdisk_option = '--vendor_ramdisk_fragment'
+            mkbootimg_cmd.extend(['--vendor_boot', self._bootimg])
+        elif self._bootimg_type == BootImageType.MULTIPLE_RAMDISK_FRAGMENTS:
+            mkbootimg_cmd.extend(['--ramdisk_type', 'PLATFORM'])
+            ramdisk_name = (
+                'RAMDISK_' +
+                datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S'))
+            mkbootimg_cmd.extend(['--ramdisk_name', ramdisk_name])
+            mkbootimg_cmd.extend(['--vendor_ramdisk_fragment', new_ramdisk])
+            mkbootimg_cmd.extend(['--vendor_boot', self._bootimg])
+
+        if ramdisk_option and ramdisk_option not in mkbootimg_cmd:
+            raise RuntimeError("Failed to find '{}' from:\n  {}".format(
+                ramdisk_option, shlex.join(mkbootimg_cmd)))
+        # Replaces the original ramdisk with the newly packed ramdisk.
+        if ramdisk_option:
+            ramdisk_index = mkbootimg_cmd.index(ramdisk_option) + 1
+            mkbootimg_cmd[ramdisk_index] = new_ramdisk
+
+        subprocess.check_call(mkbootimg_cmd)
+        print("=== Repacked boot image: '{}' ===".format(self._bootimg))
+
+    def add_files(self, src_dir, files):
+        """Copy files from the src_dir into current ramdisk.
+
+        Args:
+            src_dir: a source dir containing the files to copy from.
+            files: a list of files or src_file:dst_file pairs to copy from
+              src_dir to the current ramdisk.
+        """
+        # Creates missing parent dirs with 0o755.
+        original_mask = os.umask(0o022)
+        for f in files:
+            if ':' in f:
+                src_file = os.path.join(src_dir, f.split(':')[0])
+                dst_file = os.path.join(self.ramdisk_dir, f.split(':')[1])
+            else:
+                src_file = os.path.join(src_dir, f)
+                dst_file = os.path.join(self.ramdisk_dir, f)
+
+            dst_dir = os.path.dirname(dst_file)
+            if not os.path.exists(dst_dir):
+                print("Creating dir '{}'".format(dst_dir))
+                os.makedirs(dst_dir, 0o755)
+            print("Copying file '{}' into '{}'".format(src_file, dst_file))
+            shutil.copy2(src_file, dst_file)
+        os.umask(original_mask)
+
+    @property
+    def ramdisk_dir(self):
+        """Returns the internal ramdisk dir."""
+        return self._ramdisk.ramdisk_dir
+
+
+def _get_repack_usage():
+    return """Usage examples:
+
+  * --ramdisk_add
+
+    Specifies a list of files or src_file:dst_file pairs to copy from
+    --src_bootimg's ramdisk into --dst_bootimg's ramdisk.
+
+    $ repack_bootimg \\
+        --src_bootimg boot-debug-5.4.img --dst_bootimg vendor_boot-debug.img \\
+        --ramdisk_add first_stage_ramdisk/userdebug_plat_sepolicy.cil:userdebug_plat_sepolicy.cil
+
+    The above command copies '/first_stage_ramdisk/userdebug_plat_sepolicy.cil'
+    from --src_bootimg's ramdisk to '/userdebug_plat_sepolicy.cil' of
+    --dst_bootimg's ramdisk, then repacks the --dst_bootimg.
+
+    $ repack_bootimg \\
+        --src_bootimg boot-debug-5.4.img --dst_bootimg vendor_boot-debug.img \\
+        --ramdisk_add first_stage_ramdisk/userdebug_plat_sepolicy.cil
+
+    This is similar to the previous example, but the source file path and
+    destination file path are the same:
+        '/first_stage_ramdisk/userdebug_plat_sepolicy.cil'.
+
+    We can also combine both usage together with a list of copy instructions.
+    For example:
+
+    $ repack_bootimg \\
+        --src_bootimg boot-debug-5.4.img --dst_bootimg vendor_boot-debug.img \\
+        --ramdisk_add file1 file2:/subdir/file2 file3
+"""
+
+
+def _parse_args():
+    """Parse command-line options."""
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        description='Repacks boot, recovery or vendor_boot image by importing'
+                    'ramdisk files from --src_bootimg to --dst_bootimg.',
+        epilog=_get_repack_usage(),
+    )
+
+    parser.add_argument(
+        '--src_bootimg', help='filename to source boot image',
+        type=str, required=True)
+    parser.add_argument(
+        '--dst_bootimg', help='filename to destination boot image',
+        type=str, required=True)
+    parser.add_argument(
+        '--ramdisk_add', nargs='+',
+        help='a list of files or src_file:dst_file pairs to add into '
+             'the ramdisk',
+        default=['userdebug_plat_sepolicy.cil']
+    )
+
+    return parser.parse_args()
+
+
+def main():
+    """Parse arguments and repack boot image."""
+    args = _parse_args()
+    src_bootimg = BootImage(args.src_bootimg)
+    dst_bootimg = BootImage(args.dst_bootimg)
+    dst_bootimg.add_files(src_bootimg.ramdisk_dir, args.ramdisk_add)
+    dst_bootimg.repack_bootimg()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/data/testkey_rsa2048.pem b/tests/data/testkey_rsa2048.pem
new file mode 100644
index 0000000..867dcff
--- /dev/null
+++ b/tests/data/testkey_rsa2048.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAxlVR3TIkouAOvH79vaJTgFhpfvVKQIeVkFRZPVXK/zY0Gvrh
+4JAqGjJoW/PfrQv5sdD36qtHH3a+G5hLZ6Ni+t/mtfjucxZfuLGC3kmJ1T3XqEKZ
+gXXI2IR7vVSoImREvDQGEDyJwtHzLANlkbGg0cghVhWZSCAndO8BenalC2v94/rt
+DfkPekH6dgU3Sf40T0sBSeSY94mOzTaqOR2pfV1rWlLRdWmo33zeHBv52Rlbt0dM
+uXAureXWiHztkm5GCBC1dgM+CaxNtizNEgC91KcD0xuRCCM2WxH+r1lpszyIJDct
+YbrFmVEYl/kjQpafhy7Nsk1fqSTyRdriZSYmTQIDAQABAoIBAQC+kJgaCuX8wYAn
+SXWQ0fmdZlXnMNRpcF0a0pD0SAzGb1RdYBXMaXiqtyhiwc53PPxsCDdNecjayIMd
+jJVXPTwLhTruOgMS/bp3gcgWwV34UHV4LJXGOGAE+jbS0hbDBMiudOYmj6RmVshp
+z9G1zZCSQNMXHaWsEYkX59XpzzoB384nRul2QgEtwzUNR9XlpzgtJBLk3SACkvsN
+mQ/DW8IWHXLg8vLn1LzVJ2e3B16H4MoE2TCHxqfMgr03IDRRJogkenQuQsFhevYT
+o/mJyHSWavVgzMHG9I5m+eepF4Wyhj1Y4WyKAuMI+9dHAX/h7Lt8XFCQCh5DbkVG
+zGr34sWBAoGBAOs7n7YZqNaaguovfIdRRsxxZr1yJAyDsr6w3yGImDZYju4c4WY9
+5esO2kP3FA4p0c7FhQF5oOb1rBuHEPp36cpL4aGeK87caqTfq63WZAujoTZpr9Lp
+BRbkL7w/xG7jpQ/clpA8sHzHGQs/nelxoOtC7E118FiRgvD/jdhlMyL9AoGBANfX
+vyoN1pplfT2xR8QOjSZ+Q35S/+SAtMuBnHx3l0qH2bbBjcvM1MNDWjnRDyaYhiRu
+i+KA7tqfib09+XpB3g5D6Ov7ls/Ldx0S/VcmVWtia2HK8y8iLGtokoBZKQ5AaFX2
+iQU8+tC4h69GnJYQKqNwgCUzh8+gHX5Y46oDiTmRAoGAYpOx8lX+czB8/Da6MNrW
+mIZNT8atZLEsDs2ANEVRxDSIcTCZJId7+m1W+nRoaycLTWNowZ1+2ErLvR10+AGY
+b7Ys79Wg9idYaY9yGn9lnZsMzAiuLeyIvXcSqgjvAKlVWrhOQFOughvNWvFl85Yy
+oWSCMlPiTLtt7CCsCKsgKuECgYBgdIp6GZsIfkgclKe0hqgvRoeU4TR3gcjJlM9A
+lBTo+pKhaBectplx9RxR8AnsPobbqwcaHnIfAuKDzjk5mEvKZjClnFXF4HAHbyAF
+nRzZEy9XkWFhc80T5rRpZO7C7qdxmu2aiKixM3V3L3/0U58qULEDbubHMw9bEhAT
+PudI8QKBgHEEiMm/hr9T41hbQi/LYanWnlFw1ue+osKuF8bXQuxnnHNuFT/c+9/A
+vWhgqG6bOEHu+p/IPrYm4tBMYlwsyh4nXCyGgDJLbLIfzKwKAWCtH9LwnyDVhOow
+GH9shdR+sW3Ew97xef02KAH4VlNANEmBV4sQNqWWvsYrcFm2rOdL
+-----END RSA PRIVATE KEY-----
diff --git a/tests/mkbootimg_test.py b/tests/mkbootimg_test.py
new file mode 100644
index 0000000..ae5cf6b
--- /dev/null
+++ b/tests/mkbootimg_test.py
@@ -0,0 +1,735 @@
+#!/usr/bin/env python3
+#
+# Copyright 2020, 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.
+
+"""Tests mkbootimg and unpack_bootimg."""
+
+import filecmp
+import logging
+import os
+import random
+import shlex
+import subprocess
+import sys
+import tempfile
+import unittest
+
+BOOT_ARGS_OFFSET = 64
+BOOT_ARGS_SIZE = 512
+BOOT_EXTRA_ARGS_OFFSET = 608
+BOOT_EXTRA_ARGS_SIZE = 1024
+BOOT_V3_ARGS_OFFSET = 44
+VENDOR_BOOT_ARGS_OFFSET = 28
+VENDOR_BOOT_ARGS_SIZE = 2048
+
+BOOT_IMAGE_V4_SIGNATURE_SIZE = 4096
+
+TEST_KERNEL_CMDLINE = (
+    'printk.devkmsg=on firmware_class.path=/vendor/etc/ init=/init '
+    'kfence.sample_interval=500 loop.max_part=7 bootconfig'
+)
+
+
+def generate_test_file(pathname, size, seed=None):
+    """Generates a gibberish-filled test file and returns its pathname."""
+    random.seed(os.path.basename(pathname) if seed is None else seed)
+    with open(pathname, 'wb') as f:
+        f.write(random.randbytes(size))
+    return pathname
+
+
+def subsequence_of(list1, list2):
+    """Returns True if list1 is a subsequence of list2.
+
+    >>> subsequence_of([], [1])
+    True
+    >>> subsequence_of([2, 4], [1, 2, 3, 4])
+    True
+    >>> subsequence_of([1, 2, 2], [1, 2, 3])
+    False
+    """
+    if len(list1) == 0:
+        return True
+    if len(list2) == 0:
+        return False
+    if list1[0] == list2[0]:
+        return subsequence_of(list1[1:], list2[1:])
+    return subsequence_of(list1, list2[1:])
+
+
+class MkbootimgTest(unittest.TestCase):
+    """Tests the functionalities of mkbootimg and unpack_bootimg."""
+
+    def setUp(self):
+        # Saves the test executable directory so that relative path references
+        # to test dependencies don't rely on being manually run from the
+        # executable directory.
+        # With this, we can just open "./tests/data/testkey_rsa2048.pem" in the
+        # following tests with subprocess.run(..., cwd=self._exec_dir, ...).
+        self._exec_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
+
+        self._avbtool_path = os.path.join(self._exec_dir, 'avbtool')
+
+        # Set self.maxDiff to None to see full diff in assertion.
+        # C0103: invalid-name for maxDiff.
+        self.maxDiff = None  # pylint: disable=C0103
+
+    def _test_boot_image_v4_signature(self, avbtool_path):
+        """Tests the boot_signature in boot.img v4."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            kernel = generate_test_file(os.path.join(temp_out_dir, 'kernel'),
+                                        0x1000)
+            ramdisk = generate_test_file(os.path.join(temp_out_dir, 'ramdisk'),
+                                         0x1000)
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '4',
+                '--kernel', kernel,
+                '--ramdisk', ramdisk,
+                '--cmdline', TEST_KERNEL_CMDLINE,
+                '--os_version', '11.0.0',
+                '--os_patch_level', '2021-01',
+                '--gki_signing_algorithm', 'SHA256_RSA2048',
+                '--gki_signing_key', './tests/data/testkey_rsa2048.pem',
+                '--gki_signing_signature_args',
+                '--prop foo:bar --prop gki:nice',
+                '--output', boot_img,
+            ]
+
+            if avbtool_path:
+                mkbootimg_cmds.extend(
+                    ['--gki_signing_avbtool_path', avbtool_path])
+
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+            ]
+
+            # cwd=self._exec_dir is required to read
+            # ./tests/data/testkey_rsa2048.pem for --gki_signing_key.
+            subprocess.run(mkbootimg_cmds, check=True, cwd=self._exec_dir)
+            subprocess.run(unpack_bootimg_cmds, check=True)
+
+            # Checks the content of the boot signature.
+            expected_boot_signature_info = (
+                'Minimum libavb version:   1.0\n'
+                'Header Block:             256 bytes\n'
+                'Authentication Block:     320 bytes\n'
+                'Auxiliary Block:          832 bytes\n'
+                'Public key (sha1):        '
+                'cdbb77177f731920bbe0a0f94f84d9038ae0617d\n'
+                'Algorithm:                SHA256_RSA2048\n'
+                'Rollback Index:           0\n'
+                'Flags:                    0\n'
+                'Rollback Index Location:  0\n'
+                "Release String:           'avbtool 1.2.0'\n"
+                'Descriptors:\n'
+                '    Hash descriptor:\n'
+                '      Image Size:            12288 bytes\n'
+                '      Hash Algorithm:        sha256\n'
+                '      Partition Name:        boot\n'
+                '      Salt:                  d00df00d\n'
+                '      Digest:                '
+                'cf3755630856f23ab70e501900050fee'
+                'f30b633b3e82a9085a578617e344f9c7\n'
+                '      Flags:                 0\n'
+                "    Prop: foo -> 'bar'\n"
+                "    Prop: gki -> 'nice'\n"
+            )
+
+            avbtool_info_cmds = [
+                # use avbtool_path if it is not None.
+                avbtool_path or 'avbtool',
+                'info_image', '--image',
+                os.path.join(temp_out_dir, 'out', 'boot_signature')
+            ]
+            result = subprocess.run(avbtool_info_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+
+            self.assertEqual(result.stdout, expected_boot_signature_info)
+
+    def test_boot_image_v4_signature_without_avbtool_path(self):
+        """Boot signature generation without --gki_signing_avbtool_path."""
+        self._test_boot_image_v4_signature(avbtool_path=None)
+
+    def test_boot_image_v4_signature_with_avbtool_path(self):
+        """Boot signature generation with --gki_signing_avbtool_path."""
+        self._test_boot_image_v4_signature(avbtool_path=self._avbtool_path)
+
+    def test_boot_image_v4_signature_exceed_size(self):
+        """Tests the boot signature size exceeded in a boot image version 4."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            kernel = generate_test_file(os.path.join(temp_out_dir, 'kernel'),
+                                        0x1000)
+            ramdisk = generate_test_file(os.path.join(temp_out_dir, 'ramdisk'),
+                                         0x1000)
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '4',
+                '--kernel', kernel,
+                '--ramdisk', ramdisk,
+                '--cmdline', TEST_KERNEL_CMDLINE,
+                '--os_version', '11.0.0',
+                '--os_patch_level', '2021-01',
+                '--gki_signing_avbtool_path', self._avbtool_path,
+                '--gki_signing_algorithm', 'SHA256_RSA2048',
+                '--gki_signing_key', './tests/data/testkey_rsa2048.pem',
+                '--gki_signing_signature_args',
+                # Makes it exceed the signature max size.
+                '--prop foo:bar --prop gki:nice ' * 64,
+                '--output', boot_img,
+            ]
+
+            # cwd=self._exec_dir is required to read
+            # ./tests/data/testkey_rsa2048.pem for --gki_signing_key.
+            try:
+                subprocess.run(mkbootimg_cmds, check=True, capture_output=True,
+                               cwd=self._exec_dir, encoding='utf-8')
+                self.fail('Exceeding signature size assertion is not raised')
+            except subprocess.CalledProcessError as e:
+                self.assertIn('ValueError: boot sigature size is > 4096',
+                              e.stderr)
+
+    def test_boot_image_v4_signature_zeros(self):
+        """Tests no boot signature in a boot image version 4."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            kernel = generate_test_file(os.path.join(temp_out_dir, 'kernel'),
+                                        0x1000)
+            ramdisk = generate_test_file(os.path.join(temp_out_dir, 'ramdisk'),
+                                         0x1000)
+
+            # The boot signature will be zeros if no
+            # --gki_signing_[algorithm|key] is provided.
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '4',
+                '--kernel', kernel,
+                '--ramdisk', ramdisk,
+                '--cmdline', TEST_KERNEL_CMDLINE,
+                '--os_version', '11.0.0',
+                '--os_patch_level', '2021-01',
+                '--output', boot_img,
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            subprocess.run(unpack_bootimg_cmds, check=True)
+
+            boot_signature = os.path.join(
+                temp_out_dir, 'out', 'boot_signature')
+            with open(boot_signature) as f:
+                zeros = '\x00' * BOOT_IMAGE_V4_SIGNATURE_SIZE
+                self.assertEqual(f.read(), zeros)
+
+    def test_vendor_boot_v4(self):
+        """Tests vendor_boot version 4."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            vendor_boot_img = os.path.join(temp_out_dir, 'vendor_boot.img')
+            dtb = generate_test_file(os.path.join(temp_out_dir, 'dtb'), 0x1000)
+            ramdisk1 = generate_test_file(
+                os.path.join(temp_out_dir, 'ramdisk1'), 0x1000)
+            ramdisk2 = generate_test_file(
+                os.path.join(temp_out_dir, 'ramdisk2'), 0x2000)
+            bootconfig = generate_test_file(
+                os.path.join(temp_out_dir, 'bootconfig'), 0x1000)
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '4',
+                '--vendor_boot', vendor_boot_img,
+                '--dtb', dtb,
+                '--vendor_ramdisk', ramdisk1,
+                '--ramdisk_type', 'PLATFORM',
+                '--ramdisk_name', 'RAMDISK1',
+                '--vendor_ramdisk_fragment', ramdisk1,
+                '--ramdisk_type', 'DLKM',
+                '--ramdisk_name', 'RAMDISK2',
+                '--board_id0', '0xC0FFEE',
+                '--board_id15', '0x15151515',
+                '--vendor_ramdisk_fragment', ramdisk2,
+                '--vendor_cmdline', TEST_KERNEL_CMDLINE,
+                '--vendor_bootconfig', bootconfig,
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', vendor_boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+            ]
+            expected_output = [
+                'boot magic: VNDRBOOT',
+                'vendor boot image header version: 4',
+                'vendor ramdisk total size: 16384',
+                f'vendor command line args: {TEST_KERNEL_CMDLINE}',
+                'dtb size: 4096',
+                'vendor ramdisk table size: 324',
+                'size: 4096', 'offset: 0', 'type: 0x1', 'name:',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                'size: 4096', 'offset: 4096', 'type: 0x1', 'name: RAMDISK1',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                'size: 8192', 'offset: 8192', 'type: 0x3', 'name: RAMDISK2',
+                '0x00c0ffee, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x00000000,',
+                '0x00000000, 0x00000000, 0x00000000, 0x15151515,',
+                'vendor bootconfig size: 4096',
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            output = [line.strip() for line in result.stdout.splitlines()]
+            if not subsequence_of(expected_output, output):
+                msg = '\n'.join([
+                    'Unexpected unpack_bootimg output:',
+                    'Expected:',
+                    ' ' + '\n '.join(expected_output),
+                    '',
+                    'Actual:',
+                    ' ' + '\n '.join(output),
+                ])
+                self.fail(msg)
+
+    def test_unpack_vendor_boot_image_v4(self):
+        """Tests that mkbootimg(unpack_bootimg(image)) is an identity."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            vendor_boot_img = os.path.join(temp_out_dir, 'vendor_boot.img')
+            vendor_boot_img_reconstructed = os.path.join(
+                temp_out_dir, 'vendor_boot.img.reconstructed')
+            dtb = generate_test_file(os.path.join(temp_out_dir, 'dtb'), 0x1000)
+            ramdisk1 = generate_test_file(
+                os.path.join(temp_out_dir, 'ramdisk1'), 0x121212)
+            ramdisk2 = generate_test_file(
+                os.path.join(temp_out_dir, 'ramdisk2'), 0x212121)
+            bootconfig = generate_test_file(
+                os.path.join(temp_out_dir, 'bootconfig'), 0x1000)
+
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '4',
+                '--vendor_boot', vendor_boot_img,
+                '--dtb', dtb,
+                '--vendor_ramdisk', ramdisk1,
+                '--ramdisk_type', 'PLATFORM',
+                '--ramdisk_name', 'RAMDISK1',
+                '--vendor_ramdisk_fragment', ramdisk1,
+                '--ramdisk_type', 'DLKM',
+                '--ramdisk_name', 'RAMDISK2',
+                '--board_id0', '0xC0FFEE',
+                '--board_id15', '0x15151515',
+                '--vendor_ramdisk_fragment', ramdisk2,
+                '--vendor_cmdline', TEST_KERNEL_CMDLINE,
+                '--vendor_bootconfig', bootconfig,
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', vendor_boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+                '--format=mkbootimg',
+            ]
+            subprocess.run(mkbootimg_cmds, check=True)
+            result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--vendor_boot', vendor_boot_img_reconstructed,
+            ]
+            unpack_format_args = shlex.split(result.stdout)
+            mkbootimg_cmds.extend(unpack_format_args)
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            self.assertTrue(
+                filecmp.cmp(vendor_boot_img, vendor_boot_img_reconstructed),
+                'reconstructed vendor_boot image differ from the original')
+
+            # Also check that -0, --null are as expected.
+            unpack_bootimg_cmds.append('--null')
+            result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            unpack_format_null_args = result.stdout
+            self.assertEqual('\0'.join(unpack_format_args) + '\0',
+                             unpack_format_null_args)
+
+    def test_unpack_vendor_boot_image_v3(self):
+        """Tests that mkbootimg(unpack_bootimg(image)) is an identity."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            vendor_boot_img = os.path.join(temp_out_dir, 'vendor_boot.img')
+            vendor_boot_img_reconstructed = os.path.join(
+                temp_out_dir, 'vendor_boot.img.reconstructed')
+            dtb = generate_test_file(os.path.join(temp_out_dir, 'dtb'), 0x1000)
+            ramdisk = generate_test_file(os.path.join(temp_out_dir, 'ramdisk'),
+                                         0x121212)
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '3',
+                '--vendor_boot', vendor_boot_img,
+                '--vendor_ramdisk', ramdisk,
+                '--dtb', dtb,
+                '--vendor_cmdline', TEST_KERNEL_CMDLINE,
+                '--board', 'product_name',
+                '--base', '0x00000000',
+                '--dtb_offset', '0x01f00000',
+                '--kernel_offset', '0x00008000',
+                '--pagesize', '0x00001000',
+                '--ramdisk_offset', '0x01000000',
+                '--tags_offset', '0x00000100',
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', vendor_boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+                '--format=mkbootimg',
+            ]
+            subprocess.run(mkbootimg_cmds, check=True)
+            result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--vendor_boot', vendor_boot_img_reconstructed,
+            ]
+            mkbootimg_cmds.extend(shlex.split(result.stdout))
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            self.assertTrue(
+                filecmp.cmp(vendor_boot_img, vendor_boot_img_reconstructed),
+                'reconstructed vendor_boot image differ from the original')
+
+    def test_unpack_boot_image_v3(self):
+        """Tests that mkbootimg(unpack_bootimg(image)) is an identity."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            boot_img_reconstructed = os.path.join(
+                temp_out_dir, 'boot.img.reconstructed')
+            kernel = generate_test_file(os.path.join(temp_out_dir, 'kernel'),
+                                        0x1000)
+            ramdisk = generate_test_file(os.path.join(temp_out_dir, 'ramdisk'),
+                                         0x1000)
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '3',
+                '--kernel', kernel,
+                '--ramdisk', ramdisk,
+                '--cmdline', TEST_KERNEL_CMDLINE,
+                '--os_version', '11.0.0',
+                '--os_patch_level', '2021-01',
+                '--output', boot_img,
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+                '--format=mkbootimg',
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--out', boot_img_reconstructed,
+            ]
+            mkbootimg_cmds.extend(shlex.split(result.stdout))
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            self.assertTrue(
+                filecmp.cmp(boot_img, boot_img_reconstructed),
+                'reconstructed boot image differ from the original')
+
+    def test_unpack_boot_image_v2(self):
+        """Tests that mkbootimg(unpack_bootimg(image)) is an identity."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            # Output image path.
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            boot_img_reconstructed = os.path.join(
+                temp_out_dir, 'boot.img.reconstructed')
+            # Creates blank images first.
+            kernel = generate_test_file(
+                os.path.join(temp_out_dir, 'kernel'), 0x1000)
+            ramdisk = generate_test_file(
+                os.path.join(temp_out_dir, 'ramdisk'), 0x1000)
+            second = generate_test_file(
+                os.path.join(temp_out_dir, 'second'), 0x1000)
+            recovery_dtbo = generate_test_file(
+                os.path.join(temp_out_dir, 'recovery_dtbo'), 0x1000)
+            dtb = generate_test_file(
+                os.path.join(temp_out_dir, 'dtb'), 0x1000)
+
+            cmdline = (BOOT_ARGS_SIZE - 1) * 'x'
+            extra_cmdline = (BOOT_EXTRA_ARGS_SIZE - 1) * 'y'
+
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '2',
+                '--base', '0x00000000',
+                '--kernel', kernel,
+                '--kernel_offset', '0x00008000',
+                '--ramdisk', ramdisk,
+                '--ramdisk_offset', '0x01000000',
+                '--second', second,
+                '--second_offset', '0x40000000',
+                '--recovery_dtbo', recovery_dtbo,
+                '--dtb', dtb,
+                '--dtb_offset', '0x01f00000',
+                '--tags_offset', '0x00000100',
+                '--pagesize', '0x00001000',
+                '--os_version', '11.0.0',
+                '--os_patch_level', '2021-03',
+                '--board', 'boot_v2',
+                '--cmdline', cmdline + extra_cmdline,
+                '--output', boot_img,
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+                '--format=mkbootimg',
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--out', boot_img_reconstructed,
+            ]
+            mkbootimg_cmds.extend(shlex.split(result.stdout))
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            self.assertTrue(
+                filecmp.cmp(boot_img, boot_img_reconstructed),
+                'reconstructed boot image differ from the original')
+
+    def test_unpack_boot_image_v1(self):
+        """Tests that mkbootimg(unpack_bootimg(image)) is an identity."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            # Output image path.
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            boot_img_reconstructed = os.path.join(
+                temp_out_dir, 'boot.img.reconstructed')
+            # Creates blank images first.
+            kernel = generate_test_file(
+                os.path.join(temp_out_dir, 'kernel'), 0x1000)
+            ramdisk = generate_test_file(
+                os.path.join(temp_out_dir, 'ramdisk'), 0x1000)
+            recovery_dtbo = generate_test_file(
+                os.path.join(temp_out_dir, 'recovery_dtbo'), 0x1000)
+
+            cmdline = (BOOT_ARGS_SIZE - 1) * 'x'
+            extra_cmdline = (BOOT_EXTRA_ARGS_SIZE - 1) * 'y'
+
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '1',
+                '--base', '0x00000000',
+                '--kernel', kernel,
+                '--kernel_offset', '0x00008000',
+                '--ramdisk', ramdisk,
+                '--ramdisk_offset', '0x01000000',
+                '--recovery_dtbo', recovery_dtbo,
+                '--tags_offset', '0x00000100',
+                '--pagesize', '0x00001000',
+                '--os_version', '11.0.0',
+                '--os_patch_level', '2021-03',
+                '--board', 'boot_v1',
+                '--cmdline', cmdline + extra_cmdline,
+                '--output', boot_img,
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+                '--format=mkbootimg',
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--out', boot_img_reconstructed,
+            ]
+            mkbootimg_cmds.extend(shlex.split(result.stdout))
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            self.assertTrue(
+                filecmp.cmp(boot_img, boot_img_reconstructed),
+                'reconstructed boot image differ from the original')
+
+    def test_unpack_boot_image_v0(self):
+        """Tests that mkbootimg(unpack_bootimg(image)) is an identity."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            # Output image path.
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            boot_img_reconstructed = os.path.join(
+                temp_out_dir, 'boot.img.reconstructed')
+            # Creates blank images first.
+            kernel = generate_test_file(
+                os.path.join(temp_out_dir, 'kernel'), 0x1000)
+            ramdisk = generate_test_file(
+                os.path.join(temp_out_dir, 'ramdisk'), 0x1000)
+            second = generate_test_file(
+                os.path.join(temp_out_dir, 'second'), 0x1000)
+
+            cmdline = (BOOT_ARGS_SIZE - 1) * 'x'
+            extra_cmdline = (BOOT_EXTRA_ARGS_SIZE - 1) * 'y'
+
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '0',
+                '--base', '0x00000000',
+                '--kernel', kernel,
+                '--kernel_offset', '0x00008000',
+                '--ramdisk', ramdisk,
+                '--ramdisk_offset', '0x01000000',
+                '--second', second,
+                '--second_offset', '0x40000000',
+                '--tags_offset', '0x00000100',
+                '--pagesize', '0x00001000',
+                '--os_version', '11.0.0',
+                '--os_patch_level', '2021-03',
+                '--board', 'boot_v0',
+                '--cmdline', cmdline + extra_cmdline,
+                '--output', boot_img,
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+            ]
+            unpack_bootimg_cmds = [
+                'unpack_bootimg',
+                '--boot_img', boot_img,
+                '--out', os.path.join(temp_out_dir, 'out'),
+                '--format=mkbootimg',
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            result = subprocess.run(unpack_bootimg_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--out', boot_img_reconstructed,
+            ]
+            mkbootimg_cmds.extend(shlex.split(result.stdout))
+
+            subprocess.run(mkbootimg_cmds, check=True)
+            self.assertTrue(
+                filecmp.cmp(boot_img, boot_img_reconstructed),
+                'reconstructed boot image differ from the original')
+
+    def test_boot_image_v2_cmdline_null_terminator(self):
+        """Tests that kernel commandline is null-terminated."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            dtb = generate_test_file(os.path.join(temp_out_dir, 'dtb'), 0x1000)
+            kernel = generate_test_file(os.path.join(temp_out_dir, 'kernel'),
+                                        0x1000)
+            ramdisk = generate_test_file(os.path.join(temp_out_dir, 'ramdisk'),
+                                         0x1000)
+            cmdline = (BOOT_ARGS_SIZE - 1) * 'x'
+            extra_cmdline = (BOOT_EXTRA_ARGS_SIZE - 1) * 'y'
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '2',
+                '--dtb', dtb,
+                '--kernel', kernel,
+                '--ramdisk', ramdisk,
+                '--cmdline', cmdline + extra_cmdline,
+                '--output', boot_img,
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+
+            with open(boot_img, 'rb') as f:
+                raw_boot_img = f.read()
+            raw_cmdline = raw_boot_img[BOOT_ARGS_OFFSET:][:BOOT_ARGS_SIZE]
+            raw_extra_cmdline = (raw_boot_img[BOOT_EXTRA_ARGS_OFFSET:]
+                                 [:BOOT_EXTRA_ARGS_SIZE])
+            self.assertEqual(raw_cmdline, cmdline.encode() + b'\x00')
+            self.assertEqual(raw_extra_cmdline,
+                             extra_cmdline.encode() + b'\x00')
+
+    def test_boot_image_v3_cmdline_null_terminator(self):
+        """Tests that kernel commandline is null-terminated."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            kernel = generate_test_file(os.path.join(temp_out_dir, 'kernel'),
+                                        0x1000)
+            ramdisk = generate_test_file(os.path.join(temp_out_dir, 'ramdisk'),
+                                         0x1000)
+            cmdline = BOOT_ARGS_SIZE * 'x' + (BOOT_EXTRA_ARGS_SIZE - 1) * 'y'
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '3',
+                '--kernel', kernel,
+                '--ramdisk', ramdisk,
+                '--cmdline', cmdline,
+                '--output', boot_img,
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+
+            with open(boot_img, 'rb') as f:
+                raw_boot_img = f.read()
+            raw_cmdline = (raw_boot_img[BOOT_V3_ARGS_OFFSET:]
+                           [:BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE])
+            self.assertEqual(raw_cmdline, cmdline.encode() + b'\x00')
+
+    def test_vendor_boot_image_v3_cmdline_null_terminator(self):
+        """Tests that kernel commandline is null-terminated."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            dtb = generate_test_file(os.path.join(temp_out_dir, 'dtb'), 0x1000)
+            ramdisk = generate_test_file(os.path.join(temp_out_dir, 'ramdisk'),
+                                         0x1000)
+            vendor_cmdline = (VENDOR_BOOT_ARGS_SIZE - 1) * 'x'
+            vendor_boot_img = os.path.join(temp_out_dir, 'vendor_boot.img')
+            mkbootimg_cmds = [
+                'mkbootimg',
+                '--header_version', '3',
+                '--dtb', dtb,
+                '--vendor_ramdisk', ramdisk,
+                '--vendor_cmdline', vendor_cmdline,
+                '--vendor_boot', vendor_boot_img,
+            ]
+
+            subprocess.run(mkbootimg_cmds, check=True)
+
+            with open(vendor_boot_img, 'rb') as f:
+                raw_vendor_boot_img = f.read()
+            raw_vendor_cmdline = (raw_vendor_boot_img[VENDOR_BOOT_ARGS_OFFSET:]
+                                  [:VENDOR_BOOT_ARGS_SIZE])
+            self.assertEqual(raw_vendor_cmdline,
+                             vendor_cmdline.encode() + b'\x00')
+
+
+# I don't know how, but we need both the logger configuration and verbosity
+# level > 2 to make atest work. And yes this line needs to be at the very top
+# level, not even in the "__main__" indentation block.
+logging.basicConfig(stream=sys.stdout)
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/unpack_bootimg.py b/unpack_bootimg.py
index 83c2bbe..2b176e5 100755
--- a/unpack_bootimg.py
+++ b/unpack_bootimg.py
@@ -1,4 +1,5 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
+#
 # Copyright 2018, The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,18 +14,20 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""unpacks the bootimage.
+"""Unpacks the boot image.
 
 Extracts the kernel, ramdisk, second bootloader, dtb and recovery dtbo images.
 """
 
-from __future__ import print_function
-from argparse import ArgumentParser, FileType
+from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
 from struct import unpack
 import os
+import shlex
 
 BOOT_IMAGE_HEADER_V3_PAGESIZE = 4096
-VENDOR_BOOT_IMAGE_HEADER_V3_SIZE = 2112
+VENDOR_RAMDISK_NAME_SIZE = 32
+VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE = 16
+
 
 def create_out_dir(dir_path):
     """creates a directory 'dir_path' if it does not exist"""
@@ -63,182 +66,480 @@
     return '{:04d}-{:02d}'.format(y, m)
 
 
-def print_os_version_patch_level(value):
-    os_version = value >> 11
-    os_patch_level = value & ((1<<11) - 1)
-    print('os version: %s' % format_os_version(os_version))
-    print('os patch level: %s' % format_os_patch_level(os_patch_level))
+def decode_os_version_patch_level(os_version_patch_level):
+    """Returns a tuple of (os_version, os_patch_level)."""
+    os_version = os_version_patch_level >> 11
+    os_patch_level = os_version_patch_level & ((1<<11) - 1)
+    return (format_os_version(os_version),
+            format_os_patch_level(os_patch_level))
 
 
-def unpack_bootimage(args):
+class BootImageInfoFormatter:
+    """Formats the boot image info."""
+
+    def format_pretty_text(self):
+        lines = []
+        lines.append(f'boot magic: {self.boot_magic}')
+
+        if self.header_version < 3:
+            lines.append(f'kernel_size: {self.kernel_size}')
+            lines.append(
+                f'kernel load address: {self.kernel_load_address:#010x}')
+            lines.append(f'ramdisk size: {self.ramdisk_size}')
+            lines.append(
+                f'ramdisk load address: {self.ramdisk_load_address:#010x}')
+            lines.append(f'second bootloader size: {self.second_size}')
+            lines.append(
+                f'second bootloader load address: '
+                f'{self.second_load_address:#010x}')
+            lines.append(
+                f'kernel tags load address: {self.tags_load_address:#010x}')
+            lines.append(f'page size: {self.page_size}')
+        else:
+            lines.append(f'kernel_size: {self.kernel_size}')
+            lines.append(f'ramdisk size: {self.ramdisk_size}')
+
+        lines.append(f'os version: {self.os_version}')
+        lines.append(f'os patch level: {self.os_patch_level}')
+        lines.append(f'boot image header version: {self.header_version}')
+
+        if self.header_version < 3:
+            lines.append(f'product name: {self.product_name}')
+
+        lines.append(f'command line args: {self.cmdline}')
+
+        if self.header_version < 3:
+            lines.append(f'additional command line args: {self.extra_cmdline}')
+
+        if self.header_version in {1, 2}:
+            lines.append(f'recovery dtbo size: {self.recovery_dtbo_size}')
+            lines.append(
+                f'recovery dtbo offset: {self.recovery_dtbo_offset:#018x}')
+            lines.append(f'boot header size: {self.boot_header_size}')
+
+        if self.header_version == 2:
+            lines.append(f'dtb size: {self.dtb_size}')
+            lines.append(f'dtb address: {self.dtb_load_address:#018x}')
+
+        if self.header_version >= 4:
+            lines.append(
+                f'boot.img signature size: {self.boot_signature_size}')
+
+        return '\n'.join(lines)
+
+    def format_mkbootimg_argument(self):
+        args = []
+        args.extend(['--header_version', str(self.header_version)])
+        args.extend(['--os_version', self.os_version])
+        args.extend(['--os_patch_level', self.os_patch_level])
+
+        args.extend(['--kernel', os.path.join(self.image_dir, 'kernel')])
+        args.extend(['--ramdisk', os.path.join(self.image_dir, 'ramdisk')])
+
+        if self.header_version <= 2:
+            if self.second_size > 0:
+                args.extend(['--second',
+                             os.path.join(self.image_dir, 'second')])
+            if self.recovery_dtbo_size > 0:
+                args.extend(['--recovery_dtbo',
+                             os.path.join(self.image_dir, 'recovery_dtbo')])
+            if self.dtb_size > 0:
+                args.extend(['--dtb', os.path.join(self.image_dir, 'dtb')])
+
+            args.extend(['--pagesize', f'{self.page_size:#010x}'])
+
+            # Kernel load address is base + kernel_offset in mkbootimg.py.
+            # However we don't know the value of 'base' when unpacking a boot
+            # image in this script, so we set 'base' to zero and 'kernel_offset'
+            # to the kernel load address, 'ramdisk_offset' to the ramdisk load
+            # address, ... etc.
+            args.extend(['--base', f'{0:#010x}'])
+            args.extend(['--kernel_offset',
+                         f'{self.kernel_load_address:#010x}'])
+            args.extend(['--ramdisk_offset',
+                         f'{self.ramdisk_load_address:#010x}'])
+            args.extend(['--second_offset',
+                         f'{self.second_load_address:#010x}'])
+            args.extend(['--tags_offset', f'{self.tags_load_address:#010x}'])
+
+            # dtb is added in boot image v2, and is absent in v1 or v0.
+            if self.header_version == 2:
+                # dtb_offset is uint64_t.
+                args.extend(['--dtb_offset', f'{self.dtb_load_address:#018x}'])
+
+            args.extend(['--board', self.product_name])
+            args.extend(['--cmdline', self.cmdline + self.extra_cmdline])
+        else:
+            args.extend(['--cmdline', self.cmdline])
+
+        return args
+
+
+def unpack_boot_image(args):
     """extracts kernel, ramdisk, second bootloader and recovery dtbo"""
+    info = BootImageInfoFormatter()
+    info.boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
+
     kernel_ramdisk_second_info = unpack('9I', args.boot_img.read(9 * 4))
-    version = kernel_ramdisk_second_info[8]
-    if version < 3:
-        print('kernel_size: %s' % kernel_ramdisk_second_info[0])
-        print('kernel load address: %#x' % kernel_ramdisk_second_info[1])
-        print('ramdisk size: %s' % kernel_ramdisk_second_info[2])
-        print('ramdisk load address: %#x' % kernel_ramdisk_second_info[3])
-        print('second bootloader size: %s' % kernel_ramdisk_second_info[4])
-        print('second bootloader load address: %#x' % kernel_ramdisk_second_info[5])
-        print('kernel tags load address: %#x' % kernel_ramdisk_second_info[6])
-        print('page size: %s' % kernel_ramdisk_second_info[7])
-        print_os_version_patch_level(unpack('I', args.boot_img.read(1 * 4))[0])
+    # header_version is always at [8] regardless of the value of header_version.
+    info.header_version = kernel_ramdisk_second_info[8]
+
+    if info.header_version < 3:
+        info.kernel_size = kernel_ramdisk_second_info[0]
+        info.kernel_load_address = kernel_ramdisk_second_info[1]
+        info.ramdisk_size = kernel_ramdisk_second_info[2]
+        info.ramdisk_load_address = kernel_ramdisk_second_info[3]
+        info.second_size = kernel_ramdisk_second_info[4]
+        info.second_load_address = kernel_ramdisk_second_info[5]
+        info.tags_load_address = kernel_ramdisk_second_info[6]
+        info.page_size = kernel_ramdisk_second_info[7]
+        os_version_patch_level = unpack('I', args.boot_img.read(1 * 4))[0]
     else:
-        print('kernel_size: %s' % kernel_ramdisk_second_info[0])
-        print('ramdisk size: %s' % kernel_ramdisk_second_info[1])
-        print_os_version_patch_level(kernel_ramdisk_second_info[2])
+        info.kernel_size = kernel_ramdisk_second_info[0]
+        info.ramdisk_size = kernel_ramdisk_second_info[1]
+        os_version_patch_level = kernel_ramdisk_second_info[2]
+        info.second_size = 0
+        info.page_size = BOOT_IMAGE_HEADER_V3_PAGESIZE
 
-    print('boot image header version: %s' % version)
+    info.os_version, info.os_patch_level = decode_os_version_patch_level(
+        os_version_patch_level)
 
-    if version < 3:
-        product_name = cstr(unpack('16s', args.boot_img.read(16))[0].decode())
-        print('product name: %s' % product_name)
-        cmdline = cstr(unpack('512s', args.boot_img.read(512))[0].decode())
-        print('command line args: %s' % cmdline)
-    else:
-        cmdline = cstr(unpack('1536s', args.boot_img.read(1536))[0].decode())
-        print('command line args: %s' % cmdline)
-
-    if version < 3:
+    if info.header_version < 3:
+        info.product_name = cstr(unpack('16s',
+                                        args.boot_img.read(16))[0].decode())
+        info.cmdline = cstr(unpack('512s', args.boot_img.read(512))[0].decode())
         args.boot_img.read(32)  # ignore SHA
-
-    if version < 3:
-        extra_cmdline = cstr(unpack('1024s',
-                                    args.boot_img.read(1024))[0].decode())
-        print('additional command line args: %s' % extra_cmdline)
-
-    if version < 3:
-        kernel_size = kernel_ramdisk_second_info[0]
-        ramdisk_size = kernel_ramdisk_second_info[2]
-        second_size = kernel_ramdisk_second_info[4]
-        page_size = kernel_ramdisk_second_info[7]
+        info.extra_cmdline = cstr(unpack('1024s',
+                                         args.boot_img.read(1024))[0].decode())
     else:
-        kernel_size = kernel_ramdisk_second_info[0]
-        ramdisk_size = kernel_ramdisk_second_info[1]
-        second_size = 0
-        page_size = BOOT_IMAGE_HEADER_V3_PAGESIZE
+        info.cmdline = cstr(unpack('1536s',
+                                   args.boot_img.read(1536))[0].decode())
 
-    if 0 < version < 3:
-        recovery_dtbo_size = unpack('I', args.boot_img.read(1 * 4))[0]
-        print('recovery dtbo size: %s' % recovery_dtbo_size)
-        recovery_dtbo_offset = unpack('Q', args.boot_img.read(8))[0]
-        print('recovery dtbo offset: %#x' % recovery_dtbo_offset)
-        boot_header_size = unpack('I', args.boot_img.read(4))[0]
-        print('boot header size: %s' % boot_header_size)
+    if info.header_version in {1, 2}:
+        info.recovery_dtbo_size = unpack('I', args.boot_img.read(1 * 4))[0]
+        info.recovery_dtbo_offset = unpack('Q', args.boot_img.read(8))[0]
+        info.boot_header_size = unpack('I', args.boot_img.read(4))[0]
     else:
-        recovery_dtbo_size = 0
-    if 1 < version < 3:
-        dtb_size = unpack('I', args.boot_img.read(4))[0]
-        print('dtb size: %s' % dtb_size)
-        dtb_load_address = unpack('Q', args.boot_img.read(8))[0]
-        print('dtb address: %#x' % dtb_load_address)
-    else:
-        dtb_size = 0
+        info.recovery_dtbo_size = 0
 
+    if info.header_version == 2:
+        info.dtb_size = unpack('I', args.boot_img.read(4))[0]
+        info.dtb_load_address = unpack('Q', args.boot_img.read(8))[0]
+    else:
+        info.dtb_size = 0
+        info.dtb_load_address = 0
+
+    if info.header_version >= 4:
+        info.boot_signature_size = unpack('I', args.boot_img.read(4))[0]
+    else:
+        info.boot_signature_size = 0
 
     # The first page contains the boot header
     num_header_pages = 1
 
-    num_kernel_pages = get_number_of_pages(kernel_size, page_size)
-    kernel_offset = page_size * num_header_pages  # header occupies a page
-    image_info_list = [(kernel_offset, kernel_size, 'kernel')]
+    # Convenient shorthand.
+    page_size = info.page_size
 
-    num_ramdisk_pages = get_number_of_pages(ramdisk_size, page_size)
+    num_kernel_pages = get_number_of_pages(info.kernel_size, page_size)
+    kernel_offset = page_size * num_header_pages  # header occupies a page
+    image_info_list = [(kernel_offset, info.kernel_size, 'kernel')]
+
+    num_ramdisk_pages = get_number_of_pages(info.ramdisk_size, page_size)
     ramdisk_offset = page_size * (num_header_pages + num_kernel_pages
                                  ) # header + kernel
-    image_info_list.append((ramdisk_offset, ramdisk_size, 'ramdisk'))
+    image_info_list.append((ramdisk_offset, info.ramdisk_size, 'ramdisk'))
 
-    if second_size > 0:
+    if info.second_size > 0:
         second_offset = page_size * (
             num_header_pages + num_kernel_pages + num_ramdisk_pages
             )  # header + kernel + ramdisk
-        image_info_list.append((second_offset, second_size, 'second'))
+        image_info_list.append((second_offset, info.second_size, 'second'))
 
-    if recovery_dtbo_size > 0:
-        image_info_list.append((recovery_dtbo_offset, recovery_dtbo_size,
+    if info.recovery_dtbo_size > 0:
+        image_info_list.append((info.recovery_dtbo_offset,
+                                info.recovery_dtbo_size,
                                 'recovery_dtbo'))
-    if dtb_size > 0:
-        num_second_pages = get_number_of_pages(second_size, page_size)
-        num_recovery_dtbo_pages = get_number_of_pages(recovery_dtbo_size, page_size)
+    if info.dtb_size > 0:
+        num_second_pages = get_number_of_pages(info.second_size, page_size)
+        num_recovery_dtbo_pages = get_number_of_pages(
+            info.recovery_dtbo_size, page_size)
         dtb_offset = page_size * (
-            num_header_pages + num_kernel_pages + num_ramdisk_pages + num_second_pages +
-            num_recovery_dtbo_pages
-        )
+            num_header_pages + num_kernel_pages + num_ramdisk_pages +
+            num_second_pages + num_recovery_dtbo_pages)
 
-        image_info_list.append((dtb_offset, dtb_size, 'dtb'))
+        image_info_list.append((dtb_offset, info.dtb_size, 'dtb'))
 
-    for image_info in image_info_list:
-        extract_image(image_info[0], image_info[1], args.boot_img,
-                      os.path.join(args.out, image_info[2]))
+    if info.boot_signature_size > 0:
+        # boot signature only exists in boot.img version >= v4.
+        # There are only kernel and ramdisk pages before the signature.
+        boot_signature_offset = page_size * (
+            num_header_pages + num_kernel_pages + num_ramdisk_pages)
+
+        image_info_list.append((boot_signature_offset, info.boot_signature_size,
+                                'boot_signature'))
+
+    create_out_dir(args.out)
+    for offset, size, name in image_info_list:
+        extract_image(offset, size, args.boot_img, os.path.join(args.out, name))
+    info.image_dir = args.out
+
+    return info
 
 
-def unpack_vendor_bootimage(args):
-    kernel_ramdisk_info = unpack('5I', args.boot_img.read(5 * 4))
-    print('vendor boot image header version: %s' % kernel_ramdisk_info[0])
-    print('kernel load address: %#x' % kernel_ramdisk_info[2])
-    print('ramdisk load address: %#x' % kernel_ramdisk_info[3])
-    print('vendor ramdisk size: %s' % kernel_ramdisk_info[4])
+class VendorBootImageInfoFormatter:
+    """Formats the vendor_boot image info."""
 
-    cmdline = cstr(unpack('2048s', args.boot_img.read(2048))[0].decode())
-    print('vendor command line args: %s' % cmdline)
+    def format_pretty_text(self):
+        lines = []
+        lines.append(f'boot magic: {self.boot_magic}')
+        lines.append(f'vendor boot image header version: {self.header_version}')
+        lines.append(f'page size: {self.page_size:#010x}')
+        lines.append(f'kernel load address: {self.kernel_load_address:#010x}')
+        lines.append(f'ramdisk load address: {self.ramdisk_load_address:#010x}')
+        if self.header_version > 3:
+            lines.append(
+                f'vendor ramdisk total size: {self.vendor_ramdisk_size}')
+        else:
+            lines.append(f'vendor ramdisk size: {self.vendor_ramdisk_size}')
+        lines.append(f'vendor command line args: {self.cmdline}')
+        lines.append(
+            f'kernel tags load address: {self.tags_load_address:#010x}')
+        lines.append(f'product name: {self.product_name}')
+        lines.append(f'vendor boot image header size: {self.header_size}')
+        lines.append(f'dtb size: {self.dtb_size}')
+        lines.append(f'dtb address: {self.dtb_load_address:#018x}')
+        if self.header_version > 3:
+            lines.append(
+                f'vendor ramdisk table size: {self.vendor_ramdisk_table_size}')
+            lines.append('vendor ramdisk table: [')
+            indent = lambda level: ' ' * 4 * level
+            for entry in self.vendor_ramdisk_table:
+                (output_ramdisk_name, ramdisk_size, ramdisk_offset,
+                 ramdisk_type, ramdisk_name, board_id) = entry
+                lines.append(indent(1) + f'{output_ramdisk_name}: ''{')
+                lines.append(indent(2) + f'size: {ramdisk_size}')
+                lines.append(indent(2) + f'offset: {ramdisk_offset}')
+                lines.append(indent(2) + f'type: {ramdisk_type:#x}')
+                lines.append(indent(2) + f'name: {ramdisk_name}')
+                lines.append(indent(2) + 'board_id: [')
+                stride = 4
+                for row_idx in range(0, len(board_id), stride):
+                    row = board_id[row_idx:row_idx + stride]
+                    lines.append(
+                        indent(3) + ' '.join(f'{e:#010x},' for e in row))
+                lines.append(indent(2) + ']')
+                lines.append(indent(1) + '}')
+            lines.append(']')
+            lines.append(
+                f'vendor bootconfig size: {self.vendor_bootconfig_size}')
 
-    tags_load_address = unpack('I', args.boot_img.read(1 * 4))[0]
-    print('kernel tags load address: %#x' % tags_load_address)
+        return '\n'.join(lines)
 
-    product_name = cstr(unpack('16s', args.boot_img.read(16))[0].decode())
-    print('product name: %s' % product_name)
+    def format_mkbootimg_argument(self):
+        args = []
+        args.extend(['--header_version', str(self.header_version)])
+        args.extend(['--pagesize', f'{self.page_size:#010x}'])
+        args.extend(['--base', f'{0:#010x}'])
+        args.extend(['--kernel_offset', f'{self.kernel_load_address:#010x}'])
+        args.extend(['--ramdisk_offset', f'{self.ramdisk_load_address:#010x}'])
+        args.extend(['--tags_offset', f'{self.tags_load_address:#010x}'])
+        args.extend(['--dtb_offset', f'{self.dtb_load_address:#018x}'])
+        args.extend(['--vendor_cmdline', self.cmdline])
+        args.extend(['--board', self.product_name])
 
-    dtb_size = unpack('2I', args.boot_img.read(2 * 4))[1]
-    print('dtb size: %s' % dtb_size)
-    dtb_load_address = unpack('Q', args.boot_img.read(8))[0]
-    print('dtb address: %#x' % dtb_load_address)
+        args.extend(['--dtb', os.path.join(self.image_dir, 'dtb')])
 
-    ramdisk_size = kernel_ramdisk_info[4]
-    page_size = kernel_ramdisk_info[1]
+        if self.header_version > 3:
+            args.extend(['--vendor_bootconfig',
+                         os.path.join(self.image_dir, 'bootconfig')])
 
+            for entry in self.vendor_ramdisk_table:
+                (output_ramdisk_name, _, _, ramdisk_type,
+                 ramdisk_name, board_id) = entry
+                args.extend(['--ramdisk_type', str(ramdisk_type)])
+                args.extend(['--ramdisk_name', ramdisk_name])
+                for idx, e in enumerate(board_id):
+                    if e:
+                        args.extend([f'--board_id{idx}', f'{e:#010x}'])
+                vendor_ramdisk_path = os.path.join(
+                    self.image_dir, output_ramdisk_name)
+                args.extend(['--vendor_ramdisk_fragment', vendor_ramdisk_path])
+        else:
+            args.extend(['--vendor_ramdisk',
+                         os.path.join(self.image_dir, 'vendor_ramdisk')])
+
+        return args
+
+
+def unpack_vendor_boot_image(args):
+    info = VendorBootImageInfoFormatter()
+    info.boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
+    info.header_version = unpack('I', args.boot_img.read(4))[0]
+    info.page_size = unpack('I', args.boot_img.read(4))[0]
+    info.kernel_load_address = unpack('I', args.boot_img.read(4))[0]
+    info.ramdisk_load_address = unpack('I', args.boot_img.read(4))[0]
+    info.vendor_ramdisk_size = unpack('I', args.boot_img.read(4))[0]
+    info.cmdline = cstr(unpack('2048s', args.boot_img.read(2048))[0].decode())
+    info.tags_load_address = unpack('I', args.boot_img.read(4))[0]
+    info.product_name = cstr(unpack('16s', args.boot_img.read(16))[0].decode())
+    info.header_size = unpack('I', args.boot_img.read(4))[0]
+    info.dtb_size = unpack('I', args.boot_img.read(4))[0]
+    info.dtb_load_address = unpack('Q', args.boot_img.read(8))[0]
+
+    # Convenient shorthand.
+    page_size = info.page_size
     # The first pages contain the boot header
-    num_boot_header_pages = get_number_of_pages(VENDOR_BOOT_IMAGE_HEADER_V3_SIZE, page_size)
-    num_boot_ramdisk_pages = get_number_of_pages(ramdisk_size, page_size)
-    ramdisk_offset = page_size * num_boot_header_pages
-    image_info_list = [(ramdisk_offset, ramdisk_size, 'vendor_ramdisk')]
+    num_boot_header_pages = get_number_of_pages(info.header_size, page_size)
+    num_boot_ramdisk_pages = get_number_of_pages(
+        info.vendor_ramdisk_size, page_size)
+    num_boot_dtb_pages = get_number_of_pages(info.dtb_size, page_size)
+
+    ramdisk_offset_base = page_size * num_boot_header_pages
+    image_info_list = []
+
+    if info.header_version > 3:
+        info.vendor_ramdisk_table_size = unpack('I', args.boot_img.read(4))[0]
+        vendor_ramdisk_table_entry_num = unpack('I', args.boot_img.read(4))[0]
+        vendor_ramdisk_table_entry_size = unpack('I', args.boot_img.read(4))[0]
+        info.vendor_bootconfig_size = unpack('I', args.boot_img.read(4))[0]
+        num_vendor_ramdisk_table_pages = get_number_of_pages(
+            info.vendor_ramdisk_table_size, page_size)
+        vendor_ramdisk_table_offset = page_size * (
+            num_boot_header_pages + num_boot_ramdisk_pages + num_boot_dtb_pages)
+
+        vendor_ramdisk_table = []
+        vendor_ramdisk_symlinks = []
+        for idx in range(vendor_ramdisk_table_entry_num):
+            entry_offset = vendor_ramdisk_table_offset + (
+                vendor_ramdisk_table_entry_size * idx)
+            args.boot_img.seek(entry_offset)
+            ramdisk_size = unpack('I', args.boot_img.read(4))[0]
+            ramdisk_offset = unpack('I', args.boot_img.read(4))[0]
+            ramdisk_type = unpack('I', args.boot_img.read(4))[0]
+            ramdisk_name = cstr(unpack(
+                f'{VENDOR_RAMDISK_NAME_SIZE}s',
+                args.boot_img.read(VENDOR_RAMDISK_NAME_SIZE))[0].decode())
+            board_id = unpack(
+                f'{VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE}I',
+                args.boot_img.read(
+                    4 * VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE))
+            output_ramdisk_name = f'vendor_ramdisk{idx:02}'
+
+            image_info_list.append((ramdisk_offset_base + ramdisk_offset,
+                                    ramdisk_size, output_ramdisk_name))
+            vendor_ramdisk_symlinks.append((output_ramdisk_name, ramdisk_name))
+            vendor_ramdisk_table.append(
+                (output_ramdisk_name, ramdisk_size, ramdisk_offset,
+                 ramdisk_type, ramdisk_name, board_id))
+
+        info.vendor_ramdisk_table = vendor_ramdisk_table
+
+        bootconfig_offset = page_size * (num_boot_header_pages
+            + num_boot_ramdisk_pages + num_boot_dtb_pages
+            + num_vendor_ramdisk_table_pages)
+        image_info_list.append((bootconfig_offset, info.vendor_bootconfig_size,
+            'bootconfig'))
+    else:
+        image_info_list.append(
+            (ramdisk_offset_base, info.vendor_ramdisk_size, 'vendor_ramdisk'))
 
     dtb_offset = page_size * (num_boot_header_pages + num_boot_ramdisk_pages
                              ) # header + vendor_ramdisk
-    image_info_list.append((dtb_offset, dtb_size, 'dtb'))
+    image_info_list.append((dtb_offset, info.dtb_size, 'dtb'))
 
-    for image_info in image_info_list:
-        extract_image(image_info[0], image_info[1], args.boot_img,
-                      os.path.join(args.out, image_info[2]))
+    create_out_dir(args.out)
+    for offset, size, name in image_info_list:
+        extract_image(offset, size, args.boot_img, os.path.join(args.out, name))
+    info.image_dir = args.out
+
+    if info.header_version > 3:
+        vendor_ramdisk_by_name_dir = os.path.join(
+            args.out, 'vendor-ramdisk-by-name')
+        create_out_dir(vendor_ramdisk_by_name_dir)
+        for src, dst in vendor_ramdisk_symlinks:
+            src_pathname = os.path.join('..', src)
+            dst_pathname = os.path.join(
+                vendor_ramdisk_by_name_dir, f'ramdisk_{dst}')
+            if os.path.lexists(dst_pathname):
+                os.remove(dst_pathname)
+            os.symlink(src_pathname, dst_pathname)
+
+    return info
 
 
 def unpack_image(args):
     boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
-    print('boot_magic: %s' % boot_magic)
-    if boot_magic == "ANDROID!":
-        unpack_bootimage(args)
-    elif boot_magic == "VNDRBOOT":
-        unpack_vendor_bootimage(args)
+    args.boot_img.seek(0)
+    if boot_magic == 'ANDROID!':
+        info = unpack_boot_image(args)
+    elif boot_magic == 'VNDRBOOT':
+        info = unpack_vendor_boot_image(args)
+    else:
+        raise ValueError(f'Not an Android boot image, magic: {boot_magic}')
+
+    if args.format == 'mkbootimg':
+        mkbootimg_args = info.format_mkbootimg_argument()
+        if args.null:
+            print('\0'.join(mkbootimg_args) + '\0', end='')
+        else:
+            print(shlex.join(mkbootimg_args))
+    else:
+        print(info.format_pretty_text())
+
+
+def get_unpack_usage():
+    return """Output format:
+
+  * info
+
+    Pretty-printed info-rich text format suitable for human inspection.
+
+  * mkbootimg
+
+    Output shell-escaped (quoted) argument strings that can be used to
+    reconstruct the boot image. For example:
+
+    $ unpack_bootimg --boot_img vendor_boot.img --out out --format=mkbootimg |
+        tee mkbootimg_args
+    $ sh -c "mkbootimg $(cat mkbootimg_args) --vendor_boot repacked.img"
+
+    vendor_boot.img and repacked.img would be equivalent.
+
+    If the -0 option is specified, output unescaped null-terminated argument
+    strings that are suitable to be parsed by a shell script (xargs -0 format):
+
+    $ unpack_bootimg --boot_img vendor_boot.img --out out --format=mkbootimg \\
+        -0 | tee mkbootimg_args
+    $ declare -a MKBOOTIMG_ARGS=()
+    $ while IFS= read -r -d '' ARG; do
+        MKBOOTIMG_ARGS+=("${ARG}")
+      done <mkbootimg_args
+    $ mkbootimg "${MKBOOTIMG_ARGS[@]}" --vendor_boot repacked.img
+"""
 
 
 def parse_cmdline():
     """parse command line arguments"""
     parser = ArgumentParser(
-        description='Unpacks boot.img/recovery.img, extracts the kernel,'
-        'ramdisk, second bootloader, recovery dtbo and dtb')
-    parser.add_argument(
-        '--boot_img',
-        help='path to boot image',
-        type=FileType('rb'),
-        required=True)
-    parser.add_argument('--out', help='path to out binaries', default='out')
+        formatter_class=RawDescriptionHelpFormatter,
+        description='Unpacks boot, recovery or vendor_boot image.',
+        epilog=get_unpack_usage(),
+    )
+    parser.add_argument('--boot_img', type=FileType('rb'), required=True,
+                        help='path to the boot, recovery or vendor_boot image')
+    parser.add_argument('--out', default='out',
+                        help='output directory of the unpacked images')
+    parser.add_argument('--format', choices=['info', 'mkbootimg'],
+                        default='info',
+                        help='text output format (default: info)')
+    parser.add_argument('-0', '--null', action='store_true',
+                        help='output null-terminated argument strings')
     return parser.parse_args()
 
 
 def main():
     """parse arguments and unpack boot image"""
     args = parse_cmdline()
-    create_out_dir(args.out)
     unpack_image(args)