#!/usr/bin/env python3

# Copyright 2016, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Command-line tool for partitioning Brillo images."""


from __future__ import absolute_import
from __future__ import print_function
import argparse
import bisect
import copy
import functools
import json
import math
import numbers
import os
import struct
import sys
import uuid
import zlib
from six.moves import range

# Keywords used in JSON files.
JSON_KEYWORD_SETTINGS = 'settings'
JSON_KEYWORD_SETTINGS_AB_SUFFIXES = 'ab_suffixes'
JSON_KEYWORD_SETTINGS_DISK_SIZE = 'disk_size'
JSON_KEYWORD_SETTINGS_DISK_ALIGNMENT = 'disk_alignment'
JSON_KEYWORD_SETTINGS_DISK_GUID = 'disk_guid'
JSON_KEYWORD_SETTINGS_PARTITIONS_OFFSET_BEGIN = 'partitions_offset_begin'
JSON_KEYWORD_PARTITIONS = 'partitions'
JSON_KEYWORD_PARTITIONS_LABEL = 'label'
JSON_KEYWORD_PARTITIONS_OFFSET = 'offset'
JSON_KEYWORD_PARTITIONS_SIZE = 'size'
JSON_KEYWORD_PARTITIONS_GROW = 'grow'
JSON_KEYWORD_PARTITIONS_GUID = 'guid'
JSON_KEYWORD_PARTITIONS_TYPE_GUID = 'type_guid'
JSON_KEYWORD_PARTITIONS_FLAGS = 'flags'
JSON_KEYWORD_PARTITIONS_PERSIST = 'persist'
JSON_KEYWORD_PARTITIONS_IGNORE = 'ignore'
JSON_KEYWORD_PARTITIONS_AB = 'ab'
JSON_KEYWORD_PARTITIONS_AB_EXPANDED = 'ab_expanded'
JSON_KEYWORD_PARTITIONS_POSITION = 'position'
JSON_KEYWORD_AUTO = 'auto'

# Possible values for the --type option of the query_partition
# sub-command.
QUERY_PARTITION_TYPES = ['size',
                         'offset',
                         'guid',
                         'type_guid',
                         'flags',
                         'persist']

BPT_VERSION_MAJOR = 1
BPT_VERSION_MINOR = 0

DISK_SECTOR_SIZE = 512

GPT_NUM_LBAS = 33

GPT_MIN_PART_NUM = 1
GPT_MAX_PART_NUM = 128

KNOWN_TYPE_GUIDS = {
    'brillo_boot': 'bb499290-b57e-49f6-bf41-190386693794',
    'brillo_bootloader': '4892aeb3-a45f-4c5f-875f-da3303c0795c',
    'brillo_system': '0f2778c4-5cc1-4300-8670-6c88b7e57ed6',
    'brillo_odm': 'e99d84d7-2c1b-44cf-8c58-effae2dc2558',
    'brillo_oem': 'aa3434b2-ddc3-4065-8b1a-18e99ea15cb7',
    'brillo_userdata': '0bb7e6ed-4424-49c0-9372-7fbab465ab4c',
    'brillo_misc': '6b2378b0-0fbc-4aa9-a4f6-4d6e17281c47',
    'brillo_vbmeta': 'b598858a-5fe3-418e-b8c4-824b41f4adfc',
    'brillo_vendor_specific': '314f99d5-b2bf-4883-8d03-e2f2ce507d6a',
    'linux_fs': '0fc63daf-8483-4772-8e79-3d69d8477de4',
    'ms_basic_data': 'ebd0a0a2-b9e5-4433-87c0-68b6b72699c7',
    'efi_system': 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b'
}


def RoundToMultiple(number, size, round_down=False):
  """Rounds a number up (or down) to nearest multiple of another number.

  Args:
    number: The number to round up.
    size: The multiple to round up to.
    round_down: If True, the number will be rounded down.

  Returns:
    If |number| is a multiple of |size|, returns |number|, otherwise
    returns |number| + |size| - |remainder| (if |round_down| is False) or
    |number| - |remainder| (if |round_down| is True). Always returns
    an integer.
  """
  remainder = number % size
  if remainder == 0:
    return int(number)
  if round_down:
    return int(number - remainder)
  return int(number + size - remainder)


def ParseNumber(arg):
  """Number parser.

  If |arg| is an integer, that value is returned. Otherwise int(arg, 0)
  is returned.

  This function is suitable for use in the |type| parameter of
  |ArgumentParser|'s add_argument() function. An improvement to just
  using type=int is that this function supports numbers in other
  bases, e.g. "0x1234".

  Arguments:
    arg: Argument (int or string) to parse.

  Returns:
    The parsed value, as an integer.

  Raises:
    ValueError: If the argument could not be parsed.
  """
  if isinstance(arg, numbers.Integral):
    return arg
  return int(arg, 0)


def ParseGuid(arg):
  """Parser for RFC 4122 GUIDs.

  Arguments:
    arg: The argument, as a string.

  Returns:
    UUID in hyphenated format.

  Raises:
    ValueError: If the given string cannot be parsed.
  """
  return str(uuid.UUID(arg))


def ParseSize(arg):
  """Parser for size strings with decimal and binary unit support.

  This supports both integers and strings.

  Arguments:
    arg: The string to parse.

  Returns:
    The parsed size in bytes as an integer.

  Raises:
    ValueError: If the given string cannot be parsed.
  """
  if isinstance(arg, numbers.Integral):
    return arg

  ws_index = arg.find(' ')
  if ws_index != -1:
    num = float(arg[0:ws_index])
    factor = 1
    if arg.endswith('KiB'):
      factor = 1024
    elif arg.endswith('MiB'):
      factor = 1024*1024
    elif arg.endswith('GiB'):
      factor = 1024*1024*1024
    elif arg.endswith('TiB'):
      factor = 1024*1024*1024*1024
    elif arg.endswith('PiB'):
      factor = 1024*1024*1024*1024*1024
    elif arg.endswith('kB'):
      factor = 1000
    elif arg.endswith('MB'):
      factor = 1000*1000
    elif arg.endswith('GB'):
      factor = 1000*1000*1000
    elif arg.endswith('TB'):
      factor = 1000*1000*1000*1000
    elif arg.endswith('PB'):
      factor = 1000*1000*1000*1000*1000
    else:
      raise ValueError('Cannot parse string "{}"'.format(arg))
    value = num*factor
    # If the resulting value isn't an integer, round up.
    if not value.is_integer():
      value = int(math.ceil(value))
  else:
    value = int(arg, 0)
  return value


class ImageChunk(object):
  """Data structure used for representing chunks in Android sparse files.

  Attributes:
    chunk_type: One of TYPE_RAW, TYPE_FILL, or TYPE_DONT_CARE.
    chunk_offset: Offset in the sparse file where this chunk begins.
    output_offset: Offset in de-sparsified file where output begins.
    output_size: Number of bytes in output.
    input_offset: Offset in sparse file for data if TYPE_RAW otherwise None.
    fill_data: Blob with data to fill if TYPE_FILL otherwise None.
  """

  FORMAT = '<2H2I'
  TYPE_RAW = 0xcac1
  TYPE_FILL = 0xcac2
  TYPE_DONT_CARE = 0xcac3
  TYPE_CRC32 = 0xcac4

  def __init__(self, chunk_type, chunk_offset, output_offset, output_size,
               input_offset, fill_data):
    """Initializes an ImageChunk object.

    Arguments:
      chunk_type: One of TYPE_RAW, TYPE_FILL, or TYPE_DONT_CARE.
      chunk_offset: Offset in the sparse file where this chunk begins.
      output_offset: Offset in de-sparsified file.
      output_size: Number of bytes in output.
      input_offset: Offset in sparse file if TYPE_RAW otherwise None.
      fill_data: Blob with data to fill if TYPE_FILL otherwise None.

    Raises:
      ValueError: If data is not well-formed.
    """
    self.chunk_type = chunk_type
    self.chunk_offset = chunk_offset
    self.output_offset = output_offset
    self.output_size = output_size
    self.input_offset = input_offset
    self.fill_data = fill_data
    # Check invariants.
    if self.chunk_type == self.TYPE_RAW:
      if self.fill_data is not None:
        raise ValueError('RAW chunk cannot have fill_data set.')
      if not self.input_offset:
        raise ValueError('RAW chunk must have input_offset set.')
    elif self.chunk_type == self.TYPE_FILL:
      if self.fill_data is None:
        raise ValueError('FILL chunk must have fill_data set.')
      if self.input_offset:
        raise ValueError('FILL chunk cannot have input_offset set.')
    elif self.chunk_type == self.TYPE_DONT_CARE:
      if self.fill_data is not None:
        raise ValueError('DONT_CARE chunk cannot have fill_data set.')
      if self.input_offset:
        raise ValueError('DONT_CARE chunk cannot have input_offset set.')
    else:
      raise ValueError('Invalid chunk type')


class ImageHandler(object):
  """Abstraction for image I/O with support for Android sparse images.

  This class provides an interface for working with image files that
  may be using the Android Sparse Image format. When an instance is
  constructed, we test whether it's an Android sparse file. If so,
  operations will be on the sparse file by interpreting the sparse
  format, otherwise they will be directly on the file. Either way the
  operations do the same.

  For reading, this interface mimics a file object - it has seek(),
  tell(), and read() methods. For writing, only truncation
  (truncate()) and appending is supported (append_raw(),
  append_fill(), and append_dont_care()). Additionally, data can only
  be written in units of the block size.

  Attributes:
    is_sparse: Whether the file being operated on is sparse.
    block_size: The block size, typically 4096.
    image_size: The size of the unsparsified file.

  """
  # See system/core/libsparse/sparse_format.h for details.
  MAGIC = 0xed26ff3a
  HEADER_FORMAT = '<I4H4I'

  # These are formats and offset of just the |total_chunks| and
  # |total_blocks| fields.
  NUM_CHUNKS_AND_BLOCKS_FORMAT = '<II'
  NUM_CHUNKS_AND_BLOCKS_OFFSET = 16

  def __init__(self, image_filename):
    """Initializes an image handler.

    Arguments:
      image_filename: The name of the file to operate on.

    Raises:
      ValueError: If data in the file is invalid.
    """
    self._image_filename = image_filename
    self._read_header()

  def _read_header(self):
    """Initializes internal data structures used for reading file.

    This may be called multiple times and is typically called after
    modifying the file (e.g. appending, truncation).

    Raises:
      ValueError: If data in the file is invalid.
    """
    self.is_sparse = False
    self.block_size = 4096
    self._file_pos = 0
    self._image = open(self._image_filename, 'r+b')
    self._image.seek(0, os.SEEK_END)
    self.image_size = self._image.tell()

    self._image.seek(0, os.SEEK_SET)
    header_bin = self._image.read(struct.calcsize(self.HEADER_FORMAT))
    if len(header_bin) < struct.calcsize(self.HEADER_FORMAT):
      # Not a sparse image, our job here is done.
      return
    (magic, major_version, minor_version, file_hdr_sz, chunk_hdr_sz,
     block_size, self._num_total_blocks, self._num_total_chunks,
     _) = struct.unpack(self.HEADER_FORMAT, header_bin)
    if magic != self.MAGIC:
      # Not a sparse image, our job here is done.
      return
    if not (major_version == 1 and minor_version == 0):
      raise ValueError('Encountered sparse image format version {}.{} but '
                       'only 1.0 is supported'.format(major_version,
                                                      minor_version))
    if file_hdr_sz != struct.calcsize(self.HEADER_FORMAT):
      raise ValueError('Unexpected file_hdr_sz value {}.'.
                       format(file_hdr_sz))
    if chunk_hdr_sz != struct.calcsize(ImageChunk.FORMAT):
      raise ValueError('Unexpected chunk_hdr_sz value {}.'.
                       format(chunk_hdr_sz))

    self.block_size = block_size

    # Build an list of chunks by parsing the file.
    self._chunks = []

    # Find the smallest offset where only "Don't care" chunks
    # follow. This will be the size of the content in the sparse
    # image.
    offset = 0
    output_offset = 0
    for _ in range(1, self._num_total_chunks + 1):
      chunk_offset = self._image.tell()

      header_bin = self._image.read(struct.calcsize(ImageChunk.FORMAT))
      (chunk_type, _, chunk_sz, total_sz) = struct.unpack(ImageChunk.FORMAT,
                                                          header_bin)
      data_sz = total_sz - struct.calcsize(ImageChunk.FORMAT)

      if chunk_type == ImageChunk.TYPE_RAW:
        if data_sz != (chunk_sz * self.block_size):
          raise ValueError('Raw chunk input size ({}) does not match output '
                           'size ({})'.
                           format(data_sz, chunk_sz*self.block_size))
        self._chunks.append(ImageChunk(ImageChunk.TYPE_RAW,
                                       chunk_offset,
                                       output_offset,
                                       chunk_sz*self.block_size,
                                       self._image.tell(),
                                       None))
        self._image.read(data_sz)

      elif chunk_type == ImageChunk.TYPE_FILL:
        if data_sz != 4:
          raise ValueError('Fill chunk should have 4 bytes of fill, but this '
                           'has {}'.format(data_sz))
        fill_data = self._image.read(4)
        self._chunks.append(ImageChunk(ImageChunk.TYPE_FILL,
                                       chunk_offset,
                                       output_offset,
                                       chunk_sz*self.block_size,
                                       None,
                                       fill_data))
      elif chunk_type == ImageChunk.TYPE_DONT_CARE:
        if data_sz != 0:
          raise ValueError('Don\'t care chunk input size is non-zero ({})'.
                           format(data_sz))
        self._chunks.append(ImageChunk(ImageChunk.TYPE_DONT_CARE,
                                       chunk_offset,
                                       output_offset,
                                       chunk_sz*self.block_size,
                                       None,
                                       None))
      elif chunk_type == ImageChunk.TYPE_CRC32:
        if data_sz != 4:
          raise ValueError('CRC32 chunk should have 4 bytes of CRC, but '
                           'this has {}'.format(data_sz))
        self._image.read(4)
      else:
        raise ValueError('Unknown chunk type {}'.format(chunk_type))

      offset += chunk_sz
      output_offset += chunk_sz * self.block_size

    # Record where sparse data end.
    self._sparse_end = self._image.tell()

    # Now that we've traversed all chunks, sanity check.
    if self._num_total_blocks != offset:
      raise ValueError('The header said we should have {} output blocks, '
                       'but we saw {}'.format(self._num_total_blocks, offset))
    junk_len = len(self._image.read())
    if junk_len > 0:
      raise ValueError('There were {} bytes of extra data at the end of the '
                       'file.'.format(junk_len))

    # Assign |image_size|.
    self.image_size = output_offset

    # This is used when bisecting in read() to find the initial slice.
    self._chunk_output_offsets = [i.output_offset for i in self._chunks]

    self.is_sparse = True

  def _update_chunks_and_blocks(self):
    """Helper function to update the image header.

    The the |total_chunks| and |total_blocks| fields in the header
    will be set to value of the |_num_total_blocks| and
    |_num_total_chunks| attributes.

    """
    self._image.seek(self.NUM_CHUNKS_AND_BLOCKS_OFFSET, os.SEEK_SET)
    self._image.write(struct.pack(self.NUM_CHUNKS_AND_BLOCKS_FORMAT,
                                  self._num_total_blocks,
                                  self._num_total_chunks))

  def append_dont_care(self, num_bytes):
    """Appends a DONT_CARE chunk to the sparse file.

    The given number of bytes must be a multiple of the block size.

    Arguments:
      num_bytes: Size in number of bytes of the DONT_CARE chunk.
    """
    assert num_bytes % self.block_size == 0

    if not self.is_sparse:
      self._image.seek(0, os.SEEK_END)
      # This is more efficient that writing NUL bytes since it'll add
      # a hole on file systems that support sparse files (native
      # sparse, not Android sparse).
      self._image.truncate(self._image.tell() + num_bytes)
      self._read_header()
      return

    self._num_total_chunks += 1
    self._num_total_blocks += num_bytes // self.block_size
    self._update_chunks_and_blocks()

    self._image.seek(self._sparse_end, os.SEEK_SET)
    self._image.write(struct.pack(ImageChunk.FORMAT,
                                  ImageChunk.TYPE_DONT_CARE,
                                  0,  # Reserved
                                  num_bytes // self.block_size,
                                  struct.calcsize(ImageChunk.FORMAT)))
    self._read_header()

  def append_raw(self, data):
    """Appends a RAW chunk to the sparse file.

    The length of the given data must be a multiple of the block size.

    Arguments:
      data: Data to append.
    """
    assert len(data) % self.block_size == 0

    if not self.is_sparse:
      self._image.seek(0, os.SEEK_END)
      self._image.write(data)
      self._read_header()
      return

    self._num_total_chunks += 1
    self._num_total_blocks += len(data) // self.block_size
    self._update_chunks_and_blocks()

    self._image.seek(self._sparse_end, os.SEEK_SET)
    self._image.write(struct.pack(ImageChunk.FORMAT,
                                  ImageChunk.TYPE_RAW,
                                  0,  # Reserved
                                  len(data) // self.block_size,
                                  len(data) +
                                  struct.calcsize(ImageChunk.FORMAT)))
    self._image.write(data)
    self._read_header()

  def append_fill(self, fill_data, size):
    """Appends a fill chunk to the sparse file.

    The total length of the fill data must be a multiple of the block size.

    Arguments:
      fill_data: Fill data to append - must be four bytes.
      size: Number of chunk - must be a multiple of four and the block size.
    """
    assert len(fill_data) == 4
    assert size % 4 == 0
    assert size % self.block_size == 0

    if not self.is_sparse:
      self._image.seek(0, os.SEEK_END)
      self._image.write(fill_data * (size // 4))
      self._read_header()
      return

    self._num_total_chunks += 1
    self._num_total_blocks += size // self.block_size
    self._update_chunks_and_blocks()

    self._image.seek(self._sparse_end, os.SEEK_SET)
    self._image.write(struct.pack(ImageChunk.FORMAT,
                                  ImageChunk.TYPE_FILL,
                                  0,  # Reserved
                                  size // self.block_size,
                                  4 + struct.calcsize(ImageChunk.FORMAT)))
    self._image.write(fill_data)
    self._read_header()

  def seek(self, offset):
    """Sets the cursor position for reading from unsparsified file.

    Arguments:
      offset: Offset to seek to from the beginning of the file.
    """
    self._file_pos = offset

  def read(self, size):
    """Reads data from the unsparsified file.

    This method may return fewer than |size| bytes of data if the end
    of the file was encountered.

    The file cursor for reading is advanced by the number of bytes
    read.

    Arguments:
      size: Number of bytes to read.

    Returns:
      The data.

    """
    if not self.is_sparse:
      self._image.seek(self._file_pos)
      data = self._image.read(size)
      self._file_pos += len(data)
      return data

    # Iterate over all chunks.
    chunk_idx = bisect.bisect_right(self._chunk_output_offsets,
                                    self._file_pos) - 1
    data = bytearray()
    to_go = size
    while to_go > 0:
      chunk = self._chunks[chunk_idx]
      chunk_pos_offset = self._file_pos - chunk.output_offset
      chunk_pos_to_go = min(chunk.output_size - chunk_pos_offset, to_go)

      if chunk.chunk_type == ImageChunk.TYPE_RAW:
        self._image.seek(chunk.input_offset + chunk_pos_offset)
        data.extend(self._image.read(chunk_pos_to_go))
      elif chunk.chunk_type == ImageChunk.TYPE_FILL:
        all_data = chunk.fill_data*(chunk_pos_to_go // len(chunk.fill_data) + 2)
        offset_mod = chunk_pos_offset % len(chunk.fill_data)
        data.extend(all_data[offset_mod:(offset_mod + chunk_pos_to_go)])
      else:
        assert chunk.chunk_type == ImageChunk.TYPE_DONT_CARE
        data.extend(b'\0' * chunk_pos_to_go)

      to_go -= chunk_pos_to_go
      self._file_pos += chunk_pos_to_go
      chunk_idx += 1
      # Generate partial read in case of EOF.
      if chunk_idx >= len(self._chunks):
        break

    return data

  def tell(self):
    """Returns the file cursor position for reading from unsparsified file.

    Returns:
      The file cursor position for reading.
    """
    return self._file_pos

  def truncate(self, size):
    """Truncates the unsparsified file.

    Arguments:
      size: Desired size of unsparsified file.

    Raises:
      ValueError: If desired size isn't a multiple of the block size.
    """
    if not self.is_sparse:
      self._image.truncate(size)
      self._read_header()
      return

    if size % self.block_size != 0:
      raise ValueError('Cannot truncate to a size which is not a multiple '
                       'of the block size')

    if size == self.image_size:
      # Trivial where there's nothing to do.
      return
    elif size < self.image_size:
      chunk_idx = bisect.bisect_right(self._chunk_output_offsets, size) - 1
      chunk = self._chunks[chunk_idx]
      if chunk.output_offset != size:
        # Truncation in the middle of a trunk - need to keep the chunk
        # and modify it.
        chunk_idx_for_update = chunk_idx + 1
        num_to_keep = size - chunk.output_offset
        assert num_to_keep % self.block_size == 0
        if chunk.chunk_type == ImageChunk.TYPE_RAW:
          truncate_at = (chunk.chunk_offset +
                         struct.calcsize(ImageChunk.FORMAT) + num_to_keep)
          data_sz = num_to_keep
        elif chunk.chunk_type == ImageChunk.TYPE_FILL:
          truncate_at = (chunk.chunk_offset +
                         struct.calcsize(ImageChunk.FORMAT) + 4)
          data_sz = 4
        else:
          assert chunk.chunk_type == ImageChunk.TYPE_DONT_CARE
          truncate_at = chunk.chunk_offset + struct.calcsize(ImageChunk.FORMAT)
          data_sz = 0
        chunk_sz = num_to_keep // self.block_size
        total_sz = data_sz + struct.calcsize(ImageChunk.FORMAT)
        self._image.seek(chunk.chunk_offset)
        self._image.write(struct.pack(ImageChunk.FORMAT,
                                      chunk.chunk_type,
                                      0,  # Reserved
                                      chunk_sz,
                                      total_sz))
        chunk.output_size = num_to_keep
      else:
        # Truncation at trunk boundary.
        truncate_at = chunk.chunk_offset
        chunk_idx_for_update = chunk_idx

      self._num_total_chunks = chunk_idx_for_update
      self._num_total_blocks = 0
      for i in range(0, chunk_idx_for_update):
        self._num_total_blocks += self._chunks[i].output_size // self.block_size
      self._update_chunks_and_blocks()
      self._image.truncate(truncate_at)

      # We've modified the file so re-read all data.
      self._read_header()
    else:
      # Truncating to grow - just add a DONT_CARE section.
      self.append_dont_care(size - self.image_size)


class GuidGenerator(object):
  """An interface for obtaining strings that are GUIDs.

  To facilitate unit testing, this abstraction is used instead of the
  directly using the uuid module.
  """

  def dispense_guid(self, partition_number):
    """Dispenses a GUID.

    Arguments:
      partition_number: The partition number or 0 if requesting a GUID
                        for the whole disk.

    Returns:
      A RFC 4122 compliant GUID, as a string.
    """
    return str(uuid.uuid4())


class Partition(object):
  """Object representing a partition.

  Attributes:
    label: The partition label.
    offset: Offset of the partition on the disk, or None.
    size: Size of the partition or None if not specified.
    grow: True if partition has been requested to use all remaining space.
    guid: Instance GUID (RFC 4122 compliant) as a string or None or 'auto'
          if it should be automatically generated.
    type_guid: Type GUID (RFC 4122 compliant) as a string or a known type
               from the |KNOWN_TYPE_GUIDS| map.
    flags: GUID flags.
    persist: If true, sets bit 0 of flags indicating that this partition should
             not be deleted by the bootloader.
    ab: If True, the partition is an A/B partition.
    ab_expanded: If True, the A/B partitions have been generated.
    ignore: If True, the partition should not be included in the final output.
    position: The requested position of the partition or 0 if it doesn't matter.
  """

  def __init__(self):
    """Initializer method."""
    self.label = ''
    self.offset = None
    self.size = None
    self.grow = False
    self.guid = None
    self.type_guid = None
    self.flags = 0
    self.persist = False
    self.ab = False
    self.ab_expanded = False
    self.ignore = False
    self.position = 0

  def add_info(self, pobj):
    """Add information to partition.

    Arguments:
      pobj: A JSON object with information about the partition.
    """
    self.label = pobj[JSON_KEYWORD_PARTITIONS_LABEL]
    value = pobj.get(JSON_KEYWORD_PARTITIONS_OFFSET)
    if value is not None:
      self.offset = ParseSize(value)
    value = pobj.get(JSON_KEYWORD_PARTITIONS_SIZE)
    if value is not None:
      self.size = ParseSize(value)
    value = pobj.get(JSON_KEYWORD_PARTITIONS_GROW)
    if value is not None:
      self.grow = value
    value = pobj.get(JSON_KEYWORD_PARTITIONS_AB)
    if value is not None:
      self.ab = value
    value = pobj.get(JSON_KEYWORD_PARTITIONS_AB_EXPANDED)
    if value is not None:
      self.ab_expanded = value
    value = pobj.get(JSON_KEYWORD_PARTITIONS_GUID)
    if value is not None:
      self.guid = value
    value = pobj.get(JSON_KEYWORD_PARTITIONS_IGNORE)
    if value is not None:
      self.ignore = value
    value = pobj.get(JSON_KEYWORD_PARTITIONS_TYPE_GUID)
    if value is not None:
      self.type_guid = str.lower(str(value))
      if self.type_guid in KNOWN_TYPE_GUIDS:
        self.type_guid = KNOWN_TYPE_GUIDS[self.type_guid]
    value = pobj.get(JSON_KEYWORD_PARTITIONS_FLAGS)
    if value is not None:
      self.flags = ParseNumber(value)
    value = pobj.get(JSON_KEYWORD_PARTITIONS_PERSIST)
    if value is not None:
      self.persist = value
      if value:
        self.flags = self.flags | 0x1
    value = pobj.get(JSON_KEYWORD_PARTITIONS_POSITION)
    if value is not None:
      self.position = ParseNumber(value)

  def expand_guid(self, guid_generator, partition_number):
    """Assign instance GUID and type GUID if required.

    Arguments:
      guid_generator: A GuidGenerator object.
      partition_number: The partition number, starting from 1.
    """
    if not self.guid or self.guid == JSON_KEYWORD_AUTO:
      self.guid = guid_generator.dispense_guid(partition_number)
    if not self.type_guid:
      self.type_guid = KNOWN_TYPE_GUIDS['brillo_vendor_specific']

  def validate(self):
    """Sanity checks data in object."""

    try:
      _ = uuid.UUID(str(self.guid))
    except ValueError:
      raise ValueError('The string "{}" is not a valid GPT instance GUID on '
                       'partition with label "{}".'.format(
                           str(self.guid), self.label))

    try:
      _ = uuid.UUID(str(self.type_guid))
    except ValueError:
      raise ValueError('The string "{}" is not a valid GPT type GUID on '
                       'partition with label "{}".'.format(
                           str(self.type_guid), self.label))

    if not self.size:
      if not self.grow:
        raise ValueError('Size can only be unset if "grow" is True.')

  def cmp(self, other):
    """Comparison method."""
    self_position = self.position
    if self_position == 0:
      self_position = GPT_MAX_PART_NUM
    other_position = other.position
    if other_position == 0:
      other_position = GPT_MAX_PART_NUM
    if self_position < other_position:
      return -1
    elif self_position > other_position:
      return 1
    else:
      return 0


class Settings(object):
  """An object for holding settings.

  Attributes:
    ab_suffixes: A list of A/B suffixes to use.
    disk_size: An integer with the disk size in bytes.
    partitions_offset_begin: An integer with the disk partitions
                             offset begin size in bytes.
    disk_alignment: The alignment to use for partitions.
    disk_guid: The GUID to use for the disk or None or 'auto' if
               automatically generated.
  """

  def __init__(self):
    """Initializer with defaults."""
    self.ab_suffixes = ['_a', '_b']
    self.disk_size = None
    self.partitions_offset_begin = 0
    self.disk_alignment = 4096
    self.disk_guid = JSON_KEYWORD_AUTO


class BptError(Exception):
  """Application-specific errors.

  These errors represent issues for which a stack-trace should not be
  presented.

  Attributes:
    message: Error message.
  """

  def __init__(self, message):
    Exception.__init__(self, message)


class BptParsingError(BptError):
  """Represents an error with an input file.

  Attributes:
    message: Error message.
    filename: Name of the file that caused an error.
  """

  def __init__(self, filename, message):
    self.filename = filename
    BptError.__init__(self, message)


class Bpt(object):
  """Business logic for bpttool command-line tool."""

  def _read_json(self, input_files, ab_collapse=True):
    """Parses a stack of JSON files into suitable data structures.

    The order of files matters as later files can modify partitions
    declared in earlier files.

    Arguments:
      input_files: An ordered list of open files.
      ab_collapse: If True, collapse A/B partitions.

    Returns:
      A tuple where the first element is a list of Partition objects
      and the second element is a Settings object.

    Raises:
      BptParsingError: If an input file has an error.
    """
    partitions = []
    settings = Settings()

    # Read all input file and merge partitions and settings.
    for f in input_files:
      try:
        obj = json.loads(f.read())
      except ValueError as e:
        # Unfortunately we can't easily get the line number where the
        # error occurred.
        raise BptParsingError(f.name, e.message)

      sobj = obj.get(JSON_KEYWORD_SETTINGS)
      if sobj:
        ab_suffixes = sobj.get(JSON_KEYWORD_SETTINGS_AB_SUFFIXES)
        if ab_suffixes:
          settings.ab_suffixes = ab_suffixes
        disk_size = sobj.get(JSON_KEYWORD_SETTINGS_DISK_SIZE)
        if disk_size:
          settings.disk_size = ParseSize(disk_size)
        partitions_offset_begin = sobj.get(
                JSON_KEYWORD_SETTINGS_PARTITIONS_OFFSET_BEGIN)
        if partitions_offset_begin:
          settings.partitions_offset_begin = ParseSize(partitions_offset_begin)
        disk_alignment = sobj.get(JSON_KEYWORD_SETTINGS_DISK_ALIGNMENT)
        if disk_alignment:
          settings.disk_alignment = ParseSize(disk_alignment)
        disk_guid = sobj.get(JSON_KEYWORD_SETTINGS_DISK_GUID)
        if disk_guid:
          settings.disk_guid = disk_guid

      pobjs = obj.get(JSON_KEYWORD_PARTITIONS)
      if pobjs:
        for pobj in pobjs:
          if ab_collapse and pobj.get(JSON_KEYWORD_PARTITIONS_AB_EXPANDED):
            # If we encounter an expanded partition, unexpand it. This
            # is to make it possible to use output-JSON (from this tool)
            # and stack it with an input-JSON file that e.g. specifies
            # size='256 GiB' for the 'system' partition.
            label = pobj[JSON_KEYWORD_PARTITIONS_LABEL]
            if label.endswith(settings.ab_suffixes[0]):
              # Modify first A/B copy so it doesn't have the trailing suffix.
              new_len = len(label) - len(settings.ab_suffixes[0])
              pobj[JSON_KEYWORD_PARTITIONS_LABEL] = label[0:new_len]
              pobj[JSON_KEYWORD_PARTITIONS_AB_EXPANDED] = False
              pobj[JSON_KEYWORD_PARTITIONS_GUID] = JSON_KEYWORD_AUTO
            else:
              # Skip other A/B copies.
              continue
          # Find or create a partition.
          p = None
          for candidate in partitions:
            if candidate.label == pobj[JSON_KEYWORD_PARTITIONS_LABEL]:
              p = candidate
              break
          if not p:
            p = Partition()
            partitions.append(p)
          p.add_info(pobj)

    return partitions, settings

  def _generate_json(self, partitions, settings):
    """Generate a string with JSON representing partitions and settings.

    Arguments:
      partitions: A list of Partition objects.
      settings: A Settings object.

    Returns:
      A JSON string.
    """
    suffixes_str = '['
    for n in range(0, len(settings.ab_suffixes)):
      if n != 0:
        suffixes_str += ', '
      suffixes_str += '"{}"'.format(settings.ab_suffixes[n])
    suffixes_str += ']'

    ret = ('{{\n'
           '  "' + JSON_KEYWORD_SETTINGS + '": {{\n'
           '    "' + JSON_KEYWORD_SETTINGS_AB_SUFFIXES + '": {},\n'
           '    "' + JSON_KEYWORD_SETTINGS_PARTITIONS_OFFSET_BEGIN + '": {},\n'
           '    "' + JSON_KEYWORD_SETTINGS_DISK_SIZE + '": {},\n'
           '    "' + JSON_KEYWORD_SETTINGS_DISK_ALIGNMENT + '": {},\n'
           '    "' + JSON_KEYWORD_SETTINGS_DISK_GUID + '": "{}"\n'
           '  }},\n'
           '  "' + JSON_KEYWORD_PARTITIONS + '": [\n').format(
               suffixes_str,
               settings.partitions_offset_begin,
               settings.disk_size,
               settings.disk_alignment,
               settings.disk_guid)

    for n in range(0, len(partitions)):
      p = partitions[n]
      ret += ('    {{\n'
              '      "' + JSON_KEYWORD_PARTITIONS_LABEL + '": "{}",\n'
              '      "' + JSON_KEYWORD_PARTITIONS_OFFSET + '": {},\n'
              '      "' + JSON_KEYWORD_PARTITIONS_SIZE + '": {},\n'
              '      "' + JSON_KEYWORD_PARTITIONS_GROW + '": {},\n'
              '      "' + JSON_KEYWORD_PARTITIONS_GUID + '": "{}",\n'
              '      "' + JSON_KEYWORD_PARTITIONS_TYPE_GUID + '": "{}",\n'
              '      "' + JSON_KEYWORD_PARTITIONS_FLAGS + '": "{:#018x}",\n'
              '      "' + JSON_KEYWORD_PARTITIONS_PERSIST + '": {},\n'
              '      "' + JSON_KEYWORD_PARTITIONS_IGNORE + '": {},\n'
              '      "' + JSON_KEYWORD_PARTITIONS_AB + '": {},\n'
              '      "' + JSON_KEYWORD_PARTITIONS_AB_EXPANDED + '": {},\n'
              '      "' + JSON_KEYWORD_PARTITIONS_POSITION + '": {}\n'
              '    }}{}\n').format(p.label,
                                   p.offset,
                                   p.size,
                                   'true' if p.grow else 'false',
                                   p.guid,
                                   p.type_guid,
                                   p.flags,
                                   'true' if p.persist else 'false',
                                   'true' if p.ignore else 'false',
                                   'true' if p.ab else 'false',
                                   'true' if p.ab_expanded else 'false',
                                   p.position,
                                   '' if n == len(partitions) - 1 else ',')
    ret += ('  ]\n'
            '}\n')
    return ret

  def _lba_to_chs(self, lba):
    """Converts LBA to CHS.

    Arguments:
      lba: The sector number to convert.

    Returns:
      An array containing the CHS encoded the way it's expected in a
      MBR partition table.
    """
    # See https://en.wikipedia.org/wiki/Cylinder-head-sector
    num_heads = 255
    num_sectors = 63
    # If LBA isn't going to fit in CHS, return maximum CHS values.
    max_lba = 255*num_heads*num_sectors
    if lba > max_lba:
      return [255, 255, 255]
    c = lba // (num_heads*num_sectors)
    h = (lba // num_sectors) % num_heads
    s = lba % num_sectors
    return [h, (((c>>8) & 0x03)<<6) | (s & 0x3f), c & 0xff]

  def _generate_protective_mbr(self, settings):
    """Generate Protective MBR.

    Arguments:
      settings: A Settings object.

    Returns:
      A string with the binary protective MBR (512 bytes).
    """
    # See https://en.wikipedia.org/wiki/Master_boot_record for MBR layout.
    #
    # The first partition starts at offset 446 (0x1be).
    lba_start = 1
    lba_end = settings.disk_size // DISK_SECTOR_SIZE - 1
    start_chs = self._lba_to_chs(lba_start)
    end_chs = self._lba_to_chs(lba_end)
    pmbr = struct.pack('<446s'     # Bootloader code
                       'B'         # Status.
                       'BBB'       # CHS start.
                       'B'         # Partition type.
                       'BBB'       # CHS end.
                       'I'         # LBA of partition start.
                       'I'         # Number of sectors in partition.
                       '48x'       # Padding to get to offset 510 (0x1fe).
                       'BB',       # Boot signature.
                       b'\xfa\xeb\xfe', # cli ; jmp $ (x86)
                       0x00,
                       start_chs[0], start_chs[1], start_chs[2],
                       0xee,       # MBR Partition Type: GPT protective MBR.
                       end_chs[0], end_chs[1], end_chs[2],
                       1,          # LBA start
                       lba_end,
                       0x55, 0xaa)
    return pmbr

  def _generate_gpt(self, partitions, settings, primary=True):
    """Generate GUID Partition Table.

    Arguments:
      partitions: A list of Partition objects.
      settings: A Settings object.
      primary: True to generate primary GPT, False to generate secondary.

    Returns:
      A string with the binary GUID Partition Table (33*512 bytes).
    """
    # See https://en.wikipedia.org/wiki/Master_boot_record for MBR layout.
    #
    # The first partition starts at offset 446 (0x1be).

    disk_num_lbas = settings.disk_size // DISK_SECTOR_SIZE
    if primary:
      current_lba = 1
      other_lba = disk_num_lbas - 1
      partitions_lba = 2
    else:
      current_lba = disk_num_lbas - 1
      other_lba = 1
      partitions_lba = disk_num_lbas - GPT_NUM_LBAS
    first_usable_lba = GPT_NUM_LBAS + 1
    last_usable_lba = disk_num_lbas - GPT_NUM_LBAS - 1

    part_array = []
    for p in partitions:
      part_array.append(struct.pack(
          '<16s'    # Partition type GUID.
          '16s'     # Partition instance GUID.
          'QQ'      # First and last LBA.
          'Q'       # Flags.
          '72s',    # Name (36 UTF-16LE code units).
          uuid.UUID(p.type_guid).bytes_le,
          uuid.UUID(p.guid).bytes_le,
          p.offset // DISK_SECTOR_SIZE,
          (p.offset + p.size) // DISK_SECTOR_SIZE - 1,
          p.flags,
          p.label.encode(encoding='utf-16le')))

    part_array.append(((128 - len(partitions))*128) * b'\0')
    part_array_bytes = functools.reduce(lambda x, y: x + y, part_array)

    partitions_crc32 = zlib.crc32(part_array_bytes) % (1<<32)

    header_crc32 = 0
    while True:
      header = struct.pack(
          '<8s'    # Signature.
          '4B'     # Version.
          'I'      # Header size.
          'I'      # CRC32 (must be zero during calculation).
          'I'      # Reserved (must be zero).
          'QQ'     # Current and Other LBA.
          'QQ'     # First and last usable LBA.
          '16s'    # Disk GUID.
          'Q'      # Starting LBA of array of partitions.
          'I'      # Number of partitions.
          'I'      # Partition entry size, in bytes.
          'I'      # CRC32 of partition array
          '420x',  # Padding to get to 512 bytes.
          b'EFI PART',
          0x00, 0x00, 0x01, 0x00,
          92,
          header_crc32,
          0x00000000,
          current_lba, other_lba,
          first_usable_lba, last_usable_lba,
          uuid.UUID(settings.disk_guid).bytes_le,
          partitions_lba,
          128,
          128,
          partitions_crc32)
      if header_crc32 != 0:
        break
      header_crc32 = zlib.crc32(header[0:92]) % (1<<32)

    if primary:
      return header + part_array_bytes
    else:
      return part_array_bytes + header

  def _generate_gpt_bin(self, partitions, settings):
    """Generate a bytearray representing partitions and settings.

    The blob will have three partition tables, laid out one after
    another: 1) Protective MBR (512 bytes); 2) Primary GPT (33*512
    bytes); and 3) Secondary GPT (33*512 bytes).

    The total size will be 34,304 bytes.

    Arguments:
      partitions: A list of Partition objects.
      settings: A Settings object.

    Returns:
      A bytearray() object.
    """
    protective_mbr = self._generate_protective_mbr(settings)
    primary_gpt = self._generate_gpt(partitions, settings)
    secondary_gpt = self._generate_gpt(partitions, settings, primary=False)
    ret = protective_mbr + primary_gpt + secondary_gpt
    return ret

  def _validate_disk_partitions(self, partitions, disk_size):
    """Check that a list of partitions have assigned offsets and fits on a
       disk of a given size.

    This function checks partition offsets and sizes to see if they may fit on
    a disk image.

    Arguments:
      partitions: A list of Partition objects.
      settings: Integer size of disk image.

    Raises:
      BptError: If checked condition is not satisfied.
    """
    for p in partitions:
      if not p.offset or p.offset < (GPT_NUM_LBAS + 1)*DISK_SECTOR_SIZE:
        raise BptError('Partition with label "{}" has no offset.'
                       .format(p.label))
      if not p.size or p.size < 0:
        raise BptError('Partition with label "{}" has no size.'
                        .format(p.label))
      if (p.offset + p.size) > (disk_size - GPT_NUM_LBAS*DISK_SECTOR_SIZE):
        raise BptError('Partition with label "{}" exceeds the disk '
                       'image size.'.format(p.label))

  def make_table(self,
                 inputs,
                 ab_suffixes=None,
                 partitions_offset_begin=None,
                 disk_size=None,
                 disk_alignment=None,
                 disk_guid=None,
                 guid_generator=None):
    """Implementation of the 'make_table' command.

    This function takes a list of input partition definition files,
    flattens them, expands A/B partitions, grows partitions, and lays
    out partitions according to alignment constraints.

    Arguments:
      inputs: List of JSON files to parse.
      ab_suffixes: List of the A/B suffixes (as a comma-separated string)
                   to use or None to not override.
      partitions_offset_begin: Size of disk partitions offset
                               begin or None to not override.
      disk_size: Size of disk or None to not override.
      disk_alignment: Disk alignment or None to not override.
      disk_guid: Disk GUID as a string or None to not override.
      guid_generator: A GuidGenerator or None to use the default.

    Returns:
      A tuple where the first argument is a JSON string for the resulting
      partitions and the second argument is the binary partition tables.

    Raises:
      BptParsingError: If an input file has an error.
      BptError: If another application-specific error occurs
    """
    partitions, settings = self._read_json(inputs)

    # Command-line arguments override anything specified in input
    # files.
    if disk_size:
      settings.disk_size = int(math.ceil(disk_size))
    if disk_alignment:
      settings.disk_alignment = int(disk_alignment)
    if partitions_offset_begin:
      settings.partitions_offset_begin = int(partitions_offset_begin)
    if ab_suffixes:
      settings.ab_suffixes = ab_suffixes.split(',')
    if disk_guid:
      settings.disk_guid = disk_guid

    if not guid_generator:
      guid_generator = GuidGenerator()

    # We need to know the disk size. Also round it down to ensure it's
    # a multiple of the sector size.
    if not settings.disk_size:
      raise BptError('Disk size not specified. Use --disk_size option '
                     'or specify it in an input file.\n')
    settings.disk_size = RoundToMultiple(settings.disk_size,
                                         DISK_SECTOR_SIZE,
                                         round_down=True)

    # Alignment must be divisible by disk sector size.
    if settings.disk_alignment % DISK_SECTOR_SIZE != 0:
      raise BptError(
          'Disk alignment size of {} is not divisible by {}.\n'.format(
              settings.disk_alignment, DISK_SECTOR_SIZE))

    if settings.partitions_offset_begin != 0:
      # Disk partitions offset begin size must be
      # divisible by disk sector size.
      if settings.partitions_offset_begin % settings.disk_alignment != 0:
        raise BptError(
            'Disk Partitions offset begin size of {} '
            'is not divisible by {}.\n'.format(
                settings.partitions_offset_begin, settings.disk_alignment))
      settings.partitions_offset_begin = max(settings.partitions_offset_begin,
                                           DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS))
      settings.partitions_offset_begin = RoundToMultiple(
          settings.partitions_offset_begin, settings.disk_alignment)

    # Expand A/B partitions and skip ignored partitions.
    expanded_partitions = []
    for p in partitions:
      if p.ignore:
        continue
      if p.ab and not p.ab_expanded:
        p.ab_expanded = True
        for suffix in settings.ab_suffixes:
          new_p = copy.deepcopy(p)
          new_p.label += suffix
          expanded_partitions.append(new_p)
      else:
        expanded_partitions.append(p)
    partitions = expanded_partitions

    # Expand Disk GUID if needed.
    if not settings.disk_guid or settings.disk_guid == JSON_KEYWORD_AUTO:
      settings.disk_guid = guid_generator.dispense_guid(0)

    # Sort according to 'position' attribute.
    partitions = sorted(partitions, key=functools.cmp_to_key(lambda x, y: x.cmp(y)))

    # Automatically generate GUIDs if the GUID is unset or set to
    # 'auto'. Also validate the rest of the fields.
    part_no = 1
    for p in partitions:
      p.expand_guid(guid_generator, part_no)
      p.validate()
      part_no += 1

    # Idenfify partition to grow and lay out partitions, ignoring the
    # one to grow. This way we can figure out how much space is left.
    #
    # Right now we only support a single 'grow' partition but we could
    # support more in the future by splitting up the available bytes
    # between them.
    grow_part = None
    # offset minimal size: DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS)
    offset = max(settings.partitions_offset_begin,
                 DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS))
    for p in partitions:
      if p.grow:
        if grow_part:
          raise BptError('Only a single partition can be automatically '
                         'grown.\n')
        grow_part = p
      else:
        # Ensure size is a multiple of DISK_SECTOR_SIZE by rounding up
        # (user may specify it as e.g. "1.5 GB" which is not divisible
        # by 512).
        p.size = RoundToMultiple(p.size, DISK_SECTOR_SIZE)
        # Align offset to disk alignment.
        offset = RoundToMultiple(offset, settings.disk_alignment)
        offset += p.size

    # After laying out (respecting alignment) all non-grow
    # partitions, check that the given disk size is big enough.
    if offset > settings.disk_size - DISK_SECTOR_SIZE*GPT_NUM_LBAS:
      raise BptError('Disk size of {} bytes is too small for partitions '
                     'totaling {} bytes.\n'.format(
                         settings.disk_size, offset))

    # If we have a grow partition, it'll starts at the next
    # available alignment offset and we can calculate its size as
    # follows.
    if grow_part:
      offset = RoundToMultiple(offset, settings.disk_alignment)
      grow_part.size = RoundToMultiple(
          settings.disk_size - DISK_SECTOR_SIZE*GPT_NUM_LBAS - offset,
          settings.disk_alignment,
          round_down=True)
      if grow_part.size < DISK_SECTOR_SIZE:
        raise BptError('Not enough space for partition "{}" to be '
                       'automatically grown.\n'.format(grow_part.label))

    # Now we can assign partition start offsets for all partitions,
    # including the grow partition.
    # offset minimal size: DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS)
    offset = max(settings.partitions_offset_begin,
                 DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS))
    for p in partitions:
      # Align offset.
      offset = RoundToMultiple(offset, settings.disk_alignment)
      p.offset = offset
      offset += p.size
    assert offset <= settings.disk_size - DISK_SECTOR_SIZE*GPT_NUM_LBAS

    json_str = self._generate_json(partitions, settings)

    gpt_bin = self._generate_gpt_bin(partitions, settings)

    return json_str, gpt_bin

  def make_disk_image(self, output, bpt, images, allow_empty_partitions=False):
    """Implementation of the 'make_disk_image' command.

    This function takes in a list of partitions images and a bpt file
    for the purpose of creating a raw disk image with a protective MBR,
    primary and secondary GPT, and content for each partition as specified.

    Arguments:
      output: Output file where disk image is to be written to.
      bpt: BPT JSON file to parse.
      images: List of partition image paths to be combined (as specified by
              bpt).  Each element is of the form.
              'PARTITION_NAME:/PATH/TO/PARTITION_IMAGE'
      allow_empty_partitions: If True, partitions defined in |bpt| need not to
                              be present in |images|. Otherwise an exception is
                              thrown if a partition is referenced in |bpt| but
                              not in |images|.

    Raises:
      BptParsingError: If an image file has an error.
      BptError: If another application-specific error occurs.
    """
    # Generate partition list and settings.
    partitions, settings = self._read_json([bpt], ab_collapse=False)

    # Validated partition sizes and offsets.
    self._validate_disk_partitions(partitions, settings.disk_size)

    # Sort according to 'offset' attribute.
    partitions = sorted(partitions, key=functools.cmp_to_key(lambda x, y: x.cmp(y)))

    # Create necessary tables.
    protective_mbr = self._generate_protective_mbr(settings)
    primary_gpt = self._generate_gpt(partitions, settings)
    secondary_gpt = self._generate_gpt(partitions, settings, primary=False)

    # Start at 0 offset for mbr and primary gpt.
    output.seek(0)
    output.write(protective_mbr)
    output.write(primary_gpt)

    # Create mapping of partition name to partition image file.
    image_file_names = {}
    try:
      for name_path in images:
        name, path = name_path.split(":")
        image_file_names[name] = path
    except ValueError as e:
      raise BptParsingError(name_path, 'Bad image argument {}.'.format(
                            images[i]))

    # Read image and insert in correct offset.
    for p in partitions:
      if p.label not in image_file_names:
        if allow_empty_partitions:
          continue
        else:
          raise BptParsingError(bpt.name, 'No content specified for partition'
                                ' with label {}'.format(p.label))

      input_image = ImageHandler(image_file_names[p.label])
      output.seek(p.offset)
      partition_blob = input_image.read(p.size)
      output.write(partition_blob)

    # Put secondary GPT and end of disk.
    output.seek(settings.disk_size - len(secondary_gpt))
    output.write(secondary_gpt)

  def query_partition(self, input_file, part_label, query_type, ab_collapse):
    """Implementation of the 'query_partition' command.

    This reads the partition definition file given by |input_file| and
    returns information of type |query_type| for the partition with
    label |part_label|.

    Arguments:
      input_file: A JSON file to parse.
      part_label: Label of partition to query information about.
      query_type: The information to query, see |QUERY_PARTITION_TYPES|.
      ab_collapse: If True, collapse A/B partitions.

    Returns:
      The requested information as a string or None if there is no
      partition with the given label.

    Raises:
      BptParsingError: If an input file has an error.
      BptError: If another application-specific error occurs
    """

    partitions, _ = self._read_json([input_file], ab_collapse)

    part = None
    for p in partitions:
      if p.label == part_label:
        part = p
        break

    if not part:
      return None

    value = part.__dict__.get(query_type)
    # Print out flags as a hex-value.
    if query_type == 'flags':
      return '{:#018x}'.format(value)
    return str(value)


class BptTool(object):
  """Object for bpttool command-line tool."""

  def __init__(self):
    """Initializer method."""
    self.bpt = Bpt()

  def run(self, argv):
    """Command-line processor.

    Arguments:
      argv: Pass sys.argv from main.
    """
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(title='subcommands')

    sub_parser = subparsers.add_parser(
        'version',
        help='Prints version of bpttool.')
    sub_parser.set_defaults(func=self.version)

    sub_parser = subparsers.add_parser(
        'make_table',
        help='Lays out partitions and creates partition table.')
    sub_parser.add_argument('--input',
                            help='Path to partition definition file.',
                            type=argparse.FileType('r'),
                            action='append')
    sub_parser.add_argument('--ab_suffixes',
                            help='Set or override A/B suffixes.')
    sub_parser.add_argument('--partitions_offset_begin',
                            help='Set or override disk partitions '
                                 'offset begin size.',
                            type=ParseSize)
    sub_parser.add_argument('--disk_size',
                            help='Set or override disk size.',
                            type=ParseSize)
    sub_parser.add_argument('--disk_alignment',
                            help='Set or override disk alignment.',
                            type=ParseSize)
    sub_parser.add_argument('--disk_guid',
                            help='Set or override disk GUID.',
                            type=ParseGuid)
    sub_parser.add_argument('--output_json',
                            help='JSON output file name.',
                            type=argparse.FileType('w'))
    sub_parser.add_argument('--output_gpt',
                            help='Output file name for MBR/GPT/GPT file.',
                            type=argparse.FileType('wb'))
    sub_parser.set_defaults(func=self.make_table)

    sub_parser = subparsers.add_parser(
        'make_disk_image',
        help='Creates disk image for loaded with partitions.')
    sub_parser.add_argument('--output',
                            help='Path to image output.',
                            type=argparse.FileType('wb'),
                            required=True)
    sub_parser.add_argument('--input',
                            help='Path to bpt file input.',
                            type=argparse.FileType('r'),
                            required=True)
    sub_parser.add_argument('--image',
                            help='Partition name and path to image file.',
                            metavar='PARTITION_NAME:PATH',
                            action='append')
    sub_parser.add_argument('--allow_empty_partitions',
                            help='Allow skipping partitions in bpt file.',
                            action='store_true')
    sub_parser.set_defaults(func=self.make_disk_image)

    sub_parser = subparsers.add_parser(
        'query_partition',
        help='Looks up informtion about a partition.')
    sub_parser.add_argument('--input',
                            help='Path to partition definition file.',
                            type=argparse.FileType('r'),
                            required=True)
    sub_parser.add_argument('--label',
                            help='Label of partition to look up.',
                            required=True)
    sub_parser.add_argument('--ab_collapse',
                            help='Collapse A/B partitions.',
                            action='store_true')
    sub_parser.add_argument('--type',
                            help='Type of information to look up.',
                            choices=QUERY_PARTITION_TYPES,
                            required=True)
    sub_parser.set_defaults(func=self.query_partition)

    args = parser.parse_args(argv[1:])
    args.func(args)

  def version(self, _):
    """Implements the 'version' sub-command."""
    print('{}.{}'.format(BPT_VERSION_MAJOR, BPT_VERSION_MINOR))

  def query_partition(self, args):
    """Implements the 'query_partition' sub-command."""
    try:
      result = self.bpt.query_partition(args.input,
                                        args.label,
                                        args.type,
                                        args.ab_collapse)
    except BptParsingError as e:
      sys.stderr.write('{}: Error parsing: {}\n'.format(e.filename, e.message))
      sys.exit(1)
    except BptError as e:
      sys.stderr.write('{}\n'.format(e.message))
      sys.exit(1)

    if not result:
      sys.stderr.write('No partition with label "{}".\n'.format(args.label))
      sys.exit(1)

    print(result)

  def make_table(self, args):
    """Implements the 'make_table' sub-command."""
    if not args.input:
      sys.stderr.write('Option --input is required one or more times.\n')
      sys.exit(1)

    try:
      (json_str, gpt_bin) = self.bpt.make_table(args.input, args.ab_suffixes,
                                                args.partitions_offset_begin,
                                                args.disk_size,
                                                args.disk_alignment,
                                                args.disk_guid)
    except BptParsingError as e:
      sys.stderr.write('{}: Error parsing: {}\n'.format(e.filename, e.message))
      sys.exit(1)
    except BptError as e:
      sys.stderr.write('{}\n'.format(e.message))
      sys.exit(1)

    if args.output_json:
      args.output_json.write(json_str)
    if args.output_gpt:
      args.output_gpt.write(gpt_bin)

  def make_disk_image(self, args):
    """Implements the 'make_disk_image' sub-command."""
    if not args.input:
      sys.stderr.write('Option --input is required.\n')
      sys.exit(1)
    if not args.output:
      sys.stderr.write('Option --ouptut is required.\n')
      sys.exit(1)

    try:
      self.bpt.make_disk_image(args.output,
                               args.input,
                               args.image,
                               args.allow_empty_partitions)
    except BptParsingError as e:
      sys.stderr.write('{}: Error parsing: {}\n'.format(e.filename, e.message))
      sys.exit(1)
    except BptError as e:
      sys.stderr.write('{}\n'.format(e.message))
      sys.exit(1)

if __name__ == '__main__':
  tool = BptTool()
  tool.run(sys.argv)