blob: e14cfdb6dd6559a3871ac2402c4ce2a040ca5b5b [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
Hung-Te Linc5196682018-04-18 22:59:59 +0800528 def GetUsedPartitions(self):
529 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800530
Hung-Te Linc5196682018-04-18 22:59:59 +0800531 Use 'number' property to find the real location of partition in
532 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800533 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800534 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800535
536 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800537 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800538 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800539 return (max(p.LastLBA for p in parts)
540 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800541
542 def GetPartitionTableBlocks(self, header=None):
543 """Returns the blocks (or LBA) of partition table from given header."""
544 if header is None:
545 header = self.header
546 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800547 blocks = size / self.block_size
548 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800549 blocks += 1
550 return blocks
551
552 def Resize(self, new_size):
553 """Adjust GPT for a disk image in given size.
554
555 Args:
556 new_size: Integer for new size of disk image file.
557 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800558 old_size = self.block_size * (self.header.BackupLBA + 1)
559 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800560 raise GPTError(
561 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800562 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800563 if old_size != new_size:
564 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800565 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800566 else:
567 logging.info('Image size (%d, LBA=%d) not changed.',
568 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800569 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800570
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800571 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800572 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800573 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800574
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800575 if last_usable_lba < self.header.LastUsableLBA:
576 max_used_lba = self.GetMaxUsedLBA()
577 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800578 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800579
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800580 self.header = self.header.Clone(
581 BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800582
583 def GetFreeSpace(self):
584 """Returns the free (available) space left according to LastUsableLBA."""
585 max_lba = self.GetMaxUsedLBA()
586 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800587 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800588
589 def ExpandPartition(self, i):
590 """Expands a given partition to last usable LBA.
591
592 Args:
593 i: Index (0-based) of target partition.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800594
595 Returns:
596 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800597 """
598 # Assume no partitions overlap, we need to make sure partition[i] has
599 # largest LBA.
Hung-Te Linc5196682018-04-18 22:59:59 +0800600 if i < 0 or i >= len(self.partitions):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800601 raise GPTError('Partition number %d is invalid.' % (i + 1))
Hung-Te Linc5196682018-04-18 22:59:59 +0800602 if self.partitions[i].IsUnused():
603 raise GPTError('Partition number %d is unused.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800604 p = self.partitions[i]
605 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800606 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800607 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800608 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800609 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800610
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800611 old_blocks = p.blocks
612 p = p.Clone(LastLBA=self.header.LastUsableLBA)
613 new_blocks = p.blocks
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800614 self.partitions[i] = p
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800615 logging.warn(
616 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
617 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800618
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800619 def GetIgnoredHeader(self):
620 """Returns a primary header with signature set to 'IGNOREME'.
621
622 This is a special trick to enforce using backup header, when there is
623 some security exploit in LBA1.
624 """
625 return self.header.Clone(Signature=self.header.SIGNATURE_IGNORE)
626
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800627 def UpdateChecksum(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800628 """Updates all checksum fields in GPT objects.
629
630 The Header.CRC32 is automatically updated in Header.Clone().
631 """
632 parts = ''.join(p.blob for p in self.partitions)
633 self.header = self.header.Clone(
634 PartitionArrayCRC32=binascii.crc32(parts))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800635
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800636 def GetBackupHeader(self, header):
637 """Returns the backup header according to given header."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800638 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800639 header.BackupLBA - self.GetPartitionTableBlocks())
640 return header.Clone(
641 BackupLBA=header.CurrentLBA,
642 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800643 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800644
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800645 @classmethod
646 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
647 """Writes a protective MBR to given file.
648
649 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
650 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
651 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
652 we can follow that.
653
654 Args:
655 create: True to re-create PMBR structure.
656 bootcode: a blob of new boot code.
657 boot_guid a blob for new boot GUID.
658
659 Returns:
660 The written PMBR structure.
661 """
662 if isinstance(image, basestring):
663 with open(image, 'rb+') as f:
664 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
665
666 image.seek(0)
667 assert struct.calcsize(cls.ProtectiveMBR.FORMAT) == cls.DEFAULT_BLOCK_SIZE
668 pmbr = cls.ProtectiveMBR.ReadFrom(image)
669
670 if create:
671 legacy_sectors = min(
672 0x100000000,
673 os.path.getsize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
674 # Partition 0 must have have the fixed CHS with number of sectors
675 # (calculated as legacy_sectors later).
676 part0 = ('00000200eeffffff01000000'.decode('hex') +
677 struct.pack('<I', legacy_sectors))
678 # Partition 1~3 should be all zero.
679 part1 = '\x00' * 16
680 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
681 pmbr = pmbr.Clone(
682 BootGUID=cls.TYPE_GUID_UNUSED,
683 DiskID=0,
684 Magic=cls.ProtectiveMBR.MAGIC,
685 LegacyPart0=part0,
686 LegacyPart1=part1,
687 LegacyPart2=part1,
688 LegacyPart3=part1,
689 Signature=cls.ProtectiveMBR.SIGNATURE)
690
691 if bootcode:
692 if len(bootcode) > len(pmbr.BootCode):
693 logging.info(
694 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
695 bootcode = bootcode[:len(pmbr.BootCode)]
696 pmbr = pmbr.Clone(BootCode=bootcode)
697 if boot_guid:
698 pmbr = pmbr.Clone(BootGUID=boot_guid)
699
700 blob = pmbr.blob
701 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
702 image.seek(0)
703 image.write(blob)
704 return pmbr
705
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800706 def WriteToFile(self, image):
707 """Updates partition table in a disk image file.
708
709 Args:
710 image: a string as file path or a file-like object to write into.
711 """
712 if isinstance(image, basestring):
713 with open(image, 'rb+') as f:
714 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800715
716 def WriteData(name, blob, lba):
717 """Writes a blob into given location."""
718 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800719 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800720 image.seek(lba * self.block_size)
721 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800722
723 self.UpdateChecksum()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800724 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800725
726 header = self.header
727 WriteData('GPT Header', header.blob, header.CurrentLBA)
728 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
729 logging.info(
730 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
731 header.LastUsableLBA)
732
733 if not self.is_secondary:
734 # When is_secondary is True, the header we have is actually backup header.
735 backup_header = self.GetBackupHeader(self.header)
736 WriteData(
737 'Backup Partitions', parts_blob,
738 backup_header.PartitionEntriesStartingLBA)
739 WriteData(
740 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800741
742
743class GPTCommands(object):
744 """Collection of GPT sub commands for command line to use.
745
746 The commands are derived from `cgpt`, but not necessary to be 100% compatible
747 with cgpt.
748 """
749
750 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800751 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800752 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800753 ('type', 'type guid'),
754 ('unique', 'unique guid'),
755 ('label', 'label'),
756 ('Successful', 'Successful flag'),
757 ('Tries', 'Tries flag'),
758 ('Priority', 'Priority flag'),
759 ('Legacy', 'Legacy Boot flag'),
760 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800761
762 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800763 commands = dict(
764 (command.lower(), getattr(self, command)())
765 for command in dir(self)
766 if (isinstance(getattr(self, command), type) and
767 issubclass(getattr(self, command), self.SubCommand) and
768 getattr(self, command) is not self.SubCommand)
769 )
770 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800771
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800772 def DefineArgs(self, parser):
773 """Defines all available commands to an argparser subparsers instance."""
774 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
775 for name, instance in sorted(self.commands.iteritems()):
776 parser = subparsers.add_parser(
777 name, description=instance.__doc__,
778 formatter_class=argparse.RawDescriptionHelpFormatter,
779 help=instance.__doc__.splitlines()[0])
780 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800781
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800782 def Execute(self, args):
783 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800784 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800785
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800786 class SubCommand(object):
787 """A base class for sub commands to derive from."""
788
789 def DefineArgs(self, parser):
790 """Defines command line arguments to argparse parser.
791
792 Args:
793 parser: An argparse parser instance.
794 """
795 del parser # Unused.
796 raise NotImplementedError
797
798 def Execute(self, args):
799 """Execute the command.
800
801 Args:
802 args: An argparse parsed namespace.
803 """
804 del args # Unused.
805 raise NotImplementedError
806
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800807 class Create(SubCommand):
808 """Create or reset GPT headers and tables.
809
810 Create or reset an empty GPT.
811 """
812
813 def DefineArgs(self, parser):
814 parser.add_argument(
815 '-z', '--zero', action='store_true',
816 help='Zero the sectors of the GPT table and entries')
817 parser.add_argument(
818 '-p', '--pad_blocks', type=int, default=0,
819 help=('Size (in blocks) of the disk to pad between the '
820 'primary GPT header and its entries, default %(default)s'))
821 parser.add_argument(
822 '--block_size', type=int, default=GPT.DEFAULT_BLOCK_SIZE,
823 help='Size of each block (sector) in bytes.')
824 parser.add_argument(
825 'image_file', type=argparse.FileType('rb+'),
826 help='Disk image file to create.')
827
828 def Execute(self, args):
829 block_size = args.block_size
830 gpt = GPT.Create(
831 args.image_file.name, os.path.getsize(args.image_file.name),
832 block_size, args.pad_blocks)
833 if args.zero:
834 # In theory we only need to clear LBA 1, but to make sure images already
835 # initialized with different block size won't have GPT signature in
836 # different locations, we should zero until first usable LBA.
837 args.image_file.seek(0)
838 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
839 gpt.WriteToFile(args.image_file)
840 print('OK: Created GPT for %s' % args.image_file.name)
841
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800842 class Boot(SubCommand):
843 """Edit the PMBR sector for legacy BIOSes.
844
845 With no options, it will just print the PMBR boot guid.
846 """
847
848 def DefineArgs(self, parser):
849 parser.add_argument(
850 '-i', '--number', type=int,
851 help='Set bootable partition')
852 parser.add_argument(
853 '-b', '--bootloader', type=argparse.FileType('r'),
854 help='Install bootloader code in the PMBR')
855 parser.add_argument(
856 '-p', '--pmbr', action='store_true',
857 help='Create legacy PMBR partition table')
858 parser.add_argument(
859 'image_file', type=argparse.FileType('rb+'),
860 help='Disk image file to change PMBR.')
861
862 def Execute(self, args):
863 """Rebuilds the protective MBR."""
864 bootcode = args.bootloader.read() if args.bootloader else None
865 boot_guid = None
866 if args.number is not None:
867 gpt = GPT.LoadFromFile(args.image_file)
868 boot_guid = gpt.partitions[args.number - 1].UniqueGUID
869 pmbr = GPT.WriteProtectiveMBR(
870 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
871
872 print(str(pmbr.boot_guid).upper())
873
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800874 class Legacy(SubCommand):
875 """Switch between GPT and Legacy GPT.
876
877 Switch GPT header signature to "CHROMEOS".
878 """
879
880 def DefineArgs(self, parser):
881 parser.add_argument(
882 '-e', '--efi', action='store_true',
883 help='Switch GPT header signature back to "EFI PART"')
884 parser.add_argument(
885 '-p', '--primary-ignore', action='store_true',
886 help='Switch primary GPT header signature to "IGNOREME"')
887 parser.add_argument(
888 'image_file', type=argparse.FileType('rb+'),
889 help='Disk image file to change.')
890
891 def Execute(self, args):
892 gpt = GPT.LoadFromFile(args.image_file)
893 # cgpt behavior: if -p is specified, -e is ignored.
894 if args.primary_ignore:
895 if gpt.is_secondary:
896 raise GPTError('Sorry, the disk already has primary GPT ignored.')
897 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
898 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
899 gpt.header = gpt.GetBackupHeader(self.header)
900 gpt.is_secondary = True
901 else:
902 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
903 gpt.header = gpt.header.Clone(Signature=new_signature)
904 gpt.WriteToFile(args.image_file)
905 if args.primary_ignore:
906 print('OK: Set %s primary GPT header to %s.' %
907 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
908 else:
909 print('OK: Changed GPT signature for %s to %s.' %
910 (args.image_file.name, new_signature))
911
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800912 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800913 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800914
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800915 def DefineArgs(self, parser):
916 parser.add_argument(
917 'image_file', type=argparse.FileType('rb+'),
918 help='Disk image file to repair.')
919
920 def Execute(self, args):
921 gpt = GPT.LoadFromFile(args.image_file)
922 gpt.Resize(os.path.getsize(args.image_file.name))
923 gpt.WriteToFile(args.image_file)
924 print('Disk image file %s repaired.' % args.image_file.name)
925
926 class Expand(SubCommand):
927 """Expands a GPT partition to all available free space."""
928
929 def DefineArgs(self, parser):
930 parser.add_argument(
931 '-i', '--number', type=int, required=True,
932 help='The partition to expand.')
933 parser.add_argument(
934 'image_file', type=argparse.FileType('rb+'),
935 help='Disk image file to modify.')
936
937 def Execute(self, args):
938 gpt = GPT.LoadFromFile(args.image_file)
939 old_blocks, new_blocks = gpt.ExpandPartition(args.number - 1)
940 gpt.WriteToFile(args.image_file)
941 if old_blocks < new_blocks:
942 print(
943 'Partition %s on disk image file %s has been extended '
944 'from %s to %s .' %
945 (args.number, args.image_file.name, old_blocks * gpt.block_size,
946 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800947 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800948 print('Nothing to expand for disk image %s partition %s.' %
949 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800950
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800951 class Add(SubCommand):
952 """Add, edit, or remove a partition entry.
953
954 Use the -i option to modify an existing partition.
955 The -b, -s, and -t options must be given for new partitions.
956
957 The partition type may also be given as one of these aliases:
958
959 firmware ChromeOS firmware
960 kernel ChromeOS kernel
961 rootfs ChromeOS rootfs
962 data Linux data
963 reserved ChromeOS reserved
964 efi EFI System Partition
965 unused Unused (nonexistent) partition
966 """
967 def DefineArgs(self, parser):
968 parser.add_argument(
969 '-i', '--number', type=int,
970 help='Specify partition (default is next available)')
971 parser.add_argument(
972 '-b', '--begin', type=int,
973 help='Beginning sector')
974 parser.add_argument(
975 '-s', '--sectors', type=int,
976 help='Size in sectors (logical blocks).')
977 parser.add_argument(
978 '-t', '--type_guid',
979 help='Partition Type GUID')
980 parser.add_argument(
981 '-u', '--unique_guid',
982 help='Partition Unique ID')
983 parser.add_argument(
984 '-l', '--label',
985 help='Label')
986 parser.add_argument(
987 '-S', '--successful', type=int, choices=xrange(2),
988 help='set Successful flag')
989 parser.add_argument(
990 '-T', '--tries', type=int,
991 help='set Tries flag (0-15)')
992 parser.add_argument(
993 '-P', '--priority', type=int,
994 help='set Priority flag (0-15)')
995 parser.add_argument(
996 '-R', '--required', type=int, choices=xrange(2),
997 help='set Required flag')
998 parser.add_argument(
999 '-B', '--boot_legacy', dest='legacy_boot', type=int,
1000 choices=xrange(2),
1001 help='set Legacy Boot flag')
1002 parser.add_argument(
1003 '-A', '--attribute', dest='raw_16', type=int,
1004 help='set raw 16-bit attribute value (bits 48-63)')
1005 parser.add_argument(
1006 'image_file', type=argparse.FileType('rb+'),
1007 help='Disk image file to modify.')
1008
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001009 def Execute(self, args):
1010 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001011 number = args.number
1012 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001013 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001014
1015 # First and last LBA must be calculated explicitly because the given
1016 # argument is size.
Hung-Te Linc5196682018-04-18 22:59:59 +08001017 index = number - 1
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001018 part = gpt.partitions[index]
Hung-Te Linc5196682018-04-18 22:59:59 +08001019 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001020
1021 if is_new_part:
1022 part = part.ReadFrom(None, **part.__dict__).Clone(
Hung-Te Linc5196682018-04-18 22:59:59 +08001023 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001024 LastLBA=gpt.header.LastUsableLBA,
1025 UniqueGUID=uuid.uuid4(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001026 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001027
1028 attr = part.attrs
1029 if args.legacy_boot is not None:
1030 attr.legacy_boot = args.legacy_boot
1031 if args.required is not None:
1032 attr.required = args.required
1033 if args.priority is not None:
1034 attr.priority = args.priority
1035 if args.tries is not None:
1036 attr.tries = args.tries
1037 if args.successful is not None:
1038 attr.successful = args.successful
1039 if args.raw_16 is not None:
1040 attr.raw_16 = args.raw_16
1041
1042 first_lba = part.FirstLBA if args.begin is None else args.begin
1043 last_lba = first_lba - 1 + (
1044 part.blocks if args.sectors is None else args.sectors)
1045 dargs = dict(
1046 FirstLBA=first_lba,
1047 LastLBA=last_lba,
1048 TypeGUID=(part.TypeGUID if args.type_guid is None else
Hung-Te Linf641d302018-04-18 15:09:35 +08001049 gpt.GetTypeGUID(args.type_guid)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001050 UniqueGUID=(part.UniqueGUID if args.unique_guid is None else
1051 uuid.UUID(bytes_le=args.unique_guid)),
1052 Attributes=attr,
1053 )
1054 if args.label is not None:
1055 dargs['label'] = args.label
1056
1057 part = part.Clone(**dargs)
1058 # Wipe partition again if it should be empty.
1059 if part.IsUnused():
1060 part = part.ReadFrom(None, **part.__dict__)
1061
1062 gpt.partitions[index] = part
1063
1064 # TODO(hungte) Sanity check if part is valid.
1065 gpt.WriteToFile(args.image_file)
1066 if part.IsUnused():
1067 # If we do ('%s' % part) there will be TypeError.
1068 print('OK: Deleted (zeroed) %s.' % (part,))
1069 else:
1070 print('OK: %s %s (%s+%s).' %
1071 ('Added' if is_new_part else 'Modified',
1072 part, part.FirstLBA, part.blocks))
1073
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001074 class Show(SubCommand):
1075 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001076
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001077 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001078 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001079
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001080 def DefineArgs(self, parser):
1081 parser.add_argument(
1082 '--numeric', '-n', action='store_true',
1083 help='Numeric output only.')
1084 parser.add_argument(
1085 '--quick', '-q', action='store_true',
1086 help='Quick output.')
1087 parser.add_argument(
1088 '-i', '--number', type=int,
1089 help='Show specified partition only, with format args.')
1090 for name, help_str in GPTCommands.FORMAT_ARGS:
1091 # TODO(hungte) Alert if multiple args were specified.
1092 parser.add_argument(
1093 '--%s' % name, '-%c' % name[0], action='store_true',
1094 help='[format] %s.' % help_str)
1095 parser.add_argument(
1096 'image_file', type=argparse.FileType('rb'),
1097 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001098
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001099 def Execute(self, args):
1100 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001101
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001102 def FormatGUID(bytes_le):
1103 return str(uuid.UUID(bytes_le=bytes_le)).upper()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001104
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001105 def FormatTypeGUID(p):
1106 guid_str = FormatGUID(p.TypeGUID)
1107 if not args.numeric:
1108 names = gpt.TYPE_GUID_MAP.get(guid_str)
1109 if names:
1110 return names
1111 return guid_str
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001112
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001113 def FormatNames(p):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +08001114 return p.label
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001115
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001116 def IsBootableType(type_guid):
Hung-Te Linfe724f82018-04-18 15:03:58 +08001117 return type_guid in gpt.STR_TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001118
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001119 def FormatAttribute(attrs, chromeos_kernel=False):
1120 if args.numeric:
1121 return '[%x]' % (attrs.raw >> 48)
1122 results = []
1123 if chromeos_kernel:
1124 results += [
1125 'priority=%d' % attrs.priority,
1126 'tries=%d' % attrs.tries,
1127 'successful=%d' % attrs.successful]
1128 if attrs.required:
1129 results += ['required=1']
1130 if attrs.legacy_boot:
1131 results += ['legacy_boot=1']
1132 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001133
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001134 def ApplyFormatArgs(p):
1135 if args.begin:
1136 return p.FirstLBA
1137 elif args.size:
1138 return p.blocks
1139 elif args.type:
1140 return FormatTypeGUID(p)
1141 elif args.unique:
1142 return FormatGUID(p.UniqueGUID)
1143 elif args.label:
1144 return FormatNames(p)
1145 elif args.Successful:
1146 return p.attrs.successful
1147 elif args.Priority:
1148 return p.attrs.priority
1149 elif args.Tries:
1150 return p.attrs.tries
1151 elif args.Legacy:
1152 return p.attrs.legacy_boot
1153 elif args.Attribute:
1154 return '[%x]' % (p.Attributes >> 48)
1155 else:
1156 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001157
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001158 def IsFormatArgsSpecified():
1159 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001160
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001161 gpt = GPT.LoadFromFile(args.image_file)
1162 logging.debug('%r', gpt.header)
1163 fmt = '%12s %11s %7s %s'
1164 fmt2 = '%32s %s: %s'
1165 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001166
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001167 if IsFormatArgsSpecified() and args.number is None:
1168 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001169
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001170 if not (args.number is None or
1171 0 < args.number <= gpt.header.PartitionEntriesNumber):
1172 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001173
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001174 partitions = gpt.partitions
1175 do_print_gpt_blocks = False
1176 if not (args.quick or IsFormatArgsSpecified()):
1177 print(fmt % header)
1178 if args.number is None:
1179 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001180
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001181 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001182 if gpt.pmbr:
1183 print(fmt % (0, 1, '', 'PMBR'))
1184 if gpt.is_secondary:
1185 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1186 else:
1187 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1188 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1189 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001190
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001191 for p in partitions:
1192 if args.number is None:
1193 # Skip unused partitions.
1194 if p.IsUnused():
1195 continue
1196 elif p.number != args.number:
1197 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001198
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001199 if IsFormatArgsSpecified():
1200 print(ApplyFormatArgs(p))
1201 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001202
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001203 type_guid = FormatGUID(p.TypeGUID)
1204 print(fmt % (p.FirstLBA, p.blocks, p.number,
1205 FormatTypeGUID(p) if args.quick else
1206 'Label: "%s"' % FormatNames(p)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001207
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001208 if not args.quick:
1209 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
1210 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
1211 if args.numeric or IsBootableType(type_guid):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001212 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Linfe724f82018-04-18 15:03:58 +08001213 p.attrs, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001214
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001215 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001216 if gpt.is_secondary:
1217 header = gpt.header
1218 else:
1219 f = args.image_file
1220 f.seek(gpt.header.BackupLBA * gpt.block_size)
1221 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001222 print(fmt % (header.PartitionEntriesStartingLBA,
1223 gpt.GetPartitionTableBlocks(header), '',
1224 'Sec GPT table'))
1225 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001226
Hung-Te Linfe724f82018-04-18 15:03:58 +08001227 class Prioritize(SubCommand):
1228 """Reorder the priority of all kernel partitions.
1229
1230 Reorder the priority of all active ChromeOS Kernel partitions.
1231
1232 With no options this will set the lowest active kernel to priority 1 while
1233 maintaining the original order.
1234 """
1235
1236 def DefineArgs(self, parser):
1237 parser.add_argument(
1238 '-P', '--priority', type=int,
1239 help=('Highest priority to use in the new ordering. '
1240 'The other partitions will be ranked in decreasing '
1241 'priority while preserving their original order. '
1242 'If necessary the lowest ranks will be coalesced. '
1243 'No active kernels will be lowered to priority 0.'))
1244 parser.add_argument(
1245 '-i', '--number', type=int,
1246 help='Specify the partition to make the highest in the new order.')
1247 parser.add_argument(
1248 '-f', '--friends', action='store_true',
1249 help=('Friends of the given partition (those with the same '
1250 'starting priority) are also updated to the new '
1251 'highest priority. '))
1252 parser.add_argument(
1253 'image_file', type=argparse.FileType('rb+'),
1254 help='Disk image file to prioritize.')
1255
1256 def Execute(self, args):
1257 gpt = GPT.LoadFromFile(args.image_file)
1258 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
1259 prios = list(set(p.attrs.priority for p in parts if p.attrs.priority))
1260 prios.sort(reverse=True)
1261 groups = [[p for p in parts if p.attrs.priority == priority]
1262 for priority in prios]
1263 if args.number:
1264 p = gpt.partitions[args.number - 1]
1265 if p not in parts:
1266 raise GPTError('%s is not a ChromeOS kernel.' % p)
1267 if args.friends:
1268 group0 = [f for f in parts if f.attrs.priority == p.attrs.priority]
1269 else:
1270 group0 = [p]
1271 groups.insert(0, group0)
1272
1273 # Max priority is 0xf.
1274 highest = min(args.priority or len(prios), 0xf)
1275 logging.info('New highest priority: %s', highest)
1276 done = []
1277
1278 new_priority = highest
1279 for g in groups:
1280 has_new_part = False
1281 for p in g:
1282 if p.number in done:
1283 continue
1284 done.append(p.number)
1285 attrs = p.attrs
1286 old_priority = attrs.priority
1287 assert new_priority > 0, 'Priority must be > 0.'
1288 attrs.priority = new_priority
1289 p = p.Clone(Attributes=attrs)
1290 gpt.partitions[p.number - 1] = p
1291 has_new_part = True
1292 logging.info('%s priority changed from %s to %s.', p, old_priority,
1293 new_priority)
1294 if has_new_part:
1295 new_priority -= 1
1296
1297 gpt.WriteToFile(args.image_file)
1298
Hung-Te Linf641d302018-04-18 15:09:35 +08001299 class Find(SubCommand):
1300 """Locate a partition by its GUID.
1301
1302 Find a partition by its UUID or label. With no specified DRIVE it scans all
1303 physical drives.
1304
1305 The partition type may also be given as one of these aliases:
1306
1307 firmware ChromeOS firmware
1308 kernel ChromeOS kernel
1309 rootfs ChromeOS rootfs
1310 data Linux data
1311 reserved ChromeOS reserved
1312 efi EFI System Partition
1313 unused Unused (nonexistent) partition
1314 """
1315 def DefineArgs(self, parser):
1316 parser.add_argument(
1317 '-t', '--type-guid',
1318 help='Search for Partition Type GUID')
1319 parser.add_argument(
1320 '-u', '--unique-guid',
1321 help='Search for Partition Unique GUID')
1322 parser.add_argument(
1323 '-l', '--label',
1324 help='Search for Label')
1325 parser.add_argument(
1326 '-n', '--numeric', action='store_true',
1327 help='Numeric output only.')
1328 parser.add_argument(
1329 '-1', '--single-match', action='store_true',
1330 help='Fail if more than one match is found.')
1331 parser.add_argument(
1332 '-M', '--match-file', type=str,
1333 help='Matching partition data must also contain MATCH_FILE content.')
1334 parser.add_argument(
1335 '-O', '--offset', type=int, default=0,
1336 help='Byte offset into partition to match content (default 0).')
1337 parser.add_argument(
1338 'drive', type=argparse.FileType('rb+'), nargs='?',
1339 help='Drive or disk image file to find.')
1340
1341 def Execute(self, args):
1342 if not any((args.type_guid, args.unique_guid, args.label)):
1343 raise GPTError('You must specify at least one of -t, -u, or -l')
1344
1345 drives = [args.drive.name] if args.drive else (
1346 '/dev/%s' % name for name in subprocess.check_output(
1347 'lsblk -d -n -r -o name', shell=True).split())
1348
1349 match_pattern = None
1350 if args.match_file:
1351 with open(args.match_file) as f:
1352 match_pattern = f.read()
1353
1354 found = 0
1355 for drive in drives:
1356 try:
1357 gpt = GPT.LoadFromFile(drive)
1358 except GPTError:
1359 if args.drive:
1360 raise
1361 # When scanning all block devices on system, ignore failure.
1362
1363 for p in gpt.partitions:
1364 if p.IsUnused():
1365 continue
1366 if args.label is not None and args.label != p.label:
1367 continue
1368 if args.unique_guid is not None and (
1369 uuid.UUID(args.unique_guid) != uuid.UUID(bytes_le=p.UniqueGUID)):
1370 continue
1371 type_guid = gpt.GetTypeGUID(args.type_guid)
1372 if args.type_guid is not None and (
1373 type_guid != uuid.UUID(bytes_le=p.TypeGUID)):
1374 continue
1375 if match_pattern:
1376 with open(drive, 'rb') as f:
1377 f.seek(p.offset + args.offset)
1378 if f.read(len(match_pattern)) != match_pattern:
1379 continue
1380 # Found the partition, now print.
1381 found += 1
1382 if args.numeric:
1383 print(p.number)
1384 else:
1385 # This is actually more for block devices.
1386 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1387 p.number))
1388
1389 if found < 1 or (args.single_match and found > 1):
1390 return 1
1391 return 0
1392
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001393
1394def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001395 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001396 parser = argparse.ArgumentParser(description='GPT Utility.')
1397 parser.add_argument('--verbose', '-v', action='count', default=0,
1398 help='increase verbosity.')
1399 parser.add_argument('--debug', '-d', action='store_true',
1400 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001401 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001402
1403 args = parser.parse_args()
1404 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1405 if args.debug:
1406 log_level = logging.DEBUG
1407 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1408 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001409 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001410 code = commands.Execute(args)
1411 if type(code) is int:
1412 sys.exit(code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001413 except Exception as e:
1414 if args.verbose or args.debug:
1415 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001416 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001417
1418
1419if __name__ == '__main__':
1420 main()