blob: 00d3edb22f8f313b7e4ad6f6fe28ef10b9d97ab8 [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
Hung-Te Linf641d302018-04-18 15:09:35 +080037import subprocess
38import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080039import uuid
40
41
42# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
43# declared as 'signed' (l) instead of 'unsigned' (L).
44# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080045HEADER_DESCRIPTION = """
Hung-Te Linc772e1a2017-04-14 16:50:50 +080046 8s Signature
47 4s Revision
48 L HeaderSize
49 l CRC32
50 4s Reserved
51 Q CurrentLBA
52 Q BackupLBA
53 Q FirstUsableLBA
54 Q LastUsableLBA
55 16s DiskGUID
56 Q PartitionEntriesStartingLBA
57 L PartitionEntriesNumber
58 L PartitionEntrySize
59 l PartitionArrayCRC32
60"""
61
62# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080063PARTITION_DESCRIPTION = """
Hung-Te Linc772e1a2017-04-14 16:50:50 +080064 16s TypeGUID
65 16s UniqueGUID
66 Q FirstLBA
67 Q LastLBA
68 Q Attributes
69 72s Names
70"""
71
Hung-Te Linc6e009c2018-04-17 15:06:16 +080072# The PMBR has so many variants. The basic format is defined in
73# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
74# implementation, as derived from `cgpt`, is following syslinux as:
75# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
76PMBR_DESCRIPTION = """
77 424s BootCode
78 16s BootGUID
79 L DiskID
80 2s Magic
81 16s LegacyPart0
82 16s LegacyPart1
83 16s LegacyPart2
84 16s LegacyPart3
85 2s Signature
86"""
Hung-Te Linc772e1a2017-04-14 16:50:50 +080087
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080088def BitProperty(getter, setter, shift, mask):
89 """A generator for bit-field properties.
90
91 This is used inside a class to manipulate an integer-like variable using
92 properties. The getter and setter should be member functions to change the
93 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +080094
95 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080096 getter: a function to read integer type variable (for all the bits).
97 setter: a function to set the new changed integer type variable.
98 shift: integer for how many bits should be shifted (right).
99 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800100 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800101 def _getter(self):
102 return (getter(self) >> shift) & mask
103 def _setter(self, value):
104 assert value & mask == value, (
105 'Value %s out of range (mask=%s)' % (value, mask))
106 setter(self, getter(self) & ~(mask << shift) | value << shift)
107 return property(_getter, _setter)
108
109
110class GPTBlob(object):
111 """A decorator class to help accessing GPT blobs as named tuple.
112
113 To use this, specify the blob description (struct format and named tuple field
114 names) above the derived class, for example:
115
116 @GPTBlob(description):
117 class Header(GPTObject):
118 pass
119 """
120 def __init__(self, description):
121 spec = description.split()
122 self.struct_format = '<' + ''.join(spec[::2])
123 self.fields = spec[1::2]
124
125 def __call__(self, cls):
126 new_bases = ((
127 collections.namedtuple(cls.__name__, self.fields),) + cls.__bases__)
128 new_cls = type(cls.__name__, new_bases, dict(cls.__dict__))
129 setattr(new_cls, 'FORMAT', self.struct_format)
130 return new_cls
131
132
133class GPTObject(object):
134 """An object in GUID Partition Table.
135
136 This needs to be decorated by @GPTBlob(description) and inherited by a real
137 class. Properties (not member functions) in CamelCase should be reserved for
138 named tuple attributes.
139
140 To create a new object, use class method ReadFrom(), which takes a stream
141 as input or None to create with all elements set to zero. To make changes to
142 named tuple elements, use member function Clone(changes).
143
144 It is also possible to attach some additional properties to the object as meta
145 data (for example path of the underlying image file). To do that, specify the
146 data as keyword arguments when calling ReadFrom(). These properties will be
147 preserved when you call Clone().
148
149 A special case is "reset named tuple elements of an object but keeping all
150 properties", for example changing a partition object to unused (zeroed).
151 ReadFrom() is a class method so properties won't be copied. You need to
152 call as cls.ReadFrom(None, **p.__dict__), or a short cut - p.CloneAndZero().
153 """
154
155 FORMAT = None
156 """The struct.{pack,unpack} format string, and should be set by GPTBlob."""
157
158 CLONE_CONVERTERS = None
159 """A dict (name, cvt) to convert input arguments into named tuple data.
160
161 `name` is a string for the name of argument to convert.
162 `cvt` is a callable to convert value. The return value may be:
163 - a tuple in (new_name, value): save the value as new name.
164 - otherwise, save the value in original name.
165 Note tuple is an invalid input for struct.unpack so it's used for the
166 special value.
167 """
168
169 @classmethod
170 def ReadFrom(cls, f, **kargs):
171 """Reads and decode an object from stream.
172
173 Args:
174 f: a stream to read blob, or None to decode with all zero bytes.
175 kargs: a dict for additional attributes in object.
176 """
177 if f is None:
178 reader = lambda num: '\x00' * num
179 else:
180 reader = f.read
181 data = cls(*struct.unpack(cls.FORMAT, reader(struct.calcsize(cls.FORMAT))))
182 # Named tuples do not accept kargs in constructor.
183 data.__dict__.update(kargs)
184 return data
185
186 def Clone(self, **dargs):
187 """Clones a new instance with modifications.
188
189 GPT objects are usually named tuples that are immutable, so the only way
190 to make changes is to create a new instance with modifications.
191
192 Args:
193 dargs: a dict with all modifications.
194 """
195 for name, convert in (self.CLONE_CONVERTERS or {}).iteritems():
196 if name not in dargs:
197 continue
198 result = convert(dargs.pop(name))
199 if isinstance(result, tuple):
200 assert len(result) == 2, 'Converted tuple must be (name, value).'
201 dargs[result[0]] = result[1]
202 else:
203 dargs[name] = result
204
205 cloned = self._replace(**dargs)
206 cloned.__dict__.update(self.__dict__)
207 return cloned
208
209 def CloneAndZero(self, **dargs):
210 """Short cut to create a zeroed object while keeping all properties.
211
212 This is very similar to Clone except all named tuple elements will be zero.
213 Also different from class method ReadFrom(None) because this keeps all
214 properties from one object.
215 """
216 cloned = self.ReadFrom(None, **self.__dict__)
217 return cloned.Clone(**dargs) if dargs else cloned
218
219 @property
220 def blob(self):
221 """Returns the object in formatted bytes."""
222 return struct.pack(self.FORMAT, *self)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800223
224
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800225class GPTError(Exception):
226 """All exceptions by GPT."""
227 pass
228
229
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800230class GPT(object):
231 """A GPT helper class.
232
233 To load GPT from an existing disk image file, use `LoadFromFile`.
234 After modifications were made, use `WriteToFile` to commit changes.
235
236 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800237 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800238 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800239 partitions: a list of GPT partition entry nametuple.
240 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800241 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800242 """
243
Hung-Te Linf148d322018-04-13 10:24:42 +0800244 DEFAULT_BLOCK_SIZE = 512
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800245 TYPE_GUID_UNUSED = '\x00' * 16
246 TYPE_GUID_MAP = {
247 '00000000-0000-0000-0000-000000000000': 'Unused',
248 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7': 'Linux data',
Hung-Te Linfe724f82018-04-18 15:03:58 +0800249 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309': 'ChromeOS kernel',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800250 '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC': 'ChromeOS rootfs',
251 '2E0A753D-9E48-43B0-8337-B15192CB1B5E': 'ChromeOS reserved',
252 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3': 'ChromeOS firmware',
253 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': 'EFI System Partition',
254 }
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800255 TYPE_GUID_REVERSE_MAP = dict(
256 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
257 for k, v in TYPE_GUID_MAP.iteritems())
Hung-Te Linfe724f82018-04-18 15:03:58 +0800258 STR_TYPE_GUID_LIST_BOOTABLE = [
259 TYPE_GUID_REVERSE_MAP['kernel'],
260 TYPE_GUID_REVERSE_MAP['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800261 ]
262
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800263 @GPTBlob(PMBR_DESCRIPTION)
264 class ProtectiveMBR(GPTObject):
265 """Protective MBR (PMBR) in GPT."""
266 SIGNATURE = '\x55\xAA'
267 MAGIC = '\x1d\x9a'
268
269 CLONE_CONVERTERS = {
270 'BootGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v
271 }
272
273 @property
274 def boot_guid(self):
275 """Returns the BootGUID in decoded (uuid.UUID) format."""
276 return uuid.UUID(bytes_le=self.BootGUID)
277
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800278 @GPTBlob(HEADER_DESCRIPTION)
279 class Header(GPTObject):
280 """Wrapper to Header in GPT."""
281 SIGNATURES = ['EFI PART', 'CHROMEOS']
282 SIGNATURE_IGNORE = 'IGNOREME'
283 DEFAULT_REVISION = '\x00\x00\x01\x00'
284
285 DEFAULT_PARTITION_ENTRIES = 128
286 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
287
288 def Clone(self, **dargs):
289 """Creates a new instance with modifications.
290
291 GPT objects are usually named tuples that are immutable, so the only way
292 to make changes is to create a new instance with modifications.
293
294 CRC32 is always updated but PartitionArrayCRC32 must be updated explicitly
295 since we can't track changes in GPT.partitions automatically.
296
297 Note since GPTHeader.Clone will always update CRC, we can only check and
298 compute CRC by super(GPT.Header, header).Clone, or header._replace.
299 """
300 dargs['CRC32'] = 0
301 header = super(GPT.Header, self).Clone(**dargs)
302 return super(GPT.Header, header).Clone(CRC32=binascii.crc32(header.blob))
303
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800304 @classmethod
305 def Create(cls, size, block_size, pad_blocks=0,
306 part_entries=DEFAULT_PARTITION_ENTRIES):
307 """Creates a header with default values.
308
309 Args:
310 size: integer of expected image size.
311 block_size: integer for size of each block (sector).
312 pad_blocks: number of preserved sectors between header and partitions.
313 part_entries: number of partitions to include in header.
314 """
315 part_entry_size = struct.calcsize(GPT.Partition.FORMAT)
316 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
317 parts_bytes = part_entries * part_entry_size
318 parts_blocks = parts_bytes / block_size
319 if parts_bytes % block_size:
320 parts_blocks += 1
321 # PartitionsCRC32 must be updated later explicitly.
322 return cls.ReadFrom(None).Clone(
323 Signature=cls.SIGNATURES[0],
324 Revision=cls.DEFAULT_REVISION,
325 HeaderSize=struct.calcsize(cls.FORMAT),
326 CurrentLBA=1,
327 BackupLBA=size / block_size - 1,
328 FirstUsableLBA=parts_lba + parts_blocks,
329 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
330 DiskGUID=uuid.uuid4().get_bytes(),
331 PartitionEntriesStartingLBA=parts_lba,
332 PartitionEntriesNumber=part_entries,
333 PartitionEntrySize=part_entry_size,
334 )
335
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800336 class PartitionAttributes(object):
337 """Wrapper for Partition.Attributes.
338
339 This can be created using Partition.attrs, but the changed properties won't
340 apply to underlying Partition until an explicit call with
341 Partition.Clone(Attributes=new_attrs).
342 """
343
344 def __init__(self, attrs):
345 self._attrs = attrs
346
347 @property
348 def raw(self):
349 """Returns the raw integer type attributes."""
350 return self._Get()
351
352 def _Get(self):
353 return self._attrs
354
355 def _Set(self, value):
356 self._attrs = value
357
358 successful = BitProperty(_Get, _Set, 56, 1)
359 tries = BitProperty(_Get, _Set, 52, 0xf)
360 priority = BitProperty(_Get, _Set, 48, 0xf)
361 legacy_boot = BitProperty(_Get, _Set, 2, 1)
362 required = BitProperty(_Get, _Set, 0, 1)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800363 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800364
365 @GPTBlob(PARTITION_DESCRIPTION)
366 class Partition(GPTObject):
367 """The partition entry in GPT.
368
369 Please include following properties when creating a Partition object:
370 - image: a string for path to the image file the partition maps to.
371 - number: the 1-based partition number.
372 - block_size: an integer for size of each block (LBA, or sector).
373 """
374 NAMES_ENCODING = 'utf-16-le'
375 NAMES_LENGTH = 72
376
377 CLONE_CONVERTERS = {
378 # TODO(hungte) check if encoded name is too long.
379 'label': lambda l: (None if l is None else
380 ('Names', l.encode(GPT.Partition.NAMES_ENCODING))),
381 'TypeGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
382 'UniqueGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
383 'Attributes': (
384 lambda v: v.raw if isinstance(v, GPT.PartitionAttributes) else v),
385 }
386
387 def __str__(self):
388 return '%s#%s' % (self.image, self.number)
389
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800390 @classmethod
391 def Create(cls, block_size, image, number):
392 """Creates a new partition entry with given meta data."""
393 part = cls.ReadFrom(
394 None, image=image, number=number, block_size=block_size)
395 return part
396
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800397 def IsUnused(self):
398 """Returns if the partition is unused and can be allocated."""
399 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
400
Hung-Te Linfe724f82018-04-18 15:03:58 +0800401 def IsChromeOSKernel(self):
402 """Returns if the partition is a Chrome OS kernel partition."""
403 return self.TypeGUID == uuid.UUID(
404 GPT.TYPE_GUID_REVERSE_MAP['kernel']).bytes_le
405
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800406 @property
407 def blocks(self):
408 """Return size of partition in blocks (see block_size)."""
409 return self.LastLBA - self.FirstLBA + 1
410
411 @property
412 def offset(self):
413 """Returns offset to partition in bytes."""
414 return self.FirstLBA * self.block_size
415
416 @property
417 def size(self):
418 """Returns size of partition in bytes."""
419 return self.blocks * self.block_size
420
421 @property
422 def type_guid(self):
423 return uuid.UUID(bytes_le=self.TypeGUID)
424
425 @property
426 def unique_guid(self):
427 return uuid.UUID(bytes_le=self.UniqueGUID)
428
429 @property
430 def label(self):
431 """Returns the Names in decoded string type."""
432 return self.Names.decode(self.NAMES_ENCODING).strip('\0')
433
434 @property
435 def attrs(self):
436 return GPT.PartitionAttributes(self.Attributes)
437
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800438 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800439 """GPT constructor.
440
441 See LoadFromFile for how it's usually used.
442 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800443 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800444 self.header = None
445 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800446 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800447 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800448
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800449 @classmethod
Hung-Te Linf641d302018-04-18 15:09:35 +0800450 def GetTypeGUID(cls, input_uuid):
451 if input_uuid.lower() in cls.TYPE_GUID_REVERSE_MAP:
452 input_uuid = cls.TYPE_GUID_REVERSE_MAP[input_uuid.lower()]
453 return uuid.UUID(input_uuid)
454
455 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800456 def Create(cls, image_name, size, block_size, pad_blocks=0):
457 """Creates a new GPT instance from given size and block_size.
458
459 Args:
460 image_name: a string of underlying disk image file name.
461 size: expected size of disk image.
462 block_size: size of each block (sector) in bytes.
463 pad_blocks: number of blocks between header and partitions array.
464 """
465 gpt = cls()
466 gpt.block_size = block_size
467 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
468 gpt.partitions = [
469 cls.Partition.Create(block_size, image_name, i + 1)
470 for i in xrange(gpt.header.PartitionEntriesNumber)]
471 return gpt
472
473 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800474 def LoadFromFile(cls, image):
475 """Loads a GPT table from give disk image file object.
476
477 Args:
478 image: a string as file path or a file-like object to read from.
479 """
480 if isinstance(image, basestring):
481 with open(image, 'rb') as f:
482 return cls.LoadFromFile(f)
483
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800484 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800485 image.seek(0)
486 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
487 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
488 logging.debug('Found MBR signature in %s', image.name)
489 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
490 logging.debug('Found PMBR in %s', image.name)
491 gpt.pmbr = pmbr
492
Hung-Te Linf148d322018-04-13 10:24:42 +0800493 # Try DEFAULT_BLOCK_SIZE, then 4K.
494 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800495 # Note because there are devices setting Primary as ignored and the
496 # partition table signature accepts 'CHROMEOS' which is also used by
497 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
498 # first before trying other block sizes, otherwise we may incorrectly
499 # identify a kernel partition as LBA 1 of larger block size system.
500 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
501 (-block_size, os.SEEK_END)]):
502 image.seek(*seek)
503 header = gpt.Header.ReadFrom(image)
504 if header.Signature in cls.Header.SIGNATURES:
505 gpt.block_size = block_size
506 if i != 0:
507 gpt.is_secondary = True
508 break
509 else:
510 # Nothing found, try next block size.
511 continue
512 # Found a valid signature.
513 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800514 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800515 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800516
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800517 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800518 def ReadPartition(image, i):
519 p = gpt.Partition.ReadFrom(
520 image, image=image.name, number=i + 1, block_size=gpt.block_size)
521 return p
522
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800523 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800524 gpt.partitions = [
525 ReadPartition(image, i) for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800526 return gpt
527
528 def GetValidPartitions(self):
529 """Returns the list of partitions before entry with empty type GUID.
530
531 In partition table, the first entry with empty type GUID indicates end of
532 valid partitions. In most implementations all partitions after that should
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800533 be zeroed. However, few implementations for example cgpt, may create
534 partitions in arbitrary order so use this carefully.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800535 """
536 for i, p in enumerate(self.partitions):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800537 if p.IsUnused():
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800538 return self.partitions[:i]
539 return self.partitions
540
541 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800542 """Returns the max LastLBA from all used partitions."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800543 parts = [p for p in self.partitions if not p.IsUnused()]
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800544 return (max(p.LastLBA for p in parts)
545 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800546
547 def GetPartitionTableBlocks(self, header=None):
548 """Returns the blocks (or LBA) of partition table from given header."""
549 if header is None:
550 header = self.header
551 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800552 blocks = size / self.block_size
553 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800554 blocks += 1
555 return blocks
556
557 def Resize(self, new_size):
558 """Adjust GPT for a disk image in given size.
559
560 Args:
561 new_size: Integer for new size of disk image file.
562 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800563 old_size = self.block_size * (self.header.BackupLBA + 1)
564 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800565 raise GPTError(
566 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800567 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800568 if old_size != new_size:
569 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800570 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800571 else:
572 logging.info('Image size (%d, LBA=%d) not changed.',
573 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800574 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800575
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800576 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800577 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800578 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800579
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800580 if last_usable_lba < self.header.LastUsableLBA:
581 max_used_lba = self.GetMaxUsedLBA()
582 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800583 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800584
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800585 self.header = self.header.Clone(
586 BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800587
588 def GetFreeSpace(self):
589 """Returns the free (available) space left according to LastUsableLBA."""
590 max_lba = self.GetMaxUsedLBA()
591 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800592 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800593
594 def ExpandPartition(self, i):
595 """Expands a given partition to last usable LBA.
596
597 Args:
598 i: Index (0-based) of target partition.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800599
600 Returns:
601 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800602 """
603 # Assume no partitions overlap, we need to make sure partition[i] has
604 # largest LBA.
605 if i < 0 or i >= len(self.GetValidPartitions()):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800606 raise GPTError('Partition number %d is invalid.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800607 p = self.partitions[i]
608 max_used_lba = self.GetMaxUsedLBA()
609 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800610 raise GPTError(
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800611 'Cannot expand partition %d because it is not the last allocated '
612 'partition.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800613
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800614 old_blocks = p.blocks
615 p = p.Clone(LastLBA=self.header.LastUsableLBA)
616 new_blocks = p.blocks
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800617 self.partitions[i] = p
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800618 logging.warn(
619 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
620 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800621
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800622 def GetIgnoredHeader(self):
623 """Returns a primary header with signature set to 'IGNOREME'.
624
625 This is a special trick to enforce using backup header, when there is
626 some security exploit in LBA1.
627 """
628 return self.header.Clone(Signature=self.header.SIGNATURE_IGNORE)
629
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800630 def UpdateChecksum(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800631 """Updates all checksum fields in GPT objects.
632
633 The Header.CRC32 is automatically updated in Header.Clone().
634 """
635 parts = ''.join(p.blob for p in self.partitions)
636 self.header = self.header.Clone(
637 PartitionArrayCRC32=binascii.crc32(parts))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800638
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800639 def GetBackupHeader(self, header):
640 """Returns the backup header according to given header."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800641 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800642 header.BackupLBA - self.GetPartitionTableBlocks())
643 return header.Clone(
644 BackupLBA=header.CurrentLBA,
645 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800646 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800647
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800648 @classmethod
649 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
650 """Writes a protective MBR to given file.
651
652 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
653 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
654 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
655 we can follow that.
656
657 Args:
658 create: True to re-create PMBR structure.
659 bootcode: a blob of new boot code.
660 boot_guid a blob for new boot GUID.
661
662 Returns:
663 The written PMBR structure.
664 """
665 if isinstance(image, basestring):
666 with open(image, 'rb+') as f:
667 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
668
669 image.seek(0)
670 assert struct.calcsize(cls.ProtectiveMBR.FORMAT) == cls.DEFAULT_BLOCK_SIZE
671 pmbr = cls.ProtectiveMBR.ReadFrom(image)
672
673 if create:
674 legacy_sectors = min(
675 0x100000000,
676 os.path.getsize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
677 # Partition 0 must have have the fixed CHS with number of sectors
678 # (calculated as legacy_sectors later).
679 part0 = ('00000200eeffffff01000000'.decode('hex') +
680 struct.pack('<I', legacy_sectors))
681 # Partition 1~3 should be all zero.
682 part1 = '\x00' * 16
683 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
684 pmbr = pmbr.Clone(
685 BootGUID=cls.TYPE_GUID_UNUSED,
686 DiskID=0,
687 Magic=cls.ProtectiveMBR.MAGIC,
688 LegacyPart0=part0,
689 LegacyPart1=part1,
690 LegacyPart2=part1,
691 LegacyPart3=part1,
692 Signature=cls.ProtectiveMBR.SIGNATURE)
693
694 if bootcode:
695 if len(bootcode) > len(pmbr.BootCode):
696 logging.info(
697 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
698 bootcode = bootcode[:len(pmbr.BootCode)]
699 pmbr = pmbr.Clone(BootCode=bootcode)
700 if boot_guid:
701 pmbr = pmbr.Clone(BootGUID=boot_guid)
702
703 blob = pmbr.blob
704 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
705 image.seek(0)
706 image.write(blob)
707 return pmbr
708
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800709 def WriteToFile(self, image):
710 """Updates partition table in a disk image file.
711
712 Args:
713 image: a string as file path or a file-like object to write into.
714 """
715 if isinstance(image, basestring):
716 with open(image, 'rb+') as f:
717 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800718
719 def WriteData(name, blob, lba):
720 """Writes a blob into given location."""
721 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800722 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800723 image.seek(lba * self.block_size)
724 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800725
726 self.UpdateChecksum()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800727 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800728
729 header = self.header
730 WriteData('GPT Header', header.blob, header.CurrentLBA)
731 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
732 logging.info(
733 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
734 header.LastUsableLBA)
735
736 if not self.is_secondary:
737 # When is_secondary is True, the header we have is actually backup header.
738 backup_header = self.GetBackupHeader(self.header)
739 WriteData(
740 'Backup Partitions', parts_blob,
741 backup_header.PartitionEntriesStartingLBA)
742 WriteData(
743 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800744
745
746class GPTCommands(object):
747 """Collection of GPT sub commands for command line to use.
748
749 The commands are derived from `cgpt`, but not necessary to be 100% compatible
750 with cgpt.
751 """
752
753 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800754 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800755 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800756 ('type', 'type guid'),
757 ('unique', 'unique guid'),
758 ('label', 'label'),
759 ('Successful', 'Successful flag'),
760 ('Tries', 'Tries flag'),
761 ('Priority', 'Priority flag'),
762 ('Legacy', 'Legacy Boot flag'),
763 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800764
765 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800766 commands = dict(
767 (command.lower(), getattr(self, command)())
768 for command in dir(self)
769 if (isinstance(getattr(self, command), type) and
770 issubclass(getattr(self, command), self.SubCommand) and
771 getattr(self, command) is not self.SubCommand)
772 )
773 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800774
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800775 def DefineArgs(self, parser):
776 """Defines all available commands to an argparser subparsers instance."""
777 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
778 for name, instance in sorted(self.commands.iteritems()):
779 parser = subparsers.add_parser(
780 name, description=instance.__doc__,
781 formatter_class=argparse.RawDescriptionHelpFormatter,
782 help=instance.__doc__.splitlines()[0])
783 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800784
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800785 def Execute(self, args):
786 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800787 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800788
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800789 class SubCommand(object):
790 """A base class for sub commands to derive from."""
791
792 def DefineArgs(self, parser):
793 """Defines command line arguments to argparse parser.
794
795 Args:
796 parser: An argparse parser instance.
797 """
798 del parser # Unused.
799 raise NotImplementedError
800
801 def Execute(self, args):
802 """Execute the command.
803
804 Args:
805 args: An argparse parsed namespace.
806 """
807 del args # Unused.
808 raise NotImplementedError
809
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800810 class Create(SubCommand):
811 """Create or reset GPT headers and tables.
812
813 Create or reset an empty GPT.
814 """
815
816 def DefineArgs(self, parser):
817 parser.add_argument(
818 '-z', '--zero', action='store_true',
819 help='Zero the sectors of the GPT table and entries')
820 parser.add_argument(
821 '-p', '--pad_blocks', type=int, default=0,
822 help=('Size (in blocks) of the disk to pad between the '
823 'primary GPT header and its entries, default %(default)s'))
824 parser.add_argument(
825 '--block_size', type=int, default=GPT.DEFAULT_BLOCK_SIZE,
826 help='Size of each block (sector) in bytes.')
827 parser.add_argument(
828 'image_file', type=argparse.FileType('rb+'),
829 help='Disk image file to create.')
830
831 def Execute(self, args):
832 block_size = args.block_size
833 gpt = GPT.Create(
834 args.image_file.name, os.path.getsize(args.image_file.name),
835 block_size, args.pad_blocks)
836 if args.zero:
837 # In theory we only need to clear LBA 1, but to make sure images already
838 # initialized with different block size won't have GPT signature in
839 # different locations, we should zero until first usable LBA.
840 args.image_file.seek(0)
841 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
842 gpt.WriteToFile(args.image_file)
843 print('OK: Created GPT for %s' % args.image_file.name)
844
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800845 class Boot(SubCommand):
846 """Edit the PMBR sector for legacy BIOSes.
847
848 With no options, it will just print the PMBR boot guid.
849 """
850
851 def DefineArgs(self, parser):
852 parser.add_argument(
853 '-i', '--number', type=int,
854 help='Set bootable partition')
855 parser.add_argument(
856 '-b', '--bootloader', type=argparse.FileType('r'),
857 help='Install bootloader code in the PMBR')
858 parser.add_argument(
859 '-p', '--pmbr', action='store_true',
860 help='Create legacy PMBR partition table')
861 parser.add_argument(
862 'image_file', type=argparse.FileType('rb+'),
863 help='Disk image file to change PMBR.')
864
865 def Execute(self, args):
866 """Rebuilds the protective MBR."""
867 bootcode = args.bootloader.read() if args.bootloader else None
868 boot_guid = None
869 if args.number is not None:
870 gpt = GPT.LoadFromFile(args.image_file)
871 boot_guid = gpt.partitions[args.number - 1].UniqueGUID
872 pmbr = GPT.WriteProtectiveMBR(
873 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
874
875 print(str(pmbr.boot_guid).upper())
876
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800877 class Legacy(SubCommand):
878 """Switch between GPT and Legacy GPT.
879
880 Switch GPT header signature to "CHROMEOS".
881 """
882
883 def DefineArgs(self, parser):
884 parser.add_argument(
885 '-e', '--efi', action='store_true',
886 help='Switch GPT header signature back to "EFI PART"')
887 parser.add_argument(
888 '-p', '--primary-ignore', action='store_true',
889 help='Switch primary GPT header signature to "IGNOREME"')
890 parser.add_argument(
891 'image_file', type=argparse.FileType('rb+'),
892 help='Disk image file to change.')
893
894 def Execute(self, args):
895 gpt = GPT.LoadFromFile(args.image_file)
896 # cgpt behavior: if -p is specified, -e is ignored.
897 if args.primary_ignore:
898 if gpt.is_secondary:
899 raise GPTError('Sorry, the disk already has primary GPT ignored.')
900 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
901 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
902 gpt.header = gpt.GetBackupHeader(self.header)
903 gpt.is_secondary = True
904 else:
905 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
906 gpt.header = gpt.header.Clone(Signature=new_signature)
907 gpt.WriteToFile(args.image_file)
908 if args.primary_ignore:
909 print('OK: Set %s primary GPT header to %s.' %
910 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
911 else:
912 print('OK: Changed GPT signature for %s to %s.' %
913 (args.image_file.name, new_signature))
914
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800915 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800916 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800917
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800918 def DefineArgs(self, parser):
919 parser.add_argument(
920 'image_file', type=argparse.FileType('rb+'),
921 help='Disk image file to repair.')
922
923 def Execute(self, args):
924 gpt = GPT.LoadFromFile(args.image_file)
925 gpt.Resize(os.path.getsize(args.image_file.name))
926 gpt.WriteToFile(args.image_file)
927 print('Disk image file %s repaired.' % args.image_file.name)
928
929 class Expand(SubCommand):
930 """Expands a GPT partition to all available free space."""
931
932 def DefineArgs(self, parser):
933 parser.add_argument(
934 '-i', '--number', type=int, required=True,
935 help='The partition to expand.')
936 parser.add_argument(
937 'image_file', type=argparse.FileType('rb+'),
938 help='Disk image file to modify.')
939
940 def Execute(self, args):
941 gpt = GPT.LoadFromFile(args.image_file)
942 old_blocks, new_blocks = gpt.ExpandPartition(args.number - 1)
943 gpt.WriteToFile(args.image_file)
944 if old_blocks < new_blocks:
945 print(
946 'Partition %s on disk image file %s has been extended '
947 'from %s to %s .' %
948 (args.number, args.image_file.name, old_blocks * gpt.block_size,
949 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800950 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800951 print('Nothing to expand for disk image %s partition %s.' %
952 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800953
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800954 class Add(SubCommand):
955 """Add, edit, or remove a partition entry.
956
957 Use the -i option to modify an existing partition.
958 The -b, -s, and -t options must be given for new partitions.
959
960 The partition type may also be given as one of these aliases:
961
962 firmware ChromeOS firmware
963 kernel ChromeOS kernel
964 rootfs ChromeOS rootfs
965 data Linux data
966 reserved ChromeOS reserved
967 efi EFI System Partition
968 unused Unused (nonexistent) partition
969 """
970 def DefineArgs(self, parser):
971 parser.add_argument(
972 '-i', '--number', type=int,
973 help='Specify partition (default is next available)')
974 parser.add_argument(
975 '-b', '--begin', type=int,
976 help='Beginning sector')
977 parser.add_argument(
978 '-s', '--sectors', type=int,
979 help='Size in sectors (logical blocks).')
980 parser.add_argument(
981 '-t', '--type_guid',
982 help='Partition Type GUID')
983 parser.add_argument(
984 '-u', '--unique_guid',
985 help='Partition Unique ID')
986 parser.add_argument(
987 '-l', '--label',
988 help='Label')
989 parser.add_argument(
990 '-S', '--successful', type=int, choices=xrange(2),
991 help='set Successful flag')
992 parser.add_argument(
993 '-T', '--tries', type=int,
994 help='set Tries flag (0-15)')
995 parser.add_argument(
996 '-P', '--priority', type=int,
997 help='set Priority flag (0-15)')
998 parser.add_argument(
999 '-R', '--required', type=int, choices=xrange(2),
1000 help='set Required flag')
1001 parser.add_argument(
1002 '-B', '--boot_legacy', dest='legacy_boot', type=int,
1003 choices=xrange(2),
1004 help='set Legacy Boot flag')
1005 parser.add_argument(
1006 '-A', '--attribute', dest='raw_16', type=int,
1007 help='set raw 16-bit attribute value (bits 48-63)')
1008 parser.add_argument(
1009 'image_file', type=argparse.FileType('rb+'),
1010 help='Disk image file to modify.')
1011
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001012 def Execute(self, args):
1013 gpt = GPT.LoadFromFile(args.image_file)
1014 parts = gpt.GetValidPartitions()
1015 number = args.number
1016 if number is None:
1017 number = len(parts) + 1
1018 if number <= len(parts):
1019 is_new_part = False
1020 else:
1021 is_new_part = True
1022 index = number - 1
1023
1024 # First and last LBA must be calculated explicitly because the given
1025 # argument is size.
1026 part = gpt.partitions[index]
1027
1028 if is_new_part:
1029 part = part.ReadFrom(None, **part.__dict__).Clone(
1030 FirstLBA=(parts[-1].LastLBA + 1 if parts else
1031 gpt.header.FirstUsableLBA),
1032 LastLBA=gpt.header.LastUsableLBA,
1033 UniqueGUID=uuid.uuid4(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001034 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001035
1036 attr = part.attrs
1037 if args.legacy_boot is not None:
1038 attr.legacy_boot = args.legacy_boot
1039 if args.required is not None:
1040 attr.required = args.required
1041 if args.priority is not None:
1042 attr.priority = args.priority
1043 if args.tries is not None:
1044 attr.tries = args.tries
1045 if args.successful is not None:
1046 attr.successful = args.successful
1047 if args.raw_16 is not None:
1048 attr.raw_16 = args.raw_16
1049
1050 first_lba = part.FirstLBA if args.begin is None else args.begin
1051 last_lba = first_lba - 1 + (
1052 part.blocks if args.sectors is None else args.sectors)
1053 dargs = dict(
1054 FirstLBA=first_lba,
1055 LastLBA=last_lba,
1056 TypeGUID=(part.TypeGUID if args.type_guid is None else
Hung-Te Linf641d302018-04-18 15:09:35 +08001057 gpt.GetTypeGUID(args.type_guid)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001058 UniqueGUID=(part.UniqueGUID if args.unique_guid is None else
1059 uuid.UUID(bytes_le=args.unique_guid)),
1060 Attributes=attr,
1061 )
1062 if args.label is not None:
1063 dargs['label'] = args.label
1064
1065 part = part.Clone(**dargs)
1066 # Wipe partition again if it should be empty.
1067 if part.IsUnused():
1068 part = part.ReadFrom(None, **part.__dict__)
1069
1070 gpt.partitions[index] = part
1071
1072 # TODO(hungte) Sanity check if part is valid.
1073 gpt.WriteToFile(args.image_file)
1074 if part.IsUnused():
1075 # If we do ('%s' % part) there will be TypeError.
1076 print('OK: Deleted (zeroed) %s.' % (part,))
1077 else:
1078 print('OK: %s %s (%s+%s).' %
1079 ('Added' if is_new_part else 'Modified',
1080 part, part.FirstLBA, part.blocks))
1081
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001082 class Show(SubCommand):
1083 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001084
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001085 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001086 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001087
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001088 def DefineArgs(self, parser):
1089 parser.add_argument(
1090 '--numeric', '-n', action='store_true',
1091 help='Numeric output only.')
1092 parser.add_argument(
1093 '--quick', '-q', action='store_true',
1094 help='Quick output.')
1095 parser.add_argument(
1096 '-i', '--number', type=int,
1097 help='Show specified partition only, with format args.')
1098 for name, help_str in GPTCommands.FORMAT_ARGS:
1099 # TODO(hungte) Alert if multiple args were specified.
1100 parser.add_argument(
1101 '--%s' % name, '-%c' % name[0], action='store_true',
1102 help='[format] %s.' % help_str)
1103 parser.add_argument(
1104 'image_file', type=argparse.FileType('rb'),
1105 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001106
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001107 def Execute(self, args):
1108 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001109
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001110 def FormatGUID(bytes_le):
1111 return str(uuid.UUID(bytes_le=bytes_le)).upper()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001112
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001113 def FormatTypeGUID(p):
1114 guid_str = FormatGUID(p.TypeGUID)
1115 if not args.numeric:
1116 names = gpt.TYPE_GUID_MAP.get(guid_str)
1117 if names:
1118 return names
1119 return guid_str
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001120
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001121 def FormatNames(p):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +08001122 return p.label
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001123
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001124 def IsBootableType(type_guid):
Hung-Te Linfe724f82018-04-18 15:03:58 +08001125 return type_guid in gpt.STR_TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001126
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001127 def FormatAttribute(attrs, chromeos_kernel=False):
1128 if args.numeric:
1129 return '[%x]' % (attrs.raw >> 48)
1130 results = []
1131 if chromeos_kernel:
1132 results += [
1133 'priority=%d' % attrs.priority,
1134 'tries=%d' % attrs.tries,
1135 'successful=%d' % attrs.successful]
1136 if attrs.required:
1137 results += ['required=1']
1138 if attrs.legacy_boot:
1139 results += ['legacy_boot=1']
1140 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001141
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001142 def ApplyFormatArgs(p):
1143 if args.begin:
1144 return p.FirstLBA
1145 elif args.size:
1146 return p.blocks
1147 elif args.type:
1148 return FormatTypeGUID(p)
1149 elif args.unique:
1150 return FormatGUID(p.UniqueGUID)
1151 elif args.label:
1152 return FormatNames(p)
1153 elif args.Successful:
1154 return p.attrs.successful
1155 elif args.Priority:
1156 return p.attrs.priority
1157 elif args.Tries:
1158 return p.attrs.tries
1159 elif args.Legacy:
1160 return p.attrs.legacy_boot
1161 elif args.Attribute:
1162 return '[%x]' % (p.Attributes >> 48)
1163 else:
1164 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001165
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001166 def IsFormatArgsSpecified():
1167 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001168
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001169 gpt = GPT.LoadFromFile(args.image_file)
1170 logging.debug('%r', gpt.header)
1171 fmt = '%12s %11s %7s %s'
1172 fmt2 = '%32s %s: %s'
1173 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001174
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001175 if IsFormatArgsSpecified() and args.number is None:
1176 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001177
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001178 if not (args.number is None or
1179 0 < args.number <= gpt.header.PartitionEntriesNumber):
1180 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001181
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001182 partitions = gpt.partitions
1183 do_print_gpt_blocks = False
1184 if not (args.quick or IsFormatArgsSpecified()):
1185 print(fmt % header)
1186 if args.number is None:
1187 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001188
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001189 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001190 if gpt.pmbr:
1191 print(fmt % (0, 1, '', 'PMBR'))
1192 if gpt.is_secondary:
1193 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1194 else:
1195 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1196 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1197 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001198
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001199 for p in partitions:
1200 if args.number is None:
1201 # Skip unused partitions.
1202 if p.IsUnused():
1203 continue
1204 elif p.number != args.number:
1205 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001206
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001207 if IsFormatArgsSpecified():
1208 print(ApplyFormatArgs(p))
1209 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001210
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001211 type_guid = FormatGUID(p.TypeGUID)
1212 print(fmt % (p.FirstLBA, p.blocks, p.number,
1213 FormatTypeGUID(p) if args.quick else
1214 'Label: "%s"' % FormatNames(p)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001215
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001216 if not args.quick:
1217 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
1218 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
1219 if args.numeric or IsBootableType(type_guid):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001220 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Linfe724f82018-04-18 15:03:58 +08001221 p.attrs, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001222
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001223 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001224 if gpt.is_secondary:
1225 header = gpt.header
1226 else:
1227 f = args.image_file
1228 f.seek(gpt.header.BackupLBA * gpt.block_size)
1229 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001230 print(fmt % (header.PartitionEntriesStartingLBA,
1231 gpt.GetPartitionTableBlocks(header), '',
1232 'Sec GPT table'))
1233 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001234
Hung-Te Linfe724f82018-04-18 15:03:58 +08001235 class Prioritize(SubCommand):
1236 """Reorder the priority of all kernel partitions.
1237
1238 Reorder the priority of all active ChromeOS Kernel partitions.
1239
1240 With no options this will set the lowest active kernel to priority 1 while
1241 maintaining the original order.
1242 """
1243
1244 def DefineArgs(self, parser):
1245 parser.add_argument(
1246 '-P', '--priority', type=int,
1247 help=('Highest priority to use in the new ordering. '
1248 'The other partitions will be ranked in decreasing '
1249 'priority while preserving their original order. '
1250 'If necessary the lowest ranks will be coalesced. '
1251 'No active kernels will be lowered to priority 0.'))
1252 parser.add_argument(
1253 '-i', '--number', type=int,
1254 help='Specify the partition to make the highest in the new order.')
1255 parser.add_argument(
1256 '-f', '--friends', action='store_true',
1257 help=('Friends of the given partition (those with the same '
1258 'starting priority) are also updated to the new '
1259 'highest priority. '))
1260 parser.add_argument(
1261 'image_file', type=argparse.FileType('rb+'),
1262 help='Disk image file to prioritize.')
1263
1264 def Execute(self, args):
1265 gpt = GPT.LoadFromFile(args.image_file)
1266 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
1267 prios = list(set(p.attrs.priority for p in parts if p.attrs.priority))
1268 prios.sort(reverse=True)
1269 groups = [[p for p in parts if p.attrs.priority == priority]
1270 for priority in prios]
1271 if args.number:
1272 p = gpt.partitions[args.number - 1]
1273 if p not in parts:
1274 raise GPTError('%s is not a ChromeOS kernel.' % p)
1275 if args.friends:
1276 group0 = [f for f in parts if f.attrs.priority == p.attrs.priority]
1277 else:
1278 group0 = [p]
1279 groups.insert(0, group0)
1280
1281 # Max priority is 0xf.
1282 highest = min(args.priority or len(prios), 0xf)
1283 logging.info('New highest priority: %s', highest)
1284 done = []
1285
1286 new_priority = highest
1287 for g in groups:
1288 has_new_part = False
1289 for p in g:
1290 if p.number in done:
1291 continue
1292 done.append(p.number)
1293 attrs = p.attrs
1294 old_priority = attrs.priority
1295 assert new_priority > 0, 'Priority must be > 0.'
1296 attrs.priority = new_priority
1297 p = p.Clone(Attributes=attrs)
1298 gpt.partitions[p.number - 1] = p
1299 has_new_part = True
1300 logging.info('%s priority changed from %s to %s.', p, old_priority,
1301 new_priority)
1302 if has_new_part:
1303 new_priority -= 1
1304
1305 gpt.WriteToFile(args.image_file)
1306
Hung-Te Linf641d302018-04-18 15:09:35 +08001307 class Find(SubCommand):
1308 """Locate a partition by its GUID.
1309
1310 Find a partition by its UUID or label. With no specified DRIVE it scans all
1311 physical drives.
1312
1313 The partition type may also be given as one of these aliases:
1314
1315 firmware ChromeOS firmware
1316 kernel ChromeOS kernel
1317 rootfs ChromeOS rootfs
1318 data Linux data
1319 reserved ChromeOS reserved
1320 efi EFI System Partition
1321 unused Unused (nonexistent) partition
1322 """
1323 def DefineArgs(self, parser):
1324 parser.add_argument(
1325 '-t', '--type-guid',
1326 help='Search for Partition Type GUID')
1327 parser.add_argument(
1328 '-u', '--unique-guid',
1329 help='Search for Partition Unique GUID')
1330 parser.add_argument(
1331 '-l', '--label',
1332 help='Search for Label')
1333 parser.add_argument(
1334 '-n', '--numeric', action='store_true',
1335 help='Numeric output only.')
1336 parser.add_argument(
1337 '-1', '--single-match', action='store_true',
1338 help='Fail if more than one match is found.')
1339 parser.add_argument(
1340 '-M', '--match-file', type=str,
1341 help='Matching partition data must also contain MATCH_FILE content.')
1342 parser.add_argument(
1343 '-O', '--offset', type=int, default=0,
1344 help='Byte offset into partition to match content (default 0).')
1345 parser.add_argument(
1346 'drive', type=argparse.FileType('rb+'), nargs='?',
1347 help='Drive or disk image file to find.')
1348
1349 def Execute(self, args):
1350 if not any((args.type_guid, args.unique_guid, args.label)):
1351 raise GPTError('You must specify at least one of -t, -u, or -l')
1352
1353 drives = [args.drive.name] if args.drive else (
1354 '/dev/%s' % name for name in subprocess.check_output(
1355 'lsblk -d -n -r -o name', shell=True).split())
1356
1357 match_pattern = None
1358 if args.match_file:
1359 with open(args.match_file) as f:
1360 match_pattern = f.read()
1361
1362 found = 0
1363 for drive in drives:
1364 try:
1365 gpt = GPT.LoadFromFile(drive)
1366 except GPTError:
1367 if args.drive:
1368 raise
1369 # When scanning all block devices on system, ignore failure.
1370
1371 for p in gpt.partitions:
1372 if p.IsUnused():
1373 continue
1374 if args.label is not None and args.label != p.label:
1375 continue
1376 if args.unique_guid is not None and (
1377 uuid.UUID(args.unique_guid) != uuid.UUID(bytes_le=p.UniqueGUID)):
1378 continue
1379 type_guid = gpt.GetTypeGUID(args.type_guid)
1380 if args.type_guid is not None and (
1381 type_guid != uuid.UUID(bytes_le=p.TypeGUID)):
1382 continue
1383 if match_pattern:
1384 with open(drive, 'rb') as f:
1385 f.seek(p.offset + args.offset)
1386 if f.read(len(match_pattern)) != match_pattern:
1387 continue
1388 # Found the partition, now print.
1389 found += 1
1390 if args.numeric:
1391 print(p.number)
1392 else:
1393 # This is actually more for block devices.
1394 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1395 p.number))
1396
1397 if found < 1 or (args.single_match and found > 1):
1398 return 1
1399 return 0
1400
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001401
1402def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001403 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001404 parser = argparse.ArgumentParser(description='GPT Utility.')
1405 parser.add_argument('--verbose', '-v', action='count', default=0,
1406 help='increase verbosity.')
1407 parser.add_argument('--debug', '-d', action='store_true',
1408 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001409 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001410
1411 args = parser.parse_args()
1412 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1413 if args.debug:
1414 log_level = logging.DEBUG
1415 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1416 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001417 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001418 code = commands.Execute(args)
1419 if type(code) is int:
1420 sys.exit(code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001421 except Exception as e:
1422 if args.verbose or args.debug:
1423 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001424 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001425
1426
1427if __name__ == '__main__':
1428 main()