blob: 38d767994aa17aca3ed387804cae2adfe8e8a1d2 [file] [log] [blame]
Mike Frysinger63bb3c72019-09-01 15:16:26 -04001#!/usr/bin/env python2
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.
68 return value
69
70 def Unpack(self, value):
71 """Unpacks given value into given format."""
72 del self # Unused.
73 return value
74
75
76class UTF16StructField(StructField):
77 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080078 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080079 encoding = 'utf-16-le'
80
81 def __init__(self, max_length, name):
82 self.max_length = max_length
83 fmt = '%ds' % max_length
84 super(UTF16StructField, self).__init__(fmt, name)
85
86 def Pack(self, value):
87 new_value = value.encode(self.encoding)
88 if len(new_value) >= self.max_length:
89 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
90 (value, self.name, self.max_length))
91 return new_value
92
93 def Unpack(self, value):
94 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080095
Hung-Te Linbf8aa272018-04-19 03:02:29 +080096
97class GUID(uuid.UUID):
98 """A special UUID that defaults to upper case in str()."""
99
100 def __str__(self):
101 """Returns GUID in upper case."""
102 return super(GUID, self).__str__().upper()
103
104 @staticmethod
105 def Random():
106 return uuid.uuid4()
107
108
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800109class GUIDStructField(StructField):
110 """A GUID field."""
111
112 def __init__(self, name):
113 super(GUIDStructField, self).__init__('16s', name)
114
115 def Pack(self, value):
116 if value is None:
117 return '\x00' * 16
118 if not isinstance(value, uuid.UUID):
119 raise StructError('Field %s needs a GUID value instead of [%r].' %
120 (self.name, value))
121 return value.bytes_le
122
123 def Unpack(self, value):
124 return GUID(bytes_le=value)
125
126
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800127def BitProperty(getter, setter, shift, mask):
128 """A generator for bit-field properties.
129
130 This is used inside a class to manipulate an integer-like variable using
131 properties. The getter and setter should be member functions to change the
132 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800133
134 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800135 getter: a function to read integer type variable (for all the bits).
136 setter: a function to set the new changed integer type variable.
137 shift: integer for how many bits should be shifted (right).
138 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800139 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800140 def _getter(self):
141 return (getter(self) >> shift) & mask
142 def _setter(self, value):
143 assert value & mask == value, (
144 'Value %s out of range (mask=%s)' % (value, mask))
145 setter(self, getter(self) & ~(mask << shift) | value << shift)
146 return property(_getter, _setter)
147
148
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800149class PartitionAttributes(object):
150 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800151
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800152 This can be created using Partition.attrs, but the changed properties won't
153 apply to underlying Partition until an explicit call with
154 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800155 """
156
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800157 def __init__(self, attrs):
158 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800159
160 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800161 def raw(self):
162 """Returns the raw integer type attributes."""
163 return self._Get()
164
165 def _Get(self):
166 return self._attrs
167
168 def _Set(self, value):
169 self._attrs = value
170
171 successful = BitProperty(_Get, _Set, 56, 1)
172 tries = BitProperty(_Get, _Set, 52, 0xf)
173 priority = BitProperty(_Get, _Set, 48, 0xf)
174 legacy_boot = BitProperty(_Get, _Set, 2, 1)
175 required = BitProperty(_Get, _Set, 0, 1)
176 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
177
178
179class PartitionAttributeStructField(StructField):
180
181 def Pack(self, value):
182 if not isinstance(value, PartitionAttributes):
183 raise StructError('Given value %r is not %s.' %
184 (value, PartitionAttributes.__name__))
185 return value.raw
186
187 def Unpack(self, value):
188 return PartitionAttributes(value)
189
190
191# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
192# declared as 'signed' (l) instead of 'unsigned' (L).
193# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
194HEADER_FIELDS = [
195 StructField('8s', 'Signature'),
196 StructField('4s', 'Revision'),
197 StructField('L', 'HeaderSize'),
198 StructField('l', 'CRC32'),
199 StructField('4s', 'Reserved'),
200 StructField('Q', 'CurrentLBA'),
201 StructField('Q', 'BackupLBA'),
202 StructField('Q', 'FirstUsableLBA'),
203 StructField('Q', 'LastUsableLBA'),
204 GUIDStructField('DiskGUID'),
205 StructField('Q', 'PartitionEntriesStartingLBA'),
206 StructField('L', 'PartitionEntriesNumber'),
207 StructField('L', 'PartitionEntrySize'),
208 StructField('l', 'PartitionArrayCRC32'),
209]
210
211# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
212PARTITION_FIELDS = [
213 GUIDStructField('TypeGUID'),
214 GUIDStructField('UniqueGUID'),
215 StructField('Q', 'FirstLBA'),
216 StructField('Q', 'LastLBA'),
217 PartitionAttributeStructField('Q', 'Attributes'),
218 UTF16StructField(72, 'Names'),
219]
220
221# The PMBR has so many variants. The basic format is defined in
222# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
223# implementation, as derived from `cgpt`, is following syslinux as:
224# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
225PMBR_FIELDS = [
226 StructField('424s', 'BootCode'),
227 GUIDStructField('BootGUID'),
228 StructField('L', 'DiskID'),
229 StructField('2s', 'Magic'),
230 StructField('16s', 'LegacyPart0'),
231 StructField('16s', 'LegacyPart1'),
232 StructField('16s', 'LegacyPart2'),
233 StructField('16s', 'LegacyPart3'),
234 StructField('2s', 'Signature'),
235]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800236
237
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800238class GPTError(Exception):
239 """All exceptions by GPT."""
240 pass
241
242
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800243class GPTObject(object):
244 """A base object in GUID Partition Table.
245
246 All objects (for instance, header or partition entries) must inherit this
247 class and define the FIELD attribute with a list of field definitions using
248 StructField.
249
250 The 'name' in StructField will become the attribute name of GPT objects that
251 can be directly packed into / unpacked from. Derived (calculated from existing
252 attributes) attributes should be in lower_case.
253
254 It is also possible to attach some additional properties to the object as meta
255 data (for example path of the underlying image file). To do that, first
256 include it in __slots__ list and specify them as dictionary-type args in
257 constructors. These properties will be preserved when you call Clone().
258
259 To create a new object, call the constructor. Field data can be assigned as
260 in arguments, or give nothing to initialize as zero (see Zero()). Field data
261 and meta values can be also specified in keyword arguments (**kargs) at the
262 same time.
263
264 To read a object from file or stream, use class method ReadFrom(source).
265 To make changes, modify the field directly or use Update(dict), or create a
266 copy by Clone() first then Update.
267
268 To wipe all fields (but not meta), call Zero(). There is currently no way
269 to clear meta except setting them to None one by one.
270 """
271 __slots__ = []
272
Peter Shih533566a2018-09-05 17:48:03 +0800273 FIELDS = []
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800274 """A list of StructField definitions."""
275
276 def __init__(self, *args, **kargs):
277 if args:
278 if len(args) != len(self.FIELDS):
279 raise GPTError('%s need %s arguments (found %s).' %
280 (type(self).__name__, len(self.FIELDS), len(args)))
281 for f, value in zip(self.FIELDS, args):
282 setattr(self, f.name, value)
283 else:
284 self.Zero()
285
286 all_names = [f for f in self.__slots__]
Yilin Yangea784662019-09-26 13:51:03 +0800287 for name, value in iteritems(kargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800288 if name not in all_names:
289 raise GPTError('%s does not support keyword arg <%s>.' %
290 (type(self).__name__, name))
291 setattr(self, name, value)
292
293 def __iter__(self):
294 """An iterator to return all fields associated in the object."""
295 return (getattr(self, f.name) for f in self.FIELDS)
296
297 def __repr__(self):
298 return '(%s: %s)' % (type(self).__name__, ', '.join(
Peter Shihe6afab32018-09-11 17:16:48 +0800299 '%s=%r' % (f, getattr(self, f)) for f in self.__slots__))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800300
301 @classmethod
302 def GetStructFormat(cls):
303 """Returns a format string for struct to use."""
304 return '<' + ''.join(f.fmt for f in cls.FIELDS)
305
306 @classmethod
307 def ReadFrom(cls, source, **kargs):
308 """Returns an object from given source."""
309 obj = cls(**kargs)
310 obj.Unpack(source)
311 return obj
312
313 @property
314 def blob(self):
315 """The (packed) blob representation of the object."""
316 return self.Pack()
317
318 @property
319 def meta(self):
320 """Meta values (those not in GPT object fields)."""
321 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
322 return dict((name, getattr(self, name)) for name in metas)
323
324 def Unpack(self, source):
325 """Unpacks values from a given source.
326
327 Args:
328 source: a string of bytes or a file-like object to read from.
329 """
330 fmt = self.GetStructFormat()
331 if source is None:
332 source = '\x00' * struct.calcsize(fmt)
333 if not isinstance(source, basestring):
334 return self.Unpack(source.read(struct.calcsize(fmt)))
335 for f, value in zip(self.FIELDS, struct.unpack(fmt, source)):
336 setattr(self, f.name, f.Unpack(value))
337
338 def Pack(self):
339 """Packs values in all fields into a string by struct format."""
340 return struct.pack(self.GetStructFormat(),
341 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
342
343 def Clone(self):
344 """Clones a new instance."""
345 return type(self)(*self, **self.meta)
346
347 def Update(self, **dargs):
348 """Applies multiple values in current object."""
Yilin Yangea784662019-09-26 13:51:03 +0800349 for name, value in iteritems(dargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800350 setattr(self, name, value)
351
352 def Zero(self):
353 """Set all fields to values representing zero or empty.
354
355 Note the meta attributes won't be cleared.
356 """
357 class ZeroReader(object):
358 """A /dev/zero like stream."""
359
360 @staticmethod
361 def read(num):
362 return '\x00' * num
363
364 self.Unpack(ZeroReader())
365
366
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800367class GPT(object):
368 """A GPT helper class.
369
370 To load GPT from an existing disk image file, use `LoadFromFile`.
371 After modifications were made, use `WriteToFile` to commit changes.
372
373 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800374 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800375 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800376 partitions: a list of GPT partition entry nametuple.
377 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800378 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800379 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800380 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800381 # Old devices uses 'Basic data' type for stateful partition, and newer devices
382 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
383 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800384 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800385 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800386 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
387 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800388 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
389 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
390 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
391 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
392 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800393 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800394 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800395 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
Yilin Yangea784662019-09-26 13:51:03 +0800396 for k, v in iteritems(TYPE_GUID_MAP))
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800397 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
398 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
399 TYPE_GUID_LIST_BOOTABLE = [
400 TYPE_GUID_CHROMEOS_KERNEL,
401 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800402 ]
403
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800404 class ProtectiveMBR(GPTObject):
405 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800406 FIELDS = PMBR_FIELDS
407 __slots__ = [f.name for f in FIELDS]
408
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800409 SIGNATURE = '\x55\xAA'
410 MAGIC = '\x1d\x9a'
411
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800412 class Header(GPTObject):
413 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800414 FIELDS = HEADER_FIELDS
415 __slots__ = [f.name for f in FIELDS]
416
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800417 SIGNATURES = ['EFI PART', 'CHROMEOS']
418 SIGNATURE_IGNORE = 'IGNOREME'
419 DEFAULT_REVISION = '\x00\x00\x01\x00'
420
421 DEFAULT_PARTITION_ENTRIES = 128
422 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
423
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800424 @classmethod
425 def Create(cls, size, block_size, pad_blocks=0,
426 part_entries=DEFAULT_PARTITION_ENTRIES):
427 """Creates a header with default values.
428
429 Args:
430 size: integer of expected image size.
431 block_size: integer for size of each block (sector).
432 pad_blocks: number of preserved sectors between header and partitions.
433 part_entries: number of partitions to include in header.
434 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800435 PART_FORMAT = GPT.Partition.GetStructFormat()
436 FORMAT = cls.GetStructFormat()
437 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800438 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
439 parts_bytes = part_entries * part_entry_size
Yilin Yang14d02a22019-11-01 11:32:03 +0800440 parts_blocks = parts_bytes // block_size
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800441 if parts_bytes % block_size:
442 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800443 # CRC32 and PartitionsCRC32 must be updated later explicitly.
444 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800445 Signature=cls.SIGNATURES[0],
446 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800447 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800448 CurrentLBA=1,
Yilin Yang14d02a22019-11-01 11:32:03 +0800449 BackupLBA=size // block_size - 1,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800450 FirstUsableLBA=parts_lba + parts_blocks,
Yilin Yang14d02a22019-11-01 11:32:03 +0800451 LastUsableLBA=size // block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800452 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800453 PartitionEntriesStartingLBA=parts_lba,
454 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800455 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800456
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800457 def UpdateChecksum(self):
458 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800459
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800460 Note the PartitionArrayCRC32 is not touched - you have to make sure that
461 is correct before calling Header.UpdateChecksum().
462 """
463 self.Update(CRC32=0)
464 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800465
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800466 class Partition(GPTObject):
467 """The partition entry in GPT.
468
469 Please include following properties when creating a Partition object:
470 - image: a string for path to the image file the partition maps to.
471 - number: the 1-based partition number.
472 - block_size: an integer for size of each block (LBA, or sector).
473 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800474 FIELDS = PARTITION_FIELDS
475 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800476 NAMES_ENCODING = 'utf-16-le'
477 NAMES_LENGTH = 72
478
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800479 def __str__(self):
480 return '%s#%s' % (self.image, self.number)
481
482 def IsUnused(self):
483 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800484 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800485
Hung-Te Linfe724f82018-04-18 15:03:58 +0800486 def IsChromeOSKernel(self):
487 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800488 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800489
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800490 @property
491 def blocks(self):
492 """Return size of partition in blocks (see block_size)."""
493 return self.LastLBA - self.FirstLBA + 1
494
495 @property
496 def offset(self):
497 """Returns offset to partition in bytes."""
498 return self.FirstLBA * self.block_size
499
500 @property
501 def size(self):
502 """Returns size of partition in bytes."""
503 return self.blocks * self.block_size
504
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800505 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800506 """GPT constructor.
507
508 See LoadFromFile for how it's usually used.
509 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800510 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800511 self.header = None
512 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800513 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800514 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800515
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800516 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800517 def GetTypeGUID(cls, value):
518 """The value may be a GUID in string or a short type string."""
519 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
520 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800521
522 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800523 def Create(cls, image_name, size, block_size, pad_blocks=0):
524 """Creates a new GPT instance from given size and block_size.
525
526 Args:
527 image_name: a string of underlying disk image file name.
528 size: expected size of disk image.
529 block_size: size of each block (sector) in bytes.
530 pad_blocks: number of blocks between header and partitions array.
531 """
532 gpt = cls()
533 gpt.block_size = block_size
534 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
535 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800536 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800537 for i in xrange(gpt.header.PartitionEntriesNumber)]
538 return gpt
539
Hung-Te Lin446eb512018-05-02 18:39:16 +0800540 @staticmethod
541 def IsBlockDevice(image):
542 """Returns if the image is a block device file."""
543 return stat.S_ISBLK(os.stat(image).st_mode)
544
545 @classmethod
546 def GetImageSize(cls, image):
547 """Returns the size of specified image (plain or block device file)."""
548 if not cls.IsBlockDevice(image):
549 return os.path.getsize(image)
550
551 fd = os.open(image, os.O_RDONLY)
552 try:
553 return os.lseek(fd, 0, os.SEEK_END)
554 finally:
555 os.close(fd)
556
557 @classmethod
558 def GetLogicalBlockSize(cls, block_dev):
559 """Returns the logical block (sector) size from a block device file.
560
561 The underlying call is BLKSSZGET. An alternative command is blockdev,
562 but that needs root permission even if we just want to get sector size.
563 """
564 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
565 return int(subprocess.check_output(
566 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
567
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800568 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800569 def LoadFromFile(cls, image):
570 """Loads a GPT table from give disk image file object.
571
572 Args:
573 image: a string as file path or a file-like object to read from.
574 """
575 if isinstance(image, basestring):
576 with open(image, 'rb') as f:
577 return cls.LoadFromFile(f)
578
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800579 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800580 image.seek(0)
581 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
582 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
583 logging.debug('Found MBR signature in %s', image.name)
584 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
585 logging.debug('Found PMBR in %s', image.name)
586 gpt.pmbr = pmbr
587
Hung-Te Linf148d322018-04-13 10:24:42 +0800588 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800589 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
590 if cls.IsBlockDevice(image.name):
591 block_sizes = [cls.GetLogicalBlockSize(image.name)]
592
593 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800594 # Note because there are devices setting Primary as ignored and the
595 # partition table signature accepts 'CHROMEOS' which is also used by
596 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
597 # first before trying other block sizes, otherwise we may incorrectly
598 # identify a kernel partition as LBA 1 of larger block size system.
599 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
600 (-block_size, os.SEEK_END)]):
601 image.seek(*seek)
602 header = gpt.Header.ReadFrom(image)
603 if header.Signature in cls.Header.SIGNATURES:
604 gpt.block_size = block_size
605 if i != 0:
606 gpt.is_secondary = True
607 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800608 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800609 else:
610 # Nothing found, try next block size.
611 continue
612 # Found a valid signature.
613 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800614 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800615 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800616
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800617 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800618 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800619 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800620 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800621 return p
622
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800623 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800624 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800625 ReadPartition(image, i + 1)
626 for i in xrange(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800627 return gpt
628
Hung-Te Linc5196682018-04-18 22:59:59 +0800629 def GetUsedPartitions(self):
630 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800631
Hung-Te Linc5196682018-04-18 22:59:59 +0800632 Use 'number' property to find the real location of partition in
633 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800634 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800635 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800636
637 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800638 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800639 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800640 return (max(p.LastLBA for p in parts)
641 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800642
643 def GetPartitionTableBlocks(self, header=None):
644 """Returns the blocks (or LBA) of partition table from given header."""
645 if header is None:
646 header = self.header
647 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Yilin Yang14d02a22019-11-01 11:32:03 +0800648 blocks = size // self.block_size
Hung-Te Linf148d322018-04-13 10:24:42 +0800649 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800650 blocks += 1
651 return blocks
652
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800653 def GetPartition(self, number):
654 """Gets the Partition by given (1-based) partition number.
655
656 Args:
657 number: an integer as 1-based partition number.
658 """
659 if not 0 < number <= len(self.partitions):
660 raise GPTError('Invalid partition number %s.' % number)
661 return self.partitions[number - 1]
662
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800663 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800664 """Updates the entry in partition table by given Partition object.
665
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800666 Usually you only need to call this if you want to copy one partition to
667 different location (number of image).
668
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800669 Args:
670 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800671 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800672 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800673 ref = self.partitions[number - 1]
674 part = part.Clone()
675 part.number = number
676 part.image = ref.image
677 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800678 self.partitions[number - 1] = part
679
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800680 def GetSize(self):
681 return self.block_size * (self.header.BackupLBA + 1)
682
683 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800684 """Adjust GPT for a disk image in given size.
685
686 Args:
687 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800688 check_overlap: Checks if the backup partition table overlaps used
689 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800690 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800691 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800692 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800693 raise GPTError(
694 'New file size %d is not valid for image files.' % new_size)
Yilin Yang14d02a22019-11-01 11:32:03 +0800695 new_blocks = new_size // self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800696 if old_size != new_size:
697 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Yilin Yang14d02a22019-11-01 11:32:03 +0800698 new_size, new_blocks, old_size, old_size // self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800699 else:
700 logging.info('Image size (%d, LBA=%d) not changed.',
701 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800702 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800703
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800704 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800705 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800706 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800707
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800708 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800709 max_used_lba = self.GetMaxUsedLBA()
710 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800711 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800712
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800713 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800714
715 def GetFreeSpace(self):
716 """Returns the free (available) space left according to LastUsableLBA."""
717 max_lba = self.GetMaxUsedLBA()
718 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800719 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800720
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800721 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800722 """Expands a given partition to last usable LBA.
723
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800724 The size of the partition can actually be reduced if the last usable LBA
725 decreases.
726
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800727 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800728 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800729
730 Returns:
731 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800732 """
733 # Assume no partitions overlap, we need to make sure partition[i] has
734 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800735 p = self.GetPartition(number)
736 if p.IsUnused():
737 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800738 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800739 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800740 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800741 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800742 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800743
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800744 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800745 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800746 new_blocks = p.blocks
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800747 logging.warn(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800748 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800749 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800750
Hung-Te Lin3b491672018-04-19 01:41:20 +0800751 def CheckIntegrity(self):
752 """Checks if the GPT objects all look good."""
753 # Check if the header allocation looks good. CurrentLBA and
754 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
755 # LastUsableLBA].
756 header = self.header
757 entries_first_lba = header.PartitionEntriesStartingLBA
758 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
759
760 def CheckOutsideUsable(name, lba, outside_entries=False):
761 if lba < 1:
762 raise GPTError('%s should not live in LBA %s.' % (name, lba))
763 if lba > max(header.BackupLBA, header.CurrentLBA):
764 # Note this is "in theory" possible, but we want to report this as
765 # error as well, since it usually leads to error.
766 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
767 (name, lba, header.BackupLBA))
768 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
769 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
770 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
771 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
772 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
773 (name, lba, entries_first_lba, entries_last_lba))
774 CheckOutsideUsable('Header', header.CurrentLBA, True)
775 CheckOutsideUsable('Backup header', header.BackupLBA, True)
776 CheckOutsideUsable('Partition entries', entries_first_lba)
777 CheckOutsideUsable('Partition entries end', entries_last_lba)
778
779 parts = self.GetUsedPartitions()
780 # Check if partition entries overlap with each other.
781 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
782 lba_list.sort(key=lambda t: t[0])
783 for i in xrange(len(lba_list) - 1):
784 if lba_list[i][1] >= lba_list[i + 1][0]:
785 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
786 (lba_list[i] + lba_list[i + 1]))
787 # Now, check the first and last partition.
788 if lba_list:
789 p = lba_list[0][2]
790 if p.FirstLBA < header.FirstUsableLBA:
791 raise GPTError(
792 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
793 (p, p.FirstLBA, header.FirstLBA))
794 p = lba_list[-1][2]
795 if p.LastLBA > header.LastUsableLBA:
796 raise GPTError(
797 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
798 (p, p.LastLBA, header.LastLBA))
799 # Check if UniqueGUIDs are not unique.
800 if len(set(p.UniqueGUID for p in parts)) != len(parts):
801 raise GPTError('Partition UniqueGUIDs are duplicated.')
802 # Check if CRCs match.
803 if (binascii.crc32(''.join(p.blob for p in self.partitions)) !=
804 header.PartitionArrayCRC32):
805 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800806 header_crc = header.Clone()
807 header_crc.UpdateChecksum()
808 if header_crc.CRC32 != header.CRC32:
809 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800810
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800811 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800812 """Updates all checksum fields in GPT objects."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800813 parts = ''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800814 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
815 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800816
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800817 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800818 """Returns the backup header according to given header.
819
820 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
821 fields.
822 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800823 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800824 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800825 h = header.Clone()
826 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800827 BackupLBA=header.CurrentLBA,
828 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800829 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800830 h.UpdateChecksum()
831 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800832
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800833 @classmethod
834 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
835 """Writes a protective MBR to given file.
836
837 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
838 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
839 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
840 we can follow that.
841
842 Args:
843 create: True to re-create PMBR structure.
844 bootcode: a blob of new boot code.
845 boot_guid a blob for new boot GUID.
846
847 Returns:
848 The written PMBR structure.
849 """
850 if isinstance(image, basestring):
851 with open(image, 'rb+') as f:
852 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
853
854 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800855 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
856 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800857 pmbr = cls.ProtectiveMBR.ReadFrom(image)
858
859 if create:
860 legacy_sectors = min(
861 0x100000000,
Yilin Yang14d02a22019-11-01 11:32:03 +0800862 GPT.GetImageSize(image.name) // cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800863 # Partition 0 must have have the fixed CHS with number of sectors
864 # (calculated as legacy_sectors later).
Yilin Yangf9fe1932019-11-04 17:09:34 +0800865 part0 = (codecs.decode('00000200eeffffff01000000', 'hex') +
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800866 struct.pack('<I', legacy_sectors))
867 # Partition 1~3 should be all zero.
868 part1 = '\x00' * 16
869 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800870 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800871 BootGUID=cls.TYPE_GUID_UNUSED,
872 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800873 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800874 LegacyPart0=part0,
875 LegacyPart1=part1,
876 LegacyPart2=part1,
877 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800878 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800879
880 if bootcode:
881 if len(bootcode) > len(pmbr.BootCode):
882 logging.info(
883 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
884 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800885 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800886 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800887 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800888
889 blob = pmbr.blob
890 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
891 image.seek(0)
892 image.write(blob)
893 return pmbr
894
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800895 def WriteToFile(self, image):
896 """Updates partition table in a disk image file.
897
898 Args:
899 image: a string as file path or a file-like object to write into.
900 """
901 if isinstance(image, basestring):
902 with open(image, 'rb+') as f:
903 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800904
905 def WriteData(name, blob, lba):
906 """Writes a blob into given location."""
907 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800908 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800909 image.seek(lba * self.block_size)
910 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800911
912 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800913 self.CheckIntegrity()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800914 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800915
916 header = self.header
917 WriteData('GPT Header', header.blob, header.CurrentLBA)
918 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
919 logging.info(
920 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
921 header.LastUsableLBA)
922
923 if not self.is_secondary:
924 # When is_secondary is True, the header we have is actually backup header.
925 backup_header = self.GetBackupHeader(self.header)
926 WriteData(
927 'Backup Partitions', parts_blob,
928 backup_header.PartitionEntriesStartingLBA)
929 WriteData(
930 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800931
932
933class GPTCommands(object):
934 """Collection of GPT sub commands for command line to use.
935
936 The commands are derived from `cgpt`, but not necessary to be 100% compatible
937 with cgpt.
938 """
939
940 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800941 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800942 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800943 ('type', 'type guid'),
944 ('unique', 'unique guid'),
945 ('label', 'label'),
946 ('Successful', 'Successful flag'),
947 ('Tries', 'Tries flag'),
948 ('Priority', 'Priority flag'),
949 ('Legacy', 'Legacy Boot flag'),
950 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800951
952 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800953 commands = dict(
954 (command.lower(), getattr(self, command)())
955 for command in dir(self)
956 if (isinstance(getattr(self, command), type) and
957 issubclass(getattr(self, command), self.SubCommand) and
958 getattr(self, command) is not self.SubCommand)
959 )
960 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800961
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800962 def DefineArgs(self, parser):
963 """Defines all available commands to an argparser subparsers instance."""
964 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
Yilin Yangea784662019-09-26 13:51:03 +0800965 for name, instance in sorted(iteritems(self.commands)):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800966 parser = subparsers.add_parser(
967 name, description=instance.__doc__,
968 formatter_class=argparse.RawDescriptionHelpFormatter,
969 help=instance.__doc__.splitlines()[0])
970 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800971
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800972 def Execute(self, args):
973 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800974 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800975
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800976 class SubCommand(object):
977 """A base class for sub commands to derive from."""
978
979 def DefineArgs(self, parser):
980 """Defines command line arguments to argparse parser.
981
982 Args:
983 parser: An argparse parser instance.
984 """
985 del parser # Unused.
986 raise NotImplementedError
987
988 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800989 """Execute the command with parsed arguments.
990
991 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800992
993 Args:
994 args: An argparse parsed namespace.
995 """
996 del args # Unused.
997 raise NotImplementedError
998
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800999 def ExecuteCommandLine(self, *args):
1000 """Execute as invoked from command line.
1001
1002 This provides an easy way to execute particular sub command without
1003 creating argument parser explicitly.
1004
1005 Args:
1006 args: a list of string type command line arguments.
1007 """
1008 parser = argparse.ArgumentParser()
1009 self.DefineArgs(parser)
1010 return self.Execute(parser.parse_args(args))
1011
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001012 class Create(SubCommand):
1013 """Create or reset GPT headers and tables.
1014
1015 Create or reset an empty GPT.
1016 """
1017
1018 def DefineArgs(self, parser):
1019 parser.add_argument(
1020 '-z', '--zero', action='store_true',
1021 help='Zero the sectors of the GPT table and entries')
1022 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001023 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001024 help=('Size (in blocks) of the disk to pad between the '
1025 'primary GPT header and its entries, default %(default)s'))
1026 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001027 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001028 help='Size of each block (sector) in bytes.')
1029 parser.add_argument(
1030 'image_file', type=argparse.FileType('rb+'),
1031 help='Disk image file to create.')
1032
1033 def Execute(self, args):
1034 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001035 if block_size is None:
1036 if GPT.IsBlockDevice(args.image_file.name):
1037 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1038 else:
1039 block_size = GPT.DEFAULT_BLOCK_SIZE
1040
1041 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1042 logging.info('Block (sector) size for %s is set to %s bytes.',
1043 args.image_file.name, block_size)
1044
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001045 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001046 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001047 block_size, args.pad_blocks)
1048 if args.zero:
1049 # In theory we only need to clear LBA 1, but to make sure images already
1050 # initialized with different block size won't have GPT signature in
1051 # different locations, we should zero until first usable LBA.
1052 args.image_file.seek(0)
1053 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
1054 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001055 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001056 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001057
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001058 class Boot(SubCommand):
1059 """Edit the PMBR sector for legacy BIOSes.
1060
1061 With no options, it will just print the PMBR boot guid.
1062 """
1063
1064 def DefineArgs(self, parser):
1065 parser.add_argument(
1066 '-i', '--number', type=int,
1067 help='Set bootable partition')
1068 parser.add_argument(
1069 '-b', '--bootloader', type=argparse.FileType('r'),
1070 help='Install bootloader code in the PMBR')
1071 parser.add_argument(
1072 '-p', '--pmbr', action='store_true',
1073 help='Create legacy PMBR partition table')
1074 parser.add_argument(
1075 'image_file', type=argparse.FileType('rb+'),
1076 help='Disk image file to change PMBR.')
1077
1078 def Execute(self, args):
1079 """Rebuilds the protective MBR."""
1080 bootcode = args.bootloader.read() if args.bootloader else None
1081 boot_guid = None
1082 if args.number is not None:
1083 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001084 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001085 pmbr = GPT.WriteProtectiveMBR(
1086 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1087
You-Cheng Syufff7f422018-05-14 15:37:39 +08001088 print(pmbr.BootGUID)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001089 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001090 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001091
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001092 class Legacy(SubCommand):
1093 """Switch between GPT and Legacy GPT.
1094
1095 Switch GPT header signature to "CHROMEOS".
1096 """
1097
1098 def DefineArgs(self, parser):
1099 parser.add_argument(
1100 '-e', '--efi', action='store_true',
1101 help='Switch GPT header signature back to "EFI PART"')
1102 parser.add_argument(
1103 '-p', '--primary-ignore', action='store_true',
1104 help='Switch primary GPT header signature to "IGNOREME"')
1105 parser.add_argument(
1106 'image_file', type=argparse.FileType('rb+'),
1107 help='Disk image file to change.')
1108
1109 def Execute(self, args):
1110 gpt = GPT.LoadFromFile(args.image_file)
1111 # cgpt behavior: if -p is specified, -e is ignored.
1112 if args.primary_ignore:
1113 if gpt.is_secondary:
1114 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1115 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1116 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1117 gpt.header = gpt.GetBackupHeader(self.header)
1118 gpt.is_secondary = True
1119 else:
1120 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001121 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001122 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001123 args.image_file.close()
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001124 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001125 return ('Set %s primary GPT header to %s.' %
1126 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001127 return ('Changed GPT signature for %s to %s.' %
1128 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001129
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001130 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001131 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001132
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001133 def DefineArgs(self, parser):
1134 parser.add_argument(
1135 'image_file', type=argparse.FileType('rb+'),
1136 help='Disk image file to repair.')
1137
1138 def Execute(self, args):
1139 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001140 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001141 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001142 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001143 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001144
1145 class Expand(SubCommand):
1146 """Expands a GPT partition to all available free space."""
1147
1148 def DefineArgs(self, parser):
1149 parser.add_argument(
1150 '-i', '--number', type=int, required=True,
1151 help='The partition to expand.')
1152 parser.add_argument(
1153 'image_file', type=argparse.FileType('rb+'),
1154 help='Disk image file to modify.')
1155
1156 def Execute(self, args):
1157 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001158 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001159 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001160 args.image_file.close()
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001161 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001162 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001163 'Partition %s on disk image file %s has been extended '
1164 'from %s to %s .' %
1165 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1166 new_blocks * gpt.block_size))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001167 return ('Nothing to expand for disk image %s partition %s.' %
1168 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001169
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001170 class Add(SubCommand):
1171 """Add, edit, or remove a partition entry.
1172
1173 Use the -i option to modify an existing partition.
1174 The -b, -s, and -t options must be given for new partitions.
1175
1176 The partition type may also be given as one of these aliases:
1177
1178 firmware ChromeOS firmware
1179 kernel ChromeOS kernel
1180 rootfs ChromeOS rootfs
1181 data Linux data
1182 reserved ChromeOS reserved
1183 efi EFI System Partition
1184 unused Unused (nonexistent) partition
1185 """
1186 def DefineArgs(self, parser):
1187 parser.add_argument(
1188 '-i', '--number', type=int,
1189 help='Specify partition (default is next available)')
1190 parser.add_argument(
1191 '-b', '--begin', type=int,
1192 help='Beginning sector')
1193 parser.add_argument(
1194 '-s', '--sectors', type=int,
1195 help='Size in sectors (logical blocks).')
1196 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001197 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001198 help='Partition Type GUID')
1199 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001200 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001201 help='Partition Unique ID')
1202 parser.add_argument(
1203 '-l', '--label',
1204 help='Label')
1205 parser.add_argument(
1206 '-S', '--successful', type=int, choices=xrange(2),
1207 help='set Successful flag')
1208 parser.add_argument(
1209 '-T', '--tries', type=int,
1210 help='set Tries flag (0-15)')
1211 parser.add_argument(
1212 '-P', '--priority', type=int,
1213 help='set Priority flag (0-15)')
1214 parser.add_argument(
1215 '-R', '--required', type=int, choices=xrange(2),
1216 help='set Required flag')
1217 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001218 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001219 choices=xrange(2),
1220 help='set Legacy Boot flag')
1221 parser.add_argument(
1222 '-A', '--attribute', dest='raw_16', type=int,
1223 help='set raw 16-bit attribute value (bits 48-63)')
1224 parser.add_argument(
1225 'image_file', type=argparse.FileType('rb+'),
1226 help='Disk image file to modify.')
1227
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001228 def Execute(self, args):
1229 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001230 number = args.number
1231 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001232 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001233
1234 # First and last LBA must be calculated explicitly because the given
1235 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001236 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001237 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001238
1239 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001240 part.Zero()
1241 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001242 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001243 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001244 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001245 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001246
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001247 def UpdateAttr(name):
1248 value = getattr(args, name)
1249 if value is None:
1250 return
1251 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001252
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001253 def GetArg(arg_value, default_value):
1254 return default_value if arg_value is None else arg_value
1255
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001256 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001257 for name in ['legacy_boot', 'required', 'priority', 'tries',
1258 'successful', 'raw_16']:
1259 UpdateAttr(name)
1260 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001261 part.Update(
1262 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001263 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001264 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1265 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1266 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1267 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001268
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001269 # Wipe partition again if it should be empty.
1270 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001271 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001272
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001273 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001274 args.image_file.close()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001275 if part.IsUnused():
1276 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001277 return 'Deleted (zeroed) %s.' % (part,)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001278 return ('%s %s (%s+%s).' %
1279 ('Added' if is_new_part else 'Modified',
1280 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001281
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001282 class Show(SubCommand):
1283 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001284
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001285 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001286 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001287
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001288 def DefineArgs(self, parser):
1289 parser.add_argument(
1290 '--numeric', '-n', action='store_true',
1291 help='Numeric output only.')
1292 parser.add_argument(
1293 '--quick', '-q', action='store_true',
1294 help='Quick output.')
1295 parser.add_argument(
1296 '-i', '--number', type=int,
1297 help='Show specified partition only, with format args.')
1298 for name, help_str in GPTCommands.FORMAT_ARGS:
1299 # TODO(hungte) Alert if multiple args were specified.
1300 parser.add_argument(
1301 '--%s' % name, '-%c' % name[0], action='store_true',
1302 help='[format] %s.' % help_str)
1303 parser.add_argument(
1304 'image_file', type=argparse.FileType('rb'),
1305 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001306
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001307 def Execute(self, args):
1308 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001309
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001310 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001311 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001312 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001313 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001314 if names:
1315 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001316 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001317
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001318 def IsBootableType(guid):
1319 if not guid:
1320 return False
1321 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001322
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001323 def FormatAttribute(attrs, chromeos_kernel=False):
1324 if args.numeric:
1325 return '[%x]' % (attrs.raw >> 48)
1326 results = []
1327 if chromeos_kernel:
1328 results += [
1329 'priority=%d' % attrs.priority,
1330 'tries=%d' % attrs.tries,
1331 'successful=%d' % attrs.successful]
1332 if attrs.required:
1333 results += ['required=1']
1334 if attrs.legacy_boot:
1335 results += ['legacy_boot=1']
1336 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001337
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001338 def ApplyFormatArgs(p):
1339 if args.begin:
1340 return p.FirstLBA
1341 elif args.size:
1342 return p.blocks
1343 elif args.type:
1344 return FormatTypeGUID(p)
1345 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001346 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001347 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001348 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001349 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001350 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001351 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001352 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001353 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001354 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001355 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001356 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001357 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001358 return '[%x]' % (p.Attributes.raw >> 48)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001359 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001360
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001361 def IsFormatArgsSpecified():
1362 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001363
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001364 gpt = GPT.LoadFromFile(args.image_file)
1365 logging.debug('%r', gpt.header)
1366 fmt = '%12s %11s %7s %s'
1367 fmt2 = '%32s %s: %s'
1368 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001369
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001370 if IsFormatArgsSpecified() and args.number is None:
1371 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001372
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001373 if not (args.number is None or
1374 0 < args.number <= gpt.header.PartitionEntriesNumber):
1375 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001376
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001377 partitions = gpt.partitions
1378 do_print_gpt_blocks = False
1379 if not (args.quick or IsFormatArgsSpecified()):
1380 print(fmt % header)
1381 if args.number is None:
1382 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001383
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001384 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001385 if gpt.pmbr:
1386 print(fmt % (0, 1, '', 'PMBR'))
1387 if gpt.is_secondary:
1388 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1389 else:
1390 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1391 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1392 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001393
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001394 for p in partitions:
1395 if args.number is None:
1396 # Skip unused partitions.
1397 if p.IsUnused():
1398 continue
1399 elif p.number != args.number:
1400 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001401
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001402 if IsFormatArgsSpecified():
1403 print(ApplyFormatArgs(p))
1404 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001405
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001406 print(fmt % (p.FirstLBA, p.blocks, p.number,
1407 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001408 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001409
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001410 if not args.quick:
1411 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001412 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1413 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001414 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001415 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001416
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001417 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001418 if gpt.is_secondary:
1419 header = gpt.header
1420 else:
1421 f = args.image_file
1422 f.seek(gpt.header.BackupLBA * gpt.block_size)
1423 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001424 print(fmt % (header.PartitionEntriesStartingLBA,
1425 gpt.GetPartitionTableBlocks(header), '',
1426 'Sec GPT table'))
1427 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001428
Hung-Te Lin3b491672018-04-19 01:41:20 +08001429 # Check integrity after showing all fields.
1430 gpt.CheckIntegrity()
1431
Hung-Te Linfe724f82018-04-18 15:03:58 +08001432 class Prioritize(SubCommand):
1433 """Reorder the priority of all kernel partitions.
1434
1435 Reorder the priority of all active ChromeOS Kernel partitions.
1436
1437 With no options this will set the lowest active kernel to priority 1 while
1438 maintaining the original order.
1439 """
1440
1441 def DefineArgs(self, parser):
1442 parser.add_argument(
1443 '-P', '--priority', type=int,
1444 help=('Highest priority to use in the new ordering. '
1445 'The other partitions will be ranked in decreasing '
1446 'priority while preserving their original order. '
1447 'If necessary the lowest ranks will be coalesced. '
1448 'No active kernels will be lowered to priority 0.'))
1449 parser.add_argument(
1450 '-i', '--number', type=int,
1451 help='Specify the partition to make the highest in the new order.')
1452 parser.add_argument(
1453 '-f', '--friends', action='store_true',
1454 help=('Friends of the given partition (those with the same '
1455 'starting priority) are also updated to the new '
1456 'highest priority. '))
1457 parser.add_argument(
1458 'image_file', type=argparse.FileType('rb+'),
1459 help='Disk image file to prioritize.')
1460
1461 def Execute(self, args):
1462 gpt = GPT.LoadFromFile(args.image_file)
1463 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001464 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1465 groups = dict((k, list(g)) for k, g in itertools.groupby(
1466 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001467 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001468 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001469 if p not in parts:
1470 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001471 pri = p.Attributes.priority
1472 friends = groups.pop(pri)
1473 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001474 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001475 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001476 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001477 groups[new_pri] = [p]
1478 friends.remove(p)
1479 if friends:
1480 groups[pri] = friends
1481
1482 if 0 in groups:
1483 # Do not change any partitions with priority=0
1484 groups.pop(0)
1485
Yilin Yang78fa12e2019-09-25 14:21:10 +08001486 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001487 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001488
1489 # Max priority is 0xf.
1490 highest = min(args.priority or len(prios), 0xf)
1491 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001492
Hung-Te Lin138389f2018-05-15 17:55:00 +08001493 for i, pri in enumerate(prios):
1494 new_priority = max(1, highest - i)
1495 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001496 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001497 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001498 if old_priority == new_priority:
1499 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001500 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001501 if attrs.tries < 1 and not attrs.successful:
1502 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001503 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001504 logging.info('%s priority changed from %s to %s.', p, old_priority,
1505 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001506
1507 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001508 args.image_file.close()
Hung-Te Linfe724f82018-04-18 15:03:58 +08001509
Hung-Te Linf641d302018-04-18 15:09:35 +08001510 class Find(SubCommand):
1511 """Locate a partition by its GUID.
1512
1513 Find a partition by its UUID or label. With no specified DRIVE it scans all
1514 physical drives.
1515
1516 The partition type may also be given as one of these aliases:
1517
1518 firmware ChromeOS firmware
1519 kernel ChromeOS kernel
1520 rootfs ChromeOS rootfs
1521 data Linux data
1522 reserved ChromeOS reserved
1523 efi EFI System Partition
1524 unused Unused (nonexistent) partition
1525 """
1526 def DefineArgs(self, parser):
1527 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001528 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001529 help='Search for Partition Type GUID')
1530 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001531 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001532 help='Search for Partition Unique GUID')
1533 parser.add_argument(
1534 '-l', '--label',
1535 help='Search for Label')
1536 parser.add_argument(
1537 '-n', '--numeric', action='store_true',
1538 help='Numeric output only.')
1539 parser.add_argument(
1540 '-1', '--single-match', action='store_true',
1541 help='Fail if more than one match is found.')
1542 parser.add_argument(
1543 '-M', '--match-file', type=str,
1544 help='Matching partition data must also contain MATCH_FILE content.')
1545 parser.add_argument(
1546 '-O', '--offset', type=int, default=0,
1547 help='Byte offset into partition to match content (default 0).')
1548 parser.add_argument(
1549 'drive', type=argparse.FileType('rb+'), nargs='?',
1550 help='Drive or disk image file to find.')
1551
1552 def Execute(self, args):
1553 if not any((args.type_guid, args.unique_guid, args.label)):
1554 raise GPTError('You must specify at least one of -t, -u, or -l')
1555
1556 drives = [args.drive.name] if args.drive else (
1557 '/dev/%s' % name for name in subprocess.check_output(
1558 'lsblk -d -n -r -o name', shell=True).split())
1559
1560 match_pattern = None
1561 if args.match_file:
1562 with open(args.match_file) as f:
1563 match_pattern = f.read()
1564
1565 found = 0
1566 for drive in drives:
1567 try:
1568 gpt = GPT.LoadFromFile(drive)
1569 except GPTError:
1570 if args.drive:
1571 raise
1572 # When scanning all block devices on system, ignore failure.
1573
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001574 def Unmatch(a, b):
1575 return a is not None and a != b
1576
Hung-Te Linf641d302018-04-18 15:09:35 +08001577 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001578 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001579 Unmatch(args.label, p.Names) or
1580 Unmatch(args.unique_guid, p.UniqueGUID) or
1581 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001582 continue
1583 if match_pattern:
1584 with open(drive, 'rb') as f:
1585 f.seek(p.offset + args.offset)
1586 if f.read(len(match_pattern)) != match_pattern:
1587 continue
1588 # Found the partition, now print.
1589 found += 1
1590 if args.numeric:
1591 print(p.number)
1592 else:
1593 # This is actually more for block devices.
1594 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1595 p.number))
1596
1597 if found < 1 or (args.single_match and found > 1):
1598 return 1
1599 return 0
1600
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001601
1602def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001603 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001604 parser = argparse.ArgumentParser(description='GPT Utility.')
1605 parser.add_argument('--verbose', '-v', action='count', default=0,
1606 help='increase verbosity.')
1607 parser.add_argument('--debug', '-d', action='store_true',
1608 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001609 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001610
1611 args = parser.parse_args()
1612 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1613 if args.debug:
1614 log_level = logging.DEBUG
1615 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1616 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001617 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001618 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001619 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001620 sys.exit(code)
Hung-Te Linbad46112018-05-15 16:39:14 +08001621 elif isinstance(code, basestring):
1622 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001623 except Exception as e:
1624 if args.verbose or args.debug:
1625 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001626 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001627
1628
1629if __name__ == '__main__':
1630 main()