blob: 9dfc41ccc3e60df0247e7adc139106734614f0b9 [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
33import collections
34import logging
35import os
36import struct
37import uuid
38
39
40# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
41# declared as 'signed' (l) instead of 'unsigned' (L).
42# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080043HEADER_DESCRIPTION = """
Hung-Te Linc772e1a2017-04-14 16:50:50 +080044 8s Signature
45 4s Revision
46 L HeaderSize
47 l CRC32
48 4s Reserved
49 Q CurrentLBA
50 Q BackupLBA
51 Q FirstUsableLBA
52 Q LastUsableLBA
53 16s DiskGUID
54 Q PartitionEntriesStartingLBA
55 L PartitionEntriesNumber
56 L PartitionEntrySize
57 l PartitionArrayCRC32
58"""
59
60# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080061PARTITION_DESCRIPTION = """
Hung-Te Linc772e1a2017-04-14 16:50:50 +080062 16s TypeGUID
63 16s UniqueGUID
64 Q FirstLBA
65 Q LastLBA
66 Q Attributes
67 72s Names
68"""
69
Hung-Te Linc6e009c2018-04-17 15:06:16 +080070# The PMBR has so many variants. The basic format is defined in
71# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
72# implementation, as derived from `cgpt`, is following syslinux as:
73# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
74PMBR_DESCRIPTION = """
75 424s BootCode
76 16s BootGUID
77 L DiskID
78 2s Magic
79 16s LegacyPart0
80 16s LegacyPart1
81 16s LegacyPart2
82 16s LegacyPart3
83 2s Signature
84"""
Hung-Te Linc772e1a2017-04-14 16:50:50 +080085
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080086def BitProperty(getter, setter, shift, mask):
87 """A generator for bit-field properties.
88
89 This is used inside a class to manipulate an integer-like variable using
90 properties. The getter and setter should be member functions to change the
91 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +080092
93 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080094 getter: a function to read integer type variable (for all the bits).
95 setter: a function to set the new changed integer type variable.
96 shift: integer for how many bits should be shifted (right).
97 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +080098 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080099 def _getter(self):
100 return (getter(self) >> shift) & mask
101 def _setter(self, value):
102 assert value & mask == value, (
103 'Value %s out of range (mask=%s)' % (value, mask))
104 setter(self, getter(self) & ~(mask << shift) | value << shift)
105 return property(_getter, _setter)
106
107
108class GPTBlob(object):
109 """A decorator class to help accessing GPT blobs as named tuple.
110
111 To use this, specify the blob description (struct format and named tuple field
112 names) above the derived class, for example:
113
114 @GPTBlob(description):
115 class Header(GPTObject):
116 pass
117 """
118 def __init__(self, description):
119 spec = description.split()
120 self.struct_format = '<' + ''.join(spec[::2])
121 self.fields = spec[1::2]
122
123 def __call__(self, cls):
124 new_bases = ((
125 collections.namedtuple(cls.__name__, self.fields),) + cls.__bases__)
126 new_cls = type(cls.__name__, new_bases, dict(cls.__dict__))
127 setattr(new_cls, 'FORMAT', self.struct_format)
128 return new_cls
129
130
131class GPTObject(object):
132 """An object in GUID Partition Table.
133
134 This needs to be decorated by @GPTBlob(description) and inherited by a real
135 class. Properties (not member functions) in CamelCase should be reserved for
136 named tuple attributes.
137
138 To create a new object, use class method ReadFrom(), which takes a stream
139 as input or None to create with all elements set to zero. To make changes to
140 named tuple elements, use member function Clone(changes).
141
142 It is also possible to attach some additional properties to the object as meta
143 data (for example path of the underlying image file). To do that, specify the
144 data as keyword arguments when calling ReadFrom(). These properties will be
145 preserved when you call Clone().
146
147 A special case is "reset named tuple elements of an object but keeping all
148 properties", for example changing a partition object to unused (zeroed).
149 ReadFrom() is a class method so properties won't be copied. You need to
150 call as cls.ReadFrom(None, **p.__dict__), or a short cut - p.CloneAndZero().
151 """
152
153 FORMAT = None
154 """The struct.{pack,unpack} format string, and should be set by GPTBlob."""
155
156 CLONE_CONVERTERS = None
157 """A dict (name, cvt) to convert input arguments into named tuple data.
158
159 `name` is a string for the name of argument to convert.
160 `cvt` is a callable to convert value. The return value may be:
161 - a tuple in (new_name, value): save the value as new name.
162 - otherwise, save the value in original name.
163 Note tuple is an invalid input for struct.unpack so it's used for the
164 special value.
165 """
166
167 @classmethod
168 def ReadFrom(cls, f, **kargs):
169 """Reads and decode an object from stream.
170
171 Args:
172 f: a stream to read blob, or None to decode with all zero bytes.
173 kargs: a dict for additional attributes in object.
174 """
175 if f is None:
176 reader = lambda num: '\x00' * num
177 else:
178 reader = f.read
179 data = cls(*struct.unpack(cls.FORMAT, reader(struct.calcsize(cls.FORMAT))))
180 # Named tuples do not accept kargs in constructor.
181 data.__dict__.update(kargs)
182 return data
183
184 def Clone(self, **dargs):
185 """Clones a new instance with modifications.
186
187 GPT objects are usually named tuples that are immutable, so the only way
188 to make changes is to create a new instance with modifications.
189
190 Args:
191 dargs: a dict with all modifications.
192 """
193 for name, convert in (self.CLONE_CONVERTERS or {}).iteritems():
194 if name not in dargs:
195 continue
196 result = convert(dargs.pop(name))
197 if isinstance(result, tuple):
198 assert len(result) == 2, 'Converted tuple must be (name, value).'
199 dargs[result[0]] = result[1]
200 else:
201 dargs[name] = result
202
203 cloned = self._replace(**dargs)
204 cloned.__dict__.update(self.__dict__)
205 return cloned
206
207 def CloneAndZero(self, **dargs):
208 """Short cut to create a zeroed object while keeping all properties.
209
210 This is very similar to Clone except all named tuple elements will be zero.
211 Also different from class method ReadFrom(None) because this keeps all
212 properties from one object.
213 """
214 cloned = self.ReadFrom(None, **self.__dict__)
215 return cloned.Clone(**dargs) if dargs else cloned
216
217 @property
218 def blob(self):
219 """Returns the object in formatted bytes."""
220 return struct.pack(self.FORMAT, *self)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800221
222
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800223class GPTError(Exception):
224 """All exceptions by GPT."""
225 pass
226
227
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800228class GPT(object):
229 """A GPT helper class.
230
231 To load GPT from an existing disk image file, use `LoadFromFile`.
232 After modifications were made, use `WriteToFile` to commit changes.
233
234 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800235 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800236 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800237 partitions: a list of GPT partition entry nametuple.
238 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800239 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800240 """
241
Hung-Te Linf148d322018-04-13 10:24:42 +0800242 DEFAULT_BLOCK_SIZE = 512
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800243 TYPE_GUID_UNUSED = '\x00' * 16
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800244 TYPE_NAME_CHROMEOS_KERNEL = 'ChromeOS kernel'
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800245 TYPE_GUID_MAP = {
246 '00000000-0000-0000-0000-000000000000': 'Unused',
247 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7': 'Linux data',
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800248 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309': TYPE_NAME_CHROMEOS_KERNEL,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800249 '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC': 'ChromeOS rootfs',
250 '2E0A753D-9E48-43B0-8337-B15192CB1B5E': 'ChromeOS reserved',
251 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3': 'ChromeOS firmware',
252 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': 'EFI System Partition',
253 }
254 TYPE_GUID_LIST_BOOTABLE = [
255 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309', # ChromeOS kernel
256 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B', # EFI System Partition
257 ]
258
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800259 @GPTBlob(PMBR_DESCRIPTION)
260 class ProtectiveMBR(GPTObject):
261 """Protective MBR (PMBR) in GPT."""
262 SIGNATURE = '\x55\xAA'
263 MAGIC = '\x1d\x9a'
264
265 CLONE_CONVERTERS = {
266 'BootGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v
267 }
268
269 @property
270 def boot_guid(self):
271 """Returns the BootGUID in decoded (uuid.UUID) format."""
272 return uuid.UUID(bytes_le=self.BootGUID)
273
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800274 @GPTBlob(HEADER_DESCRIPTION)
275 class Header(GPTObject):
276 """Wrapper to Header in GPT."""
277 SIGNATURES = ['EFI PART', 'CHROMEOS']
278 SIGNATURE_IGNORE = 'IGNOREME'
279 DEFAULT_REVISION = '\x00\x00\x01\x00'
280
281 DEFAULT_PARTITION_ENTRIES = 128
282 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
283
284 def Clone(self, **dargs):
285 """Creates a new instance with modifications.
286
287 GPT objects are usually named tuples that are immutable, so the only way
288 to make changes is to create a new instance with modifications.
289
290 CRC32 is always updated but PartitionArrayCRC32 must be updated explicitly
291 since we can't track changes in GPT.partitions automatically.
292
293 Note since GPTHeader.Clone will always update CRC, we can only check and
294 compute CRC by super(GPT.Header, header).Clone, or header._replace.
295 """
296 dargs['CRC32'] = 0
297 header = super(GPT.Header, self).Clone(**dargs)
298 return super(GPT.Header, header).Clone(CRC32=binascii.crc32(header.blob))
299
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800300 @classmethod
301 def Create(cls, size, block_size, pad_blocks=0,
302 part_entries=DEFAULT_PARTITION_ENTRIES):
303 """Creates a header with default values.
304
305 Args:
306 size: integer of expected image size.
307 block_size: integer for size of each block (sector).
308 pad_blocks: number of preserved sectors between header and partitions.
309 part_entries: number of partitions to include in header.
310 """
311 part_entry_size = struct.calcsize(GPT.Partition.FORMAT)
312 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
313 parts_bytes = part_entries * part_entry_size
314 parts_blocks = parts_bytes / block_size
315 if parts_bytes % block_size:
316 parts_blocks += 1
317 # PartitionsCRC32 must be updated later explicitly.
318 return cls.ReadFrom(None).Clone(
319 Signature=cls.SIGNATURES[0],
320 Revision=cls.DEFAULT_REVISION,
321 HeaderSize=struct.calcsize(cls.FORMAT),
322 CurrentLBA=1,
323 BackupLBA=size / block_size - 1,
324 FirstUsableLBA=parts_lba + parts_blocks,
325 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
326 DiskGUID=uuid.uuid4().get_bytes(),
327 PartitionEntriesStartingLBA=parts_lba,
328 PartitionEntriesNumber=part_entries,
329 PartitionEntrySize=part_entry_size,
330 )
331
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800332 class PartitionAttributes(object):
333 """Wrapper for Partition.Attributes.
334
335 This can be created using Partition.attrs, but the changed properties won't
336 apply to underlying Partition until an explicit call with
337 Partition.Clone(Attributes=new_attrs).
338 """
339
340 def __init__(self, attrs):
341 self._attrs = attrs
342
343 @property
344 def raw(self):
345 """Returns the raw integer type attributes."""
346 return self._Get()
347
348 def _Get(self):
349 return self._attrs
350
351 def _Set(self, value):
352 self._attrs = value
353
354 successful = BitProperty(_Get, _Set, 56, 1)
355 tries = BitProperty(_Get, _Set, 52, 0xf)
356 priority = BitProperty(_Get, _Set, 48, 0xf)
357 legacy_boot = BitProperty(_Get, _Set, 2, 1)
358 required = BitProperty(_Get, _Set, 0, 1)
359
360 @GPTBlob(PARTITION_DESCRIPTION)
361 class Partition(GPTObject):
362 """The partition entry in GPT.
363
364 Please include following properties when creating a Partition object:
365 - image: a string for path to the image file the partition maps to.
366 - number: the 1-based partition number.
367 - block_size: an integer for size of each block (LBA, or sector).
368 """
369 NAMES_ENCODING = 'utf-16-le'
370 NAMES_LENGTH = 72
371
372 CLONE_CONVERTERS = {
373 # TODO(hungte) check if encoded name is too long.
374 'label': lambda l: (None if l is None else
375 ('Names', l.encode(GPT.Partition.NAMES_ENCODING))),
376 'TypeGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
377 'UniqueGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
378 'Attributes': (
379 lambda v: v.raw if isinstance(v, GPT.PartitionAttributes) else v),
380 }
381
382 def __str__(self):
383 return '%s#%s' % (self.image, self.number)
384
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800385 @classmethod
386 def Create(cls, block_size, image, number):
387 """Creates a new partition entry with given meta data."""
388 part = cls.ReadFrom(
389 None, image=image, number=number, block_size=block_size)
390 return part
391
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800392 def IsUnused(self):
393 """Returns if the partition is unused and can be allocated."""
394 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
395
396 @property
397 def blocks(self):
398 """Return size of partition in blocks (see block_size)."""
399 return self.LastLBA - self.FirstLBA + 1
400
401 @property
402 def offset(self):
403 """Returns offset to partition in bytes."""
404 return self.FirstLBA * self.block_size
405
406 @property
407 def size(self):
408 """Returns size of partition in bytes."""
409 return self.blocks * self.block_size
410
411 @property
412 def type_guid(self):
413 return uuid.UUID(bytes_le=self.TypeGUID)
414
415 @property
416 def unique_guid(self):
417 return uuid.UUID(bytes_le=self.UniqueGUID)
418
419 @property
420 def label(self):
421 """Returns the Names in decoded string type."""
422 return self.Names.decode(self.NAMES_ENCODING).strip('\0')
423
424 @property
425 def attrs(self):
426 return GPT.PartitionAttributes(self.Attributes)
427
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800428 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800429 """GPT constructor.
430
431 See LoadFromFile for how it's usually used.
432 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800433 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800434 self.header = None
435 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800436 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800437 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800438
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800439 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800440 def Create(cls, image_name, size, block_size, pad_blocks=0):
441 """Creates a new GPT instance from given size and block_size.
442
443 Args:
444 image_name: a string of underlying disk image file name.
445 size: expected size of disk image.
446 block_size: size of each block (sector) in bytes.
447 pad_blocks: number of blocks between header and partitions array.
448 """
449 gpt = cls()
450 gpt.block_size = block_size
451 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
452 gpt.partitions = [
453 cls.Partition.Create(block_size, image_name, i + 1)
454 for i in xrange(gpt.header.PartitionEntriesNumber)]
455 return gpt
456
457 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800458 def LoadFromFile(cls, image):
459 """Loads a GPT table from give disk image file object.
460
461 Args:
462 image: a string as file path or a file-like object to read from.
463 """
464 if isinstance(image, basestring):
465 with open(image, 'rb') as f:
466 return cls.LoadFromFile(f)
467
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800468 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800469 image.seek(0)
470 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
471 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
472 logging.debug('Found MBR signature in %s', image.name)
473 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
474 logging.debug('Found PMBR in %s', image.name)
475 gpt.pmbr = pmbr
476
Hung-Te Linf148d322018-04-13 10:24:42 +0800477 # Try DEFAULT_BLOCK_SIZE, then 4K.
478 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800479 # Note because there are devices setting Primary as ignored and the
480 # partition table signature accepts 'CHROMEOS' which is also used by
481 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
482 # first before trying other block sizes, otherwise we may incorrectly
483 # identify a kernel partition as LBA 1 of larger block size system.
484 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
485 (-block_size, os.SEEK_END)]):
486 image.seek(*seek)
487 header = gpt.Header.ReadFrom(image)
488 if header.Signature in cls.Header.SIGNATURES:
489 gpt.block_size = block_size
490 if i != 0:
491 gpt.is_secondary = True
492 break
493 else:
494 # Nothing found, try next block size.
495 continue
496 # Found a valid signature.
497 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800498 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800499 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800500
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800501 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800502 def ReadPartition(image, i):
503 p = gpt.Partition.ReadFrom(
504 image, image=image.name, number=i + 1, block_size=gpt.block_size)
505 return p
506
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800507 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800508 gpt.partitions = [
509 ReadPartition(image, i) for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800510 return gpt
511
512 def GetValidPartitions(self):
513 """Returns the list of partitions before entry with empty type GUID.
514
515 In partition table, the first entry with empty type GUID indicates end of
516 valid partitions. In most implementations all partitions after that should
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800517 be zeroed. However, few implementations for example cgpt, may create
518 partitions in arbitrary order so use this carefully.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800519 """
520 for i, p in enumerate(self.partitions):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800521 if p.IsUnused():
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800522 return self.partitions[:i]
523 return self.partitions
524
525 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800526 """Returns the max LastLBA from all used partitions."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800527 parts = [p for p in self.partitions if not p.IsUnused()]
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800528 return (max(p.LastLBA for p in parts)
529 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800530
531 def GetPartitionTableBlocks(self, header=None):
532 """Returns the blocks (or LBA) of partition table from given header."""
533 if header is None:
534 header = self.header
535 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800536 blocks = size / self.block_size
537 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800538 blocks += 1
539 return blocks
540
541 def Resize(self, new_size):
542 """Adjust GPT for a disk image in given size.
543
544 Args:
545 new_size: Integer for new size of disk image file.
546 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800547 old_size = self.block_size * (self.header.BackupLBA + 1)
548 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800549 raise GPTError(
550 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800551 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800552 if old_size != new_size:
553 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800554 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800555 else:
556 logging.info('Image size (%d, LBA=%d) not changed.',
557 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800558 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800559
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800560 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800561 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800562 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800563
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800564 if last_usable_lba < self.header.LastUsableLBA:
565 max_used_lba = self.GetMaxUsedLBA()
566 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800567 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800568
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800569 self.header = self.header.Clone(
570 BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800571
572 def GetFreeSpace(self):
573 """Returns the free (available) space left according to LastUsableLBA."""
574 max_lba = self.GetMaxUsedLBA()
575 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800576 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800577
578 def ExpandPartition(self, i):
579 """Expands a given partition to last usable LBA.
580
581 Args:
582 i: Index (0-based) of target partition.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800583
584 Returns:
585 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800586 """
587 # Assume no partitions overlap, we need to make sure partition[i] has
588 # largest LBA.
589 if i < 0 or i >= len(self.GetValidPartitions()):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800590 raise GPTError('Partition number %d is invalid.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800591 p = self.partitions[i]
592 max_used_lba = self.GetMaxUsedLBA()
593 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800594 raise GPTError(
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800595 'Cannot expand partition %d because it is not the last allocated '
596 'partition.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800597
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800598 old_blocks = p.blocks
599 p = p.Clone(LastLBA=self.header.LastUsableLBA)
600 new_blocks = p.blocks
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800601 self.partitions[i] = p
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800602 logging.warn(
603 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
604 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800605
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800606 def GetIgnoredHeader(self):
607 """Returns a primary header with signature set to 'IGNOREME'.
608
609 This is a special trick to enforce using backup header, when there is
610 some security exploit in LBA1.
611 """
612 return self.header.Clone(Signature=self.header.SIGNATURE_IGNORE)
613
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800614 def UpdateChecksum(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800615 """Updates all checksum fields in GPT objects.
616
617 The Header.CRC32 is automatically updated in Header.Clone().
618 """
619 parts = ''.join(p.blob for p in self.partitions)
620 self.header = self.header.Clone(
621 PartitionArrayCRC32=binascii.crc32(parts))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800622
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800623 def GetBackupHeader(self, header):
624 """Returns the backup header according to given header."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800625 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800626 header.BackupLBA - self.GetPartitionTableBlocks())
627 return header.Clone(
628 BackupLBA=header.CurrentLBA,
629 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800630 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800631
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800632 @classmethod
633 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
634 """Writes a protective MBR to given file.
635
636 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
637 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
638 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
639 we can follow that.
640
641 Args:
642 create: True to re-create PMBR structure.
643 bootcode: a blob of new boot code.
644 boot_guid a blob for new boot GUID.
645
646 Returns:
647 The written PMBR structure.
648 """
649 if isinstance(image, basestring):
650 with open(image, 'rb+') as f:
651 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
652
653 image.seek(0)
654 assert struct.calcsize(cls.ProtectiveMBR.FORMAT) == cls.DEFAULT_BLOCK_SIZE
655 pmbr = cls.ProtectiveMBR.ReadFrom(image)
656
657 if create:
658 legacy_sectors = min(
659 0x100000000,
660 os.path.getsize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
661 # Partition 0 must have have the fixed CHS with number of sectors
662 # (calculated as legacy_sectors later).
663 part0 = ('00000200eeffffff01000000'.decode('hex') +
664 struct.pack('<I', legacy_sectors))
665 # Partition 1~3 should be all zero.
666 part1 = '\x00' * 16
667 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
668 pmbr = pmbr.Clone(
669 BootGUID=cls.TYPE_GUID_UNUSED,
670 DiskID=0,
671 Magic=cls.ProtectiveMBR.MAGIC,
672 LegacyPart0=part0,
673 LegacyPart1=part1,
674 LegacyPart2=part1,
675 LegacyPart3=part1,
676 Signature=cls.ProtectiveMBR.SIGNATURE)
677
678 if bootcode:
679 if len(bootcode) > len(pmbr.BootCode):
680 logging.info(
681 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
682 bootcode = bootcode[:len(pmbr.BootCode)]
683 pmbr = pmbr.Clone(BootCode=bootcode)
684 if boot_guid:
685 pmbr = pmbr.Clone(BootGUID=boot_guid)
686
687 blob = pmbr.blob
688 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
689 image.seek(0)
690 image.write(blob)
691 return pmbr
692
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800693 def WriteToFile(self, image):
694 """Updates partition table in a disk image file.
695
696 Args:
697 image: a string as file path or a file-like object to write into.
698 """
699 if isinstance(image, basestring):
700 with open(image, 'rb+') as f:
701 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800702
703 def WriteData(name, blob, lba):
704 """Writes a blob into given location."""
705 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800706 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800707 image.seek(lba * self.block_size)
708 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800709
710 self.UpdateChecksum()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800711 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800712
713 header = self.header
714 WriteData('GPT Header', header.blob, header.CurrentLBA)
715 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
716 logging.info(
717 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
718 header.LastUsableLBA)
719
720 if not self.is_secondary:
721 # When is_secondary is True, the header we have is actually backup header.
722 backup_header = self.GetBackupHeader(self.header)
723 WriteData(
724 'Backup Partitions', parts_blob,
725 backup_header.PartitionEntriesStartingLBA)
726 WriteData(
727 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800728
729
730class GPTCommands(object):
731 """Collection of GPT sub commands for command line to use.
732
733 The commands are derived from `cgpt`, but not necessary to be 100% compatible
734 with cgpt.
735 """
736
737 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800738 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800739 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800740 ('type', 'type guid'),
741 ('unique', 'unique guid'),
742 ('label', 'label'),
743 ('Successful', 'Successful flag'),
744 ('Tries', 'Tries flag'),
745 ('Priority', 'Priority flag'),
746 ('Legacy', 'Legacy Boot flag'),
747 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800748
749 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800750 commands = dict(
751 (command.lower(), getattr(self, command)())
752 for command in dir(self)
753 if (isinstance(getattr(self, command), type) and
754 issubclass(getattr(self, command), self.SubCommand) and
755 getattr(self, command) is not self.SubCommand)
756 )
757 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800758
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800759 def DefineArgs(self, parser):
760 """Defines all available commands to an argparser subparsers instance."""
761 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
762 for name, instance in sorted(self.commands.iteritems()):
763 parser = subparsers.add_parser(
764 name, description=instance.__doc__,
765 formatter_class=argparse.RawDescriptionHelpFormatter,
766 help=instance.__doc__.splitlines()[0])
767 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800768
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800769 def Execute(self, args):
770 """Execute the sub commands by given parsed arguments."""
771 self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800772
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800773 class SubCommand(object):
774 """A base class for sub commands to derive from."""
775
776 def DefineArgs(self, parser):
777 """Defines command line arguments to argparse parser.
778
779 Args:
780 parser: An argparse parser instance.
781 """
782 del parser # Unused.
783 raise NotImplementedError
784
785 def Execute(self, args):
786 """Execute the command.
787
788 Args:
789 args: An argparse parsed namespace.
790 """
791 del args # Unused.
792 raise NotImplementedError
793
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800794 class Create(SubCommand):
795 """Create or reset GPT headers and tables.
796
797 Create or reset an empty GPT.
798 """
799
800 def DefineArgs(self, parser):
801 parser.add_argument(
802 '-z', '--zero', action='store_true',
803 help='Zero the sectors of the GPT table and entries')
804 parser.add_argument(
805 '-p', '--pad_blocks', type=int, default=0,
806 help=('Size (in blocks) of the disk to pad between the '
807 'primary GPT header and its entries, default %(default)s'))
808 parser.add_argument(
809 '--block_size', type=int, default=GPT.DEFAULT_BLOCK_SIZE,
810 help='Size of each block (sector) in bytes.')
811 parser.add_argument(
812 'image_file', type=argparse.FileType('rb+'),
813 help='Disk image file to create.')
814
815 def Execute(self, args):
816 block_size = args.block_size
817 gpt = GPT.Create(
818 args.image_file.name, os.path.getsize(args.image_file.name),
819 block_size, args.pad_blocks)
820 if args.zero:
821 # In theory we only need to clear LBA 1, but to make sure images already
822 # initialized with different block size won't have GPT signature in
823 # different locations, we should zero until first usable LBA.
824 args.image_file.seek(0)
825 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
826 gpt.WriteToFile(args.image_file)
827 print('OK: Created GPT for %s' % args.image_file.name)
828
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800829 class Boot(SubCommand):
830 """Edit the PMBR sector for legacy BIOSes.
831
832 With no options, it will just print the PMBR boot guid.
833 """
834
835 def DefineArgs(self, parser):
836 parser.add_argument(
837 '-i', '--number', type=int,
838 help='Set bootable partition')
839 parser.add_argument(
840 '-b', '--bootloader', type=argparse.FileType('r'),
841 help='Install bootloader code in the PMBR')
842 parser.add_argument(
843 '-p', '--pmbr', action='store_true',
844 help='Create legacy PMBR partition table')
845 parser.add_argument(
846 'image_file', type=argparse.FileType('rb+'),
847 help='Disk image file to change PMBR.')
848
849 def Execute(self, args):
850 """Rebuilds the protective MBR."""
851 bootcode = args.bootloader.read() if args.bootloader else None
852 boot_guid = None
853 if args.number is not None:
854 gpt = GPT.LoadFromFile(args.image_file)
855 boot_guid = gpt.partitions[args.number - 1].UniqueGUID
856 pmbr = GPT.WriteProtectiveMBR(
857 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
858
859 print(str(pmbr.boot_guid).upper())
860
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800861 class Legacy(SubCommand):
862 """Switch between GPT and Legacy GPT.
863
864 Switch GPT header signature to "CHROMEOS".
865 """
866
867 def DefineArgs(self, parser):
868 parser.add_argument(
869 '-e', '--efi', action='store_true',
870 help='Switch GPT header signature back to "EFI PART"')
871 parser.add_argument(
872 '-p', '--primary-ignore', action='store_true',
873 help='Switch primary GPT header signature to "IGNOREME"')
874 parser.add_argument(
875 'image_file', type=argparse.FileType('rb+'),
876 help='Disk image file to change.')
877
878 def Execute(self, args):
879 gpt = GPT.LoadFromFile(args.image_file)
880 # cgpt behavior: if -p is specified, -e is ignored.
881 if args.primary_ignore:
882 if gpt.is_secondary:
883 raise GPTError('Sorry, the disk already has primary GPT ignored.')
884 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
885 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
886 gpt.header = gpt.GetBackupHeader(self.header)
887 gpt.is_secondary = True
888 else:
889 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
890 gpt.header = gpt.header.Clone(Signature=new_signature)
891 gpt.WriteToFile(args.image_file)
892 if args.primary_ignore:
893 print('OK: Set %s primary GPT header to %s.' %
894 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
895 else:
896 print('OK: Changed GPT signature for %s to %s.' %
897 (args.image_file.name, new_signature))
898
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800899 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800900 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800901
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800902 def DefineArgs(self, parser):
903 parser.add_argument(
904 'image_file', type=argparse.FileType('rb+'),
905 help='Disk image file to repair.')
906
907 def Execute(self, args):
908 gpt = GPT.LoadFromFile(args.image_file)
909 gpt.Resize(os.path.getsize(args.image_file.name))
910 gpt.WriteToFile(args.image_file)
911 print('Disk image file %s repaired.' % args.image_file.name)
912
913 class Expand(SubCommand):
914 """Expands a GPT partition to all available free space."""
915
916 def DefineArgs(self, parser):
917 parser.add_argument(
918 '-i', '--number', type=int, required=True,
919 help='The partition to expand.')
920 parser.add_argument(
921 'image_file', type=argparse.FileType('rb+'),
922 help='Disk image file to modify.')
923
924 def Execute(self, args):
925 gpt = GPT.LoadFromFile(args.image_file)
926 old_blocks, new_blocks = gpt.ExpandPartition(args.number - 1)
927 gpt.WriteToFile(args.image_file)
928 if old_blocks < new_blocks:
929 print(
930 'Partition %s on disk image file %s has been extended '
931 'from %s to %s .' %
932 (args.number, args.image_file.name, old_blocks * gpt.block_size,
933 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800934 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800935 print('Nothing to expand for disk image %s partition %s.' %
936 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800937
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800938 class Show(SubCommand):
939 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800940
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800941 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800942 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800943
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800944 def DefineArgs(self, parser):
945 parser.add_argument(
946 '--numeric', '-n', action='store_true',
947 help='Numeric output only.')
948 parser.add_argument(
949 '--quick', '-q', action='store_true',
950 help='Quick output.')
951 parser.add_argument(
952 '-i', '--number', type=int,
953 help='Show specified partition only, with format args.')
954 for name, help_str in GPTCommands.FORMAT_ARGS:
955 # TODO(hungte) Alert if multiple args were specified.
956 parser.add_argument(
957 '--%s' % name, '-%c' % name[0], action='store_true',
958 help='[format] %s.' % help_str)
959 parser.add_argument(
960 'image_file', type=argparse.FileType('rb'),
961 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800962
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800963 def Execute(self, args):
964 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800965
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800966 def FormatGUID(bytes_le):
967 return str(uuid.UUID(bytes_le=bytes_le)).upper()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800968
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800969 def FormatTypeGUID(p):
970 guid_str = FormatGUID(p.TypeGUID)
971 if not args.numeric:
972 names = gpt.TYPE_GUID_MAP.get(guid_str)
973 if names:
974 return names
975 return guid_str
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800976
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800977 def FormatNames(p):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800978 return p.label
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800979
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800980 def IsBootableType(type_guid):
981 return type_guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800982
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800983 def FormatAttribute(attrs, chromeos_kernel=False):
984 if args.numeric:
985 return '[%x]' % (attrs.raw >> 48)
986 results = []
987 if chromeos_kernel:
988 results += [
989 'priority=%d' % attrs.priority,
990 'tries=%d' % attrs.tries,
991 'successful=%d' % attrs.successful]
992 if attrs.required:
993 results += ['required=1']
994 if attrs.legacy_boot:
995 results += ['legacy_boot=1']
996 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800997
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800998 def ApplyFormatArgs(p):
999 if args.begin:
1000 return p.FirstLBA
1001 elif args.size:
1002 return p.blocks
1003 elif args.type:
1004 return FormatTypeGUID(p)
1005 elif args.unique:
1006 return FormatGUID(p.UniqueGUID)
1007 elif args.label:
1008 return FormatNames(p)
1009 elif args.Successful:
1010 return p.attrs.successful
1011 elif args.Priority:
1012 return p.attrs.priority
1013 elif args.Tries:
1014 return p.attrs.tries
1015 elif args.Legacy:
1016 return p.attrs.legacy_boot
1017 elif args.Attribute:
1018 return '[%x]' % (p.Attributes >> 48)
1019 else:
1020 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001021
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001022 def IsFormatArgsSpecified():
1023 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001024
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001025 gpt = GPT.LoadFromFile(args.image_file)
1026 logging.debug('%r', gpt.header)
1027 fmt = '%12s %11s %7s %s'
1028 fmt2 = '%32s %s: %s'
1029 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001030
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001031 if IsFormatArgsSpecified() and args.number is None:
1032 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001033
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001034 if not (args.number is None or
1035 0 < args.number <= gpt.header.PartitionEntriesNumber):
1036 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001037
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001038 partitions = gpt.partitions
1039 do_print_gpt_blocks = False
1040 if not (args.quick or IsFormatArgsSpecified()):
1041 print(fmt % header)
1042 if args.number is None:
1043 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001044
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001045 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001046 if gpt.pmbr:
1047 print(fmt % (0, 1, '', 'PMBR'))
1048 if gpt.is_secondary:
1049 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1050 else:
1051 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1052 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1053 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001054
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001055 for p in partitions:
1056 if args.number is None:
1057 # Skip unused partitions.
1058 if p.IsUnused():
1059 continue
1060 elif p.number != args.number:
1061 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001062
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001063 if IsFormatArgsSpecified():
1064 print(ApplyFormatArgs(p))
1065 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001066
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001067 type_guid = FormatGUID(p.TypeGUID)
1068 print(fmt % (p.FirstLBA, p.blocks, p.number,
1069 FormatTypeGUID(p) if args.quick else
1070 'Label: "%s"' % FormatNames(p)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001071
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001072 if not args.quick:
1073 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
1074 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
1075 if args.numeric or IsBootableType(type_guid):
1076 name = GPT.TYPE_GUID_MAP[type_guid]
1077 print(fmt2 % ('', 'Attr', FormatAttribute(
1078 p.attrs, name == GPT.TYPE_NAME_CHROMEOS_KERNEL)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001079
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001080 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001081 if gpt.is_secondary:
1082 header = gpt.header
1083 else:
1084 f = args.image_file
1085 f.seek(gpt.header.BackupLBA * gpt.block_size)
1086 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001087 print(fmt % (header.PartitionEntriesStartingLBA,
1088 gpt.GetPartitionTableBlocks(header), '',
1089 'Sec GPT table'))
1090 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001091
1092
1093def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001094 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001095 parser = argparse.ArgumentParser(description='GPT Utility.')
1096 parser.add_argument('--verbose', '-v', action='count', default=0,
1097 help='increase verbosity.')
1098 parser.add_argument('--debug', '-d', action='store_true',
1099 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001100 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001101
1102 args = parser.parse_args()
1103 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1104 if args.debug:
1105 log_level = logging.DEBUG
1106 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1107 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001108 try:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001109 commands.Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001110 except Exception as e:
1111 if args.verbose or args.debug:
1112 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001113 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001114
1115
1116if __name__ == '__main__':
1117 main()