blob: 8b5f8614ac85c08e1173e37bb0ae27b29a017aea [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
244 TYPE_GUID_MAP = {
245 '00000000-0000-0000-0000-000000000000': 'Unused',
246 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7': 'Linux data',
Hung-Te Linfe724f82018-04-18 15:03:58 +0800247 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309': 'ChromeOS kernel',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800248 '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC': 'ChromeOS rootfs',
249 '2E0A753D-9E48-43B0-8337-B15192CB1B5E': 'ChromeOS reserved',
250 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3': 'ChromeOS firmware',
251 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': 'EFI System Partition',
252 }
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800253 TYPE_GUID_REVERSE_MAP = dict(
254 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
255 for k, v in TYPE_GUID_MAP.iteritems())
Hung-Te Linfe724f82018-04-18 15:03:58 +0800256 STR_TYPE_GUID_LIST_BOOTABLE = [
257 TYPE_GUID_REVERSE_MAP['kernel'],
258 TYPE_GUID_REVERSE_MAP['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800259 ]
260
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800261 @GPTBlob(PMBR_DESCRIPTION)
262 class ProtectiveMBR(GPTObject):
263 """Protective MBR (PMBR) in GPT."""
264 SIGNATURE = '\x55\xAA'
265 MAGIC = '\x1d\x9a'
266
267 CLONE_CONVERTERS = {
268 'BootGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v
269 }
270
271 @property
272 def boot_guid(self):
273 """Returns the BootGUID in decoded (uuid.UUID) format."""
274 return uuid.UUID(bytes_le=self.BootGUID)
275
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800276 @GPTBlob(HEADER_DESCRIPTION)
277 class Header(GPTObject):
278 """Wrapper to Header in GPT."""
279 SIGNATURES = ['EFI PART', 'CHROMEOS']
280 SIGNATURE_IGNORE = 'IGNOREME'
281 DEFAULT_REVISION = '\x00\x00\x01\x00'
282
283 DEFAULT_PARTITION_ENTRIES = 128
284 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
285
286 def Clone(self, **dargs):
287 """Creates a new instance with modifications.
288
289 GPT objects are usually named tuples that are immutable, so the only way
290 to make changes is to create a new instance with modifications.
291
292 CRC32 is always updated but PartitionArrayCRC32 must be updated explicitly
293 since we can't track changes in GPT.partitions automatically.
294
295 Note since GPTHeader.Clone will always update CRC, we can only check and
296 compute CRC by super(GPT.Header, header).Clone, or header._replace.
297 """
298 dargs['CRC32'] = 0
299 header = super(GPT.Header, self).Clone(**dargs)
300 return super(GPT.Header, header).Clone(CRC32=binascii.crc32(header.blob))
301
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800302 @classmethod
303 def Create(cls, size, block_size, pad_blocks=0,
304 part_entries=DEFAULT_PARTITION_ENTRIES):
305 """Creates a header with default values.
306
307 Args:
308 size: integer of expected image size.
309 block_size: integer for size of each block (sector).
310 pad_blocks: number of preserved sectors between header and partitions.
311 part_entries: number of partitions to include in header.
312 """
313 part_entry_size = struct.calcsize(GPT.Partition.FORMAT)
314 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
315 parts_bytes = part_entries * part_entry_size
316 parts_blocks = parts_bytes / block_size
317 if parts_bytes % block_size:
318 parts_blocks += 1
319 # PartitionsCRC32 must be updated later explicitly.
320 return cls.ReadFrom(None).Clone(
321 Signature=cls.SIGNATURES[0],
322 Revision=cls.DEFAULT_REVISION,
323 HeaderSize=struct.calcsize(cls.FORMAT),
324 CurrentLBA=1,
325 BackupLBA=size / block_size - 1,
326 FirstUsableLBA=parts_lba + parts_blocks,
327 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
328 DiskGUID=uuid.uuid4().get_bytes(),
329 PartitionEntriesStartingLBA=parts_lba,
330 PartitionEntriesNumber=part_entries,
331 PartitionEntrySize=part_entry_size,
332 )
333
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800334 class PartitionAttributes(object):
335 """Wrapper for Partition.Attributes.
336
337 This can be created using Partition.attrs, but the changed properties won't
338 apply to underlying Partition until an explicit call with
339 Partition.Clone(Attributes=new_attrs).
340 """
341
342 def __init__(self, attrs):
343 self._attrs = attrs
344
345 @property
346 def raw(self):
347 """Returns the raw integer type attributes."""
348 return self._Get()
349
350 def _Get(self):
351 return self._attrs
352
353 def _Set(self, value):
354 self._attrs = value
355
356 successful = BitProperty(_Get, _Set, 56, 1)
357 tries = BitProperty(_Get, _Set, 52, 0xf)
358 priority = BitProperty(_Get, _Set, 48, 0xf)
359 legacy_boot = BitProperty(_Get, _Set, 2, 1)
360 required = BitProperty(_Get, _Set, 0, 1)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800361 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800362
363 @GPTBlob(PARTITION_DESCRIPTION)
364 class Partition(GPTObject):
365 """The partition entry in GPT.
366
367 Please include following properties when creating a Partition object:
368 - image: a string for path to the image file the partition maps to.
369 - number: the 1-based partition number.
370 - block_size: an integer for size of each block (LBA, or sector).
371 """
372 NAMES_ENCODING = 'utf-16-le'
373 NAMES_LENGTH = 72
374
375 CLONE_CONVERTERS = {
376 # TODO(hungte) check if encoded name is too long.
377 'label': lambda l: (None if l is None else
378 ('Names', l.encode(GPT.Partition.NAMES_ENCODING))),
379 'TypeGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
380 'UniqueGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
381 'Attributes': (
382 lambda v: v.raw if isinstance(v, GPT.PartitionAttributes) else v),
383 }
384
385 def __str__(self):
386 return '%s#%s' % (self.image, self.number)
387
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800388 @classmethod
389 def Create(cls, block_size, image, number):
390 """Creates a new partition entry with given meta data."""
391 part = cls.ReadFrom(
392 None, image=image, number=number, block_size=block_size)
393 return part
394
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800395 def IsUnused(self):
396 """Returns if the partition is unused and can be allocated."""
397 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
398
Hung-Te Linfe724f82018-04-18 15:03:58 +0800399 def IsChromeOSKernel(self):
400 """Returns if the partition is a Chrome OS kernel partition."""
401 return self.TypeGUID == uuid.UUID(
402 GPT.TYPE_GUID_REVERSE_MAP['kernel']).bytes_le
403
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800404 @property
405 def blocks(self):
406 """Return size of partition in blocks (see block_size)."""
407 return self.LastLBA - self.FirstLBA + 1
408
409 @property
410 def offset(self):
411 """Returns offset to partition in bytes."""
412 return self.FirstLBA * self.block_size
413
414 @property
415 def size(self):
416 """Returns size of partition in bytes."""
417 return self.blocks * self.block_size
418
419 @property
420 def type_guid(self):
421 return uuid.UUID(bytes_le=self.TypeGUID)
422
423 @property
424 def unique_guid(self):
425 return uuid.UUID(bytes_le=self.UniqueGUID)
426
427 @property
428 def label(self):
429 """Returns the Names in decoded string type."""
430 return self.Names.decode(self.NAMES_ENCODING).strip('\0')
431
432 @property
433 def attrs(self):
434 return GPT.PartitionAttributes(self.Attributes)
435
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800436 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800437 """GPT constructor.
438
439 See LoadFromFile for how it's usually used.
440 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800441 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800442 self.header = None
443 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800444 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800445 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800446
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800447 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800448 def Create(cls, image_name, size, block_size, pad_blocks=0):
449 """Creates a new GPT instance from given size and block_size.
450
451 Args:
452 image_name: a string of underlying disk image file name.
453 size: expected size of disk image.
454 block_size: size of each block (sector) in bytes.
455 pad_blocks: number of blocks between header and partitions array.
456 """
457 gpt = cls()
458 gpt.block_size = block_size
459 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
460 gpt.partitions = [
461 cls.Partition.Create(block_size, image_name, i + 1)
462 for i in xrange(gpt.header.PartitionEntriesNumber)]
463 return gpt
464
465 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800466 def LoadFromFile(cls, image):
467 """Loads a GPT table from give disk image file object.
468
469 Args:
470 image: a string as file path or a file-like object to read from.
471 """
472 if isinstance(image, basestring):
473 with open(image, 'rb') as f:
474 return cls.LoadFromFile(f)
475
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800476 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800477 image.seek(0)
478 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
479 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
480 logging.debug('Found MBR signature in %s', image.name)
481 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
482 logging.debug('Found PMBR in %s', image.name)
483 gpt.pmbr = pmbr
484
Hung-Te Linf148d322018-04-13 10:24:42 +0800485 # Try DEFAULT_BLOCK_SIZE, then 4K.
486 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800487 # Note because there are devices setting Primary as ignored and the
488 # partition table signature accepts 'CHROMEOS' which is also used by
489 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
490 # first before trying other block sizes, otherwise we may incorrectly
491 # identify a kernel partition as LBA 1 of larger block size system.
492 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
493 (-block_size, os.SEEK_END)]):
494 image.seek(*seek)
495 header = gpt.Header.ReadFrom(image)
496 if header.Signature in cls.Header.SIGNATURES:
497 gpt.block_size = block_size
498 if i != 0:
499 gpt.is_secondary = True
500 break
501 else:
502 # Nothing found, try next block size.
503 continue
504 # Found a valid signature.
505 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800506 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800507 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800508
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800509 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800510 def ReadPartition(image, i):
511 p = gpt.Partition.ReadFrom(
512 image, image=image.name, number=i + 1, block_size=gpt.block_size)
513 return p
514
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800515 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800516 gpt.partitions = [
517 ReadPartition(image, i) for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800518 return gpt
519
520 def GetValidPartitions(self):
521 """Returns the list of partitions before entry with empty type GUID.
522
523 In partition table, the first entry with empty type GUID indicates end of
524 valid partitions. In most implementations all partitions after that should
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800525 be zeroed. However, few implementations for example cgpt, may create
526 partitions in arbitrary order so use this carefully.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800527 """
528 for i, p in enumerate(self.partitions):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800529 if p.IsUnused():
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800530 return self.partitions[:i]
531 return self.partitions
532
533 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800534 """Returns the max LastLBA from all used partitions."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800535 parts = [p for p in self.partitions if not p.IsUnused()]
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800536 return (max(p.LastLBA for p in parts)
537 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800538
539 def GetPartitionTableBlocks(self, header=None):
540 """Returns the blocks (or LBA) of partition table from given header."""
541 if header is None:
542 header = self.header
543 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800544 blocks = size / self.block_size
545 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800546 blocks += 1
547 return blocks
548
549 def Resize(self, new_size):
550 """Adjust GPT for a disk image in given size.
551
552 Args:
553 new_size: Integer for new size of disk image file.
554 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800555 old_size = self.block_size * (self.header.BackupLBA + 1)
556 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800557 raise GPTError(
558 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800559 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800560 if old_size != new_size:
561 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800562 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800563 else:
564 logging.info('Image size (%d, LBA=%d) not changed.',
565 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800566 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800567
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800568 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800569 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800570 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800571
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800572 if last_usable_lba < self.header.LastUsableLBA:
573 max_used_lba = self.GetMaxUsedLBA()
574 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800575 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800576
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800577 self.header = self.header.Clone(
578 BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800579
580 def GetFreeSpace(self):
581 """Returns the free (available) space left according to LastUsableLBA."""
582 max_lba = self.GetMaxUsedLBA()
583 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800584 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800585
586 def ExpandPartition(self, i):
587 """Expands a given partition to last usable LBA.
588
589 Args:
590 i: Index (0-based) of target partition.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800591
592 Returns:
593 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800594 """
595 # Assume no partitions overlap, we need to make sure partition[i] has
596 # largest LBA.
597 if i < 0 or i >= len(self.GetValidPartitions()):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800598 raise GPTError('Partition number %d is invalid.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800599 p = self.partitions[i]
600 max_used_lba = self.GetMaxUsedLBA()
601 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800602 raise GPTError(
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800603 'Cannot expand partition %d because it is not the last allocated '
604 'partition.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800605
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800606 old_blocks = p.blocks
607 p = p.Clone(LastLBA=self.header.LastUsableLBA)
608 new_blocks = p.blocks
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800609 self.partitions[i] = p
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800610 logging.warn(
611 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
612 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800613
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800614 def GetIgnoredHeader(self):
615 """Returns a primary header with signature set to 'IGNOREME'.
616
617 This is a special trick to enforce using backup header, when there is
618 some security exploit in LBA1.
619 """
620 return self.header.Clone(Signature=self.header.SIGNATURE_IGNORE)
621
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800622 def UpdateChecksum(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800623 """Updates all checksum fields in GPT objects.
624
625 The Header.CRC32 is automatically updated in Header.Clone().
626 """
627 parts = ''.join(p.blob for p in self.partitions)
628 self.header = self.header.Clone(
629 PartitionArrayCRC32=binascii.crc32(parts))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800630
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800631 def GetBackupHeader(self, header):
632 """Returns the backup header according to given header."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800633 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800634 header.BackupLBA - self.GetPartitionTableBlocks())
635 return header.Clone(
636 BackupLBA=header.CurrentLBA,
637 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800638 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800639
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800640 @classmethod
641 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
642 """Writes a protective MBR to given file.
643
644 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
645 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
646 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
647 we can follow that.
648
649 Args:
650 create: True to re-create PMBR structure.
651 bootcode: a blob of new boot code.
652 boot_guid a blob for new boot GUID.
653
654 Returns:
655 The written PMBR structure.
656 """
657 if isinstance(image, basestring):
658 with open(image, 'rb+') as f:
659 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
660
661 image.seek(0)
662 assert struct.calcsize(cls.ProtectiveMBR.FORMAT) == cls.DEFAULT_BLOCK_SIZE
663 pmbr = cls.ProtectiveMBR.ReadFrom(image)
664
665 if create:
666 legacy_sectors = min(
667 0x100000000,
668 os.path.getsize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
669 # Partition 0 must have have the fixed CHS with number of sectors
670 # (calculated as legacy_sectors later).
671 part0 = ('00000200eeffffff01000000'.decode('hex') +
672 struct.pack('<I', legacy_sectors))
673 # Partition 1~3 should be all zero.
674 part1 = '\x00' * 16
675 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
676 pmbr = pmbr.Clone(
677 BootGUID=cls.TYPE_GUID_UNUSED,
678 DiskID=0,
679 Magic=cls.ProtectiveMBR.MAGIC,
680 LegacyPart0=part0,
681 LegacyPart1=part1,
682 LegacyPart2=part1,
683 LegacyPart3=part1,
684 Signature=cls.ProtectiveMBR.SIGNATURE)
685
686 if bootcode:
687 if len(bootcode) > len(pmbr.BootCode):
688 logging.info(
689 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
690 bootcode = bootcode[:len(pmbr.BootCode)]
691 pmbr = pmbr.Clone(BootCode=bootcode)
692 if boot_guid:
693 pmbr = pmbr.Clone(BootGUID=boot_guid)
694
695 blob = pmbr.blob
696 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
697 image.seek(0)
698 image.write(blob)
699 return pmbr
700
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800701 def WriteToFile(self, image):
702 """Updates partition table in a disk image file.
703
704 Args:
705 image: a string as file path or a file-like object to write into.
706 """
707 if isinstance(image, basestring):
708 with open(image, 'rb+') as f:
709 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800710
711 def WriteData(name, blob, lba):
712 """Writes a blob into given location."""
713 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800714 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800715 image.seek(lba * self.block_size)
716 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800717
718 self.UpdateChecksum()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800719 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800720
721 header = self.header
722 WriteData('GPT Header', header.blob, header.CurrentLBA)
723 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
724 logging.info(
725 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
726 header.LastUsableLBA)
727
728 if not self.is_secondary:
729 # When is_secondary is True, the header we have is actually backup header.
730 backup_header = self.GetBackupHeader(self.header)
731 WriteData(
732 'Backup Partitions', parts_blob,
733 backup_header.PartitionEntriesStartingLBA)
734 WriteData(
735 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800736
737
738class GPTCommands(object):
739 """Collection of GPT sub commands for command line to use.
740
741 The commands are derived from `cgpt`, but not necessary to be 100% compatible
742 with cgpt.
743 """
744
745 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800746 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800747 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800748 ('type', 'type guid'),
749 ('unique', 'unique guid'),
750 ('label', 'label'),
751 ('Successful', 'Successful flag'),
752 ('Tries', 'Tries flag'),
753 ('Priority', 'Priority flag'),
754 ('Legacy', 'Legacy Boot flag'),
755 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800756
757 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800758 commands = dict(
759 (command.lower(), getattr(self, command)())
760 for command in dir(self)
761 if (isinstance(getattr(self, command), type) and
762 issubclass(getattr(self, command), self.SubCommand) and
763 getattr(self, command) is not self.SubCommand)
764 )
765 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800766
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800767 def DefineArgs(self, parser):
768 """Defines all available commands to an argparser subparsers instance."""
769 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
770 for name, instance in sorted(self.commands.iteritems()):
771 parser = subparsers.add_parser(
772 name, description=instance.__doc__,
773 formatter_class=argparse.RawDescriptionHelpFormatter,
774 help=instance.__doc__.splitlines()[0])
775 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800776
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800777 def Execute(self, args):
778 """Execute the sub commands by given parsed arguments."""
779 self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800780
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800781 class SubCommand(object):
782 """A base class for sub commands to derive from."""
783
784 def DefineArgs(self, parser):
785 """Defines command line arguments to argparse parser.
786
787 Args:
788 parser: An argparse parser instance.
789 """
790 del parser # Unused.
791 raise NotImplementedError
792
793 def Execute(self, args):
794 """Execute the command.
795
796 Args:
797 args: An argparse parsed namespace.
798 """
799 del args # Unused.
800 raise NotImplementedError
801
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800802 class Create(SubCommand):
803 """Create or reset GPT headers and tables.
804
805 Create or reset an empty GPT.
806 """
807
808 def DefineArgs(self, parser):
809 parser.add_argument(
810 '-z', '--zero', action='store_true',
811 help='Zero the sectors of the GPT table and entries')
812 parser.add_argument(
813 '-p', '--pad_blocks', type=int, default=0,
814 help=('Size (in blocks) of the disk to pad between the '
815 'primary GPT header and its entries, default %(default)s'))
816 parser.add_argument(
817 '--block_size', type=int, default=GPT.DEFAULT_BLOCK_SIZE,
818 help='Size of each block (sector) in bytes.')
819 parser.add_argument(
820 'image_file', type=argparse.FileType('rb+'),
821 help='Disk image file to create.')
822
823 def Execute(self, args):
824 block_size = args.block_size
825 gpt = GPT.Create(
826 args.image_file.name, os.path.getsize(args.image_file.name),
827 block_size, args.pad_blocks)
828 if args.zero:
829 # In theory we only need to clear LBA 1, but to make sure images already
830 # initialized with different block size won't have GPT signature in
831 # different locations, we should zero until first usable LBA.
832 args.image_file.seek(0)
833 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
834 gpt.WriteToFile(args.image_file)
835 print('OK: Created GPT for %s' % args.image_file.name)
836
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800837 class Boot(SubCommand):
838 """Edit the PMBR sector for legacy BIOSes.
839
840 With no options, it will just print the PMBR boot guid.
841 """
842
843 def DefineArgs(self, parser):
844 parser.add_argument(
845 '-i', '--number', type=int,
846 help='Set bootable partition')
847 parser.add_argument(
848 '-b', '--bootloader', type=argparse.FileType('r'),
849 help='Install bootloader code in the PMBR')
850 parser.add_argument(
851 '-p', '--pmbr', action='store_true',
852 help='Create legacy PMBR partition table')
853 parser.add_argument(
854 'image_file', type=argparse.FileType('rb+'),
855 help='Disk image file to change PMBR.')
856
857 def Execute(self, args):
858 """Rebuilds the protective MBR."""
859 bootcode = args.bootloader.read() if args.bootloader else None
860 boot_guid = None
861 if args.number is not None:
862 gpt = GPT.LoadFromFile(args.image_file)
863 boot_guid = gpt.partitions[args.number - 1].UniqueGUID
864 pmbr = GPT.WriteProtectiveMBR(
865 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
866
867 print(str(pmbr.boot_guid).upper())
868
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800869 class Legacy(SubCommand):
870 """Switch between GPT and Legacy GPT.
871
872 Switch GPT header signature to "CHROMEOS".
873 """
874
875 def DefineArgs(self, parser):
876 parser.add_argument(
877 '-e', '--efi', action='store_true',
878 help='Switch GPT header signature back to "EFI PART"')
879 parser.add_argument(
880 '-p', '--primary-ignore', action='store_true',
881 help='Switch primary GPT header signature to "IGNOREME"')
882 parser.add_argument(
883 'image_file', type=argparse.FileType('rb+'),
884 help='Disk image file to change.')
885
886 def Execute(self, args):
887 gpt = GPT.LoadFromFile(args.image_file)
888 # cgpt behavior: if -p is specified, -e is ignored.
889 if args.primary_ignore:
890 if gpt.is_secondary:
891 raise GPTError('Sorry, the disk already has primary GPT ignored.')
892 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
893 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
894 gpt.header = gpt.GetBackupHeader(self.header)
895 gpt.is_secondary = True
896 else:
897 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
898 gpt.header = gpt.header.Clone(Signature=new_signature)
899 gpt.WriteToFile(args.image_file)
900 if args.primary_ignore:
901 print('OK: Set %s primary GPT header to %s.' %
902 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
903 else:
904 print('OK: Changed GPT signature for %s to %s.' %
905 (args.image_file.name, new_signature))
906
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800907 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800908 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800909
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800910 def DefineArgs(self, parser):
911 parser.add_argument(
912 'image_file', type=argparse.FileType('rb+'),
913 help='Disk image file to repair.')
914
915 def Execute(self, args):
916 gpt = GPT.LoadFromFile(args.image_file)
917 gpt.Resize(os.path.getsize(args.image_file.name))
918 gpt.WriteToFile(args.image_file)
919 print('Disk image file %s repaired.' % args.image_file.name)
920
921 class Expand(SubCommand):
922 """Expands a GPT partition to all available free space."""
923
924 def DefineArgs(self, parser):
925 parser.add_argument(
926 '-i', '--number', type=int, required=True,
927 help='The partition to expand.')
928 parser.add_argument(
929 'image_file', type=argparse.FileType('rb+'),
930 help='Disk image file to modify.')
931
932 def Execute(self, args):
933 gpt = GPT.LoadFromFile(args.image_file)
934 old_blocks, new_blocks = gpt.ExpandPartition(args.number - 1)
935 gpt.WriteToFile(args.image_file)
936 if old_blocks < new_blocks:
937 print(
938 'Partition %s on disk image file %s has been extended '
939 'from %s to %s .' %
940 (args.number, args.image_file.name, old_blocks * gpt.block_size,
941 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800942 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800943 print('Nothing to expand for disk image %s partition %s.' %
944 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800945
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800946 class Add(SubCommand):
947 """Add, edit, or remove a partition entry.
948
949 Use the -i option to modify an existing partition.
950 The -b, -s, and -t options must be given for new partitions.
951
952 The partition type may also be given as one of these aliases:
953
954 firmware ChromeOS firmware
955 kernel ChromeOS kernel
956 rootfs ChromeOS rootfs
957 data Linux data
958 reserved ChromeOS reserved
959 efi EFI System Partition
960 unused Unused (nonexistent) partition
961 """
962 def DefineArgs(self, parser):
963 parser.add_argument(
964 '-i', '--number', type=int,
965 help='Specify partition (default is next available)')
966 parser.add_argument(
967 '-b', '--begin', type=int,
968 help='Beginning sector')
969 parser.add_argument(
970 '-s', '--sectors', type=int,
971 help='Size in sectors (logical blocks).')
972 parser.add_argument(
973 '-t', '--type_guid',
974 help='Partition Type GUID')
975 parser.add_argument(
976 '-u', '--unique_guid',
977 help='Partition Unique ID')
978 parser.add_argument(
979 '-l', '--label',
980 help='Label')
981 parser.add_argument(
982 '-S', '--successful', type=int, choices=xrange(2),
983 help='set Successful flag')
984 parser.add_argument(
985 '-T', '--tries', type=int,
986 help='set Tries flag (0-15)')
987 parser.add_argument(
988 '-P', '--priority', type=int,
989 help='set Priority flag (0-15)')
990 parser.add_argument(
991 '-R', '--required', type=int, choices=xrange(2),
992 help='set Required flag')
993 parser.add_argument(
994 '-B', '--boot_legacy', dest='legacy_boot', type=int,
995 choices=xrange(2),
996 help='set Legacy Boot flag')
997 parser.add_argument(
998 '-A', '--attribute', dest='raw_16', type=int,
999 help='set raw 16-bit attribute value (bits 48-63)')
1000 parser.add_argument(
1001 'image_file', type=argparse.FileType('rb+'),
1002 help='Disk image file to modify.')
1003
1004 @staticmethod
1005 def GetTypeGUID(input_uuid):
1006 if input_uuid.lower() in GPT.TYPE_GUID_REVERSE_MAP:
1007 input_uuid = GPT.TYPE_GUID_REVERSE_MAP[input_uuid.lower()]
1008 return uuid.UUID(input_uuid)
1009
1010 def Execute(self, args):
1011 gpt = GPT.LoadFromFile(args.image_file)
1012 parts = gpt.GetValidPartitions()
1013 number = args.number
1014 if number is None:
1015 number = len(parts) + 1
1016 if number <= len(parts):
1017 is_new_part = False
1018 else:
1019 is_new_part = True
1020 index = number - 1
1021
1022 # First and last LBA must be calculated explicitly because the given
1023 # argument is size.
1024 part = gpt.partitions[index]
1025
1026 if is_new_part:
1027 part = part.ReadFrom(None, **part.__dict__).Clone(
1028 FirstLBA=(parts[-1].LastLBA + 1 if parts else
1029 gpt.header.FirstUsableLBA),
1030 LastLBA=gpt.header.LastUsableLBA,
1031 UniqueGUID=uuid.uuid4(),
1032 TypeGUID=self.GetTypeGUID('data'))
1033
1034 attr = part.attrs
1035 if args.legacy_boot is not None:
1036 attr.legacy_boot = args.legacy_boot
1037 if args.required is not None:
1038 attr.required = args.required
1039 if args.priority is not None:
1040 attr.priority = args.priority
1041 if args.tries is not None:
1042 attr.tries = args.tries
1043 if args.successful is not None:
1044 attr.successful = args.successful
1045 if args.raw_16 is not None:
1046 attr.raw_16 = args.raw_16
1047
1048 first_lba = part.FirstLBA if args.begin is None else args.begin
1049 last_lba = first_lba - 1 + (
1050 part.blocks if args.sectors is None else args.sectors)
1051 dargs = dict(
1052 FirstLBA=first_lba,
1053 LastLBA=last_lba,
1054 TypeGUID=(part.TypeGUID if args.type_guid is None else
1055 self.GetTypeGUID(args.type_guid)),
1056 UniqueGUID=(part.UniqueGUID if args.unique_guid is None else
1057 uuid.UUID(bytes_le=args.unique_guid)),
1058 Attributes=attr,
1059 )
1060 if args.label is not None:
1061 dargs['label'] = args.label
1062
1063 part = part.Clone(**dargs)
1064 # Wipe partition again if it should be empty.
1065 if part.IsUnused():
1066 part = part.ReadFrom(None, **part.__dict__)
1067
1068 gpt.partitions[index] = part
1069
1070 # TODO(hungte) Sanity check if part is valid.
1071 gpt.WriteToFile(args.image_file)
1072 if part.IsUnused():
1073 # If we do ('%s' % part) there will be TypeError.
1074 print('OK: Deleted (zeroed) %s.' % (part,))
1075 else:
1076 print('OK: %s %s (%s+%s).' %
1077 ('Added' if is_new_part else 'Modified',
1078 part, part.FirstLBA, part.blocks))
1079
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001080 class Show(SubCommand):
1081 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001082
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001083 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001084 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001085
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001086 def DefineArgs(self, parser):
1087 parser.add_argument(
1088 '--numeric', '-n', action='store_true',
1089 help='Numeric output only.')
1090 parser.add_argument(
1091 '--quick', '-q', action='store_true',
1092 help='Quick output.')
1093 parser.add_argument(
1094 '-i', '--number', type=int,
1095 help='Show specified partition only, with format args.')
1096 for name, help_str in GPTCommands.FORMAT_ARGS:
1097 # TODO(hungte) Alert if multiple args were specified.
1098 parser.add_argument(
1099 '--%s' % name, '-%c' % name[0], action='store_true',
1100 help='[format] %s.' % help_str)
1101 parser.add_argument(
1102 'image_file', type=argparse.FileType('rb'),
1103 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001104
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001105 def Execute(self, args):
1106 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001107
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001108 def FormatGUID(bytes_le):
1109 return str(uuid.UUID(bytes_le=bytes_le)).upper()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001110
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001111 def FormatTypeGUID(p):
1112 guid_str = FormatGUID(p.TypeGUID)
1113 if not args.numeric:
1114 names = gpt.TYPE_GUID_MAP.get(guid_str)
1115 if names:
1116 return names
1117 return guid_str
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001118
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001119 def FormatNames(p):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +08001120 return p.label
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001121
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001122 def IsBootableType(type_guid):
Hung-Te Linfe724f82018-04-18 15:03:58 +08001123 return type_guid in gpt.STR_TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001124
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001125 def FormatAttribute(attrs, chromeos_kernel=False):
1126 if args.numeric:
1127 return '[%x]' % (attrs.raw >> 48)
1128 results = []
1129 if chromeos_kernel:
1130 results += [
1131 'priority=%d' % attrs.priority,
1132 'tries=%d' % attrs.tries,
1133 'successful=%d' % attrs.successful]
1134 if attrs.required:
1135 results += ['required=1']
1136 if attrs.legacy_boot:
1137 results += ['legacy_boot=1']
1138 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001139
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001140 def ApplyFormatArgs(p):
1141 if args.begin:
1142 return p.FirstLBA
1143 elif args.size:
1144 return p.blocks
1145 elif args.type:
1146 return FormatTypeGUID(p)
1147 elif args.unique:
1148 return FormatGUID(p.UniqueGUID)
1149 elif args.label:
1150 return FormatNames(p)
1151 elif args.Successful:
1152 return p.attrs.successful
1153 elif args.Priority:
1154 return p.attrs.priority
1155 elif args.Tries:
1156 return p.attrs.tries
1157 elif args.Legacy:
1158 return p.attrs.legacy_boot
1159 elif args.Attribute:
1160 return '[%x]' % (p.Attributes >> 48)
1161 else:
1162 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001163
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001164 def IsFormatArgsSpecified():
1165 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001166
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001167 gpt = GPT.LoadFromFile(args.image_file)
1168 logging.debug('%r', gpt.header)
1169 fmt = '%12s %11s %7s %s'
1170 fmt2 = '%32s %s: %s'
1171 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001172
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001173 if IsFormatArgsSpecified() and args.number is None:
1174 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001175
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001176 if not (args.number is None or
1177 0 < args.number <= gpt.header.PartitionEntriesNumber):
1178 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001179
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001180 partitions = gpt.partitions
1181 do_print_gpt_blocks = False
1182 if not (args.quick or IsFormatArgsSpecified()):
1183 print(fmt % header)
1184 if args.number is None:
1185 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001186
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001187 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001188 if gpt.pmbr:
1189 print(fmt % (0, 1, '', 'PMBR'))
1190 if gpt.is_secondary:
1191 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1192 else:
1193 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1194 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1195 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001196
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001197 for p in partitions:
1198 if args.number is None:
1199 # Skip unused partitions.
1200 if p.IsUnused():
1201 continue
1202 elif p.number != args.number:
1203 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001204
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001205 if IsFormatArgsSpecified():
1206 print(ApplyFormatArgs(p))
1207 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001208
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001209 type_guid = FormatGUID(p.TypeGUID)
1210 print(fmt % (p.FirstLBA, p.blocks, p.number,
1211 FormatTypeGUID(p) if args.quick else
1212 'Label: "%s"' % FormatNames(p)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001213
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001214 if not args.quick:
1215 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
1216 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
1217 if args.numeric or IsBootableType(type_guid):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001218 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Linfe724f82018-04-18 15:03:58 +08001219 p.attrs, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001220
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001221 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001222 if gpt.is_secondary:
1223 header = gpt.header
1224 else:
1225 f = args.image_file
1226 f.seek(gpt.header.BackupLBA * gpt.block_size)
1227 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001228 print(fmt % (header.PartitionEntriesStartingLBA,
1229 gpt.GetPartitionTableBlocks(header), '',
1230 'Sec GPT table'))
1231 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001232
Hung-Te Linfe724f82018-04-18 15:03:58 +08001233 class Prioritize(SubCommand):
1234 """Reorder the priority of all kernel partitions.
1235
1236 Reorder the priority of all active ChromeOS Kernel partitions.
1237
1238 With no options this will set the lowest active kernel to priority 1 while
1239 maintaining the original order.
1240 """
1241
1242 def DefineArgs(self, parser):
1243 parser.add_argument(
1244 '-P', '--priority', type=int,
1245 help=('Highest priority to use in the new ordering. '
1246 'The other partitions will be ranked in decreasing '
1247 'priority while preserving their original order. '
1248 'If necessary the lowest ranks will be coalesced. '
1249 'No active kernels will be lowered to priority 0.'))
1250 parser.add_argument(
1251 '-i', '--number', type=int,
1252 help='Specify the partition to make the highest in the new order.')
1253 parser.add_argument(
1254 '-f', '--friends', action='store_true',
1255 help=('Friends of the given partition (those with the same '
1256 'starting priority) are also updated to the new '
1257 'highest priority. '))
1258 parser.add_argument(
1259 'image_file', type=argparse.FileType('rb+'),
1260 help='Disk image file to prioritize.')
1261
1262 def Execute(self, args):
1263 gpt = GPT.LoadFromFile(args.image_file)
1264 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
1265 prios = list(set(p.attrs.priority for p in parts if p.attrs.priority))
1266 prios.sort(reverse=True)
1267 groups = [[p for p in parts if p.attrs.priority == priority]
1268 for priority in prios]
1269 if args.number:
1270 p = gpt.partitions[args.number - 1]
1271 if p not in parts:
1272 raise GPTError('%s is not a ChromeOS kernel.' % p)
1273 if args.friends:
1274 group0 = [f for f in parts if f.attrs.priority == p.attrs.priority]
1275 else:
1276 group0 = [p]
1277 groups.insert(0, group0)
1278
1279 # Max priority is 0xf.
1280 highest = min(args.priority or len(prios), 0xf)
1281 logging.info('New highest priority: %s', highest)
1282 done = []
1283
1284 new_priority = highest
1285 for g in groups:
1286 has_new_part = False
1287 for p in g:
1288 if p.number in done:
1289 continue
1290 done.append(p.number)
1291 attrs = p.attrs
1292 old_priority = attrs.priority
1293 assert new_priority > 0, 'Priority must be > 0.'
1294 attrs.priority = new_priority
1295 p = p.Clone(Attributes=attrs)
1296 gpt.partitions[p.number - 1] = p
1297 has_new_part = True
1298 logging.info('%s priority changed from %s to %s.', p, old_priority,
1299 new_priority)
1300 if has_new_part:
1301 new_priority -= 1
1302
1303 gpt.WriteToFile(args.image_file)
1304
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001305
1306def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001307 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001308 parser = argparse.ArgumentParser(description='GPT Utility.')
1309 parser.add_argument('--verbose', '-v', action='count', default=0,
1310 help='increase verbosity.')
1311 parser.add_argument('--debug', '-d', action='store_true',
1312 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001313 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001314
1315 args = parser.parse_args()
1316 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1317 if args.debug:
1318 log_level = logging.DEBUG
1319 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1320 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001321 try:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001322 commands.Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001323 except Exception as e:
1324 if args.verbose or args.debug:
1325 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001326 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001327
1328
1329if __name__ == '__main__':
1330 main()