blob: 6a8a0bd3c0f4b0ccd589520944835c4b74de6e46 [file] [log] [blame]
You-Cheng Syud5692942018-01-04 14:40:59 +08001#!/usr/bin/env python
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 Linc772e1a2017-04-14 16:50:50 +080033import logging
34import os
35import struct
Hung-Te Linf641d302018-04-18 15:09:35 +080036import subprocess
37import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080038import uuid
39
40
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080041class StructError(Exception):
42 """Exceptions in packing and unpacking from/to struct fields."""
43 pass
Hung-Te Linc772e1a2017-04-14 16:50:50 +080044
Hung-Te Linc772e1a2017-04-14 16:50:50 +080045
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080046class StructField(object):
47 """Definition of a field in struct.
48
49 Attributes:
50 fmt: a format string for struct.{pack,unpack} to use.
51 name: a string for name of processed field.
52 """
53 __slots__ = ['fmt', 'name']
54
55 def __init__(self, fmt, name):
56 self.fmt = fmt
57 self.name = name
58
59 def Pack(self, value):
60 """"Packs given value from given format."""
61 del self # Unused.
62 return value
63
64 def Unpack(self, value):
65 """Unpacks given value into given format."""
66 del self # Unused.
67 return value
68
69
70class UTF16StructField(StructField):
71 """A field in UTF encoded string."""
72 __slots__ = ['encoding', 'max_length']
73 encoding = 'utf-16-le'
74
75 def __init__(self, max_length, name):
76 self.max_length = max_length
77 fmt = '%ds' % max_length
78 super(UTF16StructField, self).__init__(fmt, name)
79
80 def Pack(self, value):
81 new_value = value.encode(self.encoding)
82 if len(new_value) >= self.max_length:
83 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
84 (value, self.name, self.max_length))
85 return new_value
86
87 def Unpack(self, value):
88 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080089
Hung-Te Linbf8aa272018-04-19 03:02:29 +080090
91class GUID(uuid.UUID):
92 """A special UUID that defaults to upper case in str()."""
93
94 def __str__(self):
95 """Returns GUID in upper case."""
96 return super(GUID, self).__str__().upper()
97
98 @staticmethod
99 def Random():
100 return uuid.uuid4()
101
102
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800103class GUIDStructField(StructField):
104 """A GUID field."""
105
106 def __init__(self, name):
107 super(GUIDStructField, self).__init__('16s', name)
108
109 def Pack(self, value):
110 if value is None:
111 return '\x00' * 16
112 if not isinstance(value, uuid.UUID):
113 raise StructError('Field %s needs a GUID value instead of [%r].' %
114 (self.name, value))
115 return value.bytes_le
116
117 def Unpack(self, value):
118 return GUID(bytes_le=value)
119
120
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800121def BitProperty(getter, setter, shift, mask):
122 """A generator for bit-field properties.
123
124 This is used inside a class to manipulate an integer-like variable using
125 properties. The getter and setter should be member functions to change the
126 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800127
128 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800129 getter: a function to read integer type variable (for all the bits).
130 setter: a function to set the new changed integer type variable.
131 shift: integer for how many bits should be shifted (right).
132 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800133 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800134 def _getter(self):
135 return (getter(self) >> shift) & mask
136 def _setter(self, value):
137 assert value & mask == value, (
138 'Value %s out of range (mask=%s)' % (value, mask))
139 setter(self, getter(self) & ~(mask << shift) | value << shift)
140 return property(_getter, _setter)
141
142
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800143class PartitionAttributes(object):
144 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800145
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800146 This can be created using Partition.attrs, but the changed properties won't
147 apply to underlying Partition until an explicit call with
148 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800149 """
150
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800151 def __init__(self, attrs):
152 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800153
154 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800155 def raw(self):
156 """Returns the raw integer type attributes."""
157 return self._Get()
158
159 def _Get(self):
160 return self._attrs
161
162 def _Set(self, value):
163 self._attrs = value
164
165 successful = BitProperty(_Get, _Set, 56, 1)
166 tries = BitProperty(_Get, _Set, 52, 0xf)
167 priority = BitProperty(_Get, _Set, 48, 0xf)
168 legacy_boot = BitProperty(_Get, _Set, 2, 1)
169 required = BitProperty(_Get, _Set, 0, 1)
170 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
171
172
173class PartitionAttributeStructField(StructField):
174
175 def Pack(self, value):
176 if not isinstance(value, PartitionAttributes):
177 raise StructError('Given value %r is not %s.' %
178 (value, PartitionAttributes.__name__))
179 return value.raw
180
181 def Unpack(self, value):
182 return PartitionAttributes(value)
183
184
185# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
186# declared as 'signed' (l) instead of 'unsigned' (L).
187# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
188HEADER_FIELDS = [
189 StructField('8s', 'Signature'),
190 StructField('4s', 'Revision'),
191 StructField('L', 'HeaderSize'),
192 StructField('l', 'CRC32'),
193 StructField('4s', 'Reserved'),
194 StructField('Q', 'CurrentLBA'),
195 StructField('Q', 'BackupLBA'),
196 StructField('Q', 'FirstUsableLBA'),
197 StructField('Q', 'LastUsableLBA'),
198 GUIDStructField('DiskGUID'),
199 StructField('Q', 'PartitionEntriesStartingLBA'),
200 StructField('L', 'PartitionEntriesNumber'),
201 StructField('L', 'PartitionEntrySize'),
202 StructField('l', 'PartitionArrayCRC32'),
203]
204
205# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
206PARTITION_FIELDS = [
207 GUIDStructField('TypeGUID'),
208 GUIDStructField('UniqueGUID'),
209 StructField('Q', 'FirstLBA'),
210 StructField('Q', 'LastLBA'),
211 PartitionAttributeStructField('Q', 'Attributes'),
212 UTF16StructField(72, 'Names'),
213]
214
215# The PMBR has so many variants. The basic format is defined in
216# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
217# implementation, as derived from `cgpt`, is following syslinux as:
218# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
219PMBR_FIELDS = [
220 StructField('424s', 'BootCode'),
221 GUIDStructField('BootGUID'),
222 StructField('L', 'DiskID'),
223 StructField('2s', 'Magic'),
224 StructField('16s', 'LegacyPart0'),
225 StructField('16s', 'LegacyPart1'),
226 StructField('16s', 'LegacyPart2'),
227 StructField('16s', 'LegacyPart3'),
228 StructField('2s', 'Signature'),
229]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800230
231
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800232class GPTError(Exception):
233 """All exceptions by GPT."""
234 pass
235
236
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800237class GPTObject(object):
238 """A base object in GUID Partition Table.
239
240 All objects (for instance, header or partition entries) must inherit this
241 class and define the FIELD attribute with a list of field definitions using
242 StructField.
243
244 The 'name' in StructField will become the attribute name of GPT objects that
245 can be directly packed into / unpacked from. Derived (calculated from existing
246 attributes) attributes should be in lower_case.
247
248 It is also possible to attach some additional properties to the object as meta
249 data (for example path of the underlying image file). To do that, first
250 include it in __slots__ list and specify them as dictionary-type args in
251 constructors. These properties will be preserved when you call Clone().
252
253 To create a new object, call the constructor. Field data can be assigned as
254 in arguments, or give nothing to initialize as zero (see Zero()). Field data
255 and meta values can be also specified in keyword arguments (**kargs) at the
256 same time.
257
258 To read a object from file or stream, use class method ReadFrom(source).
259 To make changes, modify the field directly or use Update(dict), or create a
260 copy by Clone() first then Update.
261
262 To wipe all fields (but not meta), call Zero(). There is currently no way
263 to clear meta except setting them to None one by one.
264 """
265 __slots__ = []
266
267 FIELDS = None
268 """A list of StructField definitions."""
269
270 def __init__(self, *args, **kargs):
271 if args:
272 if len(args) != len(self.FIELDS):
273 raise GPTError('%s need %s arguments (found %s).' %
274 (type(self).__name__, len(self.FIELDS), len(args)))
275 for f, value in zip(self.FIELDS, args):
276 setattr(self, f.name, value)
277 else:
278 self.Zero()
279
280 all_names = [f for f in self.__slots__]
281 for name, value in kargs.iteritems():
282 if name not in all_names:
283 raise GPTError('%s does not support keyword arg <%s>.' %
284 (type(self).__name__, name))
285 setattr(self, name, value)
286
287 def __iter__(self):
288 """An iterator to return all fields associated in the object."""
289 return (getattr(self, f.name) for f in self.FIELDS)
290
291 def __repr__(self):
292 return '(%s: %s)' % (type(self).__name__, ', '.join(
293 '%s=%r' %(f, getattr(self, f)) for f in self.__slots__))
294
295 @classmethod
296 def GetStructFormat(cls):
297 """Returns a format string for struct to use."""
298 return '<' + ''.join(f.fmt for f in cls.FIELDS)
299
300 @classmethod
301 def ReadFrom(cls, source, **kargs):
302 """Returns an object from given source."""
303 obj = cls(**kargs)
304 obj.Unpack(source)
305 return obj
306
307 @property
308 def blob(self):
309 """The (packed) blob representation of the object."""
310 return self.Pack()
311
312 @property
313 def meta(self):
314 """Meta values (those not in GPT object fields)."""
315 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
316 return dict((name, getattr(self, name)) for name in metas)
317
318 def Unpack(self, source):
319 """Unpacks values from a given source.
320
321 Args:
322 source: a string of bytes or a file-like object to read from.
323 """
324 fmt = self.GetStructFormat()
325 if source is None:
326 source = '\x00' * struct.calcsize(fmt)
327 if not isinstance(source, basestring):
328 return self.Unpack(source.read(struct.calcsize(fmt)))
329 for f, value in zip(self.FIELDS, struct.unpack(fmt, source)):
330 setattr(self, f.name, f.Unpack(value))
331
332 def Pack(self):
333 """Packs values in all fields into a string by struct format."""
334 return struct.pack(self.GetStructFormat(),
335 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
336
337 def Clone(self):
338 """Clones a new instance."""
339 return type(self)(*self, **self.meta)
340
341 def Update(self, **dargs):
342 """Applies multiple values in current object."""
343 for name, value in dargs.iteritems():
344 setattr(self, name, value)
345
346 def Zero(self):
347 """Set all fields to values representing zero or empty.
348
349 Note the meta attributes won't be cleared.
350 """
351 class ZeroReader(object):
352 """A /dev/zero like stream."""
353
354 @staticmethod
355 def read(num):
356 return '\x00' * num
357
358 self.Unpack(ZeroReader())
359
360
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800361class GPT(object):
362 """A GPT helper class.
363
364 To load GPT from an existing disk image file, use `LoadFromFile`.
365 After modifications were made, use `WriteToFile` to commit changes.
366
367 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800368 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800369 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800370 partitions: a list of GPT partition entry nametuple.
371 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800372 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800373 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800374 DEFAULT_BLOCK_SIZE = 512
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800375 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800376 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
377 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Linux data',
378 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
379 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
380 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
381 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
382 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800383 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800384 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800385 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
386 for k, v in TYPE_GUID_MAP.iteritems())
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800387 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
388 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
389 TYPE_GUID_LIST_BOOTABLE = [
390 TYPE_GUID_CHROMEOS_KERNEL,
391 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800392 ]
393
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800394 class ProtectiveMBR(GPTObject):
395 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800396 FIELDS = PMBR_FIELDS
397 __slots__ = [f.name for f in FIELDS]
398
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800399 SIGNATURE = '\x55\xAA'
400 MAGIC = '\x1d\x9a'
401
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800402 class Header(GPTObject):
403 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800404 FIELDS = HEADER_FIELDS
405 __slots__ = [f.name for f in FIELDS]
406
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800407 SIGNATURES = ['EFI PART', 'CHROMEOS']
408 SIGNATURE_IGNORE = 'IGNOREME'
409 DEFAULT_REVISION = '\x00\x00\x01\x00'
410
411 DEFAULT_PARTITION_ENTRIES = 128
412 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
413
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800414 @classmethod
415 def Create(cls, size, block_size, pad_blocks=0,
416 part_entries=DEFAULT_PARTITION_ENTRIES):
417 """Creates a header with default values.
418
419 Args:
420 size: integer of expected image size.
421 block_size: integer for size of each block (sector).
422 pad_blocks: number of preserved sectors between header and partitions.
423 part_entries: number of partitions to include in header.
424 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800425 PART_FORMAT = GPT.Partition.GetStructFormat()
426 FORMAT = cls.GetStructFormat()
427 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800428 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
429 parts_bytes = part_entries * part_entry_size
430 parts_blocks = parts_bytes / block_size
431 if parts_bytes % block_size:
432 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800433 # CRC32 and PartitionsCRC32 must be updated later explicitly.
434 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800435 Signature=cls.SIGNATURES[0],
436 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800437 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800438 CurrentLBA=1,
439 BackupLBA=size / block_size - 1,
440 FirstUsableLBA=parts_lba + parts_blocks,
441 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800442 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800443 PartitionEntriesStartingLBA=parts_lba,
444 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800445 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800446
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800447 def UpdateChecksum(self):
448 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800449
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800450 Note the PartitionArrayCRC32 is not touched - you have to make sure that
451 is correct before calling Header.UpdateChecksum().
452 """
453 self.Update(CRC32=0)
454 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800455
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800456 class Partition(GPTObject):
457 """The partition entry in GPT.
458
459 Please include following properties when creating a Partition object:
460 - image: a string for path to the image file the partition maps to.
461 - number: the 1-based partition number.
462 - block_size: an integer for size of each block (LBA, or sector).
463 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800464 FIELDS = PARTITION_FIELDS
465 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800466 NAMES_ENCODING = 'utf-16-le'
467 NAMES_LENGTH = 72
468
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800469 def __str__(self):
470 return '%s#%s' % (self.image, self.number)
471
472 def IsUnused(self):
473 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800474 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800475
Hung-Te Linfe724f82018-04-18 15:03:58 +0800476 def IsChromeOSKernel(self):
477 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800478 return self.type_guid == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800479
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800480 @property
481 def blocks(self):
482 """Return size of partition in blocks (see block_size)."""
483 return self.LastLBA - self.FirstLBA + 1
484
485 @property
486 def offset(self):
487 """Returns offset to partition in bytes."""
488 return self.FirstLBA * self.block_size
489
490 @property
491 def size(self):
492 """Returns size of partition in bytes."""
493 return self.blocks * self.block_size
494
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800495 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800496 """GPT constructor.
497
498 See LoadFromFile for how it's usually used.
499 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800500 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800501 self.header = None
502 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800503 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800504 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800505
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800506 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800507 def GetTypeGUID(cls, value):
508 """The value may be a GUID in string or a short type string."""
509 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
510 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800511
512 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800513 def Create(cls, image_name, size, block_size, pad_blocks=0):
514 """Creates a new GPT instance from given size and block_size.
515
516 Args:
517 image_name: a string of underlying disk image file name.
518 size: expected size of disk image.
519 block_size: size of each block (sector) in bytes.
520 pad_blocks: number of blocks between header and partitions array.
521 """
522 gpt = cls()
523 gpt.block_size = block_size
524 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
525 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800526 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800527 for i in xrange(gpt.header.PartitionEntriesNumber)]
528 return gpt
529
530 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800531 def LoadFromFile(cls, image):
532 """Loads a GPT table from give disk image file object.
533
534 Args:
535 image: a string as file path or a file-like object to read from.
536 """
537 if isinstance(image, basestring):
538 with open(image, 'rb') as f:
539 return cls.LoadFromFile(f)
540
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800541 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800542 image.seek(0)
543 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
544 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
545 logging.debug('Found MBR signature in %s', image.name)
546 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
547 logging.debug('Found PMBR in %s', image.name)
548 gpt.pmbr = pmbr
549
Hung-Te Linf148d322018-04-13 10:24:42 +0800550 # Try DEFAULT_BLOCK_SIZE, then 4K.
551 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800552 # Note because there are devices setting Primary as ignored and the
553 # partition table signature accepts 'CHROMEOS' which is also used by
554 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
555 # first before trying other block sizes, otherwise we may incorrectly
556 # identify a kernel partition as LBA 1 of larger block size system.
557 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
558 (-block_size, os.SEEK_END)]):
559 image.seek(*seek)
560 header = gpt.Header.ReadFrom(image)
561 if header.Signature in cls.Header.SIGNATURES:
562 gpt.block_size = block_size
563 if i != 0:
564 gpt.is_secondary = True
565 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800566 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800567 else:
568 # Nothing found, try next block size.
569 continue
570 # Found a valid signature.
571 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800572 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800573 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800574
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800575 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800576 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800577 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800578 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800579 return p
580
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800581 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800582 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800583 ReadPartition(image, i + 1)
584 for i in xrange(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800585 return gpt
586
Hung-Te Linc5196682018-04-18 22:59:59 +0800587 def GetUsedPartitions(self):
588 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800589
Hung-Te Linc5196682018-04-18 22:59:59 +0800590 Use 'number' property to find the real location of partition in
591 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800592 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800593 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800594
595 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800596 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800597 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800598 return (max(p.LastLBA for p in parts)
599 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800600
601 def GetPartitionTableBlocks(self, header=None):
602 """Returns the blocks (or LBA) of partition table from given header."""
603 if header is None:
604 header = self.header
605 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800606 blocks = size / self.block_size
607 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800608 blocks += 1
609 return blocks
610
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800611 def GetPartition(self, number):
612 """Gets the Partition by given (1-based) partition number.
613
614 Args:
615 number: an integer as 1-based partition number.
616 """
617 if not 0 < number <= len(self.partitions):
618 raise GPTError('Invalid partition number %s.' % number)
619 return self.partitions[number - 1]
620
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800621 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800622 """Updates the entry in partition table by given Partition object.
623
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800624 Usually you only need to call this if you want to copy one partition to
625 different location (number of image).
626
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800627 Args:
628 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800629 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800630 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800631 ref = self.partitions[number - 1]
632 part = part.Clone()
633 part.number = number
634 part.image = ref.image
635 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800636 self.partitions[number - 1] = part
637
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800638 def Resize(self, new_size):
639 """Adjust GPT for a disk image in given size.
640
641 Args:
642 new_size: Integer for new size of disk image file.
643 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800644 old_size = self.block_size * (self.header.BackupLBA + 1)
645 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800646 raise GPTError(
647 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800648 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800649 if old_size != new_size:
650 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800651 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800652 else:
653 logging.info('Image size (%d, LBA=%d) not changed.',
654 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800655 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800656
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800657 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800658 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800659 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800660
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800661 if last_usable_lba < self.header.LastUsableLBA:
662 max_used_lba = self.GetMaxUsedLBA()
663 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800664 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800665
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800666 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800667
668 def GetFreeSpace(self):
669 """Returns the free (available) space left according to LastUsableLBA."""
670 max_lba = self.GetMaxUsedLBA()
671 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800672 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800673
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800674 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800675 """Expands a given partition to last usable LBA.
676
677 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800678 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800679
680 Returns:
681 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800682 """
683 # Assume no partitions overlap, we need to make sure partition[i] has
684 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800685 p = self.GetPartition(number)
686 if p.IsUnused():
687 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800688 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800689 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800690 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800691 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800692 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800693
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800694 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800695 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800696 new_blocks = p.blocks
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800697 logging.warn(
698 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
699 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800700
Hung-Te Lin3b491672018-04-19 01:41:20 +0800701 def CheckIntegrity(self):
702 """Checks if the GPT objects all look good."""
703 # Check if the header allocation looks good. CurrentLBA and
704 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
705 # LastUsableLBA].
706 header = self.header
707 entries_first_lba = header.PartitionEntriesStartingLBA
708 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
709
710 def CheckOutsideUsable(name, lba, outside_entries=False):
711 if lba < 1:
712 raise GPTError('%s should not live in LBA %s.' % (name, lba))
713 if lba > max(header.BackupLBA, header.CurrentLBA):
714 # Note this is "in theory" possible, but we want to report this as
715 # error as well, since it usually leads to error.
716 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
717 (name, lba, header.BackupLBA))
718 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
719 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
720 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
721 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
722 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
723 (name, lba, entries_first_lba, entries_last_lba))
724 CheckOutsideUsable('Header', header.CurrentLBA, True)
725 CheckOutsideUsable('Backup header', header.BackupLBA, True)
726 CheckOutsideUsable('Partition entries', entries_first_lba)
727 CheckOutsideUsable('Partition entries end', entries_last_lba)
728
729 parts = self.GetUsedPartitions()
730 # Check if partition entries overlap with each other.
731 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
732 lba_list.sort(key=lambda t: t[0])
733 for i in xrange(len(lba_list) - 1):
734 if lba_list[i][1] >= lba_list[i + 1][0]:
735 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
736 (lba_list[i] + lba_list[i + 1]))
737 # Now, check the first and last partition.
738 if lba_list:
739 p = lba_list[0][2]
740 if p.FirstLBA < header.FirstUsableLBA:
741 raise GPTError(
742 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
743 (p, p.FirstLBA, header.FirstLBA))
744 p = lba_list[-1][2]
745 if p.LastLBA > header.LastUsableLBA:
746 raise GPTError(
747 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
748 (p, p.LastLBA, header.LastLBA))
749 # Check if UniqueGUIDs are not unique.
750 if len(set(p.UniqueGUID for p in parts)) != len(parts):
751 raise GPTError('Partition UniqueGUIDs are duplicated.')
752 # Check if CRCs match.
753 if (binascii.crc32(''.join(p.blob for p in self.partitions)) !=
754 header.PartitionArrayCRC32):
755 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800756 header_crc = header.Clone()
757 header_crc.UpdateChecksum()
758 if header_crc.CRC32 != header.CRC32:
759 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800760
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800761 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800762 """Updates all checksum fields in GPT objects."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800763 parts = ''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800764 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
765 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800766
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800767 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800768 """Returns the backup header according to given header.
769
770 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
771 fields.
772 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800773 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800774 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800775 h = header.Clone()
776 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800777 BackupLBA=header.CurrentLBA,
778 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800779 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800780 h.UpdateChecksum()
781 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800782
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800783 @classmethod
784 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
785 """Writes a protective MBR to given file.
786
787 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
788 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
789 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
790 we can follow that.
791
792 Args:
793 create: True to re-create PMBR structure.
794 bootcode: a blob of new boot code.
795 boot_guid a blob for new boot GUID.
796
797 Returns:
798 The written PMBR structure.
799 """
800 if isinstance(image, basestring):
801 with open(image, 'rb+') as f:
802 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
803
804 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800805 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
806 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800807 pmbr = cls.ProtectiveMBR.ReadFrom(image)
808
809 if create:
810 legacy_sectors = min(
811 0x100000000,
812 os.path.getsize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
813 # Partition 0 must have have the fixed CHS with number of sectors
814 # (calculated as legacy_sectors later).
815 part0 = ('00000200eeffffff01000000'.decode('hex') +
816 struct.pack('<I', legacy_sectors))
817 # Partition 1~3 should be all zero.
818 part1 = '\x00' * 16
819 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800820 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800821 BootGUID=cls.TYPE_GUID_UNUSED,
822 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800823 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800824 LegacyPart0=part0,
825 LegacyPart1=part1,
826 LegacyPart2=part1,
827 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800828 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800829
830 if bootcode:
831 if len(bootcode) > len(pmbr.BootCode):
832 logging.info(
833 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
834 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800835 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800836 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800837 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800838
839 blob = pmbr.blob
840 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
841 image.seek(0)
842 image.write(blob)
843 return pmbr
844
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800845 def WriteToFile(self, image):
846 """Updates partition table in a disk image file.
847
848 Args:
849 image: a string as file path or a file-like object to write into.
850 """
851 if isinstance(image, basestring):
852 with open(image, 'rb+') as f:
853 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800854
855 def WriteData(name, blob, lba):
856 """Writes a blob into given location."""
857 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800858 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800859 image.seek(lba * self.block_size)
860 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800861
862 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800863 self.CheckIntegrity()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800864 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800865
866 header = self.header
867 WriteData('GPT Header', header.blob, header.CurrentLBA)
868 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
869 logging.info(
870 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
871 header.LastUsableLBA)
872
873 if not self.is_secondary:
874 # When is_secondary is True, the header we have is actually backup header.
875 backup_header = self.GetBackupHeader(self.header)
876 WriteData(
877 'Backup Partitions', parts_blob,
878 backup_header.PartitionEntriesStartingLBA)
879 WriteData(
880 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800881
882
883class GPTCommands(object):
884 """Collection of GPT sub commands for command line to use.
885
886 The commands are derived from `cgpt`, but not necessary to be 100% compatible
887 with cgpt.
888 """
889
890 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800891 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800892 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800893 ('type', 'type guid'),
894 ('unique', 'unique guid'),
895 ('label', 'label'),
896 ('Successful', 'Successful flag'),
897 ('Tries', 'Tries flag'),
898 ('Priority', 'Priority flag'),
899 ('Legacy', 'Legacy Boot flag'),
900 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800901
902 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800903 commands = dict(
904 (command.lower(), getattr(self, command)())
905 for command in dir(self)
906 if (isinstance(getattr(self, command), type) and
907 issubclass(getattr(self, command), self.SubCommand) and
908 getattr(self, command) is not self.SubCommand)
909 )
910 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800911
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800912 def DefineArgs(self, parser):
913 """Defines all available commands to an argparser subparsers instance."""
914 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
915 for name, instance in sorted(self.commands.iteritems()):
916 parser = subparsers.add_parser(
917 name, description=instance.__doc__,
918 formatter_class=argparse.RawDescriptionHelpFormatter,
919 help=instance.__doc__.splitlines()[0])
920 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800921
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800922 def Execute(self, args):
923 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800924 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800925
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800926 class SubCommand(object):
927 """A base class for sub commands to derive from."""
928
929 def DefineArgs(self, parser):
930 """Defines command line arguments to argparse parser.
931
932 Args:
933 parser: An argparse parser instance.
934 """
935 del parser # Unused.
936 raise NotImplementedError
937
938 def Execute(self, args):
939 """Execute the command.
940
941 Args:
942 args: An argparse parsed namespace.
943 """
944 del args # Unused.
945 raise NotImplementedError
946
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800947 class Create(SubCommand):
948 """Create or reset GPT headers and tables.
949
950 Create or reset an empty GPT.
951 """
952
953 def DefineArgs(self, parser):
954 parser.add_argument(
955 '-z', '--zero', action='store_true',
956 help='Zero the sectors of the GPT table and entries')
957 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800958 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800959 help=('Size (in blocks) of the disk to pad between the '
960 'primary GPT header and its entries, default %(default)s'))
961 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800962 '--block-size', type=int, default=GPT.DEFAULT_BLOCK_SIZE,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800963 help='Size of each block (sector) in bytes.')
964 parser.add_argument(
965 'image_file', type=argparse.FileType('rb+'),
966 help='Disk image file to create.')
967
968 def Execute(self, args):
969 block_size = args.block_size
970 gpt = GPT.Create(
971 args.image_file.name, os.path.getsize(args.image_file.name),
972 block_size, args.pad_blocks)
973 if args.zero:
974 # In theory we only need to clear LBA 1, but to make sure images already
975 # initialized with different block size won't have GPT signature in
976 # different locations, we should zero until first usable LBA.
977 args.image_file.seek(0)
978 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
979 gpt.WriteToFile(args.image_file)
980 print('OK: Created GPT for %s' % args.image_file.name)
981
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800982 class Boot(SubCommand):
983 """Edit the PMBR sector for legacy BIOSes.
984
985 With no options, it will just print the PMBR boot guid.
986 """
987
988 def DefineArgs(self, parser):
989 parser.add_argument(
990 '-i', '--number', type=int,
991 help='Set bootable partition')
992 parser.add_argument(
993 '-b', '--bootloader', type=argparse.FileType('r'),
994 help='Install bootloader code in the PMBR')
995 parser.add_argument(
996 '-p', '--pmbr', action='store_true',
997 help='Create legacy PMBR partition table')
998 parser.add_argument(
999 'image_file', type=argparse.FileType('rb+'),
1000 help='Disk image file to change PMBR.')
1001
1002 def Execute(self, args):
1003 """Rebuilds the protective MBR."""
1004 bootcode = args.bootloader.read() if args.bootloader else None
1005 boot_guid = None
1006 if args.number is not None:
1007 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001008 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001009 pmbr = GPT.WriteProtectiveMBR(
1010 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1011
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001012 print(pmbr.boot_guid)
1013
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001014
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001015 class Legacy(SubCommand):
1016 """Switch between GPT and Legacy GPT.
1017
1018 Switch GPT header signature to "CHROMEOS".
1019 """
1020
1021 def DefineArgs(self, parser):
1022 parser.add_argument(
1023 '-e', '--efi', action='store_true',
1024 help='Switch GPT header signature back to "EFI PART"')
1025 parser.add_argument(
1026 '-p', '--primary-ignore', action='store_true',
1027 help='Switch primary GPT header signature to "IGNOREME"')
1028 parser.add_argument(
1029 'image_file', type=argparse.FileType('rb+'),
1030 help='Disk image file to change.')
1031
1032 def Execute(self, args):
1033 gpt = GPT.LoadFromFile(args.image_file)
1034 # cgpt behavior: if -p is specified, -e is ignored.
1035 if args.primary_ignore:
1036 if gpt.is_secondary:
1037 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1038 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1039 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1040 gpt.header = gpt.GetBackupHeader(self.header)
1041 gpt.is_secondary = True
1042 else:
1043 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001044 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001045 gpt.WriteToFile(args.image_file)
1046 if args.primary_ignore:
1047 print('OK: Set %s primary GPT header to %s.' %
1048 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
1049 else:
1050 print('OK: Changed GPT signature for %s to %s.' %
1051 (args.image_file.name, new_signature))
1052
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001053 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001054 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001055
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001056 def DefineArgs(self, parser):
1057 parser.add_argument(
1058 'image_file', type=argparse.FileType('rb+'),
1059 help='Disk image file to repair.')
1060
1061 def Execute(self, args):
1062 gpt = GPT.LoadFromFile(args.image_file)
1063 gpt.Resize(os.path.getsize(args.image_file.name))
1064 gpt.WriteToFile(args.image_file)
1065 print('Disk image file %s repaired.' % args.image_file.name)
1066
1067 class Expand(SubCommand):
1068 """Expands a GPT partition to all available free space."""
1069
1070 def DefineArgs(self, parser):
1071 parser.add_argument(
1072 '-i', '--number', type=int, required=True,
1073 help='The partition to expand.')
1074 parser.add_argument(
1075 'image_file', type=argparse.FileType('rb+'),
1076 help='Disk image file to modify.')
1077
1078 def Execute(self, args):
1079 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001080 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001081 gpt.WriteToFile(args.image_file)
1082 if old_blocks < new_blocks:
1083 print(
1084 'Partition %s on disk image file %s has been extended '
1085 'from %s to %s .' %
1086 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1087 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001088 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001089 print('Nothing to expand for disk image %s partition %s.' %
1090 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001091
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001092 class Add(SubCommand):
1093 """Add, edit, or remove a partition entry.
1094
1095 Use the -i option to modify an existing partition.
1096 The -b, -s, and -t options must be given for new partitions.
1097
1098 The partition type may also be given as one of these aliases:
1099
1100 firmware ChromeOS firmware
1101 kernel ChromeOS kernel
1102 rootfs ChromeOS rootfs
1103 data Linux data
1104 reserved ChromeOS reserved
1105 efi EFI System Partition
1106 unused Unused (nonexistent) partition
1107 """
1108 def DefineArgs(self, parser):
1109 parser.add_argument(
1110 '-i', '--number', type=int,
1111 help='Specify partition (default is next available)')
1112 parser.add_argument(
1113 '-b', '--begin', type=int,
1114 help='Beginning sector')
1115 parser.add_argument(
1116 '-s', '--sectors', type=int,
1117 help='Size in sectors (logical blocks).')
1118 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001119 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001120 help='Partition Type GUID')
1121 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001122 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001123 help='Partition Unique ID')
1124 parser.add_argument(
1125 '-l', '--label',
1126 help='Label')
1127 parser.add_argument(
1128 '-S', '--successful', type=int, choices=xrange(2),
1129 help='set Successful flag')
1130 parser.add_argument(
1131 '-T', '--tries', type=int,
1132 help='set Tries flag (0-15)')
1133 parser.add_argument(
1134 '-P', '--priority', type=int,
1135 help='set Priority flag (0-15)')
1136 parser.add_argument(
1137 '-R', '--required', type=int, choices=xrange(2),
1138 help='set Required flag')
1139 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001140 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001141 choices=xrange(2),
1142 help='set Legacy Boot flag')
1143 parser.add_argument(
1144 '-A', '--attribute', dest='raw_16', type=int,
1145 help='set raw 16-bit attribute value (bits 48-63)')
1146 parser.add_argument(
1147 'image_file', type=argparse.FileType('rb+'),
1148 help='Disk image file to modify.')
1149
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001150 def Execute(self, args):
1151 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001152 number = args.number
1153 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001154 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001155
1156 # First and last LBA must be calculated explicitly because the given
1157 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001158 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001159 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001160
1161 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001162 part.Zero()
1163 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001164 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001165 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001166 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001167 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001168
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001169 def UpdateAttr(name):
1170 value = getattr(args, name)
1171 if value is None:
1172 return
1173 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001174
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001175 def GetArg(arg_value, default_value):
1176 return default_value if arg_value is None else arg_value
1177
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001178 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001179 for name in ['legacy_boot', 'required', 'priority', 'tries',
1180 'successful', 'raw_16']:
1181 UpdateAttr(name)
1182 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001183 part.Update(
1184 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001185 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001186 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1187 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1188 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1189 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001190
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001191 # Wipe partition again if it should be empty.
1192 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001193 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001194
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001195 gpt.WriteToFile(args.image_file)
1196 if part.IsUnused():
1197 # If we do ('%s' % part) there will be TypeError.
1198 print('OK: Deleted (zeroed) %s.' % (part,))
1199 else:
1200 print('OK: %s %s (%s+%s).' %
1201 ('Added' if is_new_part else 'Modified',
1202 part, part.FirstLBA, part.blocks))
1203
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001204 class Show(SubCommand):
1205 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001206
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001207 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001208 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001209
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001210 def DefineArgs(self, parser):
1211 parser.add_argument(
1212 '--numeric', '-n', action='store_true',
1213 help='Numeric output only.')
1214 parser.add_argument(
1215 '--quick', '-q', action='store_true',
1216 help='Quick output.')
1217 parser.add_argument(
1218 '-i', '--number', type=int,
1219 help='Show specified partition only, with format args.')
1220 for name, help_str in GPTCommands.FORMAT_ARGS:
1221 # TODO(hungte) Alert if multiple args were specified.
1222 parser.add_argument(
1223 '--%s' % name, '-%c' % name[0], action='store_true',
1224 help='[format] %s.' % help_str)
1225 parser.add_argument(
1226 'image_file', type=argparse.FileType('rb'),
1227 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001228
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001229 def Execute(self, args):
1230 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001231
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001232 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001233 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001234 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001235 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001236 if names:
1237 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001238 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001239
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001240 def IsBootableType(guid):
1241 if not guid:
1242 return False
1243 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001244
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001245 def FormatAttribute(attrs, chromeos_kernel=False):
1246 if args.numeric:
1247 return '[%x]' % (attrs.raw >> 48)
1248 results = []
1249 if chromeos_kernel:
1250 results += [
1251 'priority=%d' % attrs.priority,
1252 'tries=%d' % attrs.tries,
1253 'successful=%d' % attrs.successful]
1254 if attrs.required:
1255 results += ['required=1']
1256 if attrs.legacy_boot:
1257 results += ['legacy_boot=1']
1258 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001259
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001260 def ApplyFormatArgs(p):
1261 if args.begin:
1262 return p.FirstLBA
1263 elif args.size:
1264 return p.blocks
1265 elif args.type:
1266 return FormatTypeGUID(p)
1267 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001268 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001269 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001270 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001271 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001272 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001273 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001274 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001275 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001276 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001277 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001278 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001279 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001280 return '[%x]' % (p.Attributes.raw >> 48)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001281 else:
1282 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001283
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001284 def IsFormatArgsSpecified():
1285 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001286
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001287 gpt = GPT.LoadFromFile(args.image_file)
1288 logging.debug('%r', gpt.header)
1289 fmt = '%12s %11s %7s %s'
1290 fmt2 = '%32s %s: %s'
1291 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001292
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001293 if IsFormatArgsSpecified() and args.number is None:
1294 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001295
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001296 if not (args.number is None or
1297 0 < args.number <= gpt.header.PartitionEntriesNumber):
1298 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001299
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001300 partitions = gpt.partitions
1301 do_print_gpt_blocks = False
1302 if not (args.quick or IsFormatArgsSpecified()):
1303 print(fmt % header)
1304 if args.number is None:
1305 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001306
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001307 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001308 if gpt.pmbr:
1309 print(fmt % (0, 1, '', 'PMBR'))
1310 if gpt.is_secondary:
1311 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1312 else:
1313 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1314 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1315 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001316
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001317 for p in partitions:
1318 if args.number is None:
1319 # Skip unused partitions.
1320 if p.IsUnused():
1321 continue
1322 elif p.number != args.number:
1323 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001324
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001325 if IsFormatArgsSpecified():
1326 print(ApplyFormatArgs(p))
1327 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001328
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001329 print(fmt % (p.FirstLBA, p.blocks, p.number,
1330 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001331 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001332
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001333 if not args.quick:
1334 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001335 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1336 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001337 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001338 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001339
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001340 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001341 if gpt.is_secondary:
1342 header = gpt.header
1343 else:
1344 f = args.image_file
1345 f.seek(gpt.header.BackupLBA * gpt.block_size)
1346 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001347 print(fmt % (header.PartitionEntriesStartingLBA,
1348 gpt.GetPartitionTableBlocks(header), '',
1349 'Sec GPT table'))
1350 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001351
Hung-Te Lin3b491672018-04-19 01:41:20 +08001352 # Check integrity after showing all fields.
1353 gpt.CheckIntegrity()
1354
Hung-Te Linfe724f82018-04-18 15:03:58 +08001355 class Prioritize(SubCommand):
1356 """Reorder the priority of all kernel partitions.
1357
1358 Reorder the priority of all active ChromeOS Kernel partitions.
1359
1360 With no options this will set the lowest active kernel to priority 1 while
1361 maintaining the original order.
1362 """
1363
1364 def DefineArgs(self, parser):
1365 parser.add_argument(
1366 '-P', '--priority', type=int,
1367 help=('Highest priority to use in the new ordering. '
1368 'The other partitions will be ranked in decreasing '
1369 'priority while preserving their original order. '
1370 'If necessary the lowest ranks will be coalesced. '
1371 'No active kernels will be lowered to priority 0.'))
1372 parser.add_argument(
1373 '-i', '--number', type=int,
1374 help='Specify the partition to make the highest in the new order.')
1375 parser.add_argument(
1376 '-f', '--friends', action='store_true',
1377 help=('Friends of the given partition (those with the same '
1378 'starting priority) are also updated to the new '
1379 'highest priority. '))
1380 parser.add_argument(
1381 'image_file', type=argparse.FileType('rb+'),
1382 help='Disk image file to prioritize.')
1383
1384 def Execute(self, args):
1385 gpt = GPT.LoadFromFile(args.image_file)
1386 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001387 prios = list(set(p.Attributes.priority for p in parts
1388 if p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001389 prios.sort(reverse=True)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001390 groups = [[p for p in parts if p.Attributes.priority == priority]
Hung-Te Linfe724f82018-04-18 15:03:58 +08001391 for priority in prios]
1392 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001393 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001394 if p not in parts:
1395 raise GPTError('%s is not a ChromeOS kernel.' % p)
1396 if args.friends:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001397 group0 = [f for f in parts
1398 if f.Attributes.priority == p.Attributes.priority]
Hung-Te Linfe724f82018-04-18 15:03:58 +08001399 else:
1400 group0 = [p]
1401 groups.insert(0, group0)
1402
1403 # Max priority is 0xf.
1404 highest = min(args.priority or len(prios), 0xf)
1405 logging.info('New highest priority: %s', highest)
1406 done = []
1407
1408 new_priority = highest
1409 for g in groups:
1410 has_new_part = False
1411 for p in g:
1412 if p.number in done:
1413 continue
1414 done.append(p.number)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001415 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001416 old_priority = attrs.priority
1417 assert new_priority > 0, 'Priority must be > 0.'
1418 attrs.priority = new_priority
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001419 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001420 has_new_part = True
1421 logging.info('%s priority changed from %s to %s.', p, old_priority,
1422 new_priority)
1423 if has_new_part:
1424 new_priority -= 1
1425
1426 gpt.WriteToFile(args.image_file)
1427
Hung-Te Linf641d302018-04-18 15:09:35 +08001428 class Find(SubCommand):
1429 """Locate a partition by its GUID.
1430
1431 Find a partition by its UUID or label. With no specified DRIVE it scans all
1432 physical drives.
1433
1434 The partition type may also be given as one of these aliases:
1435
1436 firmware ChromeOS firmware
1437 kernel ChromeOS kernel
1438 rootfs ChromeOS rootfs
1439 data Linux data
1440 reserved ChromeOS reserved
1441 efi EFI System Partition
1442 unused Unused (nonexistent) partition
1443 """
1444 def DefineArgs(self, parser):
1445 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001446 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001447 help='Search for Partition Type GUID')
1448 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001449 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001450 help='Search for Partition Unique GUID')
1451 parser.add_argument(
1452 '-l', '--label',
1453 help='Search for Label')
1454 parser.add_argument(
1455 '-n', '--numeric', action='store_true',
1456 help='Numeric output only.')
1457 parser.add_argument(
1458 '-1', '--single-match', action='store_true',
1459 help='Fail if more than one match is found.')
1460 parser.add_argument(
1461 '-M', '--match-file', type=str,
1462 help='Matching partition data must also contain MATCH_FILE content.')
1463 parser.add_argument(
1464 '-O', '--offset', type=int, default=0,
1465 help='Byte offset into partition to match content (default 0).')
1466 parser.add_argument(
1467 'drive', type=argparse.FileType('rb+'), nargs='?',
1468 help='Drive or disk image file to find.')
1469
1470 def Execute(self, args):
1471 if not any((args.type_guid, args.unique_guid, args.label)):
1472 raise GPTError('You must specify at least one of -t, -u, or -l')
1473
1474 drives = [args.drive.name] if args.drive else (
1475 '/dev/%s' % name for name in subprocess.check_output(
1476 'lsblk -d -n -r -o name', shell=True).split())
1477
1478 match_pattern = None
1479 if args.match_file:
1480 with open(args.match_file) as f:
1481 match_pattern = f.read()
1482
1483 found = 0
1484 for drive in drives:
1485 try:
1486 gpt = GPT.LoadFromFile(drive)
1487 except GPTError:
1488 if args.drive:
1489 raise
1490 # When scanning all block devices on system, ignore failure.
1491
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001492 def Unmatch(a, b):
1493 return a is not None and a != b
1494
Hung-Te Linf641d302018-04-18 15:09:35 +08001495 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001496 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001497 Unmatch(args.label, p.Names) or
1498 Unmatch(args.unique_guid, p.UniqueGUID) or
1499 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001500 continue
1501 if match_pattern:
1502 with open(drive, 'rb') as f:
1503 f.seek(p.offset + args.offset)
1504 if f.read(len(match_pattern)) != match_pattern:
1505 continue
1506 # Found the partition, now print.
1507 found += 1
1508 if args.numeric:
1509 print(p.number)
1510 else:
1511 # This is actually more for block devices.
1512 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1513 p.number))
1514
1515 if found < 1 or (args.single_match and found > 1):
1516 return 1
1517 return 0
1518
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001519
1520def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001521 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001522 parser = argparse.ArgumentParser(description='GPT Utility.')
1523 parser.add_argument('--verbose', '-v', action='count', default=0,
1524 help='increase verbosity.')
1525 parser.add_argument('--debug', '-d', action='store_true',
1526 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001527 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001528
1529 args = parser.parse_args()
1530 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1531 if args.debug:
1532 log_level = logging.DEBUG
1533 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1534 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001535 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001536 code = commands.Execute(args)
1537 if type(code) is int:
1538 sys.exit(code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001539 except Exception as e:
1540 if args.verbose or args.debug:
1541 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001542 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001543
1544
1545if __name__ == '__main__':
1546 main()