blob: cd4c0b554dbb0ed2e9955a886ed8cb4fc1a9eb01 [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
Hung-Te Lin138389f2018-05-15 17:55:00 +080033import itertools
Hung-Te Linc772e1a2017-04-14 16:50:50 +080034import logging
35import os
Hung-Te Lin446eb512018-05-02 18:39:16 +080036import stat
Hung-Te Linc772e1a2017-04-14 16:50:50 +080037import struct
Hung-Te Linf641d302018-04-18 15:09:35 +080038import subprocess
39import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080040import uuid
41
Yilin Yangea784662019-09-26 13:51:03 +080042from six import iteritems
Yilin Yange6639682019-10-03 12:49:21 +080043from six.moves import xrange
44
Hung-Te Linc772e1a2017-04-14 16:50:50 +080045
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080046class StructError(Exception):
47 """Exceptions in packing and unpacking from/to struct fields."""
48 pass
Hung-Te Linc772e1a2017-04-14 16:50:50 +080049
Hung-Te Linc772e1a2017-04-14 16:50:50 +080050
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080051class StructField(object):
52 """Definition of a field in struct.
53
54 Attributes:
55 fmt: a format string for struct.{pack,unpack} to use.
56 name: a string for name of processed field.
57 """
58 __slots__ = ['fmt', 'name']
59
60 def __init__(self, fmt, name):
61 self.fmt = fmt
62 self.name = name
63
64 def Pack(self, value):
65 """"Packs given value from given format."""
66 del self # Unused.
67 return value
68
69 def Unpack(self, value):
70 """Unpacks given value into given format."""
71 del self # Unused.
72 return value
73
74
75class UTF16StructField(StructField):
76 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080077 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080078 encoding = 'utf-16-le'
79
80 def __init__(self, max_length, name):
81 self.max_length = max_length
82 fmt = '%ds' % max_length
83 super(UTF16StructField, self).__init__(fmt, name)
84
85 def Pack(self, value):
86 new_value = value.encode(self.encoding)
87 if len(new_value) >= self.max_length:
88 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
89 (value, self.name, self.max_length))
90 return new_value
91
92 def Unpack(self, value):
93 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080094
Hung-Te Linbf8aa272018-04-19 03:02:29 +080095
96class GUID(uuid.UUID):
97 """A special UUID that defaults to upper case in str()."""
98
99 def __str__(self):
100 """Returns GUID in upper case."""
101 return super(GUID, self).__str__().upper()
102
103 @staticmethod
104 def Random():
105 return uuid.uuid4()
106
107
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800108class GUIDStructField(StructField):
109 """A GUID field."""
110
111 def __init__(self, name):
112 super(GUIDStructField, self).__init__('16s', name)
113
114 def Pack(self, value):
115 if value is None:
116 return '\x00' * 16
117 if not isinstance(value, uuid.UUID):
118 raise StructError('Field %s needs a GUID value instead of [%r].' %
119 (self.name, value))
120 return value.bytes_le
121
122 def Unpack(self, value):
123 return GUID(bytes_le=value)
124
125
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800126def BitProperty(getter, setter, shift, mask):
127 """A generator for bit-field properties.
128
129 This is used inside a class to manipulate an integer-like variable using
130 properties. The getter and setter should be member functions to change the
131 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800132
133 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800134 getter: a function to read integer type variable (for all the bits).
135 setter: a function to set the new changed integer type variable.
136 shift: integer for how many bits should be shifted (right).
137 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800138 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800139 def _getter(self):
140 return (getter(self) >> shift) & mask
141 def _setter(self, value):
142 assert value & mask == value, (
143 'Value %s out of range (mask=%s)' % (value, mask))
144 setter(self, getter(self) & ~(mask << shift) | value << shift)
145 return property(_getter, _setter)
146
147
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800148class PartitionAttributes(object):
149 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800150
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800151 This can be created using Partition.attrs, but the changed properties won't
152 apply to underlying Partition until an explicit call with
153 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800154 """
155
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800156 def __init__(self, attrs):
157 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800158
159 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800160 def raw(self):
161 """Returns the raw integer type attributes."""
162 return self._Get()
163
164 def _Get(self):
165 return self._attrs
166
167 def _Set(self, value):
168 self._attrs = value
169
170 successful = BitProperty(_Get, _Set, 56, 1)
171 tries = BitProperty(_Get, _Set, 52, 0xf)
172 priority = BitProperty(_Get, _Set, 48, 0xf)
173 legacy_boot = BitProperty(_Get, _Set, 2, 1)
174 required = BitProperty(_Get, _Set, 0, 1)
175 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
176
177
178class PartitionAttributeStructField(StructField):
179
180 def Pack(self, value):
181 if not isinstance(value, PartitionAttributes):
182 raise StructError('Given value %r is not %s.' %
183 (value, PartitionAttributes.__name__))
184 return value.raw
185
186 def Unpack(self, value):
187 return PartitionAttributes(value)
188
189
190# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
191# declared as 'signed' (l) instead of 'unsigned' (L).
192# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
193HEADER_FIELDS = [
194 StructField('8s', 'Signature'),
195 StructField('4s', 'Revision'),
196 StructField('L', 'HeaderSize'),
197 StructField('l', 'CRC32'),
198 StructField('4s', 'Reserved'),
199 StructField('Q', 'CurrentLBA'),
200 StructField('Q', 'BackupLBA'),
201 StructField('Q', 'FirstUsableLBA'),
202 StructField('Q', 'LastUsableLBA'),
203 GUIDStructField('DiskGUID'),
204 StructField('Q', 'PartitionEntriesStartingLBA'),
205 StructField('L', 'PartitionEntriesNumber'),
206 StructField('L', 'PartitionEntrySize'),
207 StructField('l', 'PartitionArrayCRC32'),
208]
209
210# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
211PARTITION_FIELDS = [
212 GUIDStructField('TypeGUID'),
213 GUIDStructField('UniqueGUID'),
214 StructField('Q', 'FirstLBA'),
215 StructField('Q', 'LastLBA'),
216 PartitionAttributeStructField('Q', 'Attributes'),
217 UTF16StructField(72, 'Names'),
218]
219
220# The PMBR has so many variants. The basic format is defined in
221# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
222# implementation, as derived from `cgpt`, is following syslinux as:
223# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
224PMBR_FIELDS = [
225 StructField('424s', 'BootCode'),
226 GUIDStructField('BootGUID'),
227 StructField('L', 'DiskID'),
228 StructField('2s', 'Magic'),
229 StructField('16s', 'LegacyPart0'),
230 StructField('16s', 'LegacyPart1'),
231 StructField('16s', 'LegacyPart2'),
232 StructField('16s', 'LegacyPart3'),
233 StructField('2s', 'Signature'),
234]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800235
236
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800237class GPTError(Exception):
238 """All exceptions by GPT."""
239 pass
240
241
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800242class GPTObject(object):
243 """A base object in GUID Partition Table.
244
245 All objects (for instance, header or partition entries) must inherit this
246 class and define the FIELD attribute with a list of field definitions using
247 StructField.
248
249 The 'name' in StructField will become the attribute name of GPT objects that
250 can be directly packed into / unpacked from. Derived (calculated from existing
251 attributes) attributes should be in lower_case.
252
253 It is also possible to attach some additional properties to the object as meta
254 data (for example path of the underlying image file). To do that, first
255 include it in __slots__ list and specify them as dictionary-type args in
256 constructors. These properties will be preserved when you call Clone().
257
258 To create a new object, call the constructor. Field data can be assigned as
259 in arguments, or give nothing to initialize as zero (see Zero()). Field data
260 and meta values can be also specified in keyword arguments (**kargs) at the
261 same time.
262
263 To read a object from file or stream, use class method ReadFrom(source).
264 To make changes, modify the field directly or use Update(dict), or create a
265 copy by Clone() first then Update.
266
267 To wipe all fields (but not meta), call Zero(). There is currently no way
268 to clear meta except setting them to None one by one.
269 """
270 __slots__ = []
271
Peter Shih533566a2018-09-05 17:48:03 +0800272 FIELDS = []
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800273 """A list of StructField definitions."""
274
275 def __init__(self, *args, **kargs):
276 if args:
277 if len(args) != len(self.FIELDS):
278 raise GPTError('%s need %s arguments (found %s).' %
279 (type(self).__name__, len(self.FIELDS), len(args)))
280 for f, value in zip(self.FIELDS, args):
281 setattr(self, f.name, value)
282 else:
283 self.Zero()
284
285 all_names = [f for f in self.__slots__]
Yilin Yangea784662019-09-26 13:51:03 +0800286 for name, value in iteritems(kargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800287 if name not in all_names:
288 raise GPTError('%s does not support keyword arg <%s>.' %
289 (type(self).__name__, name))
290 setattr(self, name, value)
291
292 def __iter__(self):
293 """An iterator to return all fields associated in the object."""
294 return (getattr(self, f.name) for f in self.FIELDS)
295
296 def __repr__(self):
297 return '(%s: %s)' % (type(self).__name__, ', '.join(
Peter Shihe6afab32018-09-11 17:16:48 +0800298 '%s=%r' % (f, getattr(self, f)) for f in self.__slots__))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800299
300 @classmethod
301 def GetStructFormat(cls):
302 """Returns a format string for struct to use."""
303 return '<' + ''.join(f.fmt for f in cls.FIELDS)
304
305 @classmethod
306 def ReadFrom(cls, source, **kargs):
307 """Returns an object from given source."""
308 obj = cls(**kargs)
309 obj.Unpack(source)
310 return obj
311
312 @property
313 def blob(self):
314 """The (packed) blob representation of the object."""
315 return self.Pack()
316
317 @property
318 def meta(self):
319 """Meta values (those not in GPT object fields)."""
320 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
321 return dict((name, getattr(self, name)) for name in metas)
322
323 def Unpack(self, source):
324 """Unpacks values from a given source.
325
326 Args:
327 source: a string of bytes or a file-like object to read from.
328 """
329 fmt = self.GetStructFormat()
330 if source is None:
331 source = '\x00' * struct.calcsize(fmt)
332 if not isinstance(source, basestring):
333 return self.Unpack(source.read(struct.calcsize(fmt)))
334 for f, value in zip(self.FIELDS, struct.unpack(fmt, source)):
335 setattr(self, f.name, f.Unpack(value))
336
337 def Pack(self):
338 """Packs values in all fields into a string by struct format."""
339 return struct.pack(self.GetStructFormat(),
340 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
341
342 def Clone(self):
343 """Clones a new instance."""
344 return type(self)(*self, **self.meta)
345
346 def Update(self, **dargs):
347 """Applies multiple values in current object."""
Yilin Yangea784662019-09-26 13:51:03 +0800348 for name, value in iteritems(dargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800349 setattr(self, name, value)
350
351 def Zero(self):
352 """Set all fields to values representing zero or empty.
353
354 Note the meta attributes won't be cleared.
355 """
356 class ZeroReader(object):
357 """A /dev/zero like stream."""
358
359 @staticmethod
360 def read(num):
361 return '\x00' * num
362
363 self.Unpack(ZeroReader())
364
365
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800366class GPT(object):
367 """A GPT helper class.
368
369 To load GPT from an existing disk image file, use `LoadFromFile`.
370 After modifications were made, use `WriteToFile` to commit changes.
371
372 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800373 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800374 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800375 partitions: a list of GPT partition entry nametuple.
376 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800377 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800378 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800379 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800380 # Old devices uses 'Basic data' type for stateful partition, and newer devices
381 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
382 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800383 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800384 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800385 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
386 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800387 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
388 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
389 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
390 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
391 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800392 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800393 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800394 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
Yilin Yangea784662019-09-26 13:51:03 +0800395 for k, v in iteritems(TYPE_GUID_MAP))
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800396 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
397 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
398 TYPE_GUID_LIST_BOOTABLE = [
399 TYPE_GUID_CHROMEOS_KERNEL,
400 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800401 ]
402
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800403 class ProtectiveMBR(GPTObject):
404 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800405 FIELDS = PMBR_FIELDS
406 __slots__ = [f.name for f in FIELDS]
407
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800408 SIGNATURE = '\x55\xAA'
409 MAGIC = '\x1d\x9a'
410
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800411 class Header(GPTObject):
412 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800413 FIELDS = HEADER_FIELDS
414 __slots__ = [f.name for f in FIELDS]
415
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800416 SIGNATURES = ['EFI PART', 'CHROMEOS']
417 SIGNATURE_IGNORE = 'IGNOREME'
418 DEFAULT_REVISION = '\x00\x00\x01\x00'
419
420 DEFAULT_PARTITION_ENTRIES = 128
421 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
422
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800423 @classmethod
424 def Create(cls, size, block_size, pad_blocks=0,
425 part_entries=DEFAULT_PARTITION_ENTRIES):
426 """Creates a header with default values.
427
428 Args:
429 size: integer of expected image size.
430 block_size: integer for size of each block (sector).
431 pad_blocks: number of preserved sectors between header and partitions.
432 part_entries: number of partitions to include in header.
433 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800434 PART_FORMAT = GPT.Partition.GetStructFormat()
435 FORMAT = cls.GetStructFormat()
436 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800437 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
438 parts_bytes = part_entries * part_entry_size
Yilin Yang14d02a22019-11-01 11:32:03 +0800439 parts_blocks = parts_bytes // block_size
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800440 if parts_bytes % block_size:
441 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800442 # CRC32 and PartitionsCRC32 must be updated later explicitly.
443 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800444 Signature=cls.SIGNATURES[0],
445 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800446 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800447 CurrentLBA=1,
Yilin Yang14d02a22019-11-01 11:32:03 +0800448 BackupLBA=size // block_size - 1,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800449 FirstUsableLBA=parts_lba + parts_blocks,
Yilin Yang14d02a22019-11-01 11:32:03 +0800450 LastUsableLBA=size // block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800451 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800452 PartitionEntriesStartingLBA=parts_lba,
453 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800454 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800455
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800456 def UpdateChecksum(self):
457 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800458
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800459 Note the PartitionArrayCRC32 is not touched - you have to make sure that
460 is correct before calling Header.UpdateChecksum().
461 """
462 self.Update(CRC32=0)
463 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800464
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800465 class Partition(GPTObject):
466 """The partition entry in GPT.
467
468 Please include following properties when creating a Partition object:
469 - image: a string for path to the image file the partition maps to.
470 - number: the 1-based partition number.
471 - block_size: an integer for size of each block (LBA, or sector).
472 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800473 FIELDS = PARTITION_FIELDS
474 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800475 NAMES_ENCODING = 'utf-16-le'
476 NAMES_LENGTH = 72
477
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800478 def __str__(self):
479 return '%s#%s' % (self.image, self.number)
480
481 def IsUnused(self):
482 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800483 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800484
Hung-Te Linfe724f82018-04-18 15:03:58 +0800485 def IsChromeOSKernel(self):
486 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800487 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800488
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800489 @property
490 def blocks(self):
491 """Return size of partition in blocks (see block_size)."""
492 return self.LastLBA - self.FirstLBA + 1
493
494 @property
495 def offset(self):
496 """Returns offset to partition in bytes."""
497 return self.FirstLBA * self.block_size
498
499 @property
500 def size(self):
501 """Returns size of partition in bytes."""
502 return self.blocks * self.block_size
503
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800504 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800505 """GPT constructor.
506
507 See LoadFromFile for how it's usually used.
508 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800509 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800510 self.header = None
511 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800512 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800513 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800514
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800515 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800516 def GetTypeGUID(cls, value):
517 """The value may be a GUID in string or a short type string."""
518 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
519 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800520
521 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800522 def Create(cls, image_name, size, block_size, pad_blocks=0):
523 """Creates a new GPT instance from given size and block_size.
524
525 Args:
526 image_name: a string of underlying disk image file name.
527 size: expected size of disk image.
528 block_size: size of each block (sector) in bytes.
529 pad_blocks: number of blocks between header and partitions array.
530 """
531 gpt = cls()
532 gpt.block_size = block_size
533 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
534 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800535 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800536 for i in xrange(gpt.header.PartitionEntriesNumber)]
537 return gpt
538
Hung-Te Lin446eb512018-05-02 18:39:16 +0800539 @staticmethod
540 def IsBlockDevice(image):
541 """Returns if the image is a block device file."""
542 return stat.S_ISBLK(os.stat(image).st_mode)
543
544 @classmethod
545 def GetImageSize(cls, image):
546 """Returns the size of specified image (plain or block device file)."""
547 if not cls.IsBlockDevice(image):
548 return os.path.getsize(image)
549
550 fd = os.open(image, os.O_RDONLY)
551 try:
552 return os.lseek(fd, 0, os.SEEK_END)
553 finally:
554 os.close(fd)
555
556 @classmethod
557 def GetLogicalBlockSize(cls, block_dev):
558 """Returns the logical block (sector) size from a block device file.
559
560 The underlying call is BLKSSZGET. An alternative command is blockdev,
561 but that needs root permission even if we just want to get sector size.
562 """
563 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
564 return int(subprocess.check_output(
565 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
566
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800567 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800568 def LoadFromFile(cls, image):
569 """Loads a GPT table from give disk image file object.
570
571 Args:
572 image: a string as file path or a file-like object to read from.
573 """
574 if isinstance(image, basestring):
575 with open(image, 'rb') as f:
576 return cls.LoadFromFile(f)
577
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800578 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800579 image.seek(0)
580 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
581 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
582 logging.debug('Found MBR signature in %s', image.name)
583 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
584 logging.debug('Found PMBR in %s', image.name)
585 gpt.pmbr = pmbr
586
Hung-Te Linf148d322018-04-13 10:24:42 +0800587 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800588 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
589 if cls.IsBlockDevice(image.name):
590 block_sizes = [cls.GetLogicalBlockSize(image.name)]
591
592 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800593 # Note because there are devices setting Primary as ignored and the
594 # partition table signature accepts 'CHROMEOS' which is also used by
595 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
596 # first before trying other block sizes, otherwise we may incorrectly
597 # identify a kernel partition as LBA 1 of larger block size system.
598 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
599 (-block_size, os.SEEK_END)]):
600 image.seek(*seek)
601 header = gpt.Header.ReadFrom(image)
602 if header.Signature in cls.Header.SIGNATURES:
603 gpt.block_size = block_size
604 if i != 0:
605 gpt.is_secondary = True
606 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800607 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800608 else:
609 # Nothing found, try next block size.
610 continue
611 # Found a valid signature.
612 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800613 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800614 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800615
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800616 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800617 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800618 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800619 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800620 return p
621
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800622 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800623 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800624 ReadPartition(image, i + 1)
625 for i in xrange(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800626 return gpt
627
Hung-Te Linc5196682018-04-18 22:59:59 +0800628 def GetUsedPartitions(self):
629 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800630
Hung-Te Linc5196682018-04-18 22:59:59 +0800631 Use 'number' property to find the real location of partition in
632 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800633 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800634 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800635
636 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800637 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800638 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800639 return (max(p.LastLBA for p in parts)
640 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800641
642 def GetPartitionTableBlocks(self, header=None):
643 """Returns the blocks (or LBA) of partition table from given header."""
644 if header is None:
645 header = self.header
646 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Yilin Yang14d02a22019-11-01 11:32:03 +0800647 blocks = size // self.block_size
Hung-Te Linf148d322018-04-13 10:24:42 +0800648 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800649 blocks += 1
650 return blocks
651
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800652 def GetPartition(self, number):
653 """Gets the Partition by given (1-based) partition number.
654
655 Args:
656 number: an integer as 1-based partition number.
657 """
658 if not 0 < number <= len(self.partitions):
659 raise GPTError('Invalid partition number %s.' % number)
660 return self.partitions[number - 1]
661
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800662 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800663 """Updates the entry in partition table by given Partition object.
664
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800665 Usually you only need to call this if you want to copy one partition to
666 different location (number of image).
667
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800668 Args:
669 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800670 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800671 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800672 ref = self.partitions[number - 1]
673 part = part.Clone()
674 part.number = number
675 part.image = ref.image
676 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800677 self.partitions[number - 1] = part
678
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800679 def GetSize(self):
680 return self.block_size * (self.header.BackupLBA + 1)
681
682 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800683 """Adjust GPT for a disk image in given size.
684
685 Args:
686 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800687 check_overlap: Checks if the backup partition table overlaps used
688 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800689 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800690 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800691 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800692 raise GPTError(
693 'New file size %d is not valid for image files.' % new_size)
Yilin Yang14d02a22019-11-01 11:32:03 +0800694 new_blocks = new_size // self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800695 if old_size != new_size:
696 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Yilin Yang14d02a22019-11-01 11:32:03 +0800697 new_size, new_blocks, old_size, old_size // self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800698 else:
699 logging.info('Image size (%d, LBA=%d) not changed.',
700 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800701 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800702
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800703 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800704 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800705 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800706
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800707 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800708 max_used_lba = self.GetMaxUsedLBA()
709 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800710 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800711
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800712 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800713
714 def GetFreeSpace(self):
715 """Returns the free (available) space left according to LastUsableLBA."""
716 max_lba = self.GetMaxUsedLBA()
717 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800718 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800719
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800720 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800721 """Expands a given partition to last usable LBA.
722
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800723 The size of the partition can actually be reduced if the last usable LBA
724 decreases.
725
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800726 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800727 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800728
729 Returns:
730 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800731 """
732 # Assume no partitions overlap, we need to make sure partition[i] has
733 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800734 p = self.GetPartition(number)
735 if p.IsUnused():
736 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800737 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800738 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800739 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800740 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800741 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800742
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800743 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800744 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800745 new_blocks = p.blocks
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800746 logging.warn(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800747 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800748 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800749
Hung-Te Lin3b491672018-04-19 01:41:20 +0800750 def CheckIntegrity(self):
751 """Checks if the GPT objects all look good."""
752 # Check if the header allocation looks good. CurrentLBA and
753 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
754 # LastUsableLBA].
755 header = self.header
756 entries_first_lba = header.PartitionEntriesStartingLBA
757 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
758
759 def CheckOutsideUsable(name, lba, outside_entries=False):
760 if lba < 1:
761 raise GPTError('%s should not live in LBA %s.' % (name, lba))
762 if lba > max(header.BackupLBA, header.CurrentLBA):
763 # Note this is "in theory" possible, but we want to report this as
764 # error as well, since it usually leads to error.
765 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
766 (name, lba, header.BackupLBA))
767 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
768 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
769 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
770 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
771 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
772 (name, lba, entries_first_lba, entries_last_lba))
773 CheckOutsideUsable('Header', header.CurrentLBA, True)
774 CheckOutsideUsable('Backup header', header.BackupLBA, True)
775 CheckOutsideUsable('Partition entries', entries_first_lba)
776 CheckOutsideUsable('Partition entries end', entries_last_lba)
777
778 parts = self.GetUsedPartitions()
779 # Check if partition entries overlap with each other.
780 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
781 lba_list.sort(key=lambda t: t[0])
782 for i in xrange(len(lba_list) - 1):
783 if lba_list[i][1] >= lba_list[i + 1][0]:
784 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
785 (lba_list[i] + lba_list[i + 1]))
786 # Now, check the first and last partition.
787 if lba_list:
788 p = lba_list[0][2]
789 if p.FirstLBA < header.FirstUsableLBA:
790 raise GPTError(
791 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
792 (p, p.FirstLBA, header.FirstLBA))
793 p = lba_list[-1][2]
794 if p.LastLBA > header.LastUsableLBA:
795 raise GPTError(
796 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
797 (p, p.LastLBA, header.LastLBA))
798 # Check if UniqueGUIDs are not unique.
799 if len(set(p.UniqueGUID for p in parts)) != len(parts):
800 raise GPTError('Partition UniqueGUIDs are duplicated.')
801 # Check if CRCs match.
802 if (binascii.crc32(''.join(p.blob for p in self.partitions)) !=
803 header.PartitionArrayCRC32):
804 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800805 header_crc = header.Clone()
806 header_crc.UpdateChecksum()
807 if header_crc.CRC32 != header.CRC32:
808 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800809
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800810 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800811 """Updates all checksum fields in GPT objects."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800812 parts = ''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800813 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
814 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800815
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800816 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800817 """Returns the backup header according to given header.
818
819 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
820 fields.
821 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800822 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800823 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800824 h = header.Clone()
825 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800826 BackupLBA=header.CurrentLBA,
827 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800828 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800829 h.UpdateChecksum()
830 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800831
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800832 @classmethod
833 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
834 """Writes a protective MBR to given file.
835
836 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
837 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
838 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
839 we can follow that.
840
841 Args:
842 create: True to re-create PMBR structure.
843 bootcode: a blob of new boot code.
844 boot_guid a blob for new boot GUID.
845
846 Returns:
847 The written PMBR structure.
848 """
849 if isinstance(image, basestring):
850 with open(image, 'rb+') as f:
851 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
852
853 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800854 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
855 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800856 pmbr = cls.ProtectiveMBR.ReadFrom(image)
857
858 if create:
859 legacy_sectors = min(
860 0x100000000,
Yilin Yang14d02a22019-11-01 11:32:03 +0800861 GPT.GetImageSize(image.name) // cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800862 # Partition 0 must have have the fixed CHS with number of sectors
863 # (calculated as legacy_sectors later).
864 part0 = ('00000200eeffffff01000000'.decode('hex') +
865 struct.pack('<I', legacy_sectors))
866 # Partition 1~3 should be all zero.
867 part1 = '\x00' * 16
868 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800869 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800870 BootGUID=cls.TYPE_GUID_UNUSED,
871 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800872 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800873 LegacyPart0=part0,
874 LegacyPart1=part1,
875 LegacyPart2=part1,
876 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800877 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800878
879 if bootcode:
880 if len(bootcode) > len(pmbr.BootCode):
881 logging.info(
882 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
883 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800884 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800885 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800886 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800887
888 blob = pmbr.blob
889 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
890 image.seek(0)
891 image.write(blob)
892 return pmbr
893
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800894 def WriteToFile(self, image):
895 """Updates partition table in a disk image file.
896
897 Args:
898 image: a string as file path or a file-like object to write into.
899 """
900 if isinstance(image, basestring):
901 with open(image, 'rb+') as f:
902 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800903
904 def WriteData(name, blob, lba):
905 """Writes a blob into given location."""
906 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800907 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800908 image.seek(lba * self.block_size)
909 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800910
911 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800912 self.CheckIntegrity()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800913 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800914
915 header = self.header
916 WriteData('GPT Header', header.blob, header.CurrentLBA)
917 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
918 logging.info(
919 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
920 header.LastUsableLBA)
921
922 if not self.is_secondary:
923 # When is_secondary is True, the header we have is actually backup header.
924 backup_header = self.GetBackupHeader(self.header)
925 WriteData(
926 'Backup Partitions', parts_blob,
927 backup_header.PartitionEntriesStartingLBA)
928 WriteData(
929 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800930
931
932class GPTCommands(object):
933 """Collection of GPT sub commands for command line to use.
934
935 The commands are derived from `cgpt`, but not necessary to be 100% compatible
936 with cgpt.
937 """
938
939 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800940 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800941 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800942 ('type', 'type guid'),
943 ('unique', 'unique guid'),
944 ('label', 'label'),
945 ('Successful', 'Successful flag'),
946 ('Tries', 'Tries flag'),
947 ('Priority', 'Priority flag'),
948 ('Legacy', 'Legacy Boot flag'),
949 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800950
951 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800952 commands = dict(
953 (command.lower(), getattr(self, command)())
954 for command in dir(self)
955 if (isinstance(getattr(self, command), type) and
956 issubclass(getattr(self, command), self.SubCommand) and
957 getattr(self, command) is not self.SubCommand)
958 )
959 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800960
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800961 def DefineArgs(self, parser):
962 """Defines all available commands to an argparser subparsers instance."""
963 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
Yilin Yangea784662019-09-26 13:51:03 +0800964 for name, instance in sorted(iteritems(self.commands)):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800965 parser = subparsers.add_parser(
966 name, description=instance.__doc__,
967 formatter_class=argparse.RawDescriptionHelpFormatter,
968 help=instance.__doc__.splitlines()[0])
969 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800970
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800971 def Execute(self, args):
972 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800973 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800974
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800975 class SubCommand(object):
976 """A base class for sub commands to derive from."""
977
978 def DefineArgs(self, parser):
979 """Defines command line arguments to argparse parser.
980
981 Args:
982 parser: An argparse parser instance.
983 """
984 del parser # Unused.
985 raise NotImplementedError
986
987 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800988 """Execute the command with parsed arguments.
989
990 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800991
992 Args:
993 args: An argparse parsed namespace.
994 """
995 del args # Unused.
996 raise NotImplementedError
997
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800998 def ExecuteCommandLine(self, *args):
999 """Execute as invoked from command line.
1000
1001 This provides an easy way to execute particular sub command without
1002 creating argument parser explicitly.
1003
1004 Args:
1005 args: a list of string type command line arguments.
1006 """
1007 parser = argparse.ArgumentParser()
1008 self.DefineArgs(parser)
1009 return self.Execute(parser.parse_args(args))
1010
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001011 class Create(SubCommand):
1012 """Create or reset GPT headers and tables.
1013
1014 Create or reset an empty GPT.
1015 """
1016
1017 def DefineArgs(self, parser):
1018 parser.add_argument(
1019 '-z', '--zero', action='store_true',
1020 help='Zero the sectors of the GPT table and entries')
1021 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001022 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001023 help=('Size (in blocks) of the disk to pad between the '
1024 'primary GPT header and its entries, default %(default)s'))
1025 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001026 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001027 help='Size of each block (sector) in bytes.')
1028 parser.add_argument(
1029 'image_file', type=argparse.FileType('rb+'),
1030 help='Disk image file to create.')
1031
1032 def Execute(self, args):
1033 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001034 if block_size is None:
1035 if GPT.IsBlockDevice(args.image_file.name):
1036 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1037 else:
1038 block_size = GPT.DEFAULT_BLOCK_SIZE
1039
1040 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1041 logging.info('Block (sector) size for %s is set to %s bytes.',
1042 args.image_file.name, block_size)
1043
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001044 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001045 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001046 block_size, args.pad_blocks)
1047 if args.zero:
1048 # In theory we only need to clear LBA 1, but to make sure images already
1049 # initialized with different block size won't have GPT signature in
1050 # different locations, we should zero until first usable LBA.
1051 args.image_file.seek(0)
1052 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
1053 gpt.WriteToFile(args.image_file)
Hung-Te Linbad46112018-05-15 16:39:14 +08001054 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001055
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001056 class Boot(SubCommand):
1057 """Edit the PMBR sector for legacy BIOSes.
1058
1059 With no options, it will just print the PMBR boot guid.
1060 """
1061
1062 def DefineArgs(self, parser):
1063 parser.add_argument(
1064 '-i', '--number', type=int,
1065 help='Set bootable partition')
1066 parser.add_argument(
1067 '-b', '--bootloader', type=argparse.FileType('r'),
1068 help='Install bootloader code in the PMBR')
1069 parser.add_argument(
1070 '-p', '--pmbr', action='store_true',
1071 help='Create legacy PMBR partition table')
1072 parser.add_argument(
1073 'image_file', type=argparse.FileType('rb+'),
1074 help='Disk image file to change PMBR.')
1075
1076 def Execute(self, args):
1077 """Rebuilds the protective MBR."""
1078 bootcode = args.bootloader.read() if args.bootloader else None
1079 boot_guid = None
1080 if args.number is not None:
1081 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001082 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001083 pmbr = GPT.WriteProtectiveMBR(
1084 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1085
You-Cheng Syufff7f422018-05-14 15:37:39 +08001086 print(pmbr.BootGUID)
Hung-Te Linbad46112018-05-15 16:39:14 +08001087 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001088
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001089 class Legacy(SubCommand):
1090 """Switch between GPT and Legacy GPT.
1091
1092 Switch GPT header signature to "CHROMEOS".
1093 """
1094
1095 def DefineArgs(self, parser):
1096 parser.add_argument(
1097 '-e', '--efi', action='store_true',
1098 help='Switch GPT header signature back to "EFI PART"')
1099 parser.add_argument(
1100 '-p', '--primary-ignore', action='store_true',
1101 help='Switch primary GPT header signature to "IGNOREME"')
1102 parser.add_argument(
1103 'image_file', type=argparse.FileType('rb+'),
1104 help='Disk image file to change.')
1105
1106 def Execute(self, args):
1107 gpt = GPT.LoadFromFile(args.image_file)
1108 # cgpt behavior: if -p is specified, -e is ignored.
1109 if args.primary_ignore:
1110 if gpt.is_secondary:
1111 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1112 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1113 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1114 gpt.header = gpt.GetBackupHeader(self.header)
1115 gpt.is_secondary = True
1116 else:
1117 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001118 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001119 gpt.WriteToFile(args.image_file)
1120 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001121 return ('Set %s primary GPT header to %s.' %
1122 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001123 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001124 return ('Changed GPT signature for %s to %s.' %
1125 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001126
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001127 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001128 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001129
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001130 def DefineArgs(self, parser):
1131 parser.add_argument(
1132 'image_file', type=argparse.FileType('rb+'),
1133 help='Disk image file to repair.')
1134
1135 def Execute(self, args):
1136 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001137 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001138 gpt.WriteToFile(args.image_file)
Hung-Te Linbad46112018-05-15 16:39:14 +08001139 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001140
1141 class Expand(SubCommand):
1142 """Expands a GPT partition to all available free space."""
1143
1144 def DefineArgs(self, parser):
1145 parser.add_argument(
1146 '-i', '--number', type=int, required=True,
1147 help='The partition to expand.')
1148 parser.add_argument(
1149 'image_file', type=argparse.FileType('rb+'),
1150 help='Disk image file to modify.')
1151
1152 def Execute(self, args):
1153 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001154 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001155 gpt.WriteToFile(args.image_file)
1156 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001157 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001158 'Partition %s on disk image file %s has been extended '
1159 'from %s to %s .' %
1160 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1161 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001162 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001163 return ('Nothing to expand for disk image %s partition %s.' %
1164 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001165
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001166 class Add(SubCommand):
1167 """Add, edit, or remove a partition entry.
1168
1169 Use the -i option to modify an existing partition.
1170 The -b, -s, and -t options must be given for new partitions.
1171
1172 The partition type may also be given as one of these aliases:
1173
1174 firmware ChromeOS firmware
1175 kernel ChromeOS kernel
1176 rootfs ChromeOS rootfs
1177 data Linux data
1178 reserved ChromeOS reserved
1179 efi EFI System Partition
1180 unused Unused (nonexistent) partition
1181 """
1182 def DefineArgs(self, parser):
1183 parser.add_argument(
1184 '-i', '--number', type=int,
1185 help='Specify partition (default is next available)')
1186 parser.add_argument(
1187 '-b', '--begin', type=int,
1188 help='Beginning sector')
1189 parser.add_argument(
1190 '-s', '--sectors', type=int,
1191 help='Size in sectors (logical blocks).')
1192 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001193 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001194 help='Partition Type GUID')
1195 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001196 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001197 help='Partition Unique ID')
1198 parser.add_argument(
1199 '-l', '--label',
1200 help='Label')
1201 parser.add_argument(
1202 '-S', '--successful', type=int, choices=xrange(2),
1203 help='set Successful flag')
1204 parser.add_argument(
1205 '-T', '--tries', type=int,
1206 help='set Tries flag (0-15)')
1207 parser.add_argument(
1208 '-P', '--priority', type=int,
1209 help='set Priority flag (0-15)')
1210 parser.add_argument(
1211 '-R', '--required', type=int, choices=xrange(2),
1212 help='set Required flag')
1213 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001214 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001215 choices=xrange(2),
1216 help='set Legacy Boot flag')
1217 parser.add_argument(
1218 '-A', '--attribute', dest='raw_16', type=int,
1219 help='set raw 16-bit attribute value (bits 48-63)')
1220 parser.add_argument(
1221 'image_file', type=argparse.FileType('rb+'),
1222 help='Disk image file to modify.')
1223
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001224 def Execute(self, args):
1225 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001226 number = args.number
1227 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001228 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001229
1230 # First and last LBA must be calculated explicitly because the given
1231 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001232 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001233 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001234
1235 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001236 part.Zero()
1237 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001238 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001239 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001240 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001241 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001242
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001243 def UpdateAttr(name):
1244 value = getattr(args, name)
1245 if value is None:
1246 return
1247 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001248
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001249 def GetArg(arg_value, default_value):
1250 return default_value if arg_value is None else arg_value
1251
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001252 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001253 for name in ['legacy_boot', 'required', 'priority', 'tries',
1254 'successful', 'raw_16']:
1255 UpdateAttr(name)
1256 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001257 part.Update(
1258 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001259 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001260 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1261 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1262 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1263 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001264
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001265 # Wipe partition again if it should be empty.
1266 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001267 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001268
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001269 gpt.WriteToFile(args.image_file)
1270 if part.IsUnused():
1271 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001272 return 'Deleted (zeroed) %s.' % (part,)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001273 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001274 return ('%s %s (%s+%s).' %
1275 ('Added' if is_new_part else 'Modified',
1276 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001277
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001278 class Show(SubCommand):
1279 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001280
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001281 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001282 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001283
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001284 def DefineArgs(self, parser):
1285 parser.add_argument(
1286 '--numeric', '-n', action='store_true',
1287 help='Numeric output only.')
1288 parser.add_argument(
1289 '--quick', '-q', action='store_true',
1290 help='Quick output.')
1291 parser.add_argument(
1292 '-i', '--number', type=int,
1293 help='Show specified partition only, with format args.')
1294 for name, help_str in GPTCommands.FORMAT_ARGS:
1295 # TODO(hungte) Alert if multiple args were specified.
1296 parser.add_argument(
1297 '--%s' % name, '-%c' % name[0], action='store_true',
1298 help='[format] %s.' % help_str)
1299 parser.add_argument(
1300 'image_file', type=argparse.FileType('rb'),
1301 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001302
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001303 def Execute(self, args):
1304 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001305
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001306 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001307 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001308 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001309 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001310 if names:
1311 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001312 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001313
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001314 def IsBootableType(guid):
1315 if not guid:
1316 return False
1317 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001318
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001319 def FormatAttribute(attrs, chromeos_kernel=False):
1320 if args.numeric:
1321 return '[%x]' % (attrs.raw >> 48)
1322 results = []
1323 if chromeos_kernel:
1324 results += [
1325 'priority=%d' % attrs.priority,
1326 'tries=%d' % attrs.tries,
1327 'successful=%d' % attrs.successful]
1328 if attrs.required:
1329 results += ['required=1']
1330 if attrs.legacy_boot:
1331 results += ['legacy_boot=1']
1332 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001333
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001334 def ApplyFormatArgs(p):
1335 if args.begin:
1336 return p.FirstLBA
1337 elif args.size:
1338 return p.blocks
1339 elif args.type:
1340 return FormatTypeGUID(p)
1341 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001342 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001343 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001344 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001345 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001346 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001347 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001348 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001349 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001350 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001351 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001352 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001353 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001354 return '[%x]' % (p.Attributes.raw >> 48)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001355 else:
1356 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001357
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001358 def IsFormatArgsSpecified():
1359 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001360
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001361 gpt = GPT.LoadFromFile(args.image_file)
1362 logging.debug('%r', gpt.header)
1363 fmt = '%12s %11s %7s %s'
1364 fmt2 = '%32s %s: %s'
1365 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001366
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001367 if IsFormatArgsSpecified() and args.number is None:
1368 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001369
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001370 if not (args.number is None or
1371 0 < args.number <= gpt.header.PartitionEntriesNumber):
1372 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001373
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001374 partitions = gpt.partitions
1375 do_print_gpt_blocks = False
1376 if not (args.quick or IsFormatArgsSpecified()):
1377 print(fmt % header)
1378 if args.number is None:
1379 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001380
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001381 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001382 if gpt.pmbr:
1383 print(fmt % (0, 1, '', 'PMBR'))
1384 if gpt.is_secondary:
1385 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1386 else:
1387 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1388 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1389 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001390
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001391 for p in partitions:
1392 if args.number is None:
1393 # Skip unused partitions.
1394 if p.IsUnused():
1395 continue
1396 elif p.number != args.number:
1397 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001398
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001399 if IsFormatArgsSpecified():
1400 print(ApplyFormatArgs(p))
1401 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001402
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001403 print(fmt % (p.FirstLBA, p.blocks, p.number,
1404 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001405 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001406
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001407 if not args.quick:
1408 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001409 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1410 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001411 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001412 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001413
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001414 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001415 if gpt.is_secondary:
1416 header = gpt.header
1417 else:
1418 f = args.image_file
1419 f.seek(gpt.header.BackupLBA * gpt.block_size)
1420 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001421 print(fmt % (header.PartitionEntriesStartingLBA,
1422 gpt.GetPartitionTableBlocks(header), '',
1423 'Sec GPT table'))
1424 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001425
Hung-Te Lin3b491672018-04-19 01:41:20 +08001426 # Check integrity after showing all fields.
1427 gpt.CheckIntegrity()
1428
Hung-Te Linfe724f82018-04-18 15:03:58 +08001429 class Prioritize(SubCommand):
1430 """Reorder the priority of all kernel partitions.
1431
1432 Reorder the priority of all active ChromeOS Kernel partitions.
1433
1434 With no options this will set the lowest active kernel to priority 1 while
1435 maintaining the original order.
1436 """
1437
1438 def DefineArgs(self, parser):
1439 parser.add_argument(
1440 '-P', '--priority', type=int,
1441 help=('Highest priority to use in the new ordering. '
1442 'The other partitions will be ranked in decreasing '
1443 'priority while preserving their original order. '
1444 'If necessary the lowest ranks will be coalesced. '
1445 'No active kernels will be lowered to priority 0.'))
1446 parser.add_argument(
1447 '-i', '--number', type=int,
1448 help='Specify the partition to make the highest in the new order.')
1449 parser.add_argument(
1450 '-f', '--friends', action='store_true',
1451 help=('Friends of the given partition (those with the same '
1452 'starting priority) are also updated to the new '
1453 'highest priority. '))
1454 parser.add_argument(
1455 'image_file', type=argparse.FileType('rb+'),
1456 help='Disk image file to prioritize.')
1457
1458 def Execute(self, args):
1459 gpt = GPT.LoadFromFile(args.image_file)
1460 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001461 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1462 groups = dict((k, list(g)) for k, g in itertools.groupby(
1463 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001464 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001465 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001466 if p not in parts:
1467 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001468 pri = p.Attributes.priority
1469 friends = groups.pop(pri)
1470 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001471 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001472 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001473 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001474 groups[new_pri] = [p]
1475 friends.remove(p)
1476 if friends:
1477 groups[pri] = friends
1478
1479 if 0 in groups:
1480 # Do not change any partitions with priority=0
1481 groups.pop(0)
1482
Yilin Yang78fa12e2019-09-25 14:21:10 +08001483 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001484 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001485
1486 # Max priority is 0xf.
1487 highest = min(args.priority or len(prios), 0xf)
1488 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001489
Hung-Te Lin138389f2018-05-15 17:55:00 +08001490 for i, pri in enumerate(prios):
1491 new_priority = max(1, highest - i)
1492 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001493 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001494 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001495 if old_priority == new_priority:
1496 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001497 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001498 if attrs.tries < 1 and not attrs.successful:
1499 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001500 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001501 logging.info('%s priority changed from %s to %s.', p, old_priority,
1502 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001503
1504 gpt.WriteToFile(args.image_file)
1505
Hung-Te Linf641d302018-04-18 15:09:35 +08001506 class Find(SubCommand):
1507 """Locate a partition by its GUID.
1508
1509 Find a partition by its UUID or label. With no specified DRIVE it scans all
1510 physical drives.
1511
1512 The partition type may also be given as one of these aliases:
1513
1514 firmware ChromeOS firmware
1515 kernel ChromeOS kernel
1516 rootfs ChromeOS rootfs
1517 data Linux data
1518 reserved ChromeOS reserved
1519 efi EFI System Partition
1520 unused Unused (nonexistent) partition
1521 """
1522 def DefineArgs(self, parser):
1523 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001524 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001525 help='Search for Partition Type GUID')
1526 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001527 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001528 help='Search for Partition Unique GUID')
1529 parser.add_argument(
1530 '-l', '--label',
1531 help='Search for Label')
1532 parser.add_argument(
1533 '-n', '--numeric', action='store_true',
1534 help='Numeric output only.')
1535 parser.add_argument(
1536 '-1', '--single-match', action='store_true',
1537 help='Fail if more than one match is found.')
1538 parser.add_argument(
1539 '-M', '--match-file', type=str,
1540 help='Matching partition data must also contain MATCH_FILE content.')
1541 parser.add_argument(
1542 '-O', '--offset', type=int, default=0,
1543 help='Byte offset into partition to match content (default 0).')
1544 parser.add_argument(
1545 'drive', type=argparse.FileType('rb+'), nargs='?',
1546 help='Drive or disk image file to find.')
1547
1548 def Execute(self, args):
1549 if not any((args.type_guid, args.unique_guid, args.label)):
1550 raise GPTError('You must specify at least one of -t, -u, or -l')
1551
1552 drives = [args.drive.name] if args.drive else (
1553 '/dev/%s' % name for name in subprocess.check_output(
1554 'lsblk -d -n -r -o name', shell=True).split())
1555
1556 match_pattern = None
1557 if args.match_file:
1558 with open(args.match_file) as f:
1559 match_pattern = f.read()
1560
1561 found = 0
1562 for drive in drives:
1563 try:
1564 gpt = GPT.LoadFromFile(drive)
1565 except GPTError:
1566 if args.drive:
1567 raise
1568 # When scanning all block devices on system, ignore failure.
1569
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001570 def Unmatch(a, b):
1571 return a is not None and a != b
1572
Hung-Te Linf641d302018-04-18 15:09:35 +08001573 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001574 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001575 Unmatch(args.label, p.Names) or
1576 Unmatch(args.unique_guid, p.UniqueGUID) or
1577 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001578 continue
1579 if match_pattern:
1580 with open(drive, 'rb') as f:
1581 f.seek(p.offset + args.offset)
1582 if f.read(len(match_pattern)) != match_pattern:
1583 continue
1584 # Found the partition, now print.
1585 found += 1
1586 if args.numeric:
1587 print(p.number)
1588 else:
1589 # This is actually more for block devices.
1590 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1591 p.number))
1592
1593 if found < 1 or (args.single_match and found > 1):
1594 return 1
1595 return 0
1596
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001597
1598def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001599 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001600 parser = argparse.ArgumentParser(description='GPT Utility.')
1601 parser.add_argument('--verbose', '-v', action='count', default=0,
1602 help='increase verbosity.')
1603 parser.add_argument('--debug', '-d', action='store_true',
1604 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001605 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001606
1607 args = parser.parse_args()
1608 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1609 if args.debug:
1610 log_level = logging.DEBUG
1611 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1612 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001613 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001614 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001615 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001616 sys.exit(code)
Hung-Te Linbad46112018-05-15 16:39:14 +08001617 elif isinstance(code, basestring):
1618 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001619 except Exception as e:
1620 if args.verbose or args.debug:
1621 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001622 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001623
1624
1625if __name__ == '__main__':
1626 main()