blob: 87176d6f0554f9236fb5717712383ced2e673c86 [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
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 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
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800672 def GetSize(self):
673 return self.block_size * (self.header.BackupLBA + 1)
674
675 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800676 """Adjust GPT for a disk image in given size.
677
678 Args:
679 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800680 check_overlap: Checks if the backup partition table overlaps used
681 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800682 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800683 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800684 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800685 raise GPTError(
686 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800687 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800688 if old_size != new_size:
689 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800690 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800691 else:
692 logging.info('Image size (%d, LBA=%d) not changed.',
693 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800694 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800695
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800696 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800697 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800698 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800699
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800700 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800701 max_used_lba = self.GetMaxUsedLBA()
702 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800703 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800704
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800705 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800706
707 def GetFreeSpace(self):
708 """Returns the free (available) space left according to LastUsableLBA."""
709 max_lba = self.GetMaxUsedLBA()
710 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800711 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800712
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800713 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800714 """Expands a given partition to last usable LBA.
715
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800716 The size of the partition can actually be reduced if the last usable LBA
717 decreases.
718
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800719 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800720 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800721
722 Returns:
723 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800724 """
725 # Assume no partitions overlap, we need to make sure partition[i] has
726 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800727 p = self.GetPartition(number)
728 if p.IsUnused():
729 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800730 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800731 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800732 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800733 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800734 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800735
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800736 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800737 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800738 new_blocks = p.blocks
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800739 logging.warn(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800740 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800741 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800742
Hung-Te Lin3b491672018-04-19 01:41:20 +0800743 def CheckIntegrity(self):
744 """Checks if the GPT objects all look good."""
745 # Check if the header allocation looks good. CurrentLBA and
746 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
747 # LastUsableLBA].
748 header = self.header
749 entries_first_lba = header.PartitionEntriesStartingLBA
750 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
751
752 def CheckOutsideUsable(name, lba, outside_entries=False):
753 if lba < 1:
754 raise GPTError('%s should not live in LBA %s.' % (name, lba))
755 if lba > max(header.BackupLBA, header.CurrentLBA):
756 # Note this is "in theory" possible, but we want to report this as
757 # error as well, since it usually leads to error.
758 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
759 (name, lba, header.BackupLBA))
760 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
761 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
762 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
763 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
764 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
765 (name, lba, entries_first_lba, entries_last_lba))
766 CheckOutsideUsable('Header', header.CurrentLBA, True)
767 CheckOutsideUsable('Backup header', header.BackupLBA, True)
768 CheckOutsideUsable('Partition entries', entries_first_lba)
769 CheckOutsideUsable('Partition entries end', entries_last_lba)
770
771 parts = self.GetUsedPartitions()
772 # Check if partition entries overlap with each other.
773 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
774 lba_list.sort(key=lambda t: t[0])
775 for i in xrange(len(lba_list) - 1):
776 if lba_list[i][1] >= lba_list[i + 1][0]:
777 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
778 (lba_list[i] + lba_list[i + 1]))
779 # Now, check the first and last partition.
780 if lba_list:
781 p = lba_list[0][2]
782 if p.FirstLBA < header.FirstUsableLBA:
783 raise GPTError(
784 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
785 (p, p.FirstLBA, header.FirstLBA))
786 p = lba_list[-1][2]
787 if p.LastLBA > header.LastUsableLBA:
788 raise GPTError(
789 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
790 (p, p.LastLBA, header.LastLBA))
791 # Check if UniqueGUIDs are not unique.
792 if len(set(p.UniqueGUID for p in parts)) != len(parts):
793 raise GPTError('Partition UniqueGUIDs are duplicated.')
794 # Check if CRCs match.
795 if (binascii.crc32(''.join(p.blob for p in self.partitions)) !=
796 header.PartitionArrayCRC32):
797 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800798 header_crc = header.Clone()
799 header_crc.UpdateChecksum()
800 if header_crc.CRC32 != header.CRC32:
801 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800802
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800803 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800804 """Updates all checksum fields in GPT objects."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800805 parts = ''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800806 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
807 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800808
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800809 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800810 """Returns the backup header according to given header.
811
812 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
813 fields.
814 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800815 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800816 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800817 h = header.Clone()
818 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800819 BackupLBA=header.CurrentLBA,
820 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800821 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800822 h.UpdateChecksum()
823 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800824
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800825 @classmethod
826 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
827 """Writes a protective MBR to given file.
828
829 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
830 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
831 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
832 we can follow that.
833
834 Args:
835 create: True to re-create PMBR structure.
836 bootcode: a blob of new boot code.
837 boot_guid a blob for new boot GUID.
838
839 Returns:
840 The written PMBR structure.
841 """
842 if isinstance(image, basestring):
843 with open(image, 'rb+') as f:
844 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
845
846 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800847 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
848 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800849 pmbr = cls.ProtectiveMBR.ReadFrom(image)
850
851 if create:
852 legacy_sectors = min(
853 0x100000000,
Hung-Te Lin446eb512018-05-02 18:39:16 +0800854 GPT.GetImageSize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800855 # Partition 0 must have have the fixed CHS with number of sectors
856 # (calculated as legacy_sectors later).
857 part0 = ('00000200eeffffff01000000'.decode('hex') +
858 struct.pack('<I', legacy_sectors))
859 # Partition 1~3 should be all zero.
860 part1 = '\x00' * 16
861 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800862 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800863 BootGUID=cls.TYPE_GUID_UNUSED,
864 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800865 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800866 LegacyPart0=part0,
867 LegacyPart1=part1,
868 LegacyPart2=part1,
869 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800870 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800871
872 if bootcode:
873 if len(bootcode) > len(pmbr.BootCode):
874 logging.info(
875 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
876 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800877 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800878 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800879 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800880
881 blob = pmbr.blob
882 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
883 image.seek(0)
884 image.write(blob)
885 return pmbr
886
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800887 def WriteToFile(self, image):
888 """Updates partition table in a disk image file.
889
890 Args:
891 image: a string as file path or a file-like object to write into.
892 """
893 if isinstance(image, basestring):
894 with open(image, 'rb+') as f:
895 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800896
897 def WriteData(name, blob, lba):
898 """Writes a blob into given location."""
899 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800900 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800901 image.seek(lba * self.block_size)
902 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800903
904 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800905 self.CheckIntegrity()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800906 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800907
908 header = self.header
909 WriteData('GPT Header', header.blob, header.CurrentLBA)
910 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
911 logging.info(
912 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
913 header.LastUsableLBA)
914
915 if not self.is_secondary:
916 # When is_secondary is True, the header we have is actually backup header.
917 backup_header = self.GetBackupHeader(self.header)
918 WriteData(
919 'Backup Partitions', parts_blob,
920 backup_header.PartitionEntriesStartingLBA)
921 WriteData(
922 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800923
924
925class GPTCommands(object):
926 """Collection of GPT sub commands for command line to use.
927
928 The commands are derived from `cgpt`, but not necessary to be 100% compatible
929 with cgpt.
930 """
931
932 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800933 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800934 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800935 ('type', 'type guid'),
936 ('unique', 'unique guid'),
937 ('label', 'label'),
938 ('Successful', 'Successful flag'),
939 ('Tries', 'Tries flag'),
940 ('Priority', 'Priority flag'),
941 ('Legacy', 'Legacy Boot flag'),
942 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800943
944 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800945 commands = dict(
946 (command.lower(), getattr(self, command)())
947 for command in dir(self)
948 if (isinstance(getattr(self, command), type) and
949 issubclass(getattr(self, command), self.SubCommand) and
950 getattr(self, command) is not self.SubCommand)
951 )
952 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800953
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800954 def DefineArgs(self, parser):
955 """Defines all available commands to an argparser subparsers instance."""
956 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
957 for name, instance in sorted(self.commands.iteritems()):
958 parser = subparsers.add_parser(
959 name, description=instance.__doc__,
960 formatter_class=argparse.RawDescriptionHelpFormatter,
961 help=instance.__doc__.splitlines()[0])
962 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800963
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800964 def Execute(self, args):
965 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800966 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800967
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800968 class SubCommand(object):
969 """A base class for sub commands to derive from."""
970
971 def DefineArgs(self, parser):
972 """Defines command line arguments to argparse parser.
973
974 Args:
975 parser: An argparse parser instance.
976 """
977 del parser # Unused.
978 raise NotImplementedError
979
980 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800981 """Execute the command with parsed arguments.
982
983 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800984
985 Args:
986 args: An argparse parsed namespace.
987 """
988 del args # Unused.
989 raise NotImplementedError
990
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800991 def ExecuteCommandLine(self, *args):
992 """Execute as invoked from command line.
993
994 This provides an easy way to execute particular sub command without
995 creating argument parser explicitly.
996
997 Args:
998 args: a list of string type command line arguments.
999 """
1000 parser = argparse.ArgumentParser()
1001 self.DefineArgs(parser)
1002 return self.Execute(parser.parse_args(args))
1003
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001004 class Create(SubCommand):
1005 """Create or reset GPT headers and tables.
1006
1007 Create or reset an empty GPT.
1008 """
1009
1010 def DefineArgs(self, parser):
1011 parser.add_argument(
1012 '-z', '--zero', action='store_true',
1013 help='Zero the sectors of the GPT table and entries')
1014 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001015 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001016 help=('Size (in blocks) of the disk to pad between the '
1017 'primary GPT header and its entries, default %(default)s'))
1018 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001019 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001020 help='Size of each block (sector) in bytes.')
1021 parser.add_argument(
1022 'image_file', type=argparse.FileType('rb+'),
1023 help='Disk image file to create.')
1024
1025 def Execute(self, args):
1026 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001027 if block_size is None:
1028 if GPT.IsBlockDevice(args.image_file.name):
1029 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1030 else:
1031 block_size = GPT.DEFAULT_BLOCK_SIZE
1032
1033 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1034 logging.info('Block (sector) size for %s is set to %s bytes.',
1035 args.image_file.name, block_size)
1036
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001037 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001038 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001039 block_size, args.pad_blocks)
1040 if args.zero:
1041 # In theory we only need to clear LBA 1, but to make sure images already
1042 # initialized with different block size won't have GPT signature in
1043 # different locations, we should zero until first usable LBA.
1044 args.image_file.seek(0)
1045 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
1046 gpt.WriteToFile(args.image_file)
Hung-Te Linbad46112018-05-15 16:39:14 +08001047 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001048
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001049 class Boot(SubCommand):
1050 """Edit the PMBR sector for legacy BIOSes.
1051
1052 With no options, it will just print the PMBR boot guid.
1053 """
1054
1055 def DefineArgs(self, parser):
1056 parser.add_argument(
1057 '-i', '--number', type=int,
1058 help='Set bootable partition')
1059 parser.add_argument(
1060 '-b', '--bootloader', type=argparse.FileType('r'),
1061 help='Install bootloader code in the PMBR')
1062 parser.add_argument(
1063 '-p', '--pmbr', action='store_true',
1064 help='Create legacy PMBR partition table')
1065 parser.add_argument(
1066 'image_file', type=argparse.FileType('rb+'),
1067 help='Disk image file to change PMBR.')
1068
1069 def Execute(self, args):
1070 """Rebuilds the protective MBR."""
1071 bootcode = args.bootloader.read() if args.bootloader else None
1072 boot_guid = None
1073 if args.number is not None:
1074 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001075 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001076 pmbr = GPT.WriteProtectiveMBR(
1077 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1078
You-Cheng Syufff7f422018-05-14 15:37:39 +08001079 print(pmbr.BootGUID)
Hung-Te Linbad46112018-05-15 16:39:14 +08001080 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001081
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001082 class Legacy(SubCommand):
1083 """Switch between GPT and Legacy GPT.
1084
1085 Switch GPT header signature to "CHROMEOS".
1086 """
1087
1088 def DefineArgs(self, parser):
1089 parser.add_argument(
1090 '-e', '--efi', action='store_true',
1091 help='Switch GPT header signature back to "EFI PART"')
1092 parser.add_argument(
1093 '-p', '--primary-ignore', action='store_true',
1094 help='Switch primary GPT header signature to "IGNOREME"')
1095 parser.add_argument(
1096 'image_file', type=argparse.FileType('rb+'),
1097 help='Disk image file to change.')
1098
1099 def Execute(self, args):
1100 gpt = GPT.LoadFromFile(args.image_file)
1101 # cgpt behavior: if -p is specified, -e is ignored.
1102 if args.primary_ignore:
1103 if gpt.is_secondary:
1104 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1105 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1106 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1107 gpt.header = gpt.GetBackupHeader(self.header)
1108 gpt.is_secondary = True
1109 else:
1110 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001111 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001112 gpt.WriteToFile(args.image_file)
1113 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001114 return ('Set %s primary GPT header to %s.' %
1115 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001116 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001117 return ('Changed GPT signature for %s to %s.' %
1118 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001119
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001120 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001121 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001122
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001123 def DefineArgs(self, parser):
1124 parser.add_argument(
1125 'image_file', type=argparse.FileType('rb+'),
1126 help='Disk image file to repair.')
1127
1128 def Execute(self, args):
1129 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001130 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001131 gpt.WriteToFile(args.image_file)
Hung-Te Linbad46112018-05-15 16:39:14 +08001132 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001133
1134 class Expand(SubCommand):
1135 """Expands a GPT partition to all available free space."""
1136
1137 def DefineArgs(self, parser):
1138 parser.add_argument(
1139 '-i', '--number', type=int, required=True,
1140 help='The partition to expand.')
1141 parser.add_argument(
1142 'image_file', type=argparse.FileType('rb+'),
1143 help='Disk image file to modify.')
1144
1145 def Execute(self, args):
1146 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001147 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001148 gpt.WriteToFile(args.image_file)
1149 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001150 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001151 'Partition %s on disk image file %s has been extended '
1152 'from %s to %s .' %
1153 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1154 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001155 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001156 return ('Nothing to expand for disk image %s partition %s.' %
1157 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001158
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001159 class Add(SubCommand):
1160 """Add, edit, or remove a partition entry.
1161
1162 Use the -i option to modify an existing partition.
1163 The -b, -s, and -t options must be given for new partitions.
1164
1165 The partition type may also be given as one of these aliases:
1166
1167 firmware ChromeOS firmware
1168 kernel ChromeOS kernel
1169 rootfs ChromeOS rootfs
1170 data Linux data
1171 reserved ChromeOS reserved
1172 efi EFI System Partition
1173 unused Unused (nonexistent) partition
1174 """
1175 def DefineArgs(self, parser):
1176 parser.add_argument(
1177 '-i', '--number', type=int,
1178 help='Specify partition (default is next available)')
1179 parser.add_argument(
1180 '-b', '--begin', type=int,
1181 help='Beginning sector')
1182 parser.add_argument(
1183 '-s', '--sectors', type=int,
1184 help='Size in sectors (logical blocks).')
1185 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001186 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001187 help='Partition Type GUID')
1188 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001189 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001190 help='Partition Unique ID')
1191 parser.add_argument(
1192 '-l', '--label',
1193 help='Label')
1194 parser.add_argument(
1195 '-S', '--successful', type=int, choices=xrange(2),
1196 help='set Successful flag')
1197 parser.add_argument(
1198 '-T', '--tries', type=int,
1199 help='set Tries flag (0-15)')
1200 parser.add_argument(
1201 '-P', '--priority', type=int,
1202 help='set Priority flag (0-15)')
1203 parser.add_argument(
1204 '-R', '--required', type=int, choices=xrange(2),
1205 help='set Required flag')
1206 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001207 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001208 choices=xrange(2),
1209 help='set Legacy Boot flag')
1210 parser.add_argument(
1211 '-A', '--attribute', dest='raw_16', type=int,
1212 help='set raw 16-bit attribute value (bits 48-63)')
1213 parser.add_argument(
1214 'image_file', type=argparse.FileType('rb+'),
1215 help='Disk image file to modify.')
1216
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001217 def Execute(self, args):
1218 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001219 number = args.number
1220 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001221 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001222
1223 # First and last LBA must be calculated explicitly because the given
1224 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001225 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001226 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001227
1228 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001229 part.Zero()
1230 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001231 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001232 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001233 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001234 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001235
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001236 def UpdateAttr(name):
1237 value = getattr(args, name)
1238 if value is None:
1239 return
1240 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001241
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001242 def GetArg(arg_value, default_value):
1243 return default_value if arg_value is None else arg_value
1244
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001245 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001246 for name in ['legacy_boot', 'required', 'priority', 'tries',
1247 'successful', 'raw_16']:
1248 UpdateAttr(name)
1249 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001250 part.Update(
1251 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001252 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001253 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1254 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1255 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1256 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001257
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001258 # Wipe partition again if it should be empty.
1259 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001260 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001261
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001262 gpt.WriteToFile(args.image_file)
1263 if part.IsUnused():
1264 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001265 return 'Deleted (zeroed) %s.' % (part,)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001266 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001267 return ('%s %s (%s+%s).' %
1268 ('Added' if is_new_part else 'Modified',
1269 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001270
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001271 class Show(SubCommand):
1272 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001273
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001274 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001275 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001276
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001277 def DefineArgs(self, parser):
1278 parser.add_argument(
1279 '--numeric', '-n', action='store_true',
1280 help='Numeric output only.')
1281 parser.add_argument(
1282 '--quick', '-q', action='store_true',
1283 help='Quick output.')
1284 parser.add_argument(
1285 '-i', '--number', type=int,
1286 help='Show specified partition only, with format args.')
1287 for name, help_str in GPTCommands.FORMAT_ARGS:
1288 # TODO(hungte) Alert if multiple args were specified.
1289 parser.add_argument(
1290 '--%s' % name, '-%c' % name[0], action='store_true',
1291 help='[format] %s.' % help_str)
1292 parser.add_argument(
1293 'image_file', type=argparse.FileType('rb'),
1294 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001295
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001296 def Execute(self, args):
1297 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001298
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001299 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001300 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001301 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001302 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001303 if names:
1304 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001305 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001306
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001307 def IsBootableType(guid):
1308 if not guid:
1309 return False
1310 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001311
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001312 def FormatAttribute(attrs, chromeos_kernel=False):
1313 if args.numeric:
1314 return '[%x]' % (attrs.raw >> 48)
1315 results = []
1316 if chromeos_kernel:
1317 results += [
1318 'priority=%d' % attrs.priority,
1319 'tries=%d' % attrs.tries,
1320 'successful=%d' % attrs.successful]
1321 if attrs.required:
1322 results += ['required=1']
1323 if attrs.legacy_boot:
1324 results += ['legacy_boot=1']
1325 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001326
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001327 def ApplyFormatArgs(p):
1328 if args.begin:
1329 return p.FirstLBA
1330 elif args.size:
1331 return p.blocks
1332 elif args.type:
1333 return FormatTypeGUID(p)
1334 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001335 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001336 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001337 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001338 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001339 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001340 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001341 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001342 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001343 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001344 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001345 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001346 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001347 return '[%x]' % (p.Attributes.raw >> 48)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001348 else:
1349 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001350
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001351 def IsFormatArgsSpecified():
1352 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001353
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001354 gpt = GPT.LoadFromFile(args.image_file)
1355 logging.debug('%r', gpt.header)
1356 fmt = '%12s %11s %7s %s'
1357 fmt2 = '%32s %s: %s'
1358 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001359
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001360 if IsFormatArgsSpecified() and args.number is None:
1361 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001362
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001363 if not (args.number is None or
1364 0 < args.number <= gpt.header.PartitionEntriesNumber):
1365 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001366
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001367 partitions = gpt.partitions
1368 do_print_gpt_blocks = False
1369 if not (args.quick or IsFormatArgsSpecified()):
1370 print(fmt % header)
1371 if args.number is None:
1372 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001373
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001374 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001375 if gpt.pmbr:
1376 print(fmt % (0, 1, '', 'PMBR'))
1377 if gpt.is_secondary:
1378 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1379 else:
1380 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1381 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1382 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001383
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001384 for p in partitions:
1385 if args.number is None:
1386 # Skip unused partitions.
1387 if p.IsUnused():
1388 continue
1389 elif p.number != args.number:
1390 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001391
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001392 if IsFormatArgsSpecified():
1393 print(ApplyFormatArgs(p))
1394 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001395
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001396 print(fmt % (p.FirstLBA, p.blocks, p.number,
1397 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001398 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001399
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001400 if not args.quick:
1401 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001402 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1403 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001404 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001405 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001406
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001407 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001408 if gpt.is_secondary:
1409 header = gpt.header
1410 else:
1411 f = args.image_file
1412 f.seek(gpt.header.BackupLBA * gpt.block_size)
1413 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001414 print(fmt % (header.PartitionEntriesStartingLBA,
1415 gpt.GetPartitionTableBlocks(header), '',
1416 'Sec GPT table'))
1417 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001418
Hung-Te Lin3b491672018-04-19 01:41:20 +08001419 # Check integrity after showing all fields.
1420 gpt.CheckIntegrity()
1421
Hung-Te Linfe724f82018-04-18 15:03:58 +08001422 class Prioritize(SubCommand):
1423 """Reorder the priority of all kernel partitions.
1424
1425 Reorder the priority of all active ChromeOS Kernel partitions.
1426
1427 With no options this will set the lowest active kernel to priority 1 while
1428 maintaining the original order.
1429 """
1430
1431 def DefineArgs(self, parser):
1432 parser.add_argument(
1433 '-P', '--priority', type=int,
1434 help=('Highest priority to use in the new ordering. '
1435 'The other partitions will be ranked in decreasing '
1436 'priority while preserving their original order. '
1437 'If necessary the lowest ranks will be coalesced. '
1438 'No active kernels will be lowered to priority 0.'))
1439 parser.add_argument(
1440 '-i', '--number', type=int,
1441 help='Specify the partition to make the highest in the new order.')
1442 parser.add_argument(
1443 '-f', '--friends', action='store_true',
1444 help=('Friends of the given partition (those with the same '
1445 'starting priority) are also updated to the new '
1446 'highest priority. '))
1447 parser.add_argument(
1448 'image_file', type=argparse.FileType('rb+'),
1449 help='Disk image file to prioritize.')
1450
1451 def Execute(self, args):
1452 gpt = GPT.LoadFromFile(args.image_file)
1453 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001454 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1455 groups = dict((k, list(g)) for k, g in itertools.groupby(
1456 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001457 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001458 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001459 if p not in parts:
1460 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001461 pri = p.Attributes.priority
1462 friends = groups.pop(pri)
1463 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001464 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001465 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001466 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001467 groups[new_pri] = [p]
1468 friends.remove(p)
1469 if friends:
1470 groups[pri] = friends
1471
1472 if 0 in groups:
1473 # Do not change any partitions with priority=0
1474 groups.pop(0)
1475
1476 prios = groups.keys()
1477 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001478
1479 # Max priority is 0xf.
1480 highest = min(args.priority or len(prios), 0xf)
1481 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001482
Hung-Te Lin138389f2018-05-15 17:55:00 +08001483 for i, pri in enumerate(prios):
1484 new_priority = max(1, highest - i)
1485 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001486 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001487 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001488 if old_priority == new_priority:
1489 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001490 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001491 if attrs.tries < 1 and not attrs.successful:
1492 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001493 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001494 logging.info('%s priority changed from %s to %s.', p, old_priority,
1495 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001496
1497 gpt.WriteToFile(args.image_file)
1498
Hung-Te Linf641d302018-04-18 15:09:35 +08001499 class Find(SubCommand):
1500 """Locate a partition by its GUID.
1501
1502 Find a partition by its UUID or label. With no specified DRIVE it scans all
1503 physical drives.
1504
1505 The partition type may also be given as one of these aliases:
1506
1507 firmware ChromeOS firmware
1508 kernel ChromeOS kernel
1509 rootfs ChromeOS rootfs
1510 data Linux data
1511 reserved ChromeOS reserved
1512 efi EFI System Partition
1513 unused Unused (nonexistent) partition
1514 """
1515 def DefineArgs(self, parser):
1516 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001517 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001518 help='Search for Partition Type GUID')
1519 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001520 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001521 help='Search for Partition Unique GUID')
1522 parser.add_argument(
1523 '-l', '--label',
1524 help='Search for Label')
1525 parser.add_argument(
1526 '-n', '--numeric', action='store_true',
1527 help='Numeric output only.')
1528 parser.add_argument(
1529 '-1', '--single-match', action='store_true',
1530 help='Fail if more than one match is found.')
1531 parser.add_argument(
1532 '-M', '--match-file', type=str,
1533 help='Matching partition data must also contain MATCH_FILE content.')
1534 parser.add_argument(
1535 '-O', '--offset', type=int, default=0,
1536 help='Byte offset into partition to match content (default 0).')
1537 parser.add_argument(
1538 'drive', type=argparse.FileType('rb+'), nargs='?',
1539 help='Drive or disk image file to find.')
1540
1541 def Execute(self, args):
1542 if not any((args.type_guid, args.unique_guid, args.label)):
1543 raise GPTError('You must specify at least one of -t, -u, or -l')
1544
1545 drives = [args.drive.name] if args.drive else (
1546 '/dev/%s' % name for name in subprocess.check_output(
1547 'lsblk -d -n -r -o name', shell=True).split())
1548
1549 match_pattern = None
1550 if args.match_file:
1551 with open(args.match_file) as f:
1552 match_pattern = f.read()
1553
1554 found = 0
1555 for drive in drives:
1556 try:
1557 gpt = GPT.LoadFromFile(drive)
1558 except GPTError:
1559 if args.drive:
1560 raise
1561 # When scanning all block devices on system, ignore failure.
1562
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001563 def Unmatch(a, b):
1564 return a is not None and a != b
1565
Hung-Te Linf641d302018-04-18 15:09:35 +08001566 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001567 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001568 Unmatch(args.label, p.Names) or
1569 Unmatch(args.unique_guid, p.UniqueGUID) or
1570 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001571 continue
1572 if match_pattern:
1573 with open(drive, 'rb') as f:
1574 f.seek(p.offset + args.offset)
1575 if f.read(len(match_pattern)) != match_pattern:
1576 continue
1577 # Found the partition, now print.
1578 found += 1
1579 if args.numeric:
1580 print(p.number)
1581 else:
1582 # This is actually more for block devices.
1583 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1584 p.number))
1585
1586 if found < 1 or (args.single_match and found > 1):
1587 return 1
1588 return 0
1589
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001590
1591def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001592 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001593 parser = argparse.ArgumentParser(description='GPT Utility.')
1594 parser.add_argument('--verbose', '-v', action='count', default=0,
1595 help='increase verbosity.')
1596 parser.add_argument('--debug', '-d', action='store_true',
1597 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001598 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001599
1600 args = parser.parse_args()
1601 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1602 if args.debug:
1603 log_level = logging.DEBUG
1604 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1605 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001606 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001607 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001608 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001609 sys.exit(code)
Hung-Te Linbad46112018-05-15 16:39:14 +08001610 elif isinstance(code, basestring):
1611 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001612 except Exception as e:
1613 if args.verbose or args.debug:
1614 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001615 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001616
1617
1618if __name__ == '__main__':
1619 main()