blob: a69d63a498303e87e5627d8ac3a64b02edd21d96 [file] [log] [blame]
Mike Frysinger63bb3c72019-09-01 15:16:26 -04001#!/usr/bin/env python2
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
Hung-Te Lin138389f2018-05-15 17:55:00 +080033import itertools
Hung-Te Linc772e1a2017-04-14 16:50:50 +080034import logging
35import os
Hung-Te Lin446eb512018-05-02 18:39:16 +080036import stat
Hung-Te Linc772e1a2017-04-14 16:50:50 +080037import struct
Hung-Te Linf641d302018-04-18 15:09:35 +080038import subprocess
39import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080040import uuid
41
42
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080043class StructError(Exception):
44 """Exceptions in packing and unpacking from/to struct fields."""
45 pass
Hung-Te Linc772e1a2017-04-14 16:50:50 +080046
Hung-Te Linc772e1a2017-04-14 16:50:50 +080047
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080048class StructField(object):
49 """Definition of a field in struct.
50
51 Attributes:
52 fmt: a format string for struct.{pack,unpack} to use.
53 name: a string for name of processed field.
54 """
55 __slots__ = ['fmt', 'name']
56
57 def __init__(self, fmt, name):
58 self.fmt = fmt
59 self.name = name
60
61 def Pack(self, value):
62 """"Packs given value from given format."""
63 del self # Unused.
64 return value
65
66 def Unpack(self, value):
67 """Unpacks given value into given format."""
68 del self # Unused.
69 return value
70
71
72class UTF16StructField(StructField):
73 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080074 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080075 encoding = 'utf-16-le'
76
77 def __init__(self, max_length, name):
78 self.max_length = max_length
79 fmt = '%ds' % max_length
80 super(UTF16StructField, self).__init__(fmt, name)
81
82 def Pack(self, value):
83 new_value = value.encode(self.encoding)
84 if len(new_value) >= self.max_length:
85 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
86 (value, self.name, self.max_length))
87 return new_value
88
89 def Unpack(self, value):
90 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080091
Hung-Te Linbf8aa272018-04-19 03:02:29 +080092
93class GUID(uuid.UUID):
94 """A special UUID that defaults to upper case in str()."""
95
96 def __str__(self):
97 """Returns GUID in upper case."""
98 return super(GUID, self).__str__().upper()
99
100 @staticmethod
101 def Random():
102 return uuid.uuid4()
103
104
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800105class GUIDStructField(StructField):
106 """A GUID field."""
107
108 def __init__(self, name):
109 super(GUIDStructField, self).__init__('16s', name)
110
111 def Pack(self, value):
112 if value is None:
113 return '\x00' * 16
114 if not isinstance(value, uuid.UUID):
115 raise StructError('Field %s needs a GUID value instead of [%r].' %
116 (self.name, value))
117 return value.bytes_le
118
119 def Unpack(self, value):
120 return GUID(bytes_le=value)
121
122
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800123def BitProperty(getter, setter, shift, mask):
124 """A generator for bit-field properties.
125
126 This is used inside a class to manipulate an integer-like variable using
127 properties. The getter and setter should be member functions to change the
128 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800129
130 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800131 getter: a function to read integer type variable (for all the bits).
132 setter: a function to set the new changed integer type variable.
133 shift: integer for how many bits should be shifted (right).
134 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800135 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800136 def _getter(self):
137 return (getter(self) >> shift) & mask
138 def _setter(self, value):
139 assert value & mask == value, (
140 'Value %s out of range (mask=%s)' % (value, mask))
141 setter(self, getter(self) & ~(mask << shift) | value << shift)
142 return property(_getter, _setter)
143
144
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800145class PartitionAttributes(object):
146 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800147
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800148 This can be created using Partition.attrs, but the changed properties won't
149 apply to underlying Partition until an explicit call with
150 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800151 """
152
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800153 def __init__(self, attrs):
154 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800155
156 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800157 def raw(self):
158 """Returns the raw integer type attributes."""
159 return self._Get()
160
161 def _Get(self):
162 return self._attrs
163
164 def _Set(self, value):
165 self._attrs = value
166
167 successful = BitProperty(_Get, _Set, 56, 1)
168 tries = BitProperty(_Get, _Set, 52, 0xf)
169 priority = BitProperty(_Get, _Set, 48, 0xf)
170 legacy_boot = BitProperty(_Get, _Set, 2, 1)
171 required = BitProperty(_Get, _Set, 0, 1)
172 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
173
174
175class PartitionAttributeStructField(StructField):
176
177 def Pack(self, value):
178 if not isinstance(value, PartitionAttributes):
179 raise StructError('Given value %r is not %s.' %
180 (value, PartitionAttributes.__name__))
181 return value.raw
182
183 def Unpack(self, value):
184 return PartitionAttributes(value)
185
186
187# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
188# declared as 'signed' (l) instead of 'unsigned' (L).
189# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
190HEADER_FIELDS = [
191 StructField('8s', 'Signature'),
192 StructField('4s', 'Revision'),
193 StructField('L', 'HeaderSize'),
194 StructField('l', 'CRC32'),
195 StructField('4s', 'Reserved'),
196 StructField('Q', 'CurrentLBA'),
197 StructField('Q', 'BackupLBA'),
198 StructField('Q', 'FirstUsableLBA'),
199 StructField('Q', 'LastUsableLBA'),
200 GUIDStructField('DiskGUID'),
201 StructField('Q', 'PartitionEntriesStartingLBA'),
202 StructField('L', 'PartitionEntriesNumber'),
203 StructField('L', 'PartitionEntrySize'),
204 StructField('l', 'PartitionArrayCRC32'),
205]
206
207# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
208PARTITION_FIELDS = [
209 GUIDStructField('TypeGUID'),
210 GUIDStructField('UniqueGUID'),
211 StructField('Q', 'FirstLBA'),
212 StructField('Q', 'LastLBA'),
213 PartitionAttributeStructField('Q', 'Attributes'),
214 UTF16StructField(72, 'Names'),
215]
216
217# The PMBR has so many variants. The basic format is defined in
218# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
219# implementation, as derived from `cgpt`, is following syslinux as:
220# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
221PMBR_FIELDS = [
222 StructField('424s', 'BootCode'),
223 GUIDStructField('BootGUID'),
224 StructField('L', 'DiskID'),
225 StructField('2s', 'Magic'),
226 StructField('16s', 'LegacyPart0'),
227 StructField('16s', 'LegacyPart1'),
228 StructField('16s', 'LegacyPart2'),
229 StructField('16s', 'LegacyPart3'),
230 StructField('2s', 'Signature'),
231]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800232
233
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800234class GPTError(Exception):
235 """All exceptions by GPT."""
236 pass
237
238
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800239class GPTObject(object):
240 """A base object in GUID Partition Table.
241
242 All objects (for instance, header or partition entries) must inherit this
243 class and define the FIELD attribute with a list of field definitions using
244 StructField.
245
246 The 'name' in StructField will become the attribute name of GPT objects that
247 can be directly packed into / unpacked from. Derived (calculated from existing
248 attributes) attributes should be in lower_case.
249
250 It is also possible to attach some additional properties to the object as meta
251 data (for example path of the underlying image file). To do that, first
252 include it in __slots__ list and specify them as dictionary-type args in
253 constructors. These properties will be preserved when you call Clone().
254
255 To create a new object, call the constructor. Field data can be assigned as
256 in arguments, or give nothing to initialize as zero (see Zero()). Field data
257 and meta values can be also specified in keyword arguments (**kargs) at the
258 same time.
259
260 To read a object from file or stream, use class method ReadFrom(source).
261 To make changes, modify the field directly or use Update(dict), or create a
262 copy by Clone() first then Update.
263
264 To wipe all fields (but not meta), call Zero(). There is currently no way
265 to clear meta except setting them to None one by one.
266 """
267 __slots__ = []
268
Peter Shih533566a2018-09-05 17:48:03 +0800269 FIELDS = []
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800270 """A list of StructField definitions."""
271
272 def __init__(self, *args, **kargs):
273 if args:
274 if len(args) != len(self.FIELDS):
275 raise GPTError('%s need %s arguments (found %s).' %
276 (type(self).__name__, len(self.FIELDS), len(args)))
277 for f, value in zip(self.FIELDS, args):
278 setattr(self, f.name, value)
279 else:
280 self.Zero()
281
282 all_names = [f for f in self.__slots__]
283 for name, value in kargs.iteritems():
284 if name not in all_names:
285 raise GPTError('%s does not support keyword arg <%s>.' %
286 (type(self).__name__, name))
287 setattr(self, name, value)
288
289 def __iter__(self):
290 """An iterator to return all fields associated in the object."""
291 return (getattr(self, f.name) for f in self.FIELDS)
292
293 def __repr__(self):
294 return '(%s: %s)' % (type(self).__name__, ', '.join(
Peter Shihe6afab32018-09-11 17:16:48 +0800295 '%s=%r' % (f, getattr(self, f)) for f in self.__slots__))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800296
297 @classmethod
298 def GetStructFormat(cls):
299 """Returns a format string for struct to use."""
300 return '<' + ''.join(f.fmt for f in cls.FIELDS)
301
302 @classmethod
303 def ReadFrom(cls, source, **kargs):
304 """Returns an object from given source."""
305 obj = cls(**kargs)
306 obj.Unpack(source)
307 return obj
308
309 @property
310 def blob(self):
311 """The (packed) blob representation of the object."""
312 return self.Pack()
313
314 @property
315 def meta(self):
316 """Meta values (those not in GPT object fields)."""
317 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
318 return dict((name, getattr(self, name)) for name in metas)
319
320 def Unpack(self, source):
321 """Unpacks values from a given source.
322
323 Args:
324 source: a string of bytes or a file-like object to read from.
325 """
326 fmt = self.GetStructFormat()
327 if source is None:
328 source = '\x00' * struct.calcsize(fmt)
329 if not isinstance(source, basestring):
330 return self.Unpack(source.read(struct.calcsize(fmt)))
331 for f, value in zip(self.FIELDS, struct.unpack(fmt, source)):
332 setattr(self, f.name, f.Unpack(value))
333
334 def Pack(self):
335 """Packs values in all fields into a string by struct format."""
336 return struct.pack(self.GetStructFormat(),
337 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
338
339 def Clone(self):
340 """Clones a new instance."""
341 return type(self)(*self, **self.meta)
342
343 def Update(self, **dargs):
344 """Applies multiple values in current object."""
345 for name, value in dargs.iteritems():
346 setattr(self, name, value)
347
348 def Zero(self):
349 """Set all fields to values representing zero or empty.
350
351 Note the meta attributes won't be cleared.
352 """
353 class ZeroReader(object):
354 """A /dev/zero like stream."""
355
356 @staticmethod
357 def read(num):
358 return '\x00' * num
359
360 self.Unpack(ZeroReader())
361
362
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800363class GPT(object):
364 """A GPT helper class.
365
366 To load GPT from an existing disk image file, use `LoadFromFile`.
367 After modifications were made, use `WriteToFile` to commit changes.
368
369 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800370 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800371 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800372 partitions: a list of GPT partition entry nametuple.
373 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800374 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800375 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800376 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800377 # Old devices uses 'Basic data' type for stateful partition, and newer devices
378 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
379 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800380 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800381 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800382 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
383 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800384 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
385 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
386 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
387 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
388 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800389 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800390 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800391 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
392 for k, v in TYPE_GUID_MAP.iteritems())
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800393 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
394 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
395 TYPE_GUID_LIST_BOOTABLE = [
396 TYPE_GUID_CHROMEOS_KERNEL,
397 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800398 ]
399
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800400 class ProtectiveMBR(GPTObject):
401 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800402 FIELDS = PMBR_FIELDS
403 __slots__ = [f.name for f in FIELDS]
404
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800405 SIGNATURE = '\x55\xAA'
406 MAGIC = '\x1d\x9a'
407
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800408 class Header(GPTObject):
409 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800410 FIELDS = HEADER_FIELDS
411 __slots__ = [f.name for f in FIELDS]
412
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800413 SIGNATURES = ['EFI PART', 'CHROMEOS']
414 SIGNATURE_IGNORE = 'IGNOREME'
415 DEFAULT_REVISION = '\x00\x00\x01\x00'
416
417 DEFAULT_PARTITION_ENTRIES = 128
418 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
419
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800420 @classmethod
421 def Create(cls, size, block_size, pad_blocks=0,
422 part_entries=DEFAULT_PARTITION_ENTRIES):
423 """Creates a header with default values.
424
425 Args:
426 size: integer of expected image size.
427 block_size: integer for size of each block (sector).
428 pad_blocks: number of preserved sectors between header and partitions.
429 part_entries: number of partitions to include in header.
430 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800431 PART_FORMAT = GPT.Partition.GetStructFormat()
432 FORMAT = cls.GetStructFormat()
433 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800434 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
435 parts_bytes = part_entries * part_entry_size
436 parts_blocks = parts_bytes / block_size
437 if parts_bytes % block_size:
438 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800439 # CRC32 and PartitionsCRC32 must be updated later explicitly.
440 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800441 Signature=cls.SIGNATURES[0],
442 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800443 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800444 CurrentLBA=1,
445 BackupLBA=size / block_size - 1,
446 FirstUsableLBA=parts_lba + parts_blocks,
447 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800448 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800449 PartitionEntriesStartingLBA=parts_lba,
450 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800451 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800452
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800453 def UpdateChecksum(self):
454 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800455
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800456 Note the PartitionArrayCRC32 is not touched - you have to make sure that
457 is correct before calling Header.UpdateChecksum().
458 """
459 self.Update(CRC32=0)
460 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800461
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800462 class Partition(GPTObject):
463 """The partition entry in GPT.
464
465 Please include following properties when creating a Partition object:
466 - image: a string for path to the image file the partition maps to.
467 - number: the 1-based partition number.
468 - block_size: an integer for size of each block (LBA, or sector).
469 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800470 FIELDS = PARTITION_FIELDS
471 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800472 NAMES_ENCODING = 'utf-16-le'
473 NAMES_LENGTH = 72
474
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800475 def __str__(self):
476 return '%s#%s' % (self.image, self.number)
477
478 def IsUnused(self):
479 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800480 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800481
Hung-Te Linfe724f82018-04-18 15:03:58 +0800482 def IsChromeOSKernel(self):
483 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800484 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800485
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800486 @property
487 def blocks(self):
488 """Return size of partition in blocks (see block_size)."""
489 return self.LastLBA - self.FirstLBA + 1
490
491 @property
492 def offset(self):
493 """Returns offset to partition in bytes."""
494 return self.FirstLBA * self.block_size
495
496 @property
497 def size(self):
498 """Returns size of partition in bytes."""
499 return self.blocks * self.block_size
500
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800501 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800502 """GPT constructor.
503
504 See LoadFromFile for how it's usually used.
505 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800506 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800507 self.header = None
508 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800509 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800510 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800511
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800512 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800513 def GetTypeGUID(cls, value):
514 """The value may be a GUID in string or a short type string."""
515 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
516 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800517
518 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800519 def Create(cls, image_name, size, block_size, pad_blocks=0):
520 """Creates a new GPT instance from given size and block_size.
521
522 Args:
523 image_name: a string of underlying disk image file name.
524 size: expected size of disk image.
525 block_size: size of each block (sector) in bytes.
526 pad_blocks: number of blocks between header and partitions array.
527 """
528 gpt = cls()
529 gpt.block_size = block_size
530 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
531 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800532 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800533 for i in xrange(gpt.header.PartitionEntriesNumber)]
534 return gpt
535
Hung-Te Lin446eb512018-05-02 18:39:16 +0800536 @staticmethod
537 def IsBlockDevice(image):
538 """Returns if the image is a block device file."""
539 return stat.S_ISBLK(os.stat(image).st_mode)
540
541 @classmethod
542 def GetImageSize(cls, image):
543 """Returns the size of specified image (plain or block device file)."""
544 if not cls.IsBlockDevice(image):
545 return os.path.getsize(image)
546
547 fd = os.open(image, os.O_RDONLY)
548 try:
549 return os.lseek(fd, 0, os.SEEK_END)
550 finally:
551 os.close(fd)
552
553 @classmethod
554 def GetLogicalBlockSize(cls, block_dev):
555 """Returns the logical block (sector) size from a block device file.
556
557 The underlying call is BLKSSZGET. An alternative command is blockdev,
558 but that needs root permission even if we just want to get sector size.
559 """
560 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
561 return int(subprocess.check_output(
562 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
563
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800564 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800565 def LoadFromFile(cls, image):
566 """Loads a GPT table from give disk image file object.
567
568 Args:
569 image: a string as file path or a file-like object to read from.
570 """
571 if isinstance(image, basestring):
572 with open(image, 'rb') as f:
573 return cls.LoadFromFile(f)
574
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800575 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800576 image.seek(0)
577 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
578 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
579 logging.debug('Found MBR signature in %s', image.name)
580 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
581 logging.debug('Found PMBR in %s', image.name)
582 gpt.pmbr = pmbr
583
Hung-Te Linf148d322018-04-13 10:24:42 +0800584 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800585 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
586 if cls.IsBlockDevice(image.name):
587 block_sizes = [cls.GetLogicalBlockSize(image.name)]
588
589 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800590 # Note because there are devices setting Primary as ignored and the
591 # partition table signature accepts 'CHROMEOS' which is also used by
592 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
593 # first before trying other block sizes, otherwise we may incorrectly
594 # identify a kernel partition as LBA 1 of larger block size system.
595 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
596 (-block_size, os.SEEK_END)]):
597 image.seek(*seek)
598 header = gpt.Header.ReadFrom(image)
599 if header.Signature in cls.Header.SIGNATURES:
600 gpt.block_size = block_size
601 if i != 0:
602 gpt.is_secondary = True
603 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800604 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800605 else:
606 # Nothing found, try next block size.
607 continue
608 # Found a valid signature.
609 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800610 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800611 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800612
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800613 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800614 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800615 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800616 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800617 return p
618
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800619 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800620 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800621 ReadPartition(image, i + 1)
622 for i in xrange(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800623 return gpt
624
Hung-Te Linc5196682018-04-18 22:59:59 +0800625 def GetUsedPartitions(self):
626 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800627
Hung-Te Linc5196682018-04-18 22:59:59 +0800628 Use 'number' property to find the real location of partition in
629 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800630 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800631 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800632
633 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800634 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800635 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800636 return (max(p.LastLBA for p in parts)
637 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800638
639 def GetPartitionTableBlocks(self, header=None):
640 """Returns the blocks (or LBA) of partition table from given header."""
641 if header is None:
642 header = self.header
643 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800644 blocks = size / self.block_size
645 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800646 blocks += 1
647 return blocks
648
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800649 def GetPartition(self, number):
650 """Gets the Partition by given (1-based) partition number.
651
652 Args:
653 number: an integer as 1-based partition number.
654 """
655 if not 0 < number <= len(self.partitions):
656 raise GPTError('Invalid partition number %s.' % number)
657 return self.partitions[number - 1]
658
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800659 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800660 """Updates the entry in partition table by given Partition object.
661
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800662 Usually you only need to call this if you want to copy one partition to
663 different location (number of image).
664
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800665 Args:
666 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800667 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800668 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800669 ref = self.partitions[number - 1]
670 part = part.Clone()
671 part.number = number
672 part.image = ref.image
673 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800674 self.partitions[number - 1] = part
675
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800676 def GetSize(self):
677 return self.block_size * (self.header.BackupLBA + 1)
678
679 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800680 """Adjust GPT for a disk image in given size.
681
682 Args:
683 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800684 check_overlap: Checks if the backup partition table overlaps used
685 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800686 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800687 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800688 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800689 raise GPTError(
690 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800691 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800692 if old_size != new_size:
693 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800694 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800695 else:
696 logging.info('Image size (%d, LBA=%d) not changed.',
697 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800698 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800699
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800700 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800701 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800702 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800703
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800704 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800705 max_used_lba = self.GetMaxUsedLBA()
706 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800707 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800708
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800709 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800710
711 def GetFreeSpace(self):
712 """Returns the free (available) space left according to LastUsableLBA."""
713 max_lba = self.GetMaxUsedLBA()
714 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800715 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800716
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800717 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800718 """Expands a given partition to last usable LBA.
719
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800720 The size of the partition can actually be reduced if the last usable LBA
721 decreases.
722
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800723 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800724 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800725
726 Returns:
727 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800728 """
729 # Assume no partitions overlap, we need to make sure partition[i] has
730 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800731 p = self.GetPartition(number)
732 if p.IsUnused():
733 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800734 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800735 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800736 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800737 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800738 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800739
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800740 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800741 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800742 new_blocks = p.blocks
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800743 logging.warn(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800744 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800745 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800746
Hung-Te Lin3b491672018-04-19 01:41:20 +0800747 def CheckIntegrity(self):
748 """Checks if the GPT objects all look good."""
749 # Check if the header allocation looks good. CurrentLBA and
750 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
751 # LastUsableLBA].
752 header = self.header
753 entries_first_lba = header.PartitionEntriesStartingLBA
754 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
755
756 def CheckOutsideUsable(name, lba, outside_entries=False):
757 if lba < 1:
758 raise GPTError('%s should not live in LBA %s.' % (name, lba))
759 if lba > max(header.BackupLBA, header.CurrentLBA):
760 # Note this is "in theory" possible, but we want to report this as
761 # error as well, since it usually leads to error.
762 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
763 (name, lba, header.BackupLBA))
764 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
765 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
766 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
767 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
768 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
769 (name, lba, entries_first_lba, entries_last_lba))
770 CheckOutsideUsable('Header', header.CurrentLBA, True)
771 CheckOutsideUsable('Backup header', header.BackupLBA, True)
772 CheckOutsideUsable('Partition entries', entries_first_lba)
773 CheckOutsideUsable('Partition entries end', entries_last_lba)
774
775 parts = self.GetUsedPartitions()
776 # Check if partition entries overlap with each other.
777 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
778 lba_list.sort(key=lambda t: t[0])
779 for i in xrange(len(lba_list) - 1):
780 if lba_list[i][1] >= lba_list[i + 1][0]:
781 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
782 (lba_list[i] + lba_list[i + 1]))
783 # Now, check the first and last partition.
784 if lba_list:
785 p = lba_list[0][2]
786 if p.FirstLBA < header.FirstUsableLBA:
787 raise GPTError(
788 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
789 (p, p.FirstLBA, header.FirstLBA))
790 p = lba_list[-1][2]
791 if p.LastLBA > header.LastUsableLBA:
792 raise GPTError(
793 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
794 (p, p.LastLBA, header.LastLBA))
795 # Check if UniqueGUIDs are not unique.
796 if len(set(p.UniqueGUID for p in parts)) != len(parts):
797 raise GPTError('Partition UniqueGUIDs are duplicated.')
798 # Check if CRCs match.
799 if (binascii.crc32(''.join(p.blob for p in self.partitions)) !=
800 header.PartitionArrayCRC32):
801 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800802 header_crc = header.Clone()
803 header_crc.UpdateChecksum()
804 if header_crc.CRC32 != header.CRC32:
805 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800806
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800807 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800808 """Updates all checksum fields in GPT objects."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800809 parts = ''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800810 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
811 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800812
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800813 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800814 """Returns the backup header according to given header.
815
816 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
817 fields.
818 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800819 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800820 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800821 h = header.Clone()
822 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800823 BackupLBA=header.CurrentLBA,
824 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800825 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800826 h.UpdateChecksum()
827 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800828
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800829 @classmethod
830 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
831 """Writes a protective MBR to given file.
832
833 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
834 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
835 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
836 we can follow that.
837
838 Args:
839 create: True to re-create PMBR structure.
840 bootcode: a blob of new boot code.
841 boot_guid a blob for new boot GUID.
842
843 Returns:
844 The written PMBR structure.
845 """
846 if isinstance(image, basestring):
847 with open(image, 'rb+') as f:
848 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
849
850 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800851 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
852 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800853 pmbr = cls.ProtectiveMBR.ReadFrom(image)
854
855 if create:
856 legacy_sectors = min(
857 0x100000000,
Hung-Te Lin446eb512018-05-02 18:39:16 +0800858 GPT.GetImageSize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800859 # Partition 0 must have have the fixed CHS with number of sectors
860 # (calculated as legacy_sectors later).
861 part0 = ('00000200eeffffff01000000'.decode('hex') +
862 struct.pack('<I', legacy_sectors))
863 # Partition 1~3 should be all zero.
864 part1 = '\x00' * 16
865 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800866 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800867 BootGUID=cls.TYPE_GUID_UNUSED,
868 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800869 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800870 LegacyPart0=part0,
871 LegacyPart1=part1,
872 LegacyPart2=part1,
873 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800874 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800875
876 if bootcode:
877 if len(bootcode) > len(pmbr.BootCode):
878 logging.info(
879 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
880 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800881 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800882 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800883 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800884
885 blob = pmbr.blob
886 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
887 image.seek(0)
888 image.write(blob)
889 return pmbr
890
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800891 def WriteToFile(self, image):
892 """Updates partition table in a disk image file.
893
894 Args:
895 image: a string as file path or a file-like object to write into.
896 """
897 if isinstance(image, basestring):
898 with open(image, 'rb+') as f:
899 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800900
901 def WriteData(name, blob, lba):
902 """Writes a blob into given location."""
903 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800904 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800905 image.seek(lba * self.block_size)
906 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800907
908 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800909 self.CheckIntegrity()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800910 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800911
912 header = self.header
913 WriteData('GPT Header', header.blob, header.CurrentLBA)
914 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
915 logging.info(
916 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
917 header.LastUsableLBA)
918
919 if not self.is_secondary:
920 # When is_secondary is True, the header we have is actually backup header.
921 backup_header = self.GetBackupHeader(self.header)
922 WriteData(
923 'Backup Partitions', parts_blob,
924 backup_header.PartitionEntriesStartingLBA)
925 WriteData(
926 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800927
928
929class GPTCommands(object):
930 """Collection of GPT sub commands for command line to use.
931
932 The commands are derived from `cgpt`, but not necessary to be 100% compatible
933 with cgpt.
934 """
935
936 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800937 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800938 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800939 ('type', 'type guid'),
940 ('unique', 'unique guid'),
941 ('label', 'label'),
942 ('Successful', 'Successful flag'),
943 ('Tries', 'Tries flag'),
944 ('Priority', 'Priority flag'),
945 ('Legacy', 'Legacy Boot flag'),
946 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800947
948 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800949 commands = dict(
950 (command.lower(), getattr(self, command)())
951 for command in dir(self)
952 if (isinstance(getattr(self, command), type) and
953 issubclass(getattr(self, command), self.SubCommand) and
954 getattr(self, command) is not self.SubCommand)
955 )
956 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800957
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800958 def DefineArgs(self, parser):
959 """Defines all available commands to an argparser subparsers instance."""
960 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
961 for name, instance in sorted(self.commands.iteritems()):
962 parser = subparsers.add_parser(
963 name, description=instance.__doc__,
964 formatter_class=argparse.RawDescriptionHelpFormatter,
965 help=instance.__doc__.splitlines()[0])
966 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800967
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800968 def Execute(self, args):
969 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800970 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800971
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800972 class SubCommand(object):
973 """A base class for sub commands to derive from."""
974
975 def DefineArgs(self, parser):
976 """Defines command line arguments to argparse parser.
977
978 Args:
979 parser: An argparse parser instance.
980 """
981 del parser # Unused.
982 raise NotImplementedError
983
984 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800985 """Execute the command with parsed arguments.
986
987 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800988
989 Args:
990 args: An argparse parsed namespace.
991 """
992 del args # Unused.
993 raise NotImplementedError
994
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800995 def ExecuteCommandLine(self, *args):
996 """Execute as invoked from command line.
997
998 This provides an easy way to execute particular sub command without
999 creating argument parser explicitly.
1000
1001 Args:
1002 args: a list of string type command line arguments.
1003 """
1004 parser = argparse.ArgumentParser()
1005 self.DefineArgs(parser)
1006 return self.Execute(parser.parse_args(args))
1007
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001008 class Create(SubCommand):
1009 """Create or reset GPT headers and tables.
1010
1011 Create or reset an empty GPT.
1012 """
1013
1014 def DefineArgs(self, parser):
1015 parser.add_argument(
1016 '-z', '--zero', action='store_true',
1017 help='Zero the sectors of the GPT table and entries')
1018 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001019 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001020 help=('Size (in blocks) of the disk to pad between the '
1021 'primary GPT header and its entries, default %(default)s'))
1022 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001023 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001024 help='Size of each block (sector) in bytes.')
1025 parser.add_argument(
1026 'image_file', type=argparse.FileType('rb+'),
1027 help='Disk image file to create.')
1028
1029 def Execute(self, args):
1030 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001031 if block_size is None:
1032 if GPT.IsBlockDevice(args.image_file.name):
1033 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1034 else:
1035 block_size = GPT.DEFAULT_BLOCK_SIZE
1036
1037 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1038 logging.info('Block (sector) size for %s is set to %s bytes.',
1039 args.image_file.name, block_size)
1040
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001041 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001042 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001043 block_size, args.pad_blocks)
1044 if args.zero:
1045 # In theory we only need to clear LBA 1, but to make sure images already
1046 # initialized with different block size won't have GPT signature in
1047 # different locations, we should zero until first usable LBA.
1048 args.image_file.seek(0)
1049 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
1050 gpt.WriteToFile(args.image_file)
Hung-Te Linbad46112018-05-15 16:39:14 +08001051 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001052
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001053 class Boot(SubCommand):
1054 """Edit the PMBR sector for legacy BIOSes.
1055
1056 With no options, it will just print the PMBR boot guid.
1057 """
1058
1059 def DefineArgs(self, parser):
1060 parser.add_argument(
1061 '-i', '--number', type=int,
1062 help='Set bootable partition')
1063 parser.add_argument(
1064 '-b', '--bootloader', type=argparse.FileType('r'),
1065 help='Install bootloader code in the PMBR')
1066 parser.add_argument(
1067 '-p', '--pmbr', action='store_true',
1068 help='Create legacy PMBR partition table')
1069 parser.add_argument(
1070 'image_file', type=argparse.FileType('rb+'),
1071 help='Disk image file to change PMBR.')
1072
1073 def Execute(self, args):
1074 """Rebuilds the protective MBR."""
1075 bootcode = args.bootloader.read() if args.bootloader else None
1076 boot_guid = None
1077 if args.number is not None:
1078 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001079 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001080 pmbr = GPT.WriteProtectiveMBR(
1081 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1082
You-Cheng Syufff7f422018-05-14 15:37:39 +08001083 print(pmbr.BootGUID)
Hung-Te Linbad46112018-05-15 16:39:14 +08001084 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001085
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001086 class Legacy(SubCommand):
1087 """Switch between GPT and Legacy GPT.
1088
1089 Switch GPT header signature to "CHROMEOS".
1090 """
1091
1092 def DefineArgs(self, parser):
1093 parser.add_argument(
1094 '-e', '--efi', action='store_true',
1095 help='Switch GPT header signature back to "EFI PART"')
1096 parser.add_argument(
1097 '-p', '--primary-ignore', action='store_true',
1098 help='Switch primary GPT header signature to "IGNOREME"')
1099 parser.add_argument(
1100 'image_file', type=argparse.FileType('rb+'),
1101 help='Disk image file to change.')
1102
1103 def Execute(self, args):
1104 gpt = GPT.LoadFromFile(args.image_file)
1105 # cgpt behavior: if -p is specified, -e is ignored.
1106 if args.primary_ignore:
1107 if gpt.is_secondary:
1108 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1109 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1110 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1111 gpt.header = gpt.GetBackupHeader(self.header)
1112 gpt.is_secondary = True
1113 else:
1114 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001115 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001116 gpt.WriteToFile(args.image_file)
1117 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001118 return ('Set %s primary GPT header to %s.' %
1119 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001120 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001121 return ('Changed GPT signature for %s to %s.' %
1122 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001123
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001124 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001125 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001126
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001127 def DefineArgs(self, parser):
1128 parser.add_argument(
1129 'image_file', type=argparse.FileType('rb+'),
1130 help='Disk image file to repair.')
1131
1132 def Execute(self, args):
1133 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001134 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001135 gpt.WriteToFile(args.image_file)
Hung-Te Linbad46112018-05-15 16:39:14 +08001136 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001137
1138 class Expand(SubCommand):
1139 """Expands a GPT partition to all available free space."""
1140
1141 def DefineArgs(self, parser):
1142 parser.add_argument(
1143 '-i', '--number', type=int, required=True,
1144 help='The partition to expand.')
1145 parser.add_argument(
1146 'image_file', type=argparse.FileType('rb+'),
1147 help='Disk image file to modify.')
1148
1149 def Execute(self, args):
1150 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001151 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001152 gpt.WriteToFile(args.image_file)
1153 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001154 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001155 'Partition %s on disk image file %s has been extended '
1156 'from %s to %s .' %
1157 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1158 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001159 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001160 return ('Nothing to expand for disk image %s partition %s.' %
1161 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001162
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001163 class Add(SubCommand):
1164 """Add, edit, or remove a partition entry.
1165
1166 Use the -i option to modify an existing partition.
1167 The -b, -s, and -t options must be given for new partitions.
1168
1169 The partition type may also be given as one of these aliases:
1170
1171 firmware ChromeOS firmware
1172 kernel ChromeOS kernel
1173 rootfs ChromeOS rootfs
1174 data Linux data
1175 reserved ChromeOS reserved
1176 efi EFI System Partition
1177 unused Unused (nonexistent) partition
1178 """
1179 def DefineArgs(self, parser):
1180 parser.add_argument(
1181 '-i', '--number', type=int,
1182 help='Specify partition (default is next available)')
1183 parser.add_argument(
1184 '-b', '--begin', type=int,
1185 help='Beginning sector')
1186 parser.add_argument(
1187 '-s', '--sectors', type=int,
1188 help='Size in sectors (logical blocks).')
1189 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001190 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001191 help='Partition Type GUID')
1192 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001193 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001194 help='Partition Unique ID')
1195 parser.add_argument(
1196 '-l', '--label',
1197 help='Label')
1198 parser.add_argument(
1199 '-S', '--successful', type=int, choices=xrange(2),
1200 help='set Successful flag')
1201 parser.add_argument(
1202 '-T', '--tries', type=int,
1203 help='set Tries flag (0-15)')
1204 parser.add_argument(
1205 '-P', '--priority', type=int,
1206 help='set Priority flag (0-15)')
1207 parser.add_argument(
1208 '-R', '--required', type=int, choices=xrange(2),
1209 help='set Required flag')
1210 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001211 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001212 choices=xrange(2),
1213 help='set Legacy Boot flag')
1214 parser.add_argument(
1215 '-A', '--attribute', dest='raw_16', type=int,
1216 help='set raw 16-bit attribute value (bits 48-63)')
1217 parser.add_argument(
1218 'image_file', type=argparse.FileType('rb+'),
1219 help='Disk image file to modify.')
1220
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001221 def Execute(self, args):
1222 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001223 number = args.number
1224 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001225 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001226
1227 # First and last LBA must be calculated explicitly because the given
1228 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001229 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001230 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001231
1232 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001233 part.Zero()
1234 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001235 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001236 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001237 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001238 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001239
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001240 def UpdateAttr(name):
1241 value = getattr(args, name)
1242 if value is None:
1243 return
1244 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001245
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001246 def GetArg(arg_value, default_value):
1247 return default_value if arg_value is None else arg_value
1248
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001249 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001250 for name in ['legacy_boot', 'required', 'priority', 'tries',
1251 'successful', 'raw_16']:
1252 UpdateAttr(name)
1253 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001254 part.Update(
1255 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001256 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001257 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1258 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1259 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1260 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001261
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001262 # Wipe partition again if it should be empty.
1263 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001264 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001265
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001266 gpt.WriteToFile(args.image_file)
1267 if part.IsUnused():
1268 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001269 return 'Deleted (zeroed) %s.' % (part,)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001270 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001271 return ('%s %s (%s+%s).' %
1272 ('Added' if is_new_part else 'Modified',
1273 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001274
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001275 class Show(SubCommand):
1276 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001277
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001278 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001279 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001280
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001281 def DefineArgs(self, parser):
1282 parser.add_argument(
1283 '--numeric', '-n', action='store_true',
1284 help='Numeric output only.')
1285 parser.add_argument(
1286 '--quick', '-q', action='store_true',
1287 help='Quick output.')
1288 parser.add_argument(
1289 '-i', '--number', type=int,
1290 help='Show specified partition only, with format args.')
1291 for name, help_str in GPTCommands.FORMAT_ARGS:
1292 # TODO(hungte) Alert if multiple args were specified.
1293 parser.add_argument(
1294 '--%s' % name, '-%c' % name[0], action='store_true',
1295 help='[format] %s.' % help_str)
1296 parser.add_argument(
1297 'image_file', type=argparse.FileType('rb'),
1298 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001299
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001300 def Execute(self, args):
1301 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001302
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001303 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001304 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001305 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001306 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001307 if names:
1308 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001309 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001310
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001311 def IsBootableType(guid):
1312 if not guid:
1313 return False
1314 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001315
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001316 def FormatAttribute(attrs, chromeos_kernel=False):
1317 if args.numeric:
1318 return '[%x]' % (attrs.raw >> 48)
1319 results = []
1320 if chromeos_kernel:
1321 results += [
1322 'priority=%d' % attrs.priority,
1323 'tries=%d' % attrs.tries,
1324 'successful=%d' % attrs.successful]
1325 if attrs.required:
1326 results += ['required=1']
1327 if attrs.legacy_boot:
1328 results += ['legacy_boot=1']
1329 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001330
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001331 def ApplyFormatArgs(p):
1332 if args.begin:
1333 return p.FirstLBA
1334 elif args.size:
1335 return p.blocks
1336 elif args.type:
1337 return FormatTypeGUID(p)
1338 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001339 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001340 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001341 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001342 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001343 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001344 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001345 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001346 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001347 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001348 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001349 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001350 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001351 return '[%x]' % (p.Attributes.raw >> 48)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001352 else:
1353 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001354
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001355 def IsFormatArgsSpecified():
1356 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001357
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001358 gpt = GPT.LoadFromFile(args.image_file)
1359 logging.debug('%r', gpt.header)
1360 fmt = '%12s %11s %7s %s'
1361 fmt2 = '%32s %s: %s'
1362 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001363
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001364 if IsFormatArgsSpecified() and args.number is None:
1365 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001366
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001367 if not (args.number is None or
1368 0 < args.number <= gpt.header.PartitionEntriesNumber):
1369 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001370
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001371 partitions = gpt.partitions
1372 do_print_gpt_blocks = False
1373 if not (args.quick or IsFormatArgsSpecified()):
1374 print(fmt % header)
1375 if args.number is None:
1376 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001377
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001378 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001379 if gpt.pmbr:
1380 print(fmt % (0, 1, '', 'PMBR'))
1381 if gpt.is_secondary:
1382 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1383 else:
1384 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1385 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1386 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001387
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001388 for p in partitions:
1389 if args.number is None:
1390 # Skip unused partitions.
1391 if p.IsUnused():
1392 continue
1393 elif p.number != args.number:
1394 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001395
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001396 if IsFormatArgsSpecified():
1397 print(ApplyFormatArgs(p))
1398 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001399
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001400 print(fmt % (p.FirstLBA, p.blocks, p.number,
1401 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001402 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001403
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001404 if not args.quick:
1405 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001406 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1407 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001408 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001409 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001410
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001411 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001412 if gpt.is_secondary:
1413 header = gpt.header
1414 else:
1415 f = args.image_file
1416 f.seek(gpt.header.BackupLBA * gpt.block_size)
1417 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001418 print(fmt % (header.PartitionEntriesStartingLBA,
1419 gpt.GetPartitionTableBlocks(header), '',
1420 'Sec GPT table'))
1421 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001422
Hung-Te Lin3b491672018-04-19 01:41:20 +08001423 # Check integrity after showing all fields.
1424 gpt.CheckIntegrity()
1425
Hung-Te Linfe724f82018-04-18 15:03:58 +08001426 class Prioritize(SubCommand):
1427 """Reorder the priority of all kernel partitions.
1428
1429 Reorder the priority of all active ChromeOS Kernel partitions.
1430
1431 With no options this will set the lowest active kernel to priority 1 while
1432 maintaining the original order.
1433 """
1434
1435 def DefineArgs(self, parser):
1436 parser.add_argument(
1437 '-P', '--priority', type=int,
1438 help=('Highest priority to use in the new ordering. '
1439 'The other partitions will be ranked in decreasing '
1440 'priority while preserving their original order. '
1441 'If necessary the lowest ranks will be coalesced. '
1442 'No active kernels will be lowered to priority 0.'))
1443 parser.add_argument(
1444 '-i', '--number', type=int,
1445 help='Specify the partition to make the highest in the new order.')
1446 parser.add_argument(
1447 '-f', '--friends', action='store_true',
1448 help=('Friends of the given partition (those with the same '
1449 'starting priority) are also updated to the new '
1450 'highest priority. '))
1451 parser.add_argument(
1452 'image_file', type=argparse.FileType('rb+'),
1453 help='Disk image file to prioritize.')
1454
1455 def Execute(self, args):
1456 gpt = GPT.LoadFromFile(args.image_file)
1457 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001458 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1459 groups = dict((k, list(g)) for k, g in itertools.groupby(
1460 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001461 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001462 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001463 if p not in parts:
1464 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001465 pri = p.Attributes.priority
1466 friends = groups.pop(pri)
1467 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001468 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001469 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001470 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001471 groups[new_pri] = [p]
1472 friends.remove(p)
1473 if friends:
1474 groups[pri] = friends
1475
1476 if 0 in groups:
1477 # Do not change any partitions with priority=0
1478 groups.pop(0)
1479
Yilin Yang78fa12e2019-09-25 14:21:10 +08001480 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001481 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001482
1483 # Max priority is 0xf.
1484 highest = min(args.priority or len(prios), 0xf)
1485 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001486
Hung-Te Lin138389f2018-05-15 17:55:00 +08001487 for i, pri in enumerate(prios):
1488 new_priority = max(1, highest - i)
1489 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001490 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001491 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001492 if old_priority == new_priority:
1493 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001494 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001495 if attrs.tries < 1 and not attrs.successful:
1496 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001497 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001498 logging.info('%s priority changed from %s to %s.', p, old_priority,
1499 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001500
1501 gpt.WriteToFile(args.image_file)
1502
Hung-Te Linf641d302018-04-18 15:09:35 +08001503 class Find(SubCommand):
1504 """Locate a partition by its GUID.
1505
1506 Find a partition by its UUID or label. With no specified DRIVE it scans all
1507 physical drives.
1508
1509 The partition type may also be given as one of these aliases:
1510
1511 firmware ChromeOS firmware
1512 kernel ChromeOS kernel
1513 rootfs ChromeOS rootfs
1514 data Linux data
1515 reserved ChromeOS reserved
1516 efi EFI System Partition
1517 unused Unused (nonexistent) partition
1518 """
1519 def DefineArgs(self, parser):
1520 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001521 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001522 help='Search for Partition Type GUID')
1523 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001524 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001525 help='Search for Partition Unique GUID')
1526 parser.add_argument(
1527 '-l', '--label',
1528 help='Search for Label')
1529 parser.add_argument(
1530 '-n', '--numeric', action='store_true',
1531 help='Numeric output only.')
1532 parser.add_argument(
1533 '-1', '--single-match', action='store_true',
1534 help='Fail if more than one match is found.')
1535 parser.add_argument(
1536 '-M', '--match-file', type=str,
1537 help='Matching partition data must also contain MATCH_FILE content.')
1538 parser.add_argument(
1539 '-O', '--offset', type=int, default=0,
1540 help='Byte offset into partition to match content (default 0).')
1541 parser.add_argument(
1542 'drive', type=argparse.FileType('rb+'), nargs='?',
1543 help='Drive or disk image file to find.')
1544
1545 def Execute(self, args):
1546 if not any((args.type_guid, args.unique_guid, args.label)):
1547 raise GPTError('You must specify at least one of -t, -u, or -l')
1548
1549 drives = [args.drive.name] if args.drive else (
1550 '/dev/%s' % name for name in subprocess.check_output(
1551 'lsblk -d -n -r -o name', shell=True).split())
1552
1553 match_pattern = None
1554 if args.match_file:
1555 with open(args.match_file) as f:
1556 match_pattern = f.read()
1557
1558 found = 0
1559 for drive in drives:
1560 try:
1561 gpt = GPT.LoadFromFile(drive)
1562 except GPTError:
1563 if args.drive:
1564 raise
1565 # When scanning all block devices on system, ignore failure.
1566
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001567 def Unmatch(a, b):
1568 return a is not None and a != b
1569
Hung-Te Linf641d302018-04-18 15:09:35 +08001570 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001571 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001572 Unmatch(args.label, p.Names) or
1573 Unmatch(args.unique_guid, p.UniqueGUID) or
1574 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001575 continue
1576 if match_pattern:
1577 with open(drive, 'rb') as f:
1578 f.seek(p.offset + args.offset)
1579 if f.read(len(match_pattern)) != match_pattern:
1580 continue
1581 # Found the partition, now print.
1582 found += 1
1583 if args.numeric:
1584 print(p.number)
1585 else:
1586 # This is actually more for block devices.
1587 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1588 p.number))
1589
1590 if found < 1 or (args.single_match and found > 1):
1591 return 1
1592 return 0
1593
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001594
1595def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001596 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001597 parser = argparse.ArgumentParser(description='GPT Utility.')
1598 parser.add_argument('--verbose', '-v', action='count', default=0,
1599 help='increase verbosity.')
1600 parser.add_argument('--debug', '-d', action='store_true',
1601 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001602 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001603
1604 args = parser.parse_args()
1605 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1606 if args.debug:
1607 log_level = logging.DEBUG
1608 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1609 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001610 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001611 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001612 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001613 sys.exit(code)
Hung-Te Linbad46112018-05-15 16:39:14 +08001614 elif isinstance(code, basestring):
1615 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001616 except Exception as e:
1617 if args.verbose or args.debug:
1618 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001619 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001620
1621
1622if __name__ == '__main__':
1623 main()