blob: a1955e8a0dda8aeede3b98e2919d73e8e07d7ca3 [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
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."""
74 __slots__ = ['encoding', 'max_length']
75 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
269 FIELDS = None
270 """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(
295 '%s=%r' %(f, getattr(self, f)) for f in self.__slots__))
296
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 Linc772e1a2017-04-14 16:50:50 +0800377 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800378 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
379 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Linux data',
380 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
381 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
382 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
383 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
384 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800385 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800386 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800387 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
388 for k, v in TYPE_GUID_MAP.iteritems())
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800389 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
390 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
391 TYPE_GUID_LIST_BOOTABLE = [
392 TYPE_GUID_CHROMEOS_KERNEL,
393 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800394 ]
395
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800396 class ProtectiveMBR(GPTObject):
397 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800398 FIELDS = PMBR_FIELDS
399 __slots__ = [f.name for f in FIELDS]
400
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800401 SIGNATURE = '\x55\xAA'
402 MAGIC = '\x1d\x9a'
403
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800404 class Header(GPTObject):
405 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800406 FIELDS = HEADER_FIELDS
407 __slots__ = [f.name for f in FIELDS]
408
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800409 SIGNATURES = ['EFI PART', 'CHROMEOS']
410 SIGNATURE_IGNORE = 'IGNOREME'
411 DEFAULT_REVISION = '\x00\x00\x01\x00'
412
413 DEFAULT_PARTITION_ENTRIES = 128
414 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
415
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800416 @classmethod
417 def Create(cls, size, block_size, pad_blocks=0,
418 part_entries=DEFAULT_PARTITION_ENTRIES):
419 """Creates a header with default values.
420
421 Args:
422 size: integer of expected image size.
423 block_size: integer for size of each block (sector).
424 pad_blocks: number of preserved sectors between header and partitions.
425 part_entries: number of partitions to include in header.
426 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800427 PART_FORMAT = GPT.Partition.GetStructFormat()
428 FORMAT = cls.GetStructFormat()
429 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800430 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
431 parts_bytes = part_entries * part_entry_size
432 parts_blocks = parts_bytes / block_size
433 if parts_bytes % block_size:
434 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800435 # CRC32 and PartitionsCRC32 must be updated later explicitly.
436 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800437 Signature=cls.SIGNATURES[0],
438 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800439 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800440 CurrentLBA=1,
441 BackupLBA=size / block_size - 1,
442 FirstUsableLBA=parts_lba + parts_blocks,
443 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800444 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800445 PartitionEntriesStartingLBA=parts_lba,
446 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800447 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800448
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800449 def UpdateChecksum(self):
450 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800451
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800452 Note the PartitionArrayCRC32 is not touched - you have to make sure that
453 is correct before calling Header.UpdateChecksum().
454 """
455 self.Update(CRC32=0)
456 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800457
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800458 class Partition(GPTObject):
459 """The partition entry in GPT.
460
461 Please include following properties when creating a Partition object:
462 - image: a string for path to the image file the partition maps to.
463 - number: the 1-based partition number.
464 - block_size: an integer for size of each block (LBA, or sector).
465 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800466 FIELDS = PARTITION_FIELDS
467 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800468 NAMES_ENCODING = 'utf-16-le'
469 NAMES_LENGTH = 72
470
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800471 def __str__(self):
472 return '%s#%s' % (self.image, self.number)
473
474 def IsUnused(self):
475 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800476 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800477
Hung-Te Linfe724f82018-04-18 15:03:58 +0800478 def IsChromeOSKernel(self):
479 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800480 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800481
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800482 @property
483 def blocks(self):
484 """Return size of partition in blocks (see block_size)."""
485 return self.LastLBA - self.FirstLBA + 1
486
487 @property
488 def offset(self):
489 """Returns offset to partition in bytes."""
490 return self.FirstLBA * self.block_size
491
492 @property
493 def size(self):
494 """Returns size of partition in bytes."""
495 return self.blocks * self.block_size
496
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800497 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800498 """GPT constructor.
499
500 See LoadFromFile for how it's usually used.
501 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800502 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800503 self.header = None
504 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800505 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800506 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800507
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800508 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800509 def GetTypeGUID(cls, value):
510 """The value may be a GUID in string or a short type string."""
511 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
512 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800513
514 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800515 def Create(cls, image_name, size, block_size, pad_blocks=0):
516 """Creates a new GPT instance from given size and block_size.
517
518 Args:
519 image_name: a string of underlying disk image file name.
520 size: expected size of disk image.
521 block_size: size of each block (sector) in bytes.
522 pad_blocks: number of blocks between header and partitions array.
523 """
524 gpt = cls()
525 gpt.block_size = block_size
526 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
527 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800528 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800529 for i in xrange(gpt.header.PartitionEntriesNumber)]
530 return gpt
531
Hung-Te Lin446eb512018-05-02 18:39:16 +0800532 @staticmethod
533 def IsBlockDevice(image):
534 """Returns if the image is a block device file."""
535 return stat.S_ISBLK(os.stat(image).st_mode)
536
537 @classmethod
538 def GetImageSize(cls, image):
539 """Returns the size of specified image (plain or block device file)."""
540 if not cls.IsBlockDevice(image):
541 return os.path.getsize(image)
542
543 fd = os.open(image, os.O_RDONLY)
544 try:
545 return os.lseek(fd, 0, os.SEEK_END)
546 finally:
547 os.close(fd)
548
549 @classmethod
550 def GetLogicalBlockSize(cls, block_dev):
551 """Returns the logical block (sector) size from a block device file.
552
553 The underlying call is BLKSSZGET. An alternative command is blockdev,
554 but that needs root permission even if we just want to get sector size.
555 """
556 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
557 return int(subprocess.check_output(
558 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
559
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800560 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800561 def LoadFromFile(cls, image):
562 """Loads a GPT table from give disk image file object.
563
564 Args:
565 image: a string as file path or a file-like object to read from.
566 """
567 if isinstance(image, basestring):
568 with open(image, 'rb') as f:
569 return cls.LoadFromFile(f)
570
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800571 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800572 image.seek(0)
573 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
574 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
575 logging.debug('Found MBR signature in %s', image.name)
576 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
577 logging.debug('Found PMBR in %s', image.name)
578 gpt.pmbr = pmbr
579
Hung-Te Linf148d322018-04-13 10:24:42 +0800580 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800581 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
582 if cls.IsBlockDevice(image.name):
583 block_sizes = [cls.GetLogicalBlockSize(image.name)]
584
585 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800586 # Note because there are devices setting Primary as ignored and the
587 # partition table signature accepts 'CHROMEOS' which is also used by
588 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
589 # first before trying other block sizes, otherwise we may incorrectly
590 # identify a kernel partition as LBA 1 of larger block size system.
591 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
592 (-block_size, os.SEEK_END)]):
593 image.seek(*seek)
594 header = gpt.Header.ReadFrom(image)
595 if header.Signature in cls.Header.SIGNATURES:
596 gpt.block_size = block_size
597 if i != 0:
598 gpt.is_secondary = True
599 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800600 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800601 else:
602 # Nothing found, try next block size.
603 continue
604 # Found a valid signature.
605 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800606 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800607 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800608
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800609 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800610 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800611 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800612 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800613 return p
614
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800615 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800616 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800617 ReadPartition(image, i + 1)
618 for i in xrange(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800619 return gpt
620
Hung-Te Linc5196682018-04-18 22:59:59 +0800621 def GetUsedPartitions(self):
622 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800623
Hung-Te Linc5196682018-04-18 22:59:59 +0800624 Use 'number' property to find the real location of partition in
625 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800626 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800627 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800628
629 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800630 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800631 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800632 return (max(p.LastLBA for p in parts)
633 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800634
635 def GetPartitionTableBlocks(self, header=None):
636 """Returns the blocks (or LBA) of partition table from given header."""
637 if header is None:
638 header = self.header
639 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800640 blocks = size / self.block_size
641 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800642 blocks += 1
643 return blocks
644
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800645 def GetPartition(self, number):
646 """Gets the Partition by given (1-based) partition number.
647
648 Args:
649 number: an integer as 1-based partition number.
650 """
651 if not 0 < number <= len(self.partitions):
652 raise GPTError('Invalid partition number %s.' % number)
653 return self.partitions[number - 1]
654
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800655 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800656 """Updates the entry in partition table by given Partition object.
657
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800658 Usually you only need to call this if you want to copy one partition to
659 different location (number of image).
660
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800661 Args:
662 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800663 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800664 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800665 ref = self.partitions[number - 1]
666 part = part.Clone()
667 part.number = number
668 part.image = ref.image
669 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800670 self.partitions[number - 1] = part
671
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800672 def Resize(self, new_size):
673 """Adjust GPT for a disk image in given size.
674
675 Args:
676 new_size: Integer for new size of disk image file.
677 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800678 old_size = self.block_size * (self.header.BackupLBA + 1)
679 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800680 raise GPTError(
681 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800682 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800683 if old_size != new_size:
684 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800685 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800686 else:
687 logging.info('Image size (%d, LBA=%d) not changed.',
688 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800689 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800690
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800691 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800692 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800693 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800694
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800695 if last_usable_lba < self.header.LastUsableLBA:
696 max_used_lba = self.GetMaxUsedLBA()
697 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800698 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800699
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800700 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800701
702 def GetFreeSpace(self):
703 """Returns the free (available) space left according to LastUsableLBA."""
704 max_lba = self.GetMaxUsedLBA()
705 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800706 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800707
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800708 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800709 """Expands a given partition to last usable LBA.
710
711 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800712 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800713
714 Returns:
715 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800716 """
717 # Assume no partitions overlap, we need to make sure partition[i] has
718 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800719 p = self.GetPartition(number)
720 if p.IsUnused():
721 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800722 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800723 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800724 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800725 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800726 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800727
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800728 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800729 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800730 new_blocks = p.blocks
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800731 logging.warn(
732 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
733 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800734
Hung-Te Lin3b491672018-04-19 01:41:20 +0800735 def CheckIntegrity(self):
736 """Checks if the GPT objects all look good."""
737 # Check if the header allocation looks good. CurrentLBA and
738 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
739 # LastUsableLBA].
740 header = self.header
741 entries_first_lba = header.PartitionEntriesStartingLBA
742 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
743
744 def CheckOutsideUsable(name, lba, outside_entries=False):
745 if lba < 1:
746 raise GPTError('%s should not live in LBA %s.' % (name, lba))
747 if lba > max(header.BackupLBA, header.CurrentLBA):
748 # Note this is "in theory" possible, but we want to report this as
749 # error as well, since it usually leads to error.
750 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
751 (name, lba, header.BackupLBA))
752 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
753 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
754 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
755 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
756 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
757 (name, lba, entries_first_lba, entries_last_lba))
758 CheckOutsideUsable('Header', header.CurrentLBA, True)
759 CheckOutsideUsable('Backup header', header.BackupLBA, True)
760 CheckOutsideUsable('Partition entries', entries_first_lba)
761 CheckOutsideUsable('Partition entries end', entries_last_lba)
762
763 parts = self.GetUsedPartitions()
764 # Check if partition entries overlap with each other.
765 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
766 lba_list.sort(key=lambda t: t[0])
767 for i in xrange(len(lba_list) - 1):
768 if lba_list[i][1] >= lba_list[i + 1][0]:
769 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
770 (lba_list[i] + lba_list[i + 1]))
771 # Now, check the first and last partition.
772 if lba_list:
773 p = lba_list[0][2]
774 if p.FirstLBA < header.FirstUsableLBA:
775 raise GPTError(
776 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
777 (p, p.FirstLBA, header.FirstLBA))
778 p = lba_list[-1][2]
779 if p.LastLBA > header.LastUsableLBA:
780 raise GPTError(
781 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
782 (p, p.LastLBA, header.LastLBA))
783 # Check if UniqueGUIDs are not unique.
784 if len(set(p.UniqueGUID for p in parts)) != len(parts):
785 raise GPTError('Partition UniqueGUIDs are duplicated.')
786 # Check if CRCs match.
787 if (binascii.crc32(''.join(p.blob for p in self.partitions)) !=
788 header.PartitionArrayCRC32):
789 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800790 header_crc = header.Clone()
791 header_crc.UpdateChecksum()
792 if header_crc.CRC32 != header.CRC32:
793 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800794
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800795 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800796 """Updates all checksum fields in GPT objects."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800797 parts = ''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800798 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
799 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800800
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800801 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800802 """Returns the backup header according to given header.
803
804 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
805 fields.
806 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800807 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800808 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800809 h = header.Clone()
810 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800811 BackupLBA=header.CurrentLBA,
812 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800813 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800814 h.UpdateChecksum()
815 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800816
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800817 @classmethod
818 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
819 """Writes a protective MBR to given file.
820
821 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
822 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
823 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
824 we can follow that.
825
826 Args:
827 create: True to re-create PMBR structure.
828 bootcode: a blob of new boot code.
829 boot_guid a blob for new boot GUID.
830
831 Returns:
832 The written PMBR structure.
833 """
834 if isinstance(image, basestring):
835 with open(image, 'rb+') as f:
836 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
837
838 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800839 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
840 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800841 pmbr = cls.ProtectiveMBR.ReadFrom(image)
842
843 if create:
844 legacy_sectors = min(
845 0x100000000,
Hung-Te Lin446eb512018-05-02 18:39:16 +0800846 GPT.GetImageSize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800847 # Partition 0 must have have the fixed CHS with number of sectors
848 # (calculated as legacy_sectors later).
849 part0 = ('00000200eeffffff01000000'.decode('hex') +
850 struct.pack('<I', legacy_sectors))
851 # Partition 1~3 should be all zero.
852 part1 = '\x00' * 16
853 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800854 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800855 BootGUID=cls.TYPE_GUID_UNUSED,
856 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800857 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800858 LegacyPart0=part0,
859 LegacyPart1=part1,
860 LegacyPart2=part1,
861 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800862 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800863
864 if bootcode:
865 if len(bootcode) > len(pmbr.BootCode):
866 logging.info(
867 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
868 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800869 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800870 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800871 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800872
873 blob = pmbr.blob
874 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
875 image.seek(0)
876 image.write(blob)
877 return pmbr
878
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800879 def WriteToFile(self, image):
880 """Updates partition table in a disk image file.
881
882 Args:
883 image: a string as file path or a file-like object to write into.
884 """
885 if isinstance(image, basestring):
886 with open(image, 'rb+') as f:
887 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800888
889 def WriteData(name, blob, lba):
890 """Writes a blob into given location."""
891 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800892 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800893 image.seek(lba * self.block_size)
894 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800895
896 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800897 self.CheckIntegrity()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800898 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800899
900 header = self.header
901 WriteData('GPT Header', header.blob, header.CurrentLBA)
902 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
903 logging.info(
904 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
905 header.LastUsableLBA)
906
907 if not self.is_secondary:
908 # When is_secondary is True, the header we have is actually backup header.
909 backup_header = self.GetBackupHeader(self.header)
910 WriteData(
911 'Backup Partitions', parts_blob,
912 backup_header.PartitionEntriesStartingLBA)
913 WriteData(
914 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800915
916
917class GPTCommands(object):
918 """Collection of GPT sub commands for command line to use.
919
920 The commands are derived from `cgpt`, but not necessary to be 100% compatible
921 with cgpt.
922 """
923
924 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800925 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800926 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800927 ('type', 'type guid'),
928 ('unique', 'unique guid'),
929 ('label', 'label'),
930 ('Successful', 'Successful flag'),
931 ('Tries', 'Tries flag'),
932 ('Priority', 'Priority flag'),
933 ('Legacy', 'Legacy Boot flag'),
934 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800935
936 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800937 commands = dict(
938 (command.lower(), getattr(self, command)())
939 for command in dir(self)
940 if (isinstance(getattr(self, command), type) and
941 issubclass(getattr(self, command), self.SubCommand) and
942 getattr(self, command) is not self.SubCommand)
943 )
944 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800945
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800946 def DefineArgs(self, parser):
947 """Defines all available commands to an argparser subparsers instance."""
948 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
949 for name, instance in sorted(self.commands.iteritems()):
950 parser = subparsers.add_parser(
951 name, description=instance.__doc__,
952 formatter_class=argparse.RawDescriptionHelpFormatter,
953 help=instance.__doc__.splitlines()[0])
954 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800955
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800956 def Execute(self, args):
957 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800958 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800959
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800960 class SubCommand(object):
961 """A base class for sub commands to derive from."""
962
963 def DefineArgs(self, parser):
964 """Defines command line arguments to argparse parser.
965
966 Args:
967 parser: An argparse parser instance.
968 """
969 del parser # Unused.
970 raise NotImplementedError
971
972 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800973 """Execute the command with parsed arguments.
974
975 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800976
977 Args:
978 args: An argparse parsed namespace.
979 """
980 del args # Unused.
981 raise NotImplementedError
982
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800983 def ExecuteCommandLine(self, *args):
984 """Execute as invoked from command line.
985
986 This provides an easy way to execute particular sub command without
987 creating argument parser explicitly.
988
989 Args:
990 args: a list of string type command line arguments.
991 """
992 parser = argparse.ArgumentParser()
993 self.DefineArgs(parser)
994 return self.Execute(parser.parse_args(args))
995
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800996 class Create(SubCommand):
997 """Create or reset GPT headers and tables.
998
999 Create or reset an empty GPT.
1000 """
1001
1002 def DefineArgs(self, parser):
1003 parser.add_argument(
1004 '-z', '--zero', action='store_true',
1005 help='Zero the sectors of the GPT table and entries')
1006 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001007 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001008 help=('Size (in blocks) of the disk to pad between the '
1009 'primary GPT header and its entries, default %(default)s'))
1010 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001011 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001012 help='Size of each block (sector) in bytes.')
1013 parser.add_argument(
1014 'image_file', type=argparse.FileType('rb+'),
1015 help='Disk image file to create.')
1016
1017 def Execute(self, args):
1018 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001019 if block_size is None:
1020 if GPT.IsBlockDevice(args.image_file.name):
1021 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1022 else:
1023 block_size = GPT.DEFAULT_BLOCK_SIZE
1024
1025 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1026 logging.info('Block (sector) size for %s is set to %s bytes.',
1027 args.image_file.name, block_size)
1028
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001029 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001030 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001031 block_size, args.pad_blocks)
1032 if args.zero:
1033 # In theory we only need to clear LBA 1, but to make sure images already
1034 # initialized with different block size won't have GPT signature in
1035 # different locations, we should zero until first usable LBA.
1036 args.image_file.seek(0)
1037 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
1038 gpt.WriteToFile(args.image_file)
1039 print('OK: Created GPT for %s' % args.image_file.name)
1040
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001041 class Boot(SubCommand):
1042 """Edit the PMBR sector for legacy BIOSes.
1043
1044 With no options, it will just print the PMBR boot guid.
1045 """
1046
1047 def DefineArgs(self, parser):
1048 parser.add_argument(
1049 '-i', '--number', type=int,
1050 help='Set bootable partition')
1051 parser.add_argument(
1052 '-b', '--bootloader', type=argparse.FileType('r'),
1053 help='Install bootloader code in the PMBR')
1054 parser.add_argument(
1055 '-p', '--pmbr', action='store_true',
1056 help='Create legacy PMBR partition table')
1057 parser.add_argument(
1058 'image_file', type=argparse.FileType('rb+'),
1059 help='Disk image file to change PMBR.')
1060
1061 def Execute(self, args):
1062 """Rebuilds the protective MBR."""
1063 bootcode = args.bootloader.read() if args.bootloader else None
1064 boot_guid = None
1065 if args.number is not None:
1066 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001067 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001068 pmbr = GPT.WriteProtectiveMBR(
1069 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1070
You-Cheng Syufff7f422018-05-14 15:37:39 +08001071 print(pmbr.BootGUID)
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001072
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001073
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001074 class Legacy(SubCommand):
1075 """Switch between GPT and Legacy GPT.
1076
1077 Switch GPT header signature to "CHROMEOS".
1078 """
1079
1080 def DefineArgs(self, parser):
1081 parser.add_argument(
1082 '-e', '--efi', action='store_true',
1083 help='Switch GPT header signature back to "EFI PART"')
1084 parser.add_argument(
1085 '-p', '--primary-ignore', action='store_true',
1086 help='Switch primary GPT header signature to "IGNOREME"')
1087 parser.add_argument(
1088 'image_file', type=argparse.FileType('rb+'),
1089 help='Disk image file to change.')
1090
1091 def Execute(self, args):
1092 gpt = GPT.LoadFromFile(args.image_file)
1093 # cgpt behavior: if -p is specified, -e is ignored.
1094 if args.primary_ignore:
1095 if gpt.is_secondary:
1096 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1097 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1098 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1099 gpt.header = gpt.GetBackupHeader(self.header)
1100 gpt.is_secondary = True
1101 else:
1102 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001103 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001104 gpt.WriteToFile(args.image_file)
1105 if args.primary_ignore:
1106 print('OK: Set %s primary GPT header to %s.' %
1107 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
1108 else:
1109 print('OK: Changed GPT signature for %s to %s.' %
1110 (args.image_file.name, new_signature))
1111
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001112 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001113 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001114
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001115 def DefineArgs(self, parser):
1116 parser.add_argument(
1117 'image_file', type=argparse.FileType('rb+'),
1118 help='Disk image file to repair.')
1119
1120 def Execute(self, args):
1121 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001122 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001123 gpt.WriteToFile(args.image_file)
1124 print('Disk image file %s repaired.' % args.image_file.name)
1125
1126 class Expand(SubCommand):
1127 """Expands a GPT partition to all available free space."""
1128
1129 def DefineArgs(self, parser):
1130 parser.add_argument(
1131 '-i', '--number', type=int, required=True,
1132 help='The partition to expand.')
1133 parser.add_argument(
1134 'image_file', type=argparse.FileType('rb+'),
1135 help='Disk image file to modify.')
1136
1137 def Execute(self, args):
1138 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001139 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001140 gpt.WriteToFile(args.image_file)
1141 if old_blocks < new_blocks:
1142 print(
1143 'Partition %s on disk image file %s has been extended '
1144 'from %s to %s .' %
1145 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1146 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001147 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001148 print('Nothing to expand for disk image %s partition %s.' %
1149 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001150
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001151 class Add(SubCommand):
1152 """Add, edit, or remove a partition entry.
1153
1154 Use the -i option to modify an existing partition.
1155 The -b, -s, and -t options must be given for new partitions.
1156
1157 The partition type may also be given as one of these aliases:
1158
1159 firmware ChromeOS firmware
1160 kernel ChromeOS kernel
1161 rootfs ChromeOS rootfs
1162 data Linux data
1163 reserved ChromeOS reserved
1164 efi EFI System Partition
1165 unused Unused (nonexistent) partition
1166 """
1167 def DefineArgs(self, parser):
1168 parser.add_argument(
1169 '-i', '--number', type=int,
1170 help='Specify partition (default is next available)')
1171 parser.add_argument(
1172 '-b', '--begin', type=int,
1173 help='Beginning sector')
1174 parser.add_argument(
1175 '-s', '--sectors', type=int,
1176 help='Size in sectors (logical blocks).')
1177 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001178 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001179 help='Partition Type GUID')
1180 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001181 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001182 help='Partition Unique ID')
1183 parser.add_argument(
1184 '-l', '--label',
1185 help='Label')
1186 parser.add_argument(
1187 '-S', '--successful', type=int, choices=xrange(2),
1188 help='set Successful flag')
1189 parser.add_argument(
1190 '-T', '--tries', type=int,
1191 help='set Tries flag (0-15)')
1192 parser.add_argument(
1193 '-P', '--priority', type=int,
1194 help='set Priority flag (0-15)')
1195 parser.add_argument(
1196 '-R', '--required', type=int, choices=xrange(2),
1197 help='set Required flag')
1198 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001199 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001200 choices=xrange(2),
1201 help='set Legacy Boot flag')
1202 parser.add_argument(
1203 '-A', '--attribute', dest='raw_16', type=int,
1204 help='set raw 16-bit attribute value (bits 48-63)')
1205 parser.add_argument(
1206 'image_file', type=argparse.FileType('rb+'),
1207 help='Disk image file to modify.')
1208
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001209 def Execute(self, args):
1210 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001211 number = args.number
1212 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001213 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001214
1215 # First and last LBA must be calculated explicitly because the given
1216 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001217 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001218 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001219
1220 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001221 part.Zero()
1222 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001223 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001224 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001225 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001226 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001227
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001228 def UpdateAttr(name):
1229 value = getattr(args, name)
1230 if value is None:
1231 return
1232 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001233
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001234 def GetArg(arg_value, default_value):
1235 return default_value if arg_value is None else arg_value
1236
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001237 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001238 for name in ['legacy_boot', 'required', 'priority', 'tries',
1239 'successful', 'raw_16']:
1240 UpdateAttr(name)
1241 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001242 part.Update(
1243 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001244 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001245 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1246 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1247 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1248 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001249
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001250 # Wipe partition again if it should be empty.
1251 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001252 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001253
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001254 gpt.WriteToFile(args.image_file)
1255 if part.IsUnused():
1256 # If we do ('%s' % part) there will be TypeError.
1257 print('OK: Deleted (zeroed) %s.' % (part,))
1258 else:
1259 print('OK: %s %s (%s+%s).' %
1260 ('Added' if is_new_part else 'Modified',
1261 part, part.FirstLBA, part.blocks))
1262
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001263 class Show(SubCommand):
1264 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001265
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001266 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001267 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001268
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001269 def DefineArgs(self, parser):
1270 parser.add_argument(
1271 '--numeric', '-n', action='store_true',
1272 help='Numeric output only.')
1273 parser.add_argument(
1274 '--quick', '-q', action='store_true',
1275 help='Quick output.')
1276 parser.add_argument(
1277 '-i', '--number', type=int,
1278 help='Show specified partition only, with format args.')
1279 for name, help_str in GPTCommands.FORMAT_ARGS:
1280 # TODO(hungte) Alert if multiple args were specified.
1281 parser.add_argument(
1282 '--%s' % name, '-%c' % name[0], action='store_true',
1283 help='[format] %s.' % help_str)
1284 parser.add_argument(
1285 'image_file', type=argparse.FileType('rb'),
1286 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001287
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001288 def Execute(self, args):
1289 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001290
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001291 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001292 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001293 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001294 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001295 if names:
1296 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001297 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001298
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001299 def IsBootableType(guid):
1300 if not guid:
1301 return False
1302 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001303
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001304 def FormatAttribute(attrs, chromeos_kernel=False):
1305 if args.numeric:
1306 return '[%x]' % (attrs.raw >> 48)
1307 results = []
1308 if chromeos_kernel:
1309 results += [
1310 'priority=%d' % attrs.priority,
1311 'tries=%d' % attrs.tries,
1312 'successful=%d' % attrs.successful]
1313 if attrs.required:
1314 results += ['required=1']
1315 if attrs.legacy_boot:
1316 results += ['legacy_boot=1']
1317 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001318
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001319 def ApplyFormatArgs(p):
1320 if args.begin:
1321 return p.FirstLBA
1322 elif args.size:
1323 return p.blocks
1324 elif args.type:
1325 return FormatTypeGUID(p)
1326 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001327 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001328 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001329 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001330 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001331 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001332 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001333 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001334 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001335 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001336 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001337 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001338 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001339 return '[%x]' % (p.Attributes.raw >> 48)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001340 else:
1341 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001342
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001343 def IsFormatArgsSpecified():
1344 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001345
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001346 gpt = GPT.LoadFromFile(args.image_file)
1347 logging.debug('%r', gpt.header)
1348 fmt = '%12s %11s %7s %s'
1349 fmt2 = '%32s %s: %s'
1350 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001351
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001352 if IsFormatArgsSpecified() and args.number is None:
1353 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001354
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001355 if not (args.number is None or
1356 0 < args.number <= gpt.header.PartitionEntriesNumber):
1357 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001358
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001359 partitions = gpt.partitions
1360 do_print_gpt_blocks = False
1361 if not (args.quick or IsFormatArgsSpecified()):
1362 print(fmt % header)
1363 if args.number is None:
1364 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001365
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001366 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001367 if gpt.pmbr:
1368 print(fmt % (0, 1, '', 'PMBR'))
1369 if gpt.is_secondary:
1370 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1371 else:
1372 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1373 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1374 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001375
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001376 for p in partitions:
1377 if args.number is None:
1378 # Skip unused partitions.
1379 if p.IsUnused():
1380 continue
1381 elif p.number != args.number:
1382 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001383
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001384 if IsFormatArgsSpecified():
1385 print(ApplyFormatArgs(p))
1386 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001387
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001388 print(fmt % (p.FirstLBA, p.blocks, p.number,
1389 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001390 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001391
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001392 if not args.quick:
1393 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001394 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1395 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001396 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001397 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001398
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001399 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001400 if gpt.is_secondary:
1401 header = gpt.header
1402 else:
1403 f = args.image_file
1404 f.seek(gpt.header.BackupLBA * gpt.block_size)
1405 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001406 print(fmt % (header.PartitionEntriesStartingLBA,
1407 gpt.GetPartitionTableBlocks(header), '',
1408 'Sec GPT table'))
1409 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001410
Hung-Te Lin3b491672018-04-19 01:41:20 +08001411 # Check integrity after showing all fields.
1412 gpt.CheckIntegrity()
1413
Hung-Te Linfe724f82018-04-18 15:03:58 +08001414 class Prioritize(SubCommand):
1415 """Reorder the priority of all kernel partitions.
1416
1417 Reorder the priority of all active ChromeOS Kernel partitions.
1418
1419 With no options this will set the lowest active kernel to priority 1 while
1420 maintaining the original order.
1421 """
1422
1423 def DefineArgs(self, parser):
1424 parser.add_argument(
1425 '-P', '--priority', type=int,
1426 help=('Highest priority to use in the new ordering. '
1427 'The other partitions will be ranked in decreasing '
1428 'priority while preserving their original order. '
1429 'If necessary the lowest ranks will be coalesced. '
1430 'No active kernels will be lowered to priority 0.'))
1431 parser.add_argument(
1432 '-i', '--number', type=int,
1433 help='Specify the partition to make the highest in the new order.')
1434 parser.add_argument(
1435 '-f', '--friends', action='store_true',
1436 help=('Friends of the given partition (those with the same '
1437 'starting priority) are also updated to the new '
1438 'highest priority. '))
1439 parser.add_argument(
1440 'image_file', type=argparse.FileType('rb+'),
1441 help='Disk image file to prioritize.')
1442
1443 def Execute(self, args):
1444 gpt = GPT.LoadFromFile(args.image_file)
1445 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001446 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1447 groups = dict((k, list(g)) for k, g in itertools.groupby(
1448 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001449 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001450 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001451 if p not in parts:
1452 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001453 pri = p.Attributes.priority
1454 friends = groups.pop(pri)
1455 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001456 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001457 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001458 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001459 groups[new_pri] = [p]
1460 friends.remove(p)
1461 if friends:
1462 groups[pri] = friends
1463
1464 if 0 in groups:
1465 # Do not change any partitions with priority=0
1466 groups.pop(0)
1467
1468 prios = groups.keys()
1469 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001470
1471 # Max priority is 0xf.
1472 highest = min(args.priority or len(prios), 0xf)
1473 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001474
Hung-Te Lin138389f2018-05-15 17:55:00 +08001475 for i, pri in enumerate(prios):
1476 new_priority = max(1, highest - i)
1477 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001478 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001479 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001480 if old_priority == new_priority:
1481 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001482 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001483 if attrs.tries < 1 and not attrs.successful:
1484 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001485 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001486 logging.info('%s priority changed from %s to %s.', p, old_priority,
1487 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001488
1489 gpt.WriteToFile(args.image_file)
1490
Hung-Te Linf641d302018-04-18 15:09:35 +08001491 class Find(SubCommand):
1492 """Locate a partition by its GUID.
1493
1494 Find a partition by its UUID or label. With no specified DRIVE it scans all
1495 physical drives.
1496
1497 The partition type may also be given as one of these aliases:
1498
1499 firmware ChromeOS firmware
1500 kernel ChromeOS kernel
1501 rootfs ChromeOS rootfs
1502 data Linux data
1503 reserved ChromeOS reserved
1504 efi EFI System Partition
1505 unused Unused (nonexistent) partition
1506 """
1507 def DefineArgs(self, parser):
1508 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001509 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001510 help='Search for Partition Type GUID')
1511 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001512 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001513 help='Search for Partition Unique GUID')
1514 parser.add_argument(
1515 '-l', '--label',
1516 help='Search for Label')
1517 parser.add_argument(
1518 '-n', '--numeric', action='store_true',
1519 help='Numeric output only.')
1520 parser.add_argument(
1521 '-1', '--single-match', action='store_true',
1522 help='Fail if more than one match is found.')
1523 parser.add_argument(
1524 '-M', '--match-file', type=str,
1525 help='Matching partition data must also contain MATCH_FILE content.')
1526 parser.add_argument(
1527 '-O', '--offset', type=int, default=0,
1528 help='Byte offset into partition to match content (default 0).')
1529 parser.add_argument(
1530 'drive', type=argparse.FileType('rb+'), nargs='?',
1531 help='Drive or disk image file to find.')
1532
1533 def Execute(self, args):
1534 if not any((args.type_guid, args.unique_guid, args.label)):
1535 raise GPTError('You must specify at least one of -t, -u, or -l')
1536
1537 drives = [args.drive.name] if args.drive else (
1538 '/dev/%s' % name for name in subprocess.check_output(
1539 'lsblk -d -n -r -o name', shell=True).split())
1540
1541 match_pattern = None
1542 if args.match_file:
1543 with open(args.match_file) as f:
1544 match_pattern = f.read()
1545
1546 found = 0
1547 for drive in drives:
1548 try:
1549 gpt = GPT.LoadFromFile(drive)
1550 except GPTError:
1551 if args.drive:
1552 raise
1553 # When scanning all block devices on system, ignore failure.
1554
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001555 def Unmatch(a, b):
1556 return a is not None and a != b
1557
Hung-Te Linf641d302018-04-18 15:09:35 +08001558 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001559 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001560 Unmatch(args.label, p.Names) or
1561 Unmatch(args.unique_guid, p.UniqueGUID) or
1562 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001563 continue
1564 if match_pattern:
1565 with open(drive, 'rb') as f:
1566 f.seek(p.offset + args.offset)
1567 if f.read(len(match_pattern)) != match_pattern:
1568 continue
1569 # Found the partition, now print.
1570 found += 1
1571 if args.numeric:
1572 print(p.number)
1573 else:
1574 # This is actually more for block devices.
1575 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1576 p.number))
1577
1578 if found < 1 or (args.single_match and found > 1):
1579 return 1
1580 return 0
1581
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001582
1583def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001584 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001585 parser = argparse.ArgumentParser(description='GPT Utility.')
1586 parser.add_argument('--verbose', '-v', action='count', default=0,
1587 help='increase verbosity.')
1588 parser.add_argument('--debug', '-d', action='store_true',
1589 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001590 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001591
1592 args = parser.parse_args()
1593 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1594 if args.debug:
1595 log_level = logging.DEBUG
1596 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1597 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001598 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001599 code = commands.Execute(args)
1600 if type(code) is int:
1601 sys.exit(code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001602 except Exception as e:
1603 if args.verbose or args.debug:
1604 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001605 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001606
1607
1608if __name__ == '__main__':
1609 main()