blob: fd4b66a7a39746784c33fe6002176933589ec8ed [file] [log] [blame]
Yilin Yang19da6932019-12-10 13:39:28 +08001#!/usr/bin/env python3
Hung-Te Linc772e1a2017-04-14 16:50:50 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""An utility to manipulate GPT on a disk image.
7
8Chromium OS factory software usually needs to access partitions from disk
9images. However, there is no good, lightweight, and portable GPT utility.
10Most Chromium OS systems use `cgpt`, but that's not by default installed on
11Ubuntu. Most systems have parted (GNU) or partx (util-linux-ng) but they have
12their own problems.
13
14For example, when a disk image is resized (usually enlarged for putting more
15resources on stateful partition), GPT table must be updated. However,
16 - `parted` can't repair partition without interactive console in exception
17 handler.
18 - `partx` cannot fix headers nor make changes to partition table.
19 - `cgpt repair` does not fix `LastUsableLBA` so we cannot enlarge partition.
20 - `gdisk` is not installed on most systems.
21
22As a result, we need a dedicated tool to help processing GPT.
23
24This pygpt.py provides a simple and customized implementation for processing
25GPT, as a replacement for `cgpt`.
26"""
27
28
29from __future__ import print_function
30
31import argparse
32import binascii
Yilin Yangf9fe1932019-11-04 17:09:34 +080033import codecs
Hung-Te Lin138389f2018-05-15 17:55:00 +080034import itertools
Hung-Te Linc772e1a2017-04-14 16:50:50 +080035import logging
36import os
Hung-Te Lin446eb512018-05-02 18:39:16 +080037import stat
Hung-Te Linc772e1a2017-04-14 16:50:50 +080038import struct
Hung-Te Linf641d302018-04-18 15:09:35 +080039import subprocess
40import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080041import uuid
42
Yilin Yangea784662019-09-26 13:51:03 +080043from six import iteritems
Yilin Yange6639682019-10-03 12:49:21 +080044from six.moves import xrange
45
Hung-Te Linc772e1a2017-04-14 16:50:50 +080046
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080047class StructError(Exception):
48 """Exceptions in packing and unpacking from/to struct fields."""
49 pass
Hung-Te Linc772e1a2017-04-14 16:50:50 +080050
Hung-Te Linc772e1a2017-04-14 16:50:50 +080051
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080052class StructField(object):
53 """Definition of a field in struct.
54
55 Attributes:
56 fmt: a format string for struct.{pack,unpack} to use.
57 name: a string for name of processed field.
58 """
59 __slots__ = ['fmt', 'name']
60
61 def __init__(self, fmt, name):
62 self.fmt = fmt
63 self.name = name
64
65 def Pack(self, value):
66 """"Packs given value from given format."""
67 del self # Unused.
Yilin Yang235e5982019-12-26 10:36:22 +080068 if isinstance(value, str):
69 value = value.encode('utf-8')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080070 return value
71
72 def Unpack(self, value):
73 """Unpacks given value into given format."""
74 del self # Unused.
75 return value
76
77
78class UTF16StructField(StructField):
79 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080080 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080081 encoding = 'utf-16-le'
82
83 def __init__(self, max_length, name):
84 self.max_length = max_length
85 fmt = '%ds' % max_length
86 super(UTF16StructField, self).__init__(fmt, name)
87
88 def Pack(self, value):
89 new_value = value.encode(self.encoding)
90 if len(new_value) >= self.max_length:
91 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
92 (value, self.name, self.max_length))
93 return new_value
94
95 def Unpack(self, value):
96 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080097
Hung-Te Linbf8aa272018-04-19 03:02:29 +080098
99class GUID(uuid.UUID):
100 """A special UUID that defaults to upper case in str()."""
101
102 def __str__(self):
103 """Returns GUID in upper case."""
104 return super(GUID, self).__str__().upper()
105
106 @staticmethod
107 def Random():
108 return uuid.uuid4()
109
110
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800111class GUIDStructField(StructField):
112 """A GUID field."""
113
114 def __init__(self, name):
115 super(GUIDStructField, self).__init__('16s', name)
116
117 def Pack(self, value):
118 if value is None:
Yilin Yang235e5982019-12-26 10:36:22 +0800119 return b'\x00' * 16
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800120 if not isinstance(value, uuid.UUID):
121 raise StructError('Field %s needs a GUID value instead of [%r].' %
122 (self.name, value))
123 return value.bytes_le
124
125 def Unpack(self, value):
126 return GUID(bytes_le=value)
127
128
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800129def BitProperty(getter, setter, shift, mask):
130 """A generator for bit-field properties.
131
132 This is used inside a class to manipulate an integer-like variable using
133 properties. The getter and setter should be member functions to change the
134 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800135
136 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800137 getter: a function to read integer type variable (for all the bits).
138 setter: a function to set the new changed integer type variable.
139 shift: integer for how many bits should be shifted (right).
140 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800141 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800142 def _getter(self):
143 return (getter(self) >> shift) & mask
144 def _setter(self, value):
145 assert value & mask == value, (
146 'Value %s out of range (mask=%s)' % (value, mask))
147 setter(self, getter(self) & ~(mask << shift) | value << shift)
148 return property(_getter, _setter)
149
150
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800151class PartitionAttributes(object):
152 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800153
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800154 This can be created using Partition.attrs, but the changed properties won't
155 apply to underlying Partition until an explicit call with
156 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800157 """
158
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800159 def __init__(self, attrs):
160 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800161
162 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800163 def raw(self):
164 """Returns the raw integer type attributes."""
165 return self._Get()
166
167 def _Get(self):
168 return self._attrs
169
170 def _Set(self, value):
171 self._attrs = value
172
173 successful = BitProperty(_Get, _Set, 56, 1)
174 tries = BitProperty(_Get, _Set, 52, 0xf)
175 priority = BitProperty(_Get, _Set, 48, 0xf)
176 legacy_boot = BitProperty(_Get, _Set, 2, 1)
177 required = BitProperty(_Get, _Set, 0, 1)
178 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
179
180
181class PartitionAttributeStructField(StructField):
182
183 def Pack(self, value):
184 if not isinstance(value, PartitionAttributes):
185 raise StructError('Given value %r is not %s.' %
186 (value, PartitionAttributes.__name__))
187 return value.raw
188
189 def Unpack(self, value):
190 return PartitionAttributes(value)
191
192
Yilin Yang9cf532e2019-12-13 12:02:59 +0800193# The binascii.crc32 returns unsigned integer in python3, so CRC32 in struct
194# must be declared as 'unsigned' (L).
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800195# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
196HEADER_FIELDS = [
197 StructField('8s', 'Signature'),
198 StructField('4s', 'Revision'),
199 StructField('L', 'HeaderSize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800200 StructField('L', 'CRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800201 StructField('4s', 'Reserved'),
202 StructField('Q', 'CurrentLBA'),
203 StructField('Q', 'BackupLBA'),
204 StructField('Q', 'FirstUsableLBA'),
205 StructField('Q', 'LastUsableLBA'),
206 GUIDStructField('DiskGUID'),
207 StructField('Q', 'PartitionEntriesStartingLBA'),
208 StructField('L', 'PartitionEntriesNumber'),
209 StructField('L', 'PartitionEntrySize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800210 StructField('L', 'PartitionArrayCRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800211]
212
213# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
214PARTITION_FIELDS = [
215 GUIDStructField('TypeGUID'),
216 GUIDStructField('UniqueGUID'),
217 StructField('Q', 'FirstLBA'),
218 StructField('Q', 'LastLBA'),
219 PartitionAttributeStructField('Q', 'Attributes'),
220 UTF16StructField(72, 'Names'),
221]
222
223# The PMBR has so many variants. The basic format is defined in
224# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
225# implementation, as derived from `cgpt`, is following syslinux as:
226# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
227PMBR_FIELDS = [
228 StructField('424s', 'BootCode'),
229 GUIDStructField('BootGUID'),
230 StructField('L', 'DiskID'),
231 StructField('2s', 'Magic'),
232 StructField('16s', 'LegacyPart0'),
233 StructField('16s', 'LegacyPart1'),
234 StructField('16s', 'LegacyPart2'),
235 StructField('16s', 'LegacyPart3'),
236 StructField('2s', 'Signature'),
237]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800238
239
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800240class GPTError(Exception):
241 """All exceptions by GPT."""
242 pass
243
244
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800245class GPTObject(object):
246 """A base object in GUID Partition Table.
247
248 All objects (for instance, header or partition entries) must inherit this
249 class and define the FIELD attribute with a list of field definitions using
250 StructField.
251
252 The 'name' in StructField will become the attribute name of GPT objects that
253 can be directly packed into / unpacked from. Derived (calculated from existing
254 attributes) attributes should be in lower_case.
255
256 It is also possible to attach some additional properties to the object as meta
257 data (for example path of the underlying image file). To do that, first
258 include it in __slots__ list and specify them as dictionary-type args in
259 constructors. These properties will be preserved when you call Clone().
260
261 To create a new object, call the constructor. Field data can be assigned as
262 in arguments, or give nothing to initialize as zero (see Zero()). Field data
263 and meta values can be also specified in keyword arguments (**kargs) at the
264 same time.
265
266 To read a object from file or stream, use class method ReadFrom(source).
267 To make changes, modify the field directly or use Update(dict), or create a
268 copy by Clone() first then Update.
269
270 To wipe all fields (but not meta), call Zero(). There is currently no way
271 to clear meta except setting them to None one by one.
272 """
273 __slots__ = []
274
Peter Shih533566a2018-09-05 17:48:03 +0800275 FIELDS = []
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800276 """A list of StructField definitions."""
277
278 def __init__(self, *args, **kargs):
279 if args:
280 if len(args) != len(self.FIELDS):
281 raise GPTError('%s need %s arguments (found %s).' %
282 (type(self).__name__, len(self.FIELDS), len(args)))
283 for f, value in zip(self.FIELDS, args):
284 setattr(self, f.name, value)
285 else:
286 self.Zero()
287
288 all_names = [f for f in self.__slots__]
Yilin Yangea784662019-09-26 13:51:03 +0800289 for name, value in iteritems(kargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800290 if name not in all_names:
291 raise GPTError('%s does not support keyword arg <%s>.' %
292 (type(self).__name__, name))
293 setattr(self, name, value)
294
295 def __iter__(self):
296 """An iterator to return all fields associated in the object."""
297 return (getattr(self, f.name) for f in self.FIELDS)
298
299 def __repr__(self):
300 return '(%s: %s)' % (type(self).__name__, ', '.join(
Peter Shihe6afab32018-09-11 17:16:48 +0800301 '%s=%r' % (f, getattr(self, f)) for f in self.__slots__))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800302
303 @classmethod
304 def GetStructFormat(cls):
305 """Returns a format string for struct to use."""
306 return '<' + ''.join(f.fmt for f in cls.FIELDS)
307
308 @classmethod
309 def ReadFrom(cls, source, **kargs):
310 """Returns an object from given source."""
311 obj = cls(**kargs)
312 obj.Unpack(source)
313 return obj
314
315 @property
316 def blob(self):
317 """The (packed) blob representation of the object."""
318 return self.Pack()
319
320 @property
321 def meta(self):
322 """Meta values (those not in GPT object fields)."""
323 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
324 return dict((name, getattr(self, name)) for name in metas)
325
326 def Unpack(self, source):
327 """Unpacks values from a given source.
328
329 Args:
330 source: a string of bytes or a file-like object to read from.
331 """
332 fmt = self.GetStructFormat()
333 if source is None:
334 source = '\x00' * struct.calcsize(fmt)
Yilin Yang235e5982019-12-26 10:36:22 +0800335 if not isinstance(source, (str, bytes)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800336 return self.Unpack(source.read(struct.calcsize(fmt)))
Yilin Yang235e5982019-12-26 10:36:22 +0800337 if isinstance(source, str):
338 source = source.encode('utf-8')
339 for f, value in zip(self.FIELDS, struct.unpack(fmt.encode('utf-8'),
340 source)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800341 setattr(self, f.name, f.Unpack(value))
Yilin Yang840fdc42020-01-16 16:37:42 +0800342 return None
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800343
344 def Pack(self):
Yilin Yang235e5982019-12-26 10:36:22 +0800345 """Packs values in all fields into a bytes by struct format."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800346 return struct.pack(self.GetStructFormat(),
347 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
348
349 def Clone(self):
350 """Clones a new instance."""
351 return type(self)(*self, **self.meta)
352
353 def Update(self, **dargs):
354 """Applies multiple values in current object."""
Yilin Yangea784662019-09-26 13:51:03 +0800355 for name, value in iteritems(dargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800356 setattr(self, name, value)
357
358 def Zero(self):
359 """Set all fields to values representing zero or empty.
360
361 Note the meta attributes won't be cleared.
362 """
363 class ZeroReader(object):
364 """A /dev/zero like stream."""
365
366 @staticmethod
367 def read(num):
368 return '\x00' * num
369
370 self.Unpack(ZeroReader())
371
372
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800373class GPT(object):
374 """A GPT helper class.
375
376 To load GPT from an existing disk image file, use `LoadFromFile`.
377 After modifications were made, use `WriteToFile` to commit changes.
378
379 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800380 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800381 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800382 partitions: a list of GPT partition entry nametuple.
383 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800384 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800385 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800386 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800387 # Old devices uses 'Basic data' type for stateful partition, and newer devices
388 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
389 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800390 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800391 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800392 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
393 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800394 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
395 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
396 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
397 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
398 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800399 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800400 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800401 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
Yilin Yangea784662019-09-26 13:51:03 +0800402 for k, v in iteritems(TYPE_GUID_MAP))
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800403 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
404 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
405 TYPE_GUID_LIST_BOOTABLE = [
406 TYPE_GUID_CHROMEOS_KERNEL,
407 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800408 ]
409
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800410 class ProtectiveMBR(GPTObject):
411 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800412 FIELDS = PMBR_FIELDS
413 __slots__ = [f.name for f in FIELDS]
414
Yilin Yang235e5982019-12-26 10:36:22 +0800415 SIGNATURE = b'\x55\xAA'
416 MAGIC = b'\x1d\x9a'
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800417
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800418 class Header(GPTObject):
419 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800420 FIELDS = HEADER_FIELDS
421 __slots__ = [f.name for f in FIELDS]
422
Yilin Yang235e5982019-12-26 10:36:22 +0800423 SIGNATURES = [b'EFI PART', b'CHROMEOS']
424 SIGNATURE_IGNORE = b'IGNOREME'
425 DEFAULT_REVISION = b'\x00\x00\x01\x00'
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800426
427 DEFAULT_PARTITION_ENTRIES = 128
428 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
429
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800430 @classmethod
431 def Create(cls, size, block_size, pad_blocks=0,
432 part_entries=DEFAULT_PARTITION_ENTRIES):
433 """Creates a header with default values.
434
435 Args:
436 size: integer of expected image size.
437 block_size: integer for size of each block (sector).
438 pad_blocks: number of preserved sectors between header and partitions.
439 part_entries: number of partitions to include in header.
440 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800441 PART_FORMAT = GPT.Partition.GetStructFormat()
442 FORMAT = cls.GetStructFormat()
443 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800444 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
445 parts_bytes = part_entries * part_entry_size
Yilin Yang14d02a22019-11-01 11:32:03 +0800446 parts_blocks = parts_bytes // block_size
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800447 if parts_bytes % block_size:
448 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800449 # CRC32 and PartitionsCRC32 must be updated later explicitly.
450 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800451 Signature=cls.SIGNATURES[0],
452 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800453 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800454 CurrentLBA=1,
Yilin Yang14d02a22019-11-01 11:32:03 +0800455 BackupLBA=size // block_size - 1,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800456 FirstUsableLBA=parts_lba + parts_blocks,
Yilin Yang14d02a22019-11-01 11:32:03 +0800457 LastUsableLBA=size // block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800458 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800459 PartitionEntriesStartingLBA=parts_lba,
460 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800461 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800462
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800463 def UpdateChecksum(self):
464 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800465
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800466 Note the PartitionArrayCRC32 is not touched - you have to make sure that
467 is correct before calling Header.UpdateChecksum().
468 """
469 self.Update(CRC32=0)
470 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800471
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800472 class Partition(GPTObject):
473 """The partition entry in GPT.
474
475 Please include following properties when creating a Partition object:
476 - image: a string for path to the image file the partition maps to.
477 - number: the 1-based partition number.
478 - block_size: an integer for size of each block (LBA, or sector).
479 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800480 FIELDS = PARTITION_FIELDS
481 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800482 NAMES_ENCODING = 'utf-16-le'
483 NAMES_LENGTH = 72
484
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800485 def __str__(self):
486 return '%s#%s' % (self.image, self.number)
487
488 def IsUnused(self):
489 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800490 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800491
Hung-Te Linfe724f82018-04-18 15:03:58 +0800492 def IsChromeOSKernel(self):
493 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800494 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800495
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800496 @property
497 def blocks(self):
498 """Return size of partition in blocks (see block_size)."""
499 return self.LastLBA - self.FirstLBA + 1
500
501 @property
502 def offset(self):
503 """Returns offset to partition in bytes."""
504 return self.FirstLBA * self.block_size
505
506 @property
507 def size(self):
508 """Returns size of partition in bytes."""
509 return self.blocks * self.block_size
510
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800511 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800512 """GPT constructor.
513
514 See LoadFromFile for how it's usually used.
515 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800516 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800517 self.header = None
518 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800519 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800520 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800521
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800522 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800523 def GetTypeGUID(cls, value):
524 """The value may be a GUID in string or a short type string."""
525 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
526 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800527
528 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800529 def Create(cls, image_name, size, block_size, pad_blocks=0):
530 """Creates a new GPT instance from given size and block_size.
531
532 Args:
533 image_name: a string of underlying disk image file name.
534 size: expected size of disk image.
535 block_size: size of each block (sector) in bytes.
536 pad_blocks: number of blocks between header and partitions array.
537 """
538 gpt = cls()
539 gpt.block_size = block_size
540 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
541 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800542 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800543 for i in xrange(gpt.header.PartitionEntriesNumber)]
544 return gpt
545
Hung-Te Lin446eb512018-05-02 18:39:16 +0800546 @staticmethod
547 def IsBlockDevice(image):
548 """Returns if the image is a block device file."""
549 return stat.S_ISBLK(os.stat(image).st_mode)
550
551 @classmethod
552 def GetImageSize(cls, image):
553 """Returns the size of specified image (plain or block device file)."""
554 if not cls.IsBlockDevice(image):
555 return os.path.getsize(image)
556
557 fd = os.open(image, os.O_RDONLY)
558 try:
559 return os.lseek(fd, 0, os.SEEK_END)
560 finally:
561 os.close(fd)
562
563 @classmethod
564 def GetLogicalBlockSize(cls, block_dev):
565 """Returns the logical block (sector) size from a block device file.
566
567 The underlying call is BLKSSZGET. An alternative command is blockdev,
568 but that needs root permission even if we just want to get sector size.
569 """
570 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
571 return int(subprocess.check_output(
572 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
573
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800574 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800575 def LoadFromFile(cls, image):
576 """Loads a GPT table from give disk image file object.
577
578 Args:
579 image: a string as file path or a file-like object to read from.
580 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800581 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800582 with open(image, 'rb') as f:
583 return cls.LoadFromFile(f)
584
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800585 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800586 image.seek(0)
587 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
588 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
589 logging.debug('Found MBR signature in %s', image.name)
590 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
591 logging.debug('Found PMBR in %s', image.name)
592 gpt.pmbr = pmbr
593
Hung-Te Linf148d322018-04-13 10:24:42 +0800594 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800595 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
596 if cls.IsBlockDevice(image.name):
597 block_sizes = [cls.GetLogicalBlockSize(image.name)]
598
599 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800600 # Note because there are devices setting Primary as ignored and the
601 # partition table signature accepts 'CHROMEOS' which is also used by
602 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
603 # first before trying other block sizes, otherwise we may incorrectly
604 # identify a kernel partition as LBA 1 of larger block size system.
605 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
606 (-block_size, os.SEEK_END)]):
607 image.seek(*seek)
608 header = gpt.Header.ReadFrom(image)
609 if header.Signature in cls.Header.SIGNATURES:
610 gpt.block_size = block_size
611 if i != 0:
612 gpt.is_secondary = True
613 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800614 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800615 else:
616 # Nothing found, try next block size.
617 continue
618 # Found a valid signature.
619 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800620 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800621 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800622
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800623 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800624 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800625 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800626 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800627 return p
628
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800629 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800630 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800631 ReadPartition(image, i + 1)
632 for i in xrange(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800633 return gpt
634
Hung-Te Linc5196682018-04-18 22:59:59 +0800635 def GetUsedPartitions(self):
636 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800637
Hung-Te Linc5196682018-04-18 22:59:59 +0800638 Use 'number' property to find the real location of partition in
639 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800640 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800641 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800642
643 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800644 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800645 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800646 return (max(p.LastLBA for p in parts)
647 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800648
649 def GetPartitionTableBlocks(self, header=None):
650 """Returns the blocks (or LBA) of partition table from given header."""
651 if header is None:
652 header = self.header
653 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Yilin Yang14d02a22019-11-01 11:32:03 +0800654 blocks = size // self.block_size
Hung-Te Linf148d322018-04-13 10:24:42 +0800655 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800656 blocks += 1
657 return blocks
658
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800659 def GetPartition(self, number):
660 """Gets the Partition by given (1-based) partition number.
661
662 Args:
663 number: an integer as 1-based partition number.
664 """
665 if not 0 < number <= len(self.partitions):
666 raise GPTError('Invalid partition number %s.' % number)
667 return self.partitions[number - 1]
668
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800669 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800670 """Updates the entry in partition table by given Partition object.
671
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800672 Usually you only need to call this if you want to copy one partition to
673 different location (number of image).
674
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800675 Args:
676 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800677 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800678 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800679 ref = self.partitions[number - 1]
680 part = part.Clone()
681 part.number = number
682 part.image = ref.image
683 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800684 self.partitions[number - 1] = part
685
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800686 def GetSize(self):
687 return self.block_size * (self.header.BackupLBA + 1)
688
689 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800690 """Adjust GPT for a disk image in given size.
691
692 Args:
693 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800694 check_overlap: Checks if the backup partition table overlaps used
695 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800696 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800697 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800698 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800699 raise GPTError(
700 'New file size %d is not valid for image files.' % new_size)
Yilin Yang14d02a22019-11-01 11:32:03 +0800701 new_blocks = new_size // self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800702 if old_size != new_size:
Yilin Yang9881b1e2019-12-11 11:47:33 +0800703 logging.warning('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
704 new_size, new_blocks, old_size,
705 old_size // self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800706 else:
707 logging.info('Image size (%d, LBA=%d) not changed.',
708 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800709 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800710
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800711 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800712 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800713 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800714
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800715 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800716 max_used_lba = self.GetMaxUsedLBA()
717 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800718 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800719
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800720 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800721
722 def GetFreeSpace(self):
723 """Returns the free (available) space left according to LastUsableLBA."""
724 max_lba = self.GetMaxUsedLBA()
725 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800726 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800727
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800728 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800729 """Expands a given partition to last usable LBA.
730
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800731 The size of the partition can actually be reduced if the last usable LBA
732 decreases.
733
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800734 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800735 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800736
737 Returns:
738 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800739 """
740 # Assume no partitions overlap, we need to make sure partition[i] has
741 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800742 p = self.GetPartition(number)
743 if p.IsUnused():
744 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800745 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800746 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800747 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800748 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800749 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800750
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800751 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800752 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800753 new_blocks = p.blocks
Yilin Yang9881b1e2019-12-11 11:47:33 +0800754 logging.warning(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800755 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800756 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800757
Hung-Te Lin3b491672018-04-19 01:41:20 +0800758 def CheckIntegrity(self):
759 """Checks if the GPT objects all look good."""
760 # Check if the header allocation looks good. CurrentLBA and
761 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
762 # LastUsableLBA].
763 header = self.header
764 entries_first_lba = header.PartitionEntriesStartingLBA
765 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
766
767 def CheckOutsideUsable(name, lba, outside_entries=False):
768 if lba < 1:
769 raise GPTError('%s should not live in LBA %s.' % (name, lba))
770 if lba > max(header.BackupLBA, header.CurrentLBA):
771 # Note this is "in theory" possible, but we want to report this as
772 # error as well, since it usually leads to error.
773 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
774 (name, lba, header.BackupLBA))
775 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
776 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
777 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
778 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
779 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
780 (name, lba, entries_first_lba, entries_last_lba))
781 CheckOutsideUsable('Header', header.CurrentLBA, True)
782 CheckOutsideUsable('Backup header', header.BackupLBA, True)
783 CheckOutsideUsable('Partition entries', entries_first_lba)
784 CheckOutsideUsable('Partition entries end', entries_last_lba)
785
786 parts = self.GetUsedPartitions()
787 # Check if partition entries overlap with each other.
788 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
789 lba_list.sort(key=lambda t: t[0])
790 for i in xrange(len(lba_list) - 1):
791 if lba_list[i][1] >= lba_list[i + 1][0]:
792 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
793 (lba_list[i] + lba_list[i + 1]))
794 # Now, check the first and last partition.
795 if lba_list:
796 p = lba_list[0][2]
797 if p.FirstLBA < header.FirstUsableLBA:
798 raise GPTError(
799 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
800 (p, p.FirstLBA, header.FirstLBA))
801 p = lba_list[-1][2]
802 if p.LastLBA > header.LastUsableLBA:
803 raise GPTError(
804 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
805 (p, p.LastLBA, header.LastLBA))
806 # Check if UniqueGUIDs are not unique.
807 if len(set(p.UniqueGUID for p in parts)) != len(parts):
808 raise GPTError('Partition UniqueGUIDs are duplicated.')
809 # Check if CRCs match.
Yilin Yang235e5982019-12-26 10:36:22 +0800810 if (binascii.crc32(b''.join(p.blob for p in self.partitions)) !=
Hung-Te Lin3b491672018-04-19 01:41:20 +0800811 header.PartitionArrayCRC32):
812 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800813 header_crc = header.Clone()
814 header_crc.UpdateChecksum()
815 if header_crc.CRC32 != header.CRC32:
816 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800817
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800818 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800819 """Updates all checksum fields in GPT objects."""
Yilin Yang235e5982019-12-26 10:36:22 +0800820 parts = b''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800821 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
822 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800823
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800824 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800825 """Returns the backup header according to given header.
826
827 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
828 fields.
829 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800830 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800831 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800832 h = header.Clone()
833 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800834 BackupLBA=header.CurrentLBA,
835 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800836 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800837 h.UpdateChecksum()
838 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800839
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800840 @classmethod
841 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
842 """Writes a protective MBR to given file.
843
844 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
845 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
846 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
847 we can follow that.
848
849 Args:
850 create: True to re-create PMBR structure.
851 bootcode: a blob of new boot code.
852 boot_guid a blob for new boot GUID.
853
854 Returns:
855 The written PMBR structure.
856 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800857 if isinstance(image, str):
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800858 with open(image, 'rb+') as f:
859 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
860
861 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800862 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
863 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800864 pmbr = cls.ProtectiveMBR.ReadFrom(image)
865
866 if create:
867 legacy_sectors = min(
868 0x100000000,
Yilin Yang14d02a22019-11-01 11:32:03 +0800869 GPT.GetImageSize(image.name) // cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800870 # Partition 0 must have have the fixed CHS with number of sectors
871 # (calculated as legacy_sectors later).
Yilin Yangf9fe1932019-11-04 17:09:34 +0800872 part0 = (codecs.decode('00000200eeffffff01000000', 'hex') +
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800873 struct.pack('<I', legacy_sectors))
874 # Partition 1~3 should be all zero.
875 part1 = '\x00' * 16
876 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800877 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800878 BootGUID=cls.TYPE_GUID_UNUSED,
879 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800880 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800881 LegacyPart0=part0,
882 LegacyPart1=part1,
883 LegacyPart2=part1,
884 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800885 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800886
887 if bootcode:
888 if len(bootcode) > len(pmbr.BootCode):
889 logging.info(
890 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
891 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800892 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800893 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800894 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800895
896 blob = pmbr.blob
897 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
898 image.seek(0)
899 image.write(blob)
900 return pmbr
901
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800902 def WriteToFile(self, image):
903 """Updates partition table in a disk image file.
904
905 Args:
906 image: a string as file path or a file-like object to write into.
907 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800908 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800909 with open(image, 'rb+') as f:
910 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800911
912 def WriteData(name, blob, lba):
913 """Writes a blob into given location."""
914 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800915 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800916 image.seek(lba * self.block_size)
917 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800918
919 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800920 self.CheckIntegrity()
Yilin Yang235e5982019-12-26 10:36:22 +0800921 parts_blob = b''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800922
923 header = self.header
924 WriteData('GPT Header', header.blob, header.CurrentLBA)
925 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
926 logging.info(
927 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
928 header.LastUsableLBA)
929
930 if not self.is_secondary:
931 # When is_secondary is True, the header we have is actually backup header.
932 backup_header = self.GetBackupHeader(self.header)
933 WriteData(
934 'Backup Partitions', parts_blob,
935 backup_header.PartitionEntriesStartingLBA)
936 WriteData(
937 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Yilin Yang840fdc42020-01-16 16:37:42 +0800938 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800939
940
941class GPTCommands(object):
942 """Collection of GPT sub commands for command line to use.
943
944 The commands are derived from `cgpt`, but not necessary to be 100% compatible
945 with cgpt.
946 """
947
948 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800949 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800950 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800951 ('type', 'type guid'),
952 ('unique', 'unique guid'),
953 ('label', 'label'),
954 ('Successful', 'Successful flag'),
955 ('Tries', 'Tries flag'),
956 ('Priority', 'Priority flag'),
957 ('Legacy', 'Legacy Boot flag'),
958 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800959
960 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800961 commands = dict(
962 (command.lower(), getattr(self, command)())
963 for command in dir(self)
964 if (isinstance(getattr(self, command), type) and
965 issubclass(getattr(self, command), self.SubCommand) and
966 getattr(self, command) is not self.SubCommand)
967 )
968 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800969
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800970 def DefineArgs(self, parser):
971 """Defines all available commands to an argparser subparsers instance."""
972 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
Yilin Yangea784662019-09-26 13:51:03 +0800973 for name, instance in sorted(iteritems(self.commands)):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800974 parser = subparsers.add_parser(
975 name, description=instance.__doc__,
976 formatter_class=argparse.RawDescriptionHelpFormatter,
977 help=instance.__doc__.splitlines()[0])
978 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800979
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800980 def Execute(self, args):
981 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800982 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800983
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800984 class SubCommand(object):
985 """A base class for sub commands to derive from."""
986
987 def DefineArgs(self, parser):
988 """Defines command line arguments to argparse parser.
989
990 Args:
991 parser: An argparse parser instance.
992 """
993 del parser # Unused.
994 raise NotImplementedError
995
996 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800997 """Execute the command with parsed arguments.
998
999 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001000
1001 Args:
1002 args: An argparse parsed namespace.
1003 """
1004 del args # Unused.
1005 raise NotImplementedError
1006
Hung-Te Line0d1fa72018-05-15 00:04:48 +08001007 def ExecuteCommandLine(self, *args):
1008 """Execute as invoked from command line.
1009
1010 This provides an easy way to execute particular sub command without
1011 creating argument parser explicitly.
1012
1013 Args:
1014 args: a list of string type command line arguments.
1015 """
1016 parser = argparse.ArgumentParser()
1017 self.DefineArgs(parser)
1018 return self.Execute(parser.parse_args(args))
1019
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001020 class Create(SubCommand):
1021 """Create or reset GPT headers and tables.
1022
1023 Create or reset an empty GPT.
1024 """
1025
1026 def DefineArgs(self, parser):
1027 parser.add_argument(
1028 '-z', '--zero', action='store_true',
1029 help='Zero the sectors of the GPT table and entries')
1030 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001031 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001032 help=('Size (in blocks) of the disk to pad between the '
1033 'primary GPT header and its entries, default %(default)s'))
1034 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001035 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001036 help='Size of each block (sector) in bytes.')
1037 parser.add_argument(
1038 'image_file', type=argparse.FileType('rb+'),
1039 help='Disk image file to create.')
1040
1041 def Execute(self, args):
1042 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001043 if block_size is None:
1044 if GPT.IsBlockDevice(args.image_file.name):
1045 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1046 else:
1047 block_size = GPT.DEFAULT_BLOCK_SIZE
1048
1049 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1050 logging.info('Block (sector) size for %s is set to %s bytes.',
1051 args.image_file.name, block_size)
1052
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001053 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001054 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001055 block_size, args.pad_blocks)
1056 if args.zero:
1057 # In theory we only need to clear LBA 1, but to make sure images already
1058 # initialized with different block size won't have GPT signature in
1059 # different locations, we should zero until first usable LBA.
1060 args.image_file.seek(0)
Yilin Yang235e5982019-12-26 10:36:22 +08001061 args.image_file.write(b'\0' * block_size * gpt.header.FirstUsableLBA)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001062 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001063 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001064 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001065
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001066 class Boot(SubCommand):
1067 """Edit the PMBR sector for legacy BIOSes.
1068
1069 With no options, it will just print the PMBR boot guid.
1070 """
1071
1072 def DefineArgs(self, parser):
1073 parser.add_argument(
1074 '-i', '--number', type=int,
1075 help='Set bootable partition')
1076 parser.add_argument(
Stimim Chen0e6071b2020-04-28 18:08:49 +08001077 '-b', '--bootloader', type=argparse.FileType('rb'),
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001078 help='Install bootloader code in the PMBR')
1079 parser.add_argument(
1080 '-p', '--pmbr', action='store_true',
1081 help='Create legacy PMBR partition table')
1082 parser.add_argument(
1083 'image_file', type=argparse.FileType('rb+'),
1084 help='Disk image file to change PMBR.')
1085
1086 def Execute(self, args):
1087 """Rebuilds the protective MBR."""
1088 bootcode = args.bootloader.read() if args.bootloader else None
1089 boot_guid = None
1090 if args.number is not None:
1091 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001092 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001093 pmbr = GPT.WriteProtectiveMBR(
1094 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1095
You-Cheng Syufff7f422018-05-14 15:37:39 +08001096 print(pmbr.BootGUID)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001097 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001098 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001099
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001100 class Legacy(SubCommand):
1101 """Switch between GPT and Legacy GPT.
1102
1103 Switch GPT header signature to "CHROMEOS".
1104 """
1105
1106 def DefineArgs(self, parser):
1107 parser.add_argument(
1108 '-e', '--efi', action='store_true',
1109 help='Switch GPT header signature back to "EFI PART"')
1110 parser.add_argument(
1111 '-p', '--primary-ignore', action='store_true',
1112 help='Switch primary GPT header signature to "IGNOREME"')
1113 parser.add_argument(
1114 'image_file', type=argparse.FileType('rb+'),
1115 help='Disk image file to change.')
1116
1117 def Execute(self, args):
1118 gpt = GPT.LoadFromFile(args.image_file)
1119 # cgpt behavior: if -p is specified, -e is ignored.
1120 if args.primary_ignore:
1121 if gpt.is_secondary:
1122 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1123 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1124 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1125 gpt.header = gpt.GetBackupHeader(self.header)
1126 gpt.is_secondary = True
1127 else:
1128 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001129 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001130 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001131 args.image_file.close()
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001132 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001133 return ('Set %s primary GPT header to %s.' %
1134 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001135 return ('Changed GPT signature for %s to %s.' %
1136 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001137
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001138 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001139 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001140
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001141 def DefineArgs(self, parser):
1142 parser.add_argument(
1143 'image_file', type=argparse.FileType('rb+'),
1144 help='Disk image file to repair.')
1145
1146 def Execute(self, args):
1147 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001148 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001149 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001150 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001151 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001152
1153 class Expand(SubCommand):
1154 """Expands a GPT partition to all available free space."""
1155
1156 def DefineArgs(self, parser):
1157 parser.add_argument(
1158 '-i', '--number', type=int, required=True,
1159 help='The partition to expand.')
1160 parser.add_argument(
1161 'image_file', type=argparse.FileType('rb+'),
1162 help='Disk image file to modify.')
1163
1164 def Execute(self, args):
1165 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001166 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001167 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001168 args.image_file.close()
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001169 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001170 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001171 'Partition %s on disk image file %s has been extended '
1172 'from %s to %s .' %
1173 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1174 new_blocks * gpt.block_size))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001175 return ('Nothing to expand for disk image %s partition %s.' %
1176 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001177
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001178 class Add(SubCommand):
1179 """Add, edit, or remove a partition entry.
1180
1181 Use the -i option to modify an existing partition.
1182 The -b, -s, and -t options must be given for new partitions.
1183
1184 The partition type may also be given as one of these aliases:
1185
1186 firmware ChromeOS firmware
1187 kernel ChromeOS kernel
1188 rootfs ChromeOS rootfs
1189 data Linux data
1190 reserved ChromeOS reserved
1191 efi EFI System Partition
1192 unused Unused (nonexistent) partition
1193 """
1194 def DefineArgs(self, parser):
1195 parser.add_argument(
1196 '-i', '--number', type=int,
1197 help='Specify partition (default is next available)')
1198 parser.add_argument(
1199 '-b', '--begin', type=int,
1200 help='Beginning sector')
1201 parser.add_argument(
1202 '-s', '--sectors', type=int,
1203 help='Size in sectors (logical blocks).')
1204 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001205 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001206 help='Partition Type GUID')
1207 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001208 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001209 help='Partition Unique ID')
1210 parser.add_argument(
1211 '-l', '--label',
1212 help='Label')
1213 parser.add_argument(
1214 '-S', '--successful', type=int, choices=xrange(2),
1215 help='set Successful flag')
1216 parser.add_argument(
1217 '-T', '--tries', type=int,
1218 help='set Tries flag (0-15)')
1219 parser.add_argument(
1220 '-P', '--priority', type=int,
1221 help='set Priority flag (0-15)')
1222 parser.add_argument(
1223 '-R', '--required', type=int, choices=xrange(2),
1224 help='set Required flag')
1225 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001226 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001227 choices=xrange(2),
1228 help='set Legacy Boot flag')
1229 parser.add_argument(
1230 '-A', '--attribute', dest='raw_16', type=int,
1231 help='set raw 16-bit attribute value (bits 48-63)')
1232 parser.add_argument(
1233 'image_file', type=argparse.FileType('rb+'),
1234 help='Disk image file to modify.')
1235
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001236 def Execute(self, args):
1237 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001238 number = args.number
1239 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001240 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001241
1242 # First and last LBA must be calculated explicitly because the given
1243 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001244 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001245 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001246
1247 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001248 part.Zero()
1249 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001250 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001251 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001252 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001253 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001254
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001255 def UpdateAttr(name):
1256 value = getattr(args, name)
1257 if value is None:
1258 return
1259 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001260
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001261 def GetArg(arg_value, default_value):
1262 return default_value if arg_value is None else arg_value
1263
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001264 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001265 for name in ['legacy_boot', 'required', 'priority', 'tries',
1266 'successful', 'raw_16']:
1267 UpdateAttr(name)
1268 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001269 part.Update(
1270 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001271 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001272 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1273 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1274 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1275 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001276
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001277 # Wipe partition again if it should be empty.
1278 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001279 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001280
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001281 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001282 args.image_file.close()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001283 if part.IsUnused():
1284 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001285 return 'Deleted (zeroed) %s.' % (part,)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001286 return ('%s %s (%s+%s).' %
1287 ('Added' if is_new_part else 'Modified',
1288 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001289
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001290 class Show(SubCommand):
1291 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001292
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001293 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001294 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001295
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001296 def DefineArgs(self, parser):
1297 parser.add_argument(
1298 '--numeric', '-n', action='store_true',
1299 help='Numeric output only.')
1300 parser.add_argument(
1301 '--quick', '-q', action='store_true',
1302 help='Quick output.')
1303 parser.add_argument(
1304 '-i', '--number', type=int,
1305 help='Show specified partition only, with format args.')
1306 for name, help_str in GPTCommands.FORMAT_ARGS:
1307 # TODO(hungte) Alert if multiple args were specified.
1308 parser.add_argument(
1309 '--%s' % name, '-%c' % name[0], action='store_true',
1310 help='[format] %s.' % help_str)
1311 parser.add_argument(
1312 'image_file', type=argparse.FileType('rb'),
1313 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001314
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001315 def Execute(self, args):
1316 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001317
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001318 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001319 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001320 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001321 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001322 if names:
1323 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001324 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001325
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001326 def IsBootableType(guid):
1327 if not guid:
1328 return False
1329 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001330
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001331 def FormatAttribute(attrs, chromeos_kernel=False):
1332 if args.numeric:
1333 return '[%x]' % (attrs.raw >> 48)
1334 results = []
1335 if chromeos_kernel:
1336 results += [
1337 'priority=%d' % attrs.priority,
1338 'tries=%d' % attrs.tries,
1339 'successful=%d' % attrs.successful]
1340 if attrs.required:
1341 results += ['required=1']
1342 if attrs.legacy_boot:
1343 results += ['legacy_boot=1']
1344 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001345
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001346 def ApplyFormatArgs(p):
1347 if args.begin:
1348 return p.FirstLBA
1349 elif args.size:
1350 return p.blocks
1351 elif args.type:
1352 return FormatTypeGUID(p)
1353 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001354 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001355 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001356 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001357 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001358 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001359 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001360 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001361 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001362 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001363 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001364 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001365 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001366 return '[%x]' % (p.Attributes.raw >> 48)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001367 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001368
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001369 def IsFormatArgsSpecified():
1370 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001371
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001372 gpt = GPT.LoadFromFile(args.image_file)
1373 logging.debug('%r', gpt.header)
1374 fmt = '%12s %11s %7s %s'
1375 fmt2 = '%32s %s: %s'
1376 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001377
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001378 if IsFormatArgsSpecified() and args.number is None:
1379 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001380
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001381 if not (args.number is None or
1382 0 < args.number <= gpt.header.PartitionEntriesNumber):
1383 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001384
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001385 partitions = gpt.partitions
1386 do_print_gpt_blocks = False
1387 if not (args.quick or IsFormatArgsSpecified()):
1388 print(fmt % header)
1389 if args.number is None:
1390 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001391
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001392 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001393 if gpt.pmbr:
1394 print(fmt % (0, 1, '', 'PMBR'))
1395 if gpt.is_secondary:
1396 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1397 else:
1398 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1399 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1400 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001401
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001402 for p in partitions:
1403 if args.number is None:
1404 # Skip unused partitions.
1405 if p.IsUnused():
1406 continue
1407 elif p.number != args.number:
1408 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001409
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001410 if IsFormatArgsSpecified():
1411 print(ApplyFormatArgs(p))
1412 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001413
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001414 print(fmt % (p.FirstLBA, p.blocks, p.number,
1415 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001416 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001417
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001418 if not args.quick:
1419 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001420 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1421 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001422 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001423 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001424
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001425 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001426 if gpt.is_secondary:
1427 header = gpt.header
1428 else:
1429 f = args.image_file
1430 f.seek(gpt.header.BackupLBA * gpt.block_size)
1431 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001432 print(fmt % (header.PartitionEntriesStartingLBA,
1433 gpt.GetPartitionTableBlocks(header), '',
1434 'Sec GPT table'))
1435 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001436
Hung-Te Lin3b491672018-04-19 01:41:20 +08001437 # Check integrity after showing all fields.
1438 gpt.CheckIntegrity()
1439
Hung-Te Linfe724f82018-04-18 15:03:58 +08001440 class Prioritize(SubCommand):
1441 """Reorder the priority of all kernel partitions.
1442
1443 Reorder the priority of all active ChromeOS Kernel partitions.
1444
1445 With no options this will set the lowest active kernel to priority 1 while
1446 maintaining the original order.
1447 """
1448
1449 def DefineArgs(self, parser):
1450 parser.add_argument(
1451 '-P', '--priority', type=int,
1452 help=('Highest priority to use in the new ordering. '
1453 'The other partitions will be ranked in decreasing '
1454 'priority while preserving their original order. '
1455 'If necessary the lowest ranks will be coalesced. '
1456 'No active kernels will be lowered to priority 0.'))
1457 parser.add_argument(
1458 '-i', '--number', type=int,
1459 help='Specify the partition to make the highest in the new order.')
1460 parser.add_argument(
1461 '-f', '--friends', action='store_true',
1462 help=('Friends of the given partition (those with the same '
1463 'starting priority) are also updated to the new '
1464 'highest priority. '))
1465 parser.add_argument(
1466 'image_file', type=argparse.FileType('rb+'),
1467 help='Disk image file to prioritize.')
1468
1469 def Execute(self, args):
1470 gpt = GPT.LoadFromFile(args.image_file)
1471 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001472 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1473 groups = dict((k, list(g)) for k, g in itertools.groupby(
1474 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001475 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001476 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001477 if p not in parts:
1478 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001479 pri = p.Attributes.priority
1480 friends = groups.pop(pri)
1481 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001482 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001483 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001484 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001485 groups[new_pri] = [p]
1486 friends.remove(p)
1487 if friends:
1488 groups[pri] = friends
1489
1490 if 0 in groups:
1491 # Do not change any partitions with priority=0
1492 groups.pop(0)
1493
Yilin Yang78fa12e2019-09-25 14:21:10 +08001494 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001495 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001496
1497 # Max priority is 0xf.
1498 highest = min(args.priority or len(prios), 0xf)
1499 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001500
Hung-Te Lin138389f2018-05-15 17:55:00 +08001501 for i, pri in enumerate(prios):
1502 new_priority = max(1, highest - i)
1503 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001504 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001505 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001506 if old_priority == new_priority:
1507 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001508 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001509 if attrs.tries < 1 and not attrs.successful:
1510 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001511 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001512 logging.info('%s priority changed from %s to %s.', p, old_priority,
1513 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001514
1515 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001516 args.image_file.close()
Hung-Te Linfe724f82018-04-18 15:03:58 +08001517
Hung-Te Linf641d302018-04-18 15:09:35 +08001518 class Find(SubCommand):
1519 """Locate a partition by its GUID.
1520
1521 Find a partition by its UUID or label. With no specified DRIVE it scans all
1522 physical drives.
1523
1524 The partition type may also be given as one of these aliases:
1525
1526 firmware ChromeOS firmware
1527 kernel ChromeOS kernel
1528 rootfs ChromeOS rootfs
1529 data Linux data
1530 reserved ChromeOS reserved
1531 efi EFI System Partition
1532 unused Unused (nonexistent) partition
1533 """
1534 def DefineArgs(self, parser):
1535 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001536 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001537 help='Search for Partition Type GUID')
1538 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001539 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001540 help='Search for Partition Unique GUID')
1541 parser.add_argument(
1542 '-l', '--label',
1543 help='Search for Label')
1544 parser.add_argument(
1545 '-n', '--numeric', action='store_true',
1546 help='Numeric output only.')
1547 parser.add_argument(
1548 '-1', '--single-match', action='store_true',
1549 help='Fail if more than one match is found.')
1550 parser.add_argument(
1551 '-M', '--match-file', type=str,
1552 help='Matching partition data must also contain MATCH_FILE content.')
1553 parser.add_argument(
1554 '-O', '--offset', type=int, default=0,
1555 help='Byte offset into partition to match content (default 0).')
1556 parser.add_argument(
1557 'drive', type=argparse.FileType('rb+'), nargs='?',
1558 help='Drive or disk image file to find.')
1559
1560 def Execute(self, args):
1561 if not any((args.type_guid, args.unique_guid, args.label)):
1562 raise GPTError('You must specify at least one of -t, -u, or -l')
1563
1564 drives = [args.drive.name] if args.drive else (
1565 '/dev/%s' % name for name in subprocess.check_output(
1566 'lsblk -d -n -r -o name', shell=True).split())
1567
1568 match_pattern = None
1569 if args.match_file:
1570 with open(args.match_file) as f:
1571 match_pattern = f.read()
1572
1573 found = 0
1574 for drive in drives:
1575 try:
1576 gpt = GPT.LoadFromFile(drive)
1577 except GPTError:
1578 if args.drive:
1579 raise
1580 # When scanning all block devices on system, ignore failure.
1581
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001582 def Unmatch(a, b):
1583 return a is not None and a != b
1584
Hung-Te Linf641d302018-04-18 15:09:35 +08001585 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001586 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001587 Unmatch(args.label, p.Names) or
1588 Unmatch(args.unique_guid, p.UniqueGUID) or
1589 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001590 continue
1591 if match_pattern:
1592 with open(drive, 'rb') as f:
1593 f.seek(p.offset + args.offset)
1594 if f.read(len(match_pattern)) != match_pattern:
1595 continue
1596 # Found the partition, now print.
1597 found += 1
1598 if args.numeric:
1599 print(p.number)
1600 else:
1601 # This is actually more for block devices.
1602 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1603 p.number))
1604
1605 if found < 1 or (args.single_match and found > 1):
1606 return 1
1607 return 0
1608
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001609
1610def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001611 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001612 parser = argparse.ArgumentParser(description='GPT Utility.')
1613 parser.add_argument('--verbose', '-v', action='count', default=0,
1614 help='increase verbosity.')
1615 parser.add_argument('--debug', '-d', action='store_true',
1616 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001617 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001618
1619 args = parser.parse_args()
1620 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1621 if args.debug:
1622 log_level = logging.DEBUG
1623 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1624 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001625 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001626 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001627 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001628 sys.exit(code)
Yilin Yang0724c9d2019-11-15 15:53:45 +08001629 elif isinstance(code, str):
Hung-Te Linbad46112018-05-15 16:39:14 +08001630 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001631 except Exception as e:
1632 if args.verbose or args.debug:
1633 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001634 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001635
1636
1637if __name__ == '__main__':
1638 main()