blob: 453580073a55d4580901294c0a2368ef4cf6202c [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 }
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800254 TYPE_GUID_REVERSE_MAP = dict(
255 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
256 for k, v in TYPE_GUID_MAP.iteritems())
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800257 TYPE_GUID_LIST_BOOTABLE = [
258 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309', # ChromeOS kernel
259 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B', # EFI System Partition
260 ]
261
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800262 @GPTBlob(PMBR_DESCRIPTION)
263 class ProtectiveMBR(GPTObject):
264 """Protective MBR (PMBR) in GPT."""
265 SIGNATURE = '\x55\xAA'
266 MAGIC = '\x1d\x9a'
267
268 CLONE_CONVERTERS = {
269 'BootGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v
270 }
271
272 @property
273 def boot_guid(self):
274 """Returns the BootGUID in decoded (uuid.UUID) format."""
275 return uuid.UUID(bytes_le=self.BootGUID)
276
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800277 @GPTBlob(HEADER_DESCRIPTION)
278 class Header(GPTObject):
279 """Wrapper to Header in GPT."""
280 SIGNATURES = ['EFI PART', 'CHROMEOS']
281 SIGNATURE_IGNORE = 'IGNOREME'
282 DEFAULT_REVISION = '\x00\x00\x01\x00'
283
284 DEFAULT_PARTITION_ENTRIES = 128
285 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
286
287 def Clone(self, **dargs):
288 """Creates a new instance with modifications.
289
290 GPT objects are usually named tuples that are immutable, so the only way
291 to make changes is to create a new instance with modifications.
292
293 CRC32 is always updated but PartitionArrayCRC32 must be updated explicitly
294 since we can't track changes in GPT.partitions automatically.
295
296 Note since GPTHeader.Clone will always update CRC, we can only check and
297 compute CRC by super(GPT.Header, header).Clone, or header._replace.
298 """
299 dargs['CRC32'] = 0
300 header = super(GPT.Header, self).Clone(**dargs)
301 return super(GPT.Header, header).Clone(CRC32=binascii.crc32(header.blob))
302
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800303 @classmethod
304 def Create(cls, size, block_size, pad_blocks=0,
305 part_entries=DEFAULT_PARTITION_ENTRIES):
306 """Creates a header with default values.
307
308 Args:
309 size: integer of expected image size.
310 block_size: integer for size of each block (sector).
311 pad_blocks: number of preserved sectors between header and partitions.
312 part_entries: number of partitions to include in header.
313 """
314 part_entry_size = struct.calcsize(GPT.Partition.FORMAT)
315 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
316 parts_bytes = part_entries * part_entry_size
317 parts_blocks = parts_bytes / block_size
318 if parts_bytes % block_size:
319 parts_blocks += 1
320 # PartitionsCRC32 must be updated later explicitly.
321 return cls.ReadFrom(None).Clone(
322 Signature=cls.SIGNATURES[0],
323 Revision=cls.DEFAULT_REVISION,
324 HeaderSize=struct.calcsize(cls.FORMAT),
325 CurrentLBA=1,
326 BackupLBA=size / block_size - 1,
327 FirstUsableLBA=parts_lba + parts_blocks,
328 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
329 DiskGUID=uuid.uuid4().get_bytes(),
330 PartitionEntriesStartingLBA=parts_lba,
331 PartitionEntriesNumber=part_entries,
332 PartitionEntrySize=part_entry_size,
333 )
334
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800335 class PartitionAttributes(object):
336 """Wrapper for Partition.Attributes.
337
338 This can be created using Partition.attrs, but the changed properties won't
339 apply to underlying Partition until an explicit call with
340 Partition.Clone(Attributes=new_attrs).
341 """
342
343 def __init__(self, attrs):
344 self._attrs = attrs
345
346 @property
347 def raw(self):
348 """Returns the raw integer type attributes."""
349 return self._Get()
350
351 def _Get(self):
352 return self._attrs
353
354 def _Set(self, value):
355 self._attrs = value
356
357 successful = BitProperty(_Get, _Set, 56, 1)
358 tries = BitProperty(_Get, _Set, 52, 0xf)
359 priority = BitProperty(_Get, _Set, 48, 0xf)
360 legacy_boot = BitProperty(_Get, _Set, 2, 1)
361 required = BitProperty(_Get, _Set, 0, 1)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800362 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800363
364 @GPTBlob(PARTITION_DESCRIPTION)
365 class Partition(GPTObject):
366 """The partition entry in GPT.
367
368 Please include following properties when creating a Partition object:
369 - image: a string for path to the image file the partition maps to.
370 - number: the 1-based partition number.
371 - block_size: an integer for size of each block (LBA, or sector).
372 """
373 NAMES_ENCODING = 'utf-16-le'
374 NAMES_LENGTH = 72
375
376 CLONE_CONVERTERS = {
377 # TODO(hungte) check if encoded name is too long.
378 'label': lambda l: (None if l is None else
379 ('Names', l.encode(GPT.Partition.NAMES_ENCODING))),
380 'TypeGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
381 'UniqueGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
382 'Attributes': (
383 lambda v: v.raw if isinstance(v, GPT.PartitionAttributes) else v),
384 }
385
386 def __str__(self):
387 return '%s#%s' % (self.image, self.number)
388
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800389 @classmethod
390 def Create(cls, block_size, image, number):
391 """Creates a new partition entry with given meta data."""
392 part = cls.ReadFrom(
393 None, image=image, number=number, block_size=block_size)
394 return part
395
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800396 def IsUnused(self):
397 """Returns if the partition is unused and can be allocated."""
398 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
399
400 @property
401 def blocks(self):
402 """Return size of partition in blocks (see block_size)."""
403 return self.LastLBA - self.FirstLBA + 1
404
405 @property
406 def offset(self):
407 """Returns offset to partition in bytes."""
408 return self.FirstLBA * self.block_size
409
410 @property
411 def size(self):
412 """Returns size of partition in bytes."""
413 return self.blocks * self.block_size
414
415 @property
416 def type_guid(self):
417 return uuid.UUID(bytes_le=self.TypeGUID)
418
419 @property
420 def unique_guid(self):
421 return uuid.UUID(bytes_le=self.UniqueGUID)
422
423 @property
424 def label(self):
425 """Returns the Names in decoded string type."""
426 return self.Names.decode(self.NAMES_ENCODING).strip('\0')
427
428 @property
429 def attrs(self):
430 return GPT.PartitionAttributes(self.Attributes)
431
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800432 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800433 """GPT constructor.
434
435 See LoadFromFile for how it's usually used.
436 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800437 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800438 self.header = None
439 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800440 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800441 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800442
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800443 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800444 def Create(cls, image_name, size, block_size, pad_blocks=0):
445 """Creates a new GPT instance from given size and block_size.
446
447 Args:
448 image_name: a string of underlying disk image file name.
449 size: expected size of disk image.
450 block_size: size of each block (sector) in bytes.
451 pad_blocks: number of blocks between header and partitions array.
452 """
453 gpt = cls()
454 gpt.block_size = block_size
455 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
456 gpt.partitions = [
457 cls.Partition.Create(block_size, image_name, i + 1)
458 for i in xrange(gpt.header.PartitionEntriesNumber)]
459 return gpt
460
461 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800462 def LoadFromFile(cls, image):
463 """Loads a GPT table from give disk image file object.
464
465 Args:
466 image: a string as file path or a file-like object to read from.
467 """
468 if isinstance(image, basestring):
469 with open(image, 'rb') as f:
470 return cls.LoadFromFile(f)
471
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800472 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800473 image.seek(0)
474 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
475 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
476 logging.debug('Found MBR signature in %s', image.name)
477 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
478 logging.debug('Found PMBR in %s', image.name)
479 gpt.pmbr = pmbr
480
Hung-Te Linf148d322018-04-13 10:24:42 +0800481 # Try DEFAULT_BLOCK_SIZE, then 4K.
482 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800483 # Note because there are devices setting Primary as ignored and the
484 # partition table signature accepts 'CHROMEOS' which is also used by
485 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
486 # first before trying other block sizes, otherwise we may incorrectly
487 # identify a kernel partition as LBA 1 of larger block size system.
488 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
489 (-block_size, os.SEEK_END)]):
490 image.seek(*seek)
491 header = gpt.Header.ReadFrom(image)
492 if header.Signature in cls.Header.SIGNATURES:
493 gpt.block_size = block_size
494 if i != 0:
495 gpt.is_secondary = True
496 break
497 else:
498 # Nothing found, try next block size.
499 continue
500 # Found a valid signature.
501 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800502 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800503 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800504
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800505 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800506 def ReadPartition(image, i):
507 p = gpt.Partition.ReadFrom(
508 image, image=image.name, number=i + 1, block_size=gpt.block_size)
509 return p
510
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800511 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800512 gpt.partitions = [
513 ReadPartition(image, i) for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800514 return gpt
515
516 def GetValidPartitions(self):
517 """Returns the list of partitions before entry with empty type GUID.
518
519 In partition table, the first entry with empty type GUID indicates end of
520 valid partitions. In most implementations all partitions after that should
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800521 be zeroed. However, few implementations for example cgpt, may create
522 partitions in arbitrary order so use this carefully.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800523 """
524 for i, p in enumerate(self.partitions):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800525 if p.IsUnused():
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800526 return self.partitions[:i]
527 return self.partitions
528
529 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800530 """Returns the max LastLBA from all used partitions."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800531 parts = [p for p in self.partitions if not p.IsUnused()]
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800532 return (max(p.LastLBA for p in parts)
533 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800534
535 def GetPartitionTableBlocks(self, header=None):
536 """Returns the blocks (or LBA) of partition table from given header."""
537 if header is None:
538 header = self.header
539 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800540 blocks = size / self.block_size
541 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800542 blocks += 1
543 return blocks
544
545 def Resize(self, new_size):
546 """Adjust GPT for a disk image in given size.
547
548 Args:
549 new_size: Integer for new size of disk image file.
550 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800551 old_size = self.block_size * (self.header.BackupLBA + 1)
552 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800553 raise GPTError(
554 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800555 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800556 if old_size != new_size:
557 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800558 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800559 else:
560 logging.info('Image size (%d, LBA=%d) not changed.',
561 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800562 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800563
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800564 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800565 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800566 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800567
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800568 if last_usable_lba < self.header.LastUsableLBA:
569 max_used_lba = self.GetMaxUsedLBA()
570 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800571 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800572
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800573 self.header = self.header.Clone(
574 BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800575
576 def GetFreeSpace(self):
577 """Returns the free (available) space left according to LastUsableLBA."""
578 max_lba = self.GetMaxUsedLBA()
579 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800580 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800581
582 def ExpandPartition(self, i):
583 """Expands a given partition to last usable LBA.
584
585 Args:
586 i: Index (0-based) of target partition.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800587
588 Returns:
589 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800590 """
591 # Assume no partitions overlap, we need to make sure partition[i] has
592 # largest LBA.
593 if i < 0 or i >= len(self.GetValidPartitions()):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800594 raise GPTError('Partition number %d is invalid.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800595 p = self.partitions[i]
596 max_used_lba = self.GetMaxUsedLBA()
597 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800598 raise GPTError(
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800599 'Cannot expand partition %d because it is not the last allocated '
600 'partition.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800601
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800602 old_blocks = p.blocks
603 p = p.Clone(LastLBA=self.header.LastUsableLBA)
604 new_blocks = p.blocks
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800605 self.partitions[i] = p
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800606 logging.warn(
607 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
608 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800609
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800610 def GetIgnoredHeader(self):
611 """Returns a primary header with signature set to 'IGNOREME'.
612
613 This is a special trick to enforce using backup header, when there is
614 some security exploit in LBA1.
615 """
616 return self.header.Clone(Signature=self.header.SIGNATURE_IGNORE)
617
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800618 def UpdateChecksum(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800619 """Updates all checksum fields in GPT objects.
620
621 The Header.CRC32 is automatically updated in Header.Clone().
622 """
623 parts = ''.join(p.blob for p in self.partitions)
624 self.header = self.header.Clone(
625 PartitionArrayCRC32=binascii.crc32(parts))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800626
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800627 def GetBackupHeader(self, header):
628 """Returns the backup header according to given header."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800629 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800630 header.BackupLBA - self.GetPartitionTableBlocks())
631 return header.Clone(
632 BackupLBA=header.CurrentLBA,
633 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800634 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800635
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800636 @classmethod
637 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
638 """Writes a protective MBR to given file.
639
640 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
641 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
642 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
643 we can follow that.
644
645 Args:
646 create: True to re-create PMBR structure.
647 bootcode: a blob of new boot code.
648 boot_guid a blob for new boot GUID.
649
650 Returns:
651 The written PMBR structure.
652 """
653 if isinstance(image, basestring):
654 with open(image, 'rb+') as f:
655 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
656
657 image.seek(0)
658 assert struct.calcsize(cls.ProtectiveMBR.FORMAT) == cls.DEFAULT_BLOCK_SIZE
659 pmbr = cls.ProtectiveMBR.ReadFrom(image)
660
661 if create:
662 legacy_sectors = min(
663 0x100000000,
664 os.path.getsize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
665 # Partition 0 must have have the fixed CHS with number of sectors
666 # (calculated as legacy_sectors later).
667 part0 = ('00000200eeffffff01000000'.decode('hex') +
668 struct.pack('<I', legacy_sectors))
669 # Partition 1~3 should be all zero.
670 part1 = '\x00' * 16
671 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
672 pmbr = pmbr.Clone(
673 BootGUID=cls.TYPE_GUID_UNUSED,
674 DiskID=0,
675 Magic=cls.ProtectiveMBR.MAGIC,
676 LegacyPart0=part0,
677 LegacyPart1=part1,
678 LegacyPart2=part1,
679 LegacyPart3=part1,
680 Signature=cls.ProtectiveMBR.SIGNATURE)
681
682 if bootcode:
683 if len(bootcode) > len(pmbr.BootCode):
684 logging.info(
685 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
686 bootcode = bootcode[:len(pmbr.BootCode)]
687 pmbr = pmbr.Clone(BootCode=bootcode)
688 if boot_guid:
689 pmbr = pmbr.Clone(BootGUID=boot_guid)
690
691 blob = pmbr.blob
692 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
693 image.seek(0)
694 image.write(blob)
695 return pmbr
696
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800697 def WriteToFile(self, image):
698 """Updates partition table in a disk image file.
699
700 Args:
701 image: a string as file path or a file-like object to write into.
702 """
703 if isinstance(image, basestring):
704 with open(image, 'rb+') as f:
705 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800706
707 def WriteData(name, blob, lba):
708 """Writes a blob into given location."""
709 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800710 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800711 image.seek(lba * self.block_size)
712 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800713
714 self.UpdateChecksum()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800715 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800716
717 header = self.header
718 WriteData('GPT Header', header.blob, header.CurrentLBA)
719 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
720 logging.info(
721 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
722 header.LastUsableLBA)
723
724 if not self.is_secondary:
725 # When is_secondary is True, the header we have is actually backup header.
726 backup_header = self.GetBackupHeader(self.header)
727 WriteData(
728 'Backup Partitions', parts_blob,
729 backup_header.PartitionEntriesStartingLBA)
730 WriteData(
731 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800732
733
734class GPTCommands(object):
735 """Collection of GPT sub commands for command line to use.
736
737 The commands are derived from `cgpt`, but not necessary to be 100% compatible
738 with cgpt.
739 """
740
741 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800742 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800743 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800744 ('type', 'type guid'),
745 ('unique', 'unique guid'),
746 ('label', 'label'),
747 ('Successful', 'Successful flag'),
748 ('Tries', 'Tries flag'),
749 ('Priority', 'Priority flag'),
750 ('Legacy', 'Legacy Boot flag'),
751 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800752
753 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800754 commands = dict(
755 (command.lower(), getattr(self, command)())
756 for command in dir(self)
757 if (isinstance(getattr(self, command), type) and
758 issubclass(getattr(self, command), self.SubCommand) and
759 getattr(self, command) is not self.SubCommand)
760 )
761 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800762
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800763 def DefineArgs(self, parser):
764 """Defines all available commands to an argparser subparsers instance."""
765 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
766 for name, instance in sorted(self.commands.iteritems()):
767 parser = subparsers.add_parser(
768 name, description=instance.__doc__,
769 formatter_class=argparse.RawDescriptionHelpFormatter,
770 help=instance.__doc__.splitlines()[0])
771 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800772
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800773 def Execute(self, args):
774 """Execute the sub commands by given parsed arguments."""
775 self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800776
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800777 class SubCommand(object):
778 """A base class for sub commands to derive from."""
779
780 def DefineArgs(self, parser):
781 """Defines command line arguments to argparse parser.
782
783 Args:
784 parser: An argparse parser instance.
785 """
786 del parser # Unused.
787 raise NotImplementedError
788
789 def Execute(self, args):
790 """Execute the command.
791
792 Args:
793 args: An argparse parsed namespace.
794 """
795 del args # Unused.
796 raise NotImplementedError
797
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800798 class Create(SubCommand):
799 """Create or reset GPT headers and tables.
800
801 Create or reset an empty GPT.
802 """
803
804 def DefineArgs(self, parser):
805 parser.add_argument(
806 '-z', '--zero', action='store_true',
807 help='Zero the sectors of the GPT table and entries')
808 parser.add_argument(
809 '-p', '--pad_blocks', type=int, default=0,
810 help=('Size (in blocks) of the disk to pad between the '
811 'primary GPT header and its entries, default %(default)s'))
812 parser.add_argument(
813 '--block_size', type=int, default=GPT.DEFAULT_BLOCK_SIZE,
814 help='Size of each block (sector) in bytes.')
815 parser.add_argument(
816 'image_file', type=argparse.FileType('rb+'),
817 help='Disk image file to create.')
818
819 def Execute(self, args):
820 block_size = args.block_size
821 gpt = GPT.Create(
822 args.image_file.name, os.path.getsize(args.image_file.name),
823 block_size, args.pad_blocks)
824 if args.zero:
825 # In theory we only need to clear LBA 1, but to make sure images already
826 # initialized with different block size won't have GPT signature in
827 # different locations, we should zero until first usable LBA.
828 args.image_file.seek(0)
829 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
830 gpt.WriteToFile(args.image_file)
831 print('OK: Created GPT for %s' % args.image_file.name)
832
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800833 class Boot(SubCommand):
834 """Edit the PMBR sector for legacy BIOSes.
835
836 With no options, it will just print the PMBR boot guid.
837 """
838
839 def DefineArgs(self, parser):
840 parser.add_argument(
841 '-i', '--number', type=int,
842 help='Set bootable partition')
843 parser.add_argument(
844 '-b', '--bootloader', type=argparse.FileType('r'),
845 help='Install bootloader code in the PMBR')
846 parser.add_argument(
847 '-p', '--pmbr', action='store_true',
848 help='Create legacy PMBR partition table')
849 parser.add_argument(
850 'image_file', type=argparse.FileType('rb+'),
851 help='Disk image file to change PMBR.')
852
853 def Execute(self, args):
854 """Rebuilds the protective MBR."""
855 bootcode = args.bootloader.read() if args.bootloader else None
856 boot_guid = None
857 if args.number is not None:
858 gpt = GPT.LoadFromFile(args.image_file)
859 boot_guid = gpt.partitions[args.number - 1].UniqueGUID
860 pmbr = GPT.WriteProtectiveMBR(
861 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
862
863 print(str(pmbr.boot_guid).upper())
864
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800865 class Legacy(SubCommand):
866 """Switch between GPT and Legacy GPT.
867
868 Switch GPT header signature to "CHROMEOS".
869 """
870
871 def DefineArgs(self, parser):
872 parser.add_argument(
873 '-e', '--efi', action='store_true',
874 help='Switch GPT header signature back to "EFI PART"')
875 parser.add_argument(
876 '-p', '--primary-ignore', action='store_true',
877 help='Switch primary GPT header signature to "IGNOREME"')
878 parser.add_argument(
879 'image_file', type=argparse.FileType('rb+'),
880 help='Disk image file to change.')
881
882 def Execute(self, args):
883 gpt = GPT.LoadFromFile(args.image_file)
884 # cgpt behavior: if -p is specified, -e is ignored.
885 if args.primary_ignore:
886 if gpt.is_secondary:
887 raise GPTError('Sorry, the disk already has primary GPT ignored.')
888 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
889 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
890 gpt.header = gpt.GetBackupHeader(self.header)
891 gpt.is_secondary = True
892 else:
893 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
894 gpt.header = gpt.header.Clone(Signature=new_signature)
895 gpt.WriteToFile(args.image_file)
896 if args.primary_ignore:
897 print('OK: Set %s primary GPT header to %s.' %
898 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
899 else:
900 print('OK: Changed GPT signature for %s to %s.' %
901 (args.image_file.name, new_signature))
902
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800903 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800904 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800905
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800906 def DefineArgs(self, parser):
907 parser.add_argument(
908 'image_file', type=argparse.FileType('rb+'),
909 help='Disk image file to repair.')
910
911 def Execute(self, args):
912 gpt = GPT.LoadFromFile(args.image_file)
913 gpt.Resize(os.path.getsize(args.image_file.name))
914 gpt.WriteToFile(args.image_file)
915 print('Disk image file %s repaired.' % args.image_file.name)
916
917 class Expand(SubCommand):
918 """Expands a GPT partition to all available free space."""
919
920 def DefineArgs(self, parser):
921 parser.add_argument(
922 '-i', '--number', type=int, required=True,
923 help='The partition to expand.')
924 parser.add_argument(
925 'image_file', type=argparse.FileType('rb+'),
926 help='Disk image file to modify.')
927
928 def Execute(self, args):
929 gpt = GPT.LoadFromFile(args.image_file)
930 old_blocks, new_blocks = gpt.ExpandPartition(args.number - 1)
931 gpt.WriteToFile(args.image_file)
932 if old_blocks < new_blocks:
933 print(
934 'Partition %s on disk image file %s has been extended '
935 'from %s to %s .' %
936 (args.number, args.image_file.name, old_blocks * gpt.block_size,
937 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800938 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800939 print('Nothing to expand for disk image %s partition %s.' %
940 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800941
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800942 class Add(SubCommand):
943 """Add, edit, or remove a partition entry.
944
945 Use the -i option to modify an existing partition.
946 The -b, -s, and -t options must be given for new partitions.
947
948 The partition type may also be given as one of these aliases:
949
950 firmware ChromeOS firmware
951 kernel ChromeOS kernel
952 rootfs ChromeOS rootfs
953 data Linux data
954 reserved ChromeOS reserved
955 efi EFI System Partition
956 unused Unused (nonexistent) partition
957 """
958 def DefineArgs(self, parser):
959 parser.add_argument(
960 '-i', '--number', type=int,
961 help='Specify partition (default is next available)')
962 parser.add_argument(
963 '-b', '--begin', type=int,
964 help='Beginning sector')
965 parser.add_argument(
966 '-s', '--sectors', type=int,
967 help='Size in sectors (logical blocks).')
968 parser.add_argument(
969 '-t', '--type_guid',
970 help='Partition Type GUID')
971 parser.add_argument(
972 '-u', '--unique_guid',
973 help='Partition Unique ID')
974 parser.add_argument(
975 '-l', '--label',
976 help='Label')
977 parser.add_argument(
978 '-S', '--successful', type=int, choices=xrange(2),
979 help='set Successful flag')
980 parser.add_argument(
981 '-T', '--tries', type=int,
982 help='set Tries flag (0-15)')
983 parser.add_argument(
984 '-P', '--priority', type=int,
985 help='set Priority flag (0-15)')
986 parser.add_argument(
987 '-R', '--required', type=int, choices=xrange(2),
988 help='set Required flag')
989 parser.add_argument(
990 '-B', '--boot_legacy', dest='legacy_boot', type=int,
991 choices=xrange(2),
992 help='set Legacy Boot flag')
993 parser.add_argument(
994 '-A', '--attribute', dest='raw_16', type=int,
995 help='set raw 16-bit attribute value (bits 48-63)')
996 parser.add_argument(
997 'image_file', type=argparse.FileType('rb+'),
998 help='Disk image file to modify.')
999
1000 @staticmethod
1001 def GetTypeGUID(input_uuid):
1002 if input_uuid.lower() in GPT.TYPE_GUID_REVERSE_MAP:
1003 input_uuid = GPT.TYPE_GUID_REVERSE_MAP[input_uuid.lower()]
1004 return uuid.UUID(input_uuid)
1005
1006 def Execute(self, args):
1007 gpt = GPT.LoadFromFile(args.image_file)
1008 parts = gpt.GetValidPartitions()
1009 number = args.number
1010 if number is None:
1011 number = len(parts) + 1
1012 if number <= len(parts):
1013 is_new_part = False
1014 else:
1015 is_new_part = True
1016 index = number - 1
1017
1018 # First and last LBA must be calculated explicitly because the given
1019 # argument is size.
1020 part = gpt.partitions[index]
1021
1022 if is_new_part:
1023 part = part.ReadFrom(None, **part.__dict__).Clone(
1024 FirstLBA=(parts[-1].LastLBA + 1 if parts else
1025 gpt.header.FirstUsableLBA),
1026 LastLBA=gpt.header.LastUsableLBA,
1027 UniqueGUID=uuid.uuid4(),
1028 TypeGUID=self.GetTypeGUID('data'))
1029
1030 attr = part.attrs
1031 if args.legacy_boot is not None:
1032 attr.legacy_boot = args.legacy_boot
1033 if args.required is not None:
1034 attr.required = args.required
1035 if args.priority is not None:
1036 attr.priority = args.priority
1037 if args.tries is not None:
1038 attr.tries = args.tries
1039 if args.successful is not None:
1040 attr.successful = args.successful
1041 if args.raw_16 is not None:
1042 attr.raw_16 = args.raw_16
1043
1044 first_lba = part.FirstLBA if args.begin is None else args.begin
1045 last_lba = first_lba - 1 + (
1046 part.blocks if args.sectors is None else args.sectors)
1047 dargs = dict(
1048 FirstLBA=first_lba,
1049 LastLBA=last_lba,
1050 TypeGUID=(part.TypeGUID if args.type_guid is None else
1051 self.GetTypeGUID(args.type_guid)),
1052 UniqueGUID=(part.UniqueGUID if args.unique_guid is None else
1053 uuid.UUID(bytes_le=args.unique_guid)),
1054 Attributes=attr,
1055 )
1056 if args.label is not None:
1057 dargs['label'] = args.label
1058
1059 part = part.Clone(**dargs)
1060 # Wipe partition again if it should be empty.
1061 if part.IsUnused():
1062 part = part.ReadFrom(None, **part.__dict__)
1063
1064 gpt.partitions[index] = part
1065
1066 # TODO(hungte) Sanity check if part is valid.
1067 gpt.WriteToFile(args.image_file)
1068 if part.IsUnused():
1069 # If we do ('%s' % part) there will be TypeError.
1070 print('OK: Deleted (zeroed) %s.' % (part,))
1071 else:
1072 print('OK: %s %s (%s+%s).' %
1073 ('Added' if is_new_part else 'Modified',
1074 part, part.FirstLBA, part.blocks))
1075
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001076 class Show(SubCommand):
1077 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001078
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001079 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001080 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001081
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001082 def DefineArgs(self, parser):
1083 parser.add_argument(
1084 '--numeric', '-n', action='store_true',
1085 help='Numeric output only.')
1086 parser.add_argument(
1087 '--quick', '-q', action='store_true',
1088 help='Quick output.')
1089 parser.add_argument(
1090 '-i', '--number', type=int,
1091 help='Show specified partition only, with format args.')
1092 for name, help_str in GPTCommands.FORMAT_ARGS:
1093 # TODO(hungte) Alert if multiple args were specified.
1094 parser.add_argument(
1095 '--%s' % name, '-%c' % name[0], action='store_true',
1096 help='[format] %s.' % help_str)
1097 parser.add_argument(
1098 'image_file', type=argparse.FileType('rb'),
1099 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001100
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001101 def Execute(self, args):
1102 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001103
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001104 def FormatGUID(bytes_le):
1105 return str(uuid.UUID(bytes_le=bytes_le)).upper()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001106
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001107 def FormatTypeGUID(p):
1108 guid_str = FormatGUID(p.TypeGUID)
1109 if not args.numeric:
1110 names = gpt.TYPE_GUID_MAP.get(guid_str)
1111 if names:
1112 return names
1113 return guid_str
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001114
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001115 def FormatNames(p):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +08001116 return p.label
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001117
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001118 def IsBootableType(type_guid):
1119 return type_guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001120
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001121 def FormatAttribute(attrs, chromeos_kernel=False):
1122 if args.numeric:
1123 return '[%x]' % (attrs.raw >> 48)
1124 results = []
1125 if chromeos_kernel:
1126 results += [
1127 'priority=%d' % attrs.priority,
1128 'tries=%d' % attrs.tries,
1129 'successful=%d' % attrs.successful]
1130 if attrs.required:
1131 results += ['required=1']
1132 if attrs.legacy_boot:
1133 results += ['legacy_boot=1']
1134 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001135
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001136 def ApplyFormatArgs(p):
1137 if args.begin:
1138 return p.FirstLBA
1139 elif args.size:
1140 return p.blocks
1141 elif args.type:
1142 return FormatTypeGUID(p)
1143 elif args.unique:
1144 return FormatGUID(p.UniqueGUID)
1145 elif args.label:
1146 return FormatNames(p)
1147 elif args.Successful:
1148 return p.attrs.successful
1149 elif args.Priority:
1150 return p.attrs.priority
1151 elif args.Tries:
1152 return p.attrs.tries
1153 elif args.Legacy:
1154 return p.attrs.legacy_boot
1155 elif args.Attribute:
1156 return '[%x]' % (p.Attributes >> 48)
1157 else:
1158 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001159
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001160 def IsFormatArgsSpecified():
1161 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001162
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001163 gpt = GPT.LoadFromFile(args.image_file)
1164 logging.debug('%r', gpt.header)
1165 fmt = '%12s %11s %7s %s'
1166 fmt2 = '%32s %s: %s'
1167 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001168
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001169 if IsFormatArgsSpecified() and args.number is None:
1170 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001171
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001172 if not (args.number is None or
1173 0 < args.number <= gpt.header.PartitionEntriesNumber):
1174 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001175
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001176 partitions = gpt.partitions
1177 do_print_gpt_blocks = False
1178 if not (args.quick or IsFormatArgsSpecified()):
1179 print(fmt % header)
1180 if args.number is None:
1181 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001182
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001183 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001184 if gpt.pmbr:
1185 print(fmt % (0, 1, '', 'PMBR'))
1186 if gpt.is_secondary:
1187 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1188 else:
1189 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1190 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1191 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001192
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001193 for p in partitions:
1194 if args.number is None:
1195 # Skip unused partitions.
1196 if p.IsUnused():
1197 continue
1198 elif p.number != args.number:
1199 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001200
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001201 if IsFormatArgsSpecified():
1202 print(ApplyFormatArgs(p))
1203 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001204
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001205 type_guid = FormatGUID(p.TypeGUID)
1206 print(fmt % (p.FirstLBA, p.blocks, p.number,
1207 FormatTypeGUID(p) if args.quick else
1208 'Label: "%s"' % FormatNames(p)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001209
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001210 if not args.quick:
1211 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
1212 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
1213 if args.numeric or IsBootableType(type_guid):
1214 name = GPT.TYPE_GUID_MAP[type_guid]
1215 print(fmt2 % ('', 'Attr', FormatAttribute(
1216 p.attrs, name == GPT.TYPE_NAME_CHROMEOS_KERNEL)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001217
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001218 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001219 if gpt.is_secondary:
1220 header = gpt.header
1221 else:
1222 f = args.image_file
1223 f.seek(gpt.header.BackupLBA * gpt.block_size)
1224 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001225 print(fmt % (header.PartitionEntriesStartingLBA,
1226 gpt.GetPartitionTableBlocks(header), '',
1227 'Sec GPT table'))
1228 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001229
1230
1231def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001232 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001233 parser = argparse.ArgumentParser(description='GPT Utility.')
1234 parser.add_argument('--verbose', '-v', action='count', default=0,
1235 help='increase verbosity.')
1236 parser.add_argument('--debug', '-d', action='store_true',
1237 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001238 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001239
1240 args = parser.parse_args()
1241 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1242 if args.debug:
1243 log_level = logging.DEBUG
1244 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1245 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001246 try:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001247 commands.Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001248 except Exception as e:
1249 if args.verbose or args.debug:
1250 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001251 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001252
1253
1254if __name__ == '__main__':
1255 main()