blob: ef042092229e7eb03c30fd8c9a5872917bf2ba0b [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 Linc772e1a2017-04-14 16:50:50 +080033import logging
34import os
Hung-Te Lin446eb512018-05-02 18:39:16 +080035import stat
Hung-Te Linc772e1a2017-04-14 16:50:50 +080036import struct
Hung-Te Linf641d302018-04-18 15:09:35 +080037import subprocess
38import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080039import uuid
40
41
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080042class StructError(Exception):
43 """Exceptions in packing and unpacking from/to struct fields."""
44 pass
Hung-Te Linc772e1a2017-04-14 16:50:50 +080045
Hung-Te Linc772e1a2017-04-14 16:50:50 +080046
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080047class StructField(object):
48 """Definition of a field in struct.
49
50 Attributes:
51 fmt: a format string for struct.{pack,unpack} to use.
52 name: a string for name of processed field.
53 """
54 __slots__ = ['fmt', 'name']
55
56 def __init__(self, fmt, name):
57 self.fmt = fmt
58 self.name = name
59
60 def Pack(self, value):
61 """"Packs given value from given format."""
62 del self # Unused.
63 return value
64
65 def Unpack(self, value):
66 """Unpacks given value into given format."""
67 del self # Unused.
68 return value
69
70
71class UTF16StructField(StructField):
72 """A field in UTF encoded string."""
73 __slots__ = ['encoding', 'max_length']
74 encoding = 'utf-16-le'
75
76 def __init__(self, max_length, name):
77 self.max_length = max_length
78 fmt = '%ds' % max_length
79 super(UTF16StructField, self).__init__(fmt, name)
80
81 def Pack(self, value):
82 new_value = value.encode(self.encoding)
83 if len(new_value) >= self.max_length:
84 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
85 (value, self.name, self.max_length))
86 return new_value
87
88 def Unpack(self, value):
89 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080090
Hung-Te Linbf8aa272018-04-19 03:02:29 +080091
92class GUID(uuid.UUID):
93 """A special UUID that defaults to upper case in str()."""
94
95 def __str__(self):
96 """Returns GUID in upper case."""
97 return super(GUID, self).__str__().upper()
98
99 @staticmethod
100 def Random():
101 return uuid.uuid4()
102
103
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800104class GUIDStructField(StructField):
105 """A GUID field."""
106
107 def __init__(self, name):
108 super(GUIDStructField, self).__init__('16s', name)
109
110 def Pack(self, value):
111 if value is None:
112 return '\x00' * 16
113 if not isinstance(value, uuid.UUID):
114 raise StructError('Field %s needs a GUID value instead of [%r].' %
115 (self.name, value))
116 return value.bytes_le
117
118 def Unpack(self, value):
119 return GUID(bytes_le=value)
120
121
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800122def BitProperty(getter, setter, shift, mask):
123 """A generator for bit-field properties.
124
125 This is used inside a class to manipulate an integer-like variable using
126 properties. The getter and setter should be member functions to change the
127 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800128
129 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800130 getter: a function to read integer type variable (for all the bits).
131 setter: a function to set the new changed integer type variable.
132 shift: integer for how many bits should be shifted (right).
133 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800134 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800135 def _getter(self):
136 return (getter(self) >> shift) & mask
137 def _setter(self, value):
138 assert value & mask == value, (
139 'Value %s out of range (mask=%s)' % (value, mask))
140 setter(self, getter(self) & ~(mask << shift) | value << shift)
141 return property(_getter, _setter)
142
143
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800144class PartitionAttributes(object):
145 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800146
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800147 This can be created using Partition.attrs, but the changed properties won't
148 apply to underlying Partition until an explicit call with
149 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800150 """
151
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800152 def __init__(self, attrs):
153 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800154
155 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800156 def raw(self):
157 """Returns the raw integer type attributes."""
158 return self._Get()
159
160 def _Get(self):
161 return self._attrs
162
163 def _Set(self, value):
164 self._attrs = value
165
166 successful = BitProperty(_Get, _Set, 56, 1)
167 tries = BitProperty(_Get, _Set, 52, 0xf)
168 priority = BitProperty(_Get, _Set, 48, 0xf)
169 legacy_boot = BitProperty(_Get, _Set, 2, 1)
170 required = BitProperty(_Get, _Set, 0, 1)
171 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
172
173
174class PartitionAttributeStructField(StructField):
175
176 def Pack(self, value):
177 if not isinstance(value, PartitionAttributes):
178 raise StructError('Given value %r is not %s.' %
179 (value, PartitionAttributes.__name__))
180 return value.raw
181
182 def Unpack(self, value):
183 return PartitionAttributes(value)
184
185
186# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
187# declared as 'signed' (l) instead of 'unsigned' (L).
188# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
189HEADER_FIELDS = [
190 StructField('8s', 'Signature'),
191 StructField('4s', 'Revision'),
192 StructField('L', 'HeaderSize'),
193 StructField('l', 'CRC32'),
194 StructField('4s', 'Reserved'),
195 StructField('Q', 'CurrentLBA'),
196 StructField('Q', 'BackupLBA'),
197 StructField('Q', 'FirstUsableLBA'),
198 StructField('Q', 'LastUsableLBA'),
199 GUIDStructField('DiskGUID'),
200 StructField('Q', 'PartitionEntriesStartingLBA'),
201 StructField('L', 'PartitionEntriesNumber'),
202 StructField('L', 'PartitionEntrySize'),
203 StructField('l', 'PartitionArrayCRC32'),
204]
205
206# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
207PARTITION_FIELDS = [
208 GUIDStructField('TypeGUID'),
209 GUIDStructField('UniqueGUID'),
210 StructField('Q', 'FirstLBA'),
211 StructField('Q', 'LastLBA'),
212 PartitionAttributeStructField('Q', 'Attributes'),
213 UTF16StructField(72, 'Names'),
214]
215
216# The PMBR has so many variants. The basic format is defined in
217# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
218# implementation, as derived from `cgpt`, is following syslinux as:
219# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
220PMBR_FIELDS = [
221 StructField('424s', 'BootCode'),
222 GUIDStructField('BootGUID'),
223 StructField('L', 'DiskID'),
224 StructField('2s', 'Magic'),
225 StructField('16s', 'LegacyPart0'),
226 StructField('16s', 'LegacyPart1'),
227 StructField('16s', 'LegacyPart2'),
228 StructField('16s', 'LegacyPart3'),
229 StructField('2s', 'Signature'),
230]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800231
232
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800233class GPTError(Exception):
234 """All exceptions by GPT."""
235 pass
236
237
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800238class GPTObject(object):
239 """A base object in GUID Partition Table.
240
241 All objects (for instance, header or partition entries) must inherit this
242 class and define the FIELD attribute with a list of field definitions using
243 StructField.
244
245 The 'name' in StructField will become the attribute name of GPT objects that
246 can be directly packed into / unpacked from. Derived (calculated from existing
247 attributes) attributes should be in lower_case.
248
249 It is also possible to attach some additional properties to the object as meta
250 data (for example path of the underlying image file). To do that, first
251 include it in __slots__ list and specify them as dictionary-type args in
252 constructors. These properties will be preserved when you call Clone().
253
254 To create a new object, call the constructor. Field data can be assigned as
255 in arguments, or give nothing to initialize as zero (see Zero()). Field data
256 and meta values can be also specified in keyword arguments (**kargs) at the
257 same time.
258
259 To read a object from file or stream, use class method ReadFrom(source).
260 To make changes, modify the field directly or use Update(dict), or create a
261 copy by Clone() first then Update.
262
263 To wipe all fields (but not meta), call Zero(). There is currently no way
264 to clear meta except setting them to None one by one.
265 """
266 __slots__ = []
267
268 FIELDS = None
269 """A list of StructField definitions."""
270
271 def __init__(self, *args, **kargs):
272 if args:
273 if len(args) != len(self.FIELDS):
274 raise GPTError('%s need %s arguments (found %s).' %
275 (type(self).__name__, len(self.FIELDS), len(args)))
276 for f, value in zip(self.FIELDS, args):
277 setattr(self, f.name, value)
278 else:
279 self.Zero()
280
281 all_names = [f for f in self.__slots__]
282 for name, value in kargs.iteritems():
283 if name not in all_names:
284 raise GPTError('%s does not support keyword arg <%s>.' %
285 (type(self).__name__, name))
286 setattr(self, name, value)
287
288 def __iter__(self):
289 """An iterator to return all fields associated in the object."""
290 return (getattr(self, f.name) for f in self.FIELDS)
291
292 def __repr__(self):
293 return '(%s: %s)' % (type(self).__name__, ', '.join(
294 '%s=%r' %(f, getattr(self, f)) for f in self.__slots__))
295
296 @classmethod
297 def GetStructFormat(cls):
298 """Returns a format string for struct to use."""
299 return '<' + ''.join(f.fmt for f in cls.FIELDS)
300
301 @classmethod
302 def ReadFrom(cls, source, **kargs):
303 """Returns an object from given source."""
304 obj = cls(**kargs)
305 obj.Unpack(source)
306 return obj
307
308 @property
309 def blob(self):
310 """The (packed) blob representation of the object."""
311 return self.Pack()
312
313 @property
314 def meta(self):
315 """Meta values (those not in GPT object fields)."""
316 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
317 return dict((name, getattr(self, name)) for name in metas)
318
319 def Unpack(self, source):
320 """Unpacks values from a given source.
321
322 Args:
323 source: a string of bytes or a file-like object to read from.
324 """
325 fmt = self.GetStructFormat()
326 if source is None:
327 source = '\x00' * struct.calcsize(fmt)
328 if not isinstance(source, basestring):
329 return self.Unpack(source.read(struct.calcsize(fmt)))
330 for f, value in zip(self.FIELDS, struct.unpack(fmt, source)):
331 setattr(self, f.name, f.Unpack(value))
332
333 def Pack(self):
334 """Packs values in all fields into a string by struct format."""
335 return struct.pack(self.GetStructFormat(),
336 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
337
338 def Clone(self):
339 """Clones a new instance."""
340 return type(self)(*self, **self.meta)
341
342 def Update(self, **dargs):
343 """Applies multiple values in current object."""
344 for name, value in dargs.iteritems():
345 setattr(self, name, value)
346
347 def Zero(self):
348 """Set all fields to values representing zero or empty.
349
350 Note the meta attributes won't be cleared.
351 """
352 class ZeroReader(object):
353 """A /dev/zero like stream."""
354
355 @staticmethod
356 def read(num):
357 return '\x00' * num
358
359 self.Unpack(ZeroReader())
360
361
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800362class GPT(object):
363 """A GPT helper class.
364
365 To load GPT from an existing disk image file, use `LoadFromFile`.
366 After modifications were made, use `WriteToFile` to commit changes.
367
368 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800369 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800370 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800371 partitions: a list of GPT partition entry nametuple.
372 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800373 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800374 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800375 DEFAULT_BLOCK_SIZE = 512
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800376 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800377 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
378 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Linux data',
379 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
380 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
381 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
382 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
383 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800384 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800385 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800386 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
387 for k, v in TYPE_GUID_MAP.iteritems())
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800388 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
389 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
390 TYPE_GUID_LIST_BOOTABLE = [
391 TYPE_GUID_CHROMEOS_KERNEL,
392 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800393 ]
394
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800395 class ProtectiveMBR(GPTObject):
396 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800397 FIELDS = PMBR_FIELDS
398 __slots__ = [f.name for f in FIELDS]
399
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800400 SIGNATURE = '\x55\xAA'
401 MAGIC = '\x1d\x9a'
402
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800403 class Header(GPTObject):
404 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800405 FIELDS = HEADER_FIELDS
406 __slots__ = [f.name for f in FIELDS]
407
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800408 SIGNATURES = ['EFI PART', 'CHROMEOS']
409 SIGNATURE_IGNORE = 'IGNOREME'
410 DEFAULT_REVISION = '\x00\x00\x01\x00'
411
412 DEFAULT_PARTITION_ENTRIES = 128
413 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
414
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800415 @classmethod
416 def Create(cls, size, block_size, pad_blocks=0,
417 part_entries=DEFAULT_PARTITION_ENTRIES):
418 """Creates a header with default values.
419
420 Args:
421 size: integer of expected image size.
422 block_size: integer for size of each block (sector).
423 pad_blocks: number of preserved sectors between header and partitions.
424 part_entries: number of partitions to include in header.
425 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800426 PART_FORMAT = GPT.Partition.GetStructFormat()
427 FORMAT = cls.GetStructFormat()
428 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800429 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
430 parts_bytes = part_entries * part_entry_size
431 parts_blocks = parts_bytes / block_size
432 if parts_bytes % block_size:
433 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800434 # CRC32 and PartitionsCRC32 must be updated later explicitly.
435 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800436 Signature=cls.SIGNATURES[0],
437 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800438 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800439 CurrentLBA=1,
440 BackupLBA=size / block_size - 1,
441 FirstUsableLBA=parts_lba + parts_blocks,
442 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800443 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800444 PartitionEntriesStartingLBA=parts_lba,
445 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800446 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800447
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800448 def UpdateChecksum(self):
449 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800450
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800451 Note the PartitionArrayCRC32 is not touched - you have to make sure that
452 is correct before calling Header.UpdateChecksum().
453 """
454 self.Update(CRC32=0)
455 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800456
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800457 class Partition(GPTObject):
458 """The partition entry in GPT.
459
460 Please include following properties when creating a Partition object:
461 - image: a string for path to the image file the partition maps to.
462 - number: the 1-based partition number.
463 - block_size: an integer for size of each block (LBA, or sector).
464 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800465 FIELDS = PARTITION_FIELDS
466 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800467 NAMES_ENCODING = 'utf-16-le'
468 NAMES_LENGTH = 72
469
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800470 def __str__(self):
471 return '%s#%s' % (self.image, self.number)
472
473 def IsUnused(self):
474 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800475 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800476
Hung-Te Linfe724f82018-04-18 15:03:58 +0800477 def IsChromeOSKernel(self):
478 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800479 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800480
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800481 @property
482 def blocks(self):
483 """Return size of partition in blocks (see block_size)."""
484 return self.LastLBA - self.FirstLBA + 1
485
486 @property
487 def offset(self):
488 """Returns offset to partition in bytes."""
489 return self.FirstLBA * self.block_size
490
491 @property
492 def size(self):
493 """Returns size of partition in bytes."""
494 return self.blocks * self.block_size
495
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800496 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800497 """GPT constructor.
498
499 See LoadFromFile for how it's usually used.
500 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800501 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800502 self.header = None
503 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800504 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800505 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800506
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800507 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800508 def GetTypeGUID(cls, value):
509 """The value may be a GUID in string or a short type string."""
510 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
511 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800512
513 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800514 def Create(cls, image_name, size, block_size, pad_blocks=0):
515 """Creates a new GPT instance from given size and block_size.
516
517 Args:
518 image_name: a string of underlying disk image file name.
519 size: expected size of disk image.
520 block_size: size of each block (sector) in bytes.
521 pad_blocks: number of blocks between header and partitions array.
522 """
523 gpt = cls()
524 gpt.block_size = block_size
525 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
526 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800527 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800528 for i in xrange(gpt.header.PartitionEntriesNumber)]
529 return gpt
530
Hung-Te Lin446eb512018-05-02 18:39:16 +0800531 @staticmethod
532 def IsBlockDevice(image):
533 """Returns if the image is a block device file."""
534 return stat.S_ISBLK(os.stat(image).st_mode)
535
536 @classmethod
537 def GetImageSize(cls, image):
538 """Returns the size of specified image (plain or block device file)."""
539 if not cls.IsBlockDevice(image):
540 return os.path.getsize(image)
541
542 fd = os.open(image, os.O_RDONLY)
543 try:
544 return os.lseek(fd, 0, os.SEEK_END)
545 finally:
546 os.close(fd)
547
548 @classmethod
549 def GetLogicalBlockSize(cls, block_dev):
550 """Returns the logical block (sector) size from a block device file.
551
552 The underlying call is BLKSSZGET. An alternative command is blockdev,
553 but that needs root permission even if we just want to get sector size.
554 """
555 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
556 return int(subprocess.check_output(
557 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
558
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800559 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800560 def LoadFromFile(cls, image):
561 """Loads a GPT table from give disk image file object.
562
563 Args:
564 image: a string as file path or a file-like object to read from.
565 """
566 if isinstance(image, basestring):
567 with open(image, 'rb') as f:
568 return cls.LoadFromFile(f)
569
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800570 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800571 image.seek(0)
572 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
573 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
574 logging.debug('Found MBR signature in %s', image.name)
575 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
576 logging.debug('Found PMBR in %s', image.name)
577 gpt.pmbr = pmbr
578
Hung-Te Linf148d322018-04-13 10:24:42 +0800579 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800580 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
581 if cls.IsBlockDevice(image.name):
582 block_sizes = [cls.GetLogicalBlockSize(image.name)]
583
584 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800585 # Note because there are devices setting Primary as ignored and the
586 # partition table signature accepts 'CHROMEOS' which is also used by
587 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
588 # first before trying other block sizes, otherwise we may incorrectly
589 # identify a kernel partition as LBA 1 of larger block size system.
590 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
591 (-block_size, os.SEEK_END)]):
592 image.seek(*seek)
593 header = gpt.Header.ReadFrom(image)
594 if header.Signature in cls.Header.SIGNATURES:
595 gpt.block_size = block_size
596 if i != 0:
597 gpt.is_secondary = True
598 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800599 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800600 else:
601 # Nothing found, try next block size.
602 continue
603 # Found a valid signature.
604 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800605 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800606 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800607
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800608 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800609 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800610 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800611 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800612 return p
613
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800614 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800615 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800616 ReadPartition(image, i + 1)
617 for i in xrange(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800618 return gpt
619
Hung-Te Linc5196682018-04-18 22:59:59 +0800620 def GetUsedPartitions(self):
621 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800622
Hung-Te Linc5196682018-04-18 22:59:59 +0800623 Use 'number' property to find the real location of partition in
624 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800625 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800626 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800627
628 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800629 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800630 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800631 return (max(p.LastLBA for p in parts)
632 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800633
634 def GetPartitionTableBlocks(self, header=None):
635 """Returns the blocks (or LBA) of partition table from given header."""
636 if header is None:
637 header = self.header
638 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800639 blocks = size / self.block_size
640 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800641 blocks += 1
642 return blocks
643
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800644 def GetPartition(self, number):
645 """Gets the Partition by given (1-based) partition number.
646
647 Args:
648 number: an integer as 1-based partition number.
649 """
650 if not 0 < number <= len(self.partitions):
651 raise GPTError('Invalid partition number %s.' % number)
652 return self.partitions[number - 1]
653
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800654 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800655 """Updates the entry in partition table by given Partition object.
656
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800657 Usually you only need to call this if you want to copy one partition to
658 different location (number of image).
659
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800660 Args:
661 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800662 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800663 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800664 ref = self.partitions[number - 1]
665 part = part.Clone()
666 part.number = number
667 part.image = ref.image
668 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800669 self.partitions[number - 1] = part
670
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800671 def Resize(self, new_size):
672 """Adjust GPT for a disk image in given size.
673
674 Args:
675 new_size: Integer for new size of disk image file.
676 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800677 old_size = self.block_size * (self.header.BackupLBA + 1)
678 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800679 raise GPTError(
680 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800681 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800682 if old_size != new_size:
683 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800684 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800685 else:
686 logging.info('Image size (%d, LBA=%d) not changed.',
687 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800688 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800689
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800690 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800691 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800692 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800693
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800694 if last_usable_lba < self.header.LastUsableLBA:
695 max_used_lba = self.GetMaxUsedLBA()
696 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800697 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800698
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800699 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800700
701 def GetFreeSpace(self):
702 """Returns the free (available) space left according to LastUsableLBA."""
703 max_lba = self.GetMaxUsedLBA()
704 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800705 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800706
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800707 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800708 """Expands a given partition to last usable LBA.
709
710 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800711 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800712
713 Returns:
714 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800715 """
716 # Assume no partitions overlap, we need to make sure partition[i] has
717 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800718 p = self.GetPartition(number)
719 if p.IsUnused():
720 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800721 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800722 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800723 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800724 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800725 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800726
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800727 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800728 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800729 new_blocks = p.blocks
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800730 logging.warn(
731 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
732 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800733
Hung-Te Lin3b491672018-04-19 01:41:20 +0800734 def CheckIntegrity(self):
735 """Checks if the GPT objects all look good."""
736 # Check if the header allocation looks good. CurrentLBA and
737 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
738 # LastUsableLBA].
739 header = self.header
740 entries_first_lba = header.PartitionEntriesStartingLBA
741 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
742
743 def CheckOutsideUsable(name, lba, outside_entries=False):
744 if lba < 1:
745 raise GPTError('%s should not live in LBA %s.' % (name, lba))
746 if lba > max(header.BackupLBA, header.CurrentLBA):
747 # Note this is "in theory" possible, but we want to report this as
748 # error as well, since it usually leads to error.
749 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
750 (name, lba, header.BackupLBA))
751 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
752 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
753 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
754 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
755 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
756 (name, lba, entries_first_lba, entries_last_lba))
757 CheckOutsideUsable('Header', header.CurrentLBA, True)
758 CheckOutsideUsable('Backup header', header.BackupLBA, True)
759 CheckOutsideUsable('Partition entries', entries_first_lba)
760 CheckOutsideUsable('Partition entries end', entries_last_lba)
761
762 parts = self.GetUsedPartitions()
763 # Check if partition entries overlap with each other.
764 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
765 lba_list.sort(key=lambda t: t[0])
766 for i in xrange(len(lba_list) - 1):
767 if lba_list[i][1] >= lba_list[i + 1][0]:
768 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
769 (lba_list[i] + lba_list[i + 1]))
770 # Now, check the first and last partition.
771 if lba_list:
772 p = lba_list[0][2]
773 if p.FirstLBA < header.FirstUsableLBA:
774 raise GPTError(
775 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
776 (p, p.FirstLBA, header.FirstLBA))
777 p = lba_list[-1][2]
778 if p.LastLBA > header.LastUsableLBA:
779 raise GPTError(
780 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
781 (p, p.LastLBA, header.LastLBA))
782 # Check if UniqueGUIDs are not unique.
783 if len(set(p.UniqueGUID for p in parts)) != len(parts):
784 raise GPTError('Partition UniqueGUIDs are duplicated.')
785 # Check if CRCs match.
786 if (binascii.crc32(''.join(p.blob for p in self.partitions)) !=
787 header.PartitionArrayCRC32):
788 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800789 header_crc = header.Clone()
790 header_crc.UpdateChecksum()
791 if header_crc.CRC32 != header.CRC32:
792 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800793
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800794 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800795 """Updates all checksum fields in GPT objects."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800796 parts = ''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800797 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
798 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800799
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800800 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800801 """Returns the backup header according to given header.
802
803 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
804 fields.
805 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800806 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800807 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800808 h = header.Clone()
809 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800810 BackupLBA=header.CurrentLBA,
811 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800812 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800813 h.UpdateChecksum()
814 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800815
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800816 @classmethod
817 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
818 """Writes a protective MBR to given file.
819
820 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
821 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
822 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
823 we can follow that.
824
825 Args:
826 create: True to re-create PMBR structure.
827 bootcode: a blob of new boot code.
828 boot_guid a blob for new boot GUID.
829
830 Returns:
831 The written PMBR structure.
832 """
833 if isinstance(image, basestring):
834 with open(image, 'rb+') as f:
835 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
836
837 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800838 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
839 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800840 pmbr = cls.ProtectiveMBR.ReadFrom(image)
841
842 if create:
843 legacy_sectors = min(
844 0x100000000,
Hung-Te Lin446eb512018-05-02 18:39:16 +0800845 GPT.GetImageSize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800846 # Partition 0 must have have the fixed CHS with number of sectors
847 # (calculated as legacy_sectors later).
848 part0 = ('00000200eeffffff01000000'.decode('hex') +
849 struct.pack('<I', legacy_sectors))
850 # Partition 1~3 should be all zero.
851 part1 = '\x00' * 16
852 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800853 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800854 BootGUID=cls.TYPE_GUID_UNUSED,
855 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800856 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800857 LegacyPart0=part0,
858 LegacyPart1=part1,
859 LegacyPart2=part1,
860 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800861 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800862
863 if bootcode:
864 if len(bootcode) > len(pmbr.BootCode):
865 logging.info(
866 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
867 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800868 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800869 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800870 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800871
872 blob = pmbr.blob
873 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
874 image.seek(0)
875 image.write(blob)
876 return pmbr
877
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800878 def WriteToFile(self, image):
879 """Updates partition table in a disk image file.
880
881 Args:
882 image: a string as file path or a file-like object to write into.
883 """
884 if isinstance(image, basestring):
885 with open(image, 'rb+') as f:
886 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800887
888 def WriteData(name, blob, lba):
889 """Writes a blob into given location."""
890 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800891 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800892 image.seek(lba * self.block_size)
893 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800894
895 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800896 self.CheckIntegrity()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800897 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800898
899 header = self.header
900 WriteData('GPT Header', header.blob, header.CurrentLBA)
901 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
902 logging.info(
903 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
904 header.LastUsableLBA)
905
906 if not self.is_secondary:
907 # When is_secondary is True, the header we have is actually backup header.
908 backup_header = self.GetBackupHeader(self.header)
909 WriteData(
910 'Backup Partitions', parts_blob,
911 backup_header.PartitionEntriesStartingLBA)
912 WriteData(
913 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800914
915
916class GPTCommands(object):
917 """Collection of GPT sub commands for command line to use.
918
919 The commands are derived from `cgpt`, but not necessary to be 100% compatible
920 with cgpt.
921 """
922
923 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800924 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800925 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800926 ('type', 'type guid'),
927 ('unique', 'unique guid'),
928 ('label', 'label'),
929 ('Successful', 'Successful flag'),
930 ('Tries', 'Tries flag'),
931 ('Priority', 'Priority flag'),
932 ('Legacy', 'Legacy Boot flag'),
933 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800934
935 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800936 commands = dict(
937 (command.lower(), getattr(self, command)())
938 for command in dir(self)
939 if (isinstance(getattr(self, command), type) and
940 issubclass(getattr(self, command), self.SubCommand) and
941 getattr(self, command) is not self.SubCommand)
942 )
943 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800944
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800945 def DefineArgs(self, parser):
946 """Defines all available commands to an argparser subparsers instance."""
947 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
948 for name, instance in sorted(self.commands.iteritems()):
949 parser = subparsers.add_parser(
950 name, description=instance.__doc__,
951 formatter_class=argparse.RawDescriptionHelpFormatter,
952 help=instance.__doc__.splitlines()[0])
953 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800954
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800955 def Execute(self, args):
956 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800957 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800958
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800959 class SubCommand(object):
960 """A base class for sub commands to derive from."""
961
962 def DefineArgs(self, parser):
963 """Defines command line arguments to argparse parser.
964
965 Args:
966 parser: An argparse parser instance.
967 """
968 del parser # Unused.
969 raise NotImplementedError
970
971 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800972 """Execute the command with parsed arguments.
973
974 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800975
976 Args:
977 args: An argparse parsed namespace.
978 """
979 del args # Unused.
980 raise NotImplementedError
981
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800982 def ExecuteCommandLine(self, *args):
983 """Execute as invoked from command line.
984
985 This provides an easy way to execute particular sub command without
986 creating argument parser explicitly.
987
988 Args:
989 args: a list of string type command line arguments.
990 """
991 parser = argparse.ArgumentParser()
992 self.DefineArgs(parser)
993 return self.Execute(parser.parse_args(args))
994
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800995 class Create(SubCommand):
996 """Create or reset GPT headers and tables.
997
998 Create or reset an empty GPT.
999 """
1000
1001 def DefineArgs(self, parser):
1002 parser.add_argument(
1003 '-z', '--zero', action='store_true',
1004 help='Zero the sectors of the GPT table and entries')
1005 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001006 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001007 help=('Size (in blocks) of the disk to pad between the '
1008 'primary GPT header and its entries, default %(default)s'))
1009 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001010 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001011 help='Size of each block (sector) in bytes.')
1012 parser.add_argument(
1013 'image_file', type=argparse.FileType('rb+'),
1014 help='Disk image file to create.')
1015
1016 def Execute(self, args):
1017 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001018 if block_size is None:
1019 if GPT.IsBlockDevice(args.image_file.name):
1020 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1021 else:
1022 block_size = GPT.DEFAULT_BLOCK_SIZE
1023
1024 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1025 logging.info('Block (sector) size for %s is set to %s bytes.',
1026 args.image_file.name, block_size)
1027
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001028 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001029 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001030 block_size, args.pad_blocks)
1031 if args.zero:
1032 # In theory we only need to clear LBA 1, but to make sure images already
1033 # initialized with different block size won't have GPT signature in
1034 # different locations, we should zero until first usable LBA.
1035 args.image_file.seek(0)
1036 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
1037 gpt.WriteToFile(args.image_file)
1038 print('OK: Created GPT for %s' % args.image_file.name)
1039
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001040 class Boot(SubCommand):
1041 """Edit the PMBR sector for legacy BIOSes.
1042
1043 With no options, it will just print the PMBR boot guid.
1044 """
1045
1046 def DefineArgs(self, parser):
1047 parser.add_argument(
1048 '-i', '--number', type=int,
1049 help='Set bootable partition')
1050 parser.add_argument(
1051 '-b', '--bootloader', type=argparse.FileType('r'),
1052 help='Install bootloader code in the PMBR')
1053 parser.add_argument(
1054 '-p', '--pmbr', action='store_true',
1055 help='Create legacy PMBR partition table')
1056 parser.add_argument(
1057 'image_file', type=argparse.FileType('rb+'),
1058 help='Disk image file to change PMBR.')
1059
1060 def Execute(self, args):
1061 """Rebuilds the protective MBR."""
1062 bootcode = args.bootloader.read() if args.bootloader else None
1063 boot_guid = None
1064 if args.number is not None:
1065 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001066 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001067 pmbr = GPT.WriteProtectiveMBR(
1068 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1069
You-Cheng Syufff7f422018-05-14 15:37:39 +08001070 print(pmbr.BootGUID)
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001071
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001072
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001073 class Legacy(SubCommand):
1074 """Switch between GPT and Legacy GPT.
1075
1076 Switch GPT header signature to "CHROMEOS".
1077 """
1078
1079 def DefineArgs(self, parser):
1080 parser.add_argument(
1081 '-e', '--efi', action='store_true',
1082 help='Switch GPT header signature back to "EFI PART"')
1083 parser.add_argument(
1084 '-p', '--primary-ignore', action='store_true',
1085 help='Switch primary GPT header signature to "IGNOREME"')
1086 parser.add_argument(
1087 'image_file', type=argparse.FileType('rb+'),
1088 help='Disk image file to change.')
1089
1090 def Execute(self, args):
1091 gpt = GPT.LoadFromFile(args.image_file)
1092 # cgpt behavior: if -p is specified, -e is ignored.
1093 if args.primary_ignore:
1094 if gpt.is_secondary:
1095 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1096 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1097 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1098 gpt.header = gpt.GetBackupHeader(self.header)
1099 gpt.is_secondary = True
1100 else:
1101 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001102 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001103 gpt.WriteToFile(args.image_file)
1104 if args.primary_ignore:
1105 print('OK: Set %s primary GPT header to %s.' %
1106 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
1107 else:
1108 print('OK: Changed GPT signature for %s to %s.' %
1109 (args.image_file.name, new_signature))
1110
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001111 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001112 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001113
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001114 def DefineArgs(self, parser):
1115 parser.add_argument(
1116 'image_file', type=argparse.FileType('rb+'),
1117 help='Disk image file to repair.')
1118
1119 def Execute(self, args):
1120 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001121 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001122 gpt.WriteToFile(args.image_file)
1123 print('Disk image file %s repaired.' % args.image_file.name)
1124
1125 class Expand(SubCommand):
1126 """Expands a GPT partition to all available free space."""
1127
1128 def DefineArgs(self, parser):
1129 parser.add_argument(
1130 '-i', '--number', type=int, required=True,
1131 help='The partition to expand.')
1132 parser.add_argument(
1133 'image_file', type=argparse.FileType('rb+'),
1134 help='Disk image file to modify.')
1135
1136 def Execute(self, args):
1137 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001138 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001139 gpt.WriteToFile(args.image_file)
1140 if old_blocks < new_blocks:
1141 print(
1142 'Partition %s on disk image file %s has been extended '
1143 'from %s to %s .' %
1144 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1145 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001146 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001147 print('Nothing to expand for disk image %s partition %s.' %
1148 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001149
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001150 class Add(SubCommand):
1151 """Add, edit, or remove a partition entry.
1152
1153 Use the -i option to modify an existing partition.
1154 The -b, -s, and -t options must be given for new partitions.
1155
1156 The partition type may also be given as one of these aliases:
1157
1158 firmware ChromeOS firmware
1159 kernel ChromeOS kernel
1160 rootfs ChromeOS rootfs
1161 data Linux data
1162 reserved ChromeOS reserved
1163 efi EFI System Partition
1164 unused Unused (nonexistent) partition
1165 """
1166 def DefineArgs(self, parser):
1167 parser.add_argument(
1168 '-i', '--number', type=int,
1169 help='Specify partition (default is next available)')
1170 parser.add_argument(
1171 '-b', '--begin', type=int,
1172 help='Beginning sector')
1173 parser.add_argument(
1174 '-s', '--sectors', type=int,
1175 help='Size in sectors (logical blocks).')
1176 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001177 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001178 help='Partition Type GUID')
1179 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001180 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001181 help='Partition Unique ID')
1182 parser.add_argument(
1183 '-l', '--label',
1184 help='Label')
1185 parser.add_argument(
1186 '-S', '--successful', type=int, choices=xrange(2),
1187 help='set Successful flag')
1188 parser.add_argument(
1189 '-T', '--tries', type=int,
1190 help='set Tries flag (0-15)')
1191 parser.add_argument(
1192 '-P', '--priority', type=int,
1193 help='set Priority flag (0-15)')
1194 parser.add_argument(
1195 '-R', '--required', type=int, choices=xrange(2),
1196 help='set Required flag')
1197 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001198 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001199 choices=xrange(2),
1200 help='set Legacy Boot flag')
1201 parser.add_argument(
1202 '-A', '--attribute', dest='raw_16', type=int,
1203 help='set raw 16-bit attribute value (bits 48-63)')
1204 parser.add_argument(
1205 'image_file', type=argparse.FileType('rb+'),
1206 help='Disk image file to modify.')
1207
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001208 def Execute(self, args):
1209 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001210 number = args.number
1211 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001212 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001213
1214 # First and last LBA must be calculated explicitly because the given
1215 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001216 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001217 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001218
1219 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001220 part.Zero()
1221 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001222 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001223 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001224 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001225 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001226
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001227 def UpdateAttr(name):
1228 value = getattr(args, name)
1229 if value is None:
1230 return
1231 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001232
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001233 def GetArg(arg_value, default_value):
1234 return default_value if arg_value is None else arg_value
1235
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001236 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001237 for name in ['legacy_boot', 'required', 'priority', 'tries',
1238 'successful', 'raw_16']:
1239 UpdateAttr(name)
1240 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001241 part.Update(
1242 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001243 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001244 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1245 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1246 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1247 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001248
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001249 # Wipe partition again if it should be empty.
1250 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001251 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001252
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001253 gpt.WriteToFile(args.image_file)
1254 if part.IsUnused():
1255 # If we do ('%s' % part) there will be TypeError.
1256 print('OK: Deleted (zeroed) %s.' % (part,))
1257 else:
1258 print('OK: %s %s (%s+%s).' %
1259 ('Added' if is_new_part else 'Modified',
1260 part, part.FirstLBA, part.blocks))
1261
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001262 class Show(SubCommand):
1263 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001264
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001265 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001266 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001267
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001268 def DefineArgs(self, parser):
1269 parser.add_argument(
1270 '--numeric', '-n', action='store_true',
1271 help='Numeric output only.')
1272 parser.add_argument(
1273 '--quick', '-q', action='store_true',
1274 help='Quick output.')
1275 parser.add_argument(
1276 '-i', '--number', type=int,
1277 help='Show specified partition only, with format args.')
1278 for name, help_str in GPTCommands.FORMAT_ARGS:
1279 # TODO(hungte) Alert if multiple args were specified.
1280 parser.add_argument(
1281 '--%s' % name, '-%c' % name[0], action='store_true',
1282 help='[format] %s.' % help_str)
1283 parser.add_argument(
1284 'image_file', type=argparse.FileType('rb'),
1285 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001286
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001287 def Execute(self, args):
1288 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001289
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001290 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001291 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001292 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001293 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001294 if names:
1295 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001296 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001297
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001298 def IsBootableType(guid):
1299 if not guid:
1300 return False
1301 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001302
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001303 def FormatAttribute(attrs, chromeos_kernel=False):
1304 if args.numeric:
1305 return '[%x]' % (attrs.raw >> 48)
1306 results = []
1307 if chromeos_kernel:
1308 results += [
1309 'priority=%d' % attrs.priority,
1310 'tries=%d' % attrs.tries,
1311 'successful=%d' % attrs.successful]
1312 if attrs.required:
1313 results += ['required=1']
1314 if attrs.legacy_boot:
1315 results += ['legacy_boot=1']
1316 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001317
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001318 def ApplyFormatArgs(p):
1319 if args.begin:
1320 return p.FirstLBA
1321 elif args.size:
1322 return p.blocks
1323 elif args.type:
1324 return FormatTypeGUID(p)
1325 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001326 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001327 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001328 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001329 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001330 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001331 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001332 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001333 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001334 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001335 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001336 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001337 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001338 return '[%x]' % (p.Attributes.raw >> 48)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001339 else:
1340 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001341
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001342 def IsFormatArgsSpecified():
1343 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001344
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001345 gpt = GPT.LoadFromFile(args.image_file)
1346 logging.debug('%r', gpt.header)
1347 fmt = '%12s %11s %7s %s'
1348 fmt2 = '%32s %s: %s'
1349 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001350
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001351 if IsFormatArgsSpecified() and args.number is None:
1352 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001353
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001354 if not (args.number is None or
1355 0 < args.number <= gpt.header.PartitionEntriesNumber):
1356 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001357
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001358 partitions = gpt.partitions
1359 do_print_gpt_blocks = False
1360 if not (args.quick or IsFormatArgsSpecified()):
1361 print(fmt % header)
1362 if args.number is None:
1363 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001364
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001365 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001366 if gpt.pmbr:
1367 print(fmt % (0, 1, '', 'PMBR'))
1368 if gpt.is_secondary:
1369 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1370 else:
1371 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1372 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1373 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001374
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001375 for p in partitions:
1376 if args.number is None:
1377 # Skip unused partitions.
1378 if p.IsUnused():
1379 continue
1380 elif p.number != args.number:
1381 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001382
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001383 if IsFormatArgsSpecified():
1384 print(ApplyFormatArgs(p))
1385 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001386
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001387 print(fmt % (p.FirstLBA, p.blocks, p.number,
1388 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001389 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001390
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001391 if not args.quick:
1392 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001393 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1394 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001395 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001396 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001397
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001398 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001399 if gpt.is_secondary:
1400 header = gpt.header
1401 else:
1402 f = args.image_file
1403 f.seek(gpt.header.BackupLBA * gpt.block_size)
1404 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001405 print(fmt % (header.PartitionEntriesStartingLBA,
1406 gpt.GetPartitionTableBlocks(header), '',
1407 'Sec GPT table'))
1408 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001409
Hung-Te Lin3b491672018-04-19 01:41:20 +08001410 # Check integrity after showing all fields.
1411 gpt.CheckIntegrity()
1412
Hung-Te Linfe724f82018-04-18 15:03:58 +08001413 class Prioritize(SubCommand):
1414 """Reorder the priority of all kernel partitions.
1415
1416 Reorder the priority of all active ChromeOS Kernel partitions.
1417
1418 With no options this will set the lowest active kernel to priority 1 while
1419 maintaining the original order.
1420 """
1421
1422 def DefineArgs(self, parser):
1423 parser.add_argument(
1424 '-P', '--priority', type=int,
1425 help=('Highest priority to use in the new ordering. '
1426 'The other partitions will be ranked in decreasing '
1427 'priority while preserving their original order. '
1428 'If necessary the lowest ranks will be coalesced. '
1429 'No active kernels will be lowered to priority 0.'))
1430 parser.add_argument(
1431 '-i', '--number', type=int,
1432 help='Specify the partition to make the highest in the new order.')
1433 parser.add_argument(
1434 '-f', '--friends', action='store_true',
1435 help=('Friends of the given partition (those with the same '
1436 'starting priority) are also updated to the new '
1437 'highest priority. '))
1438 parser.add_argument(
1439 'image_file', type=argparse.FileType('rb+'),
1440 help='Disk image file to prioritize.')
1441
1442 def Execute(self, args):
1443 gpt = GPT.LoadFromFile(args.image_file)
1444 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001445 prios = list(set(p.Attributes.priority for p in parts
1446 if p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001447 prios.sort(reverse=True)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001448 groups = [[p for p in parts if p.Attributes.priority == priority]
Hung-Te Linfe724f82018-04-18 15:03:58 +08001449 for priority in prios]
1450 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001451 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001452 if p not in parts:
1453 raise GPTError('%s is not a ChromeOS kernel.' % p)
1454 if args.friends:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001455 group0 = [f for f in parts
1456 if f.Attributes.priority == p.Attributes.priority]
Hung-Te Linfe724f82018-04-18 15:03:58 +08001457 else:
1458 group0 = [p]
1459 groups.insert(0, group0)
1460
1461 # Max priority is 0xf.
1462 highest = min(args.priority or len(prios), 0xf)
1463 logging.info('New highest priority: %s', highest)
1464 done = []
1465
1466 new_priority = highest
1467 for g in groups:
1468 has_new_part = False
1469 for p in g:
1470 if p.number in done:
1471 continue
1472 done.append(p.number)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001473 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001474 old_priority = attrs.priority
1475 assert new_priority > 0, 'Priority must be > 0.'
1476 attrs.priority = new_priority
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001477 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001478 has_new_part = True
1479 logging.info('%s priority changed from %s to %s.', p, old_priority,
1480 new_priority)
1481 if has_new_part:
1482 new_priority -= 1
1483
1484 gpt.WriteToFile(args.image_file)
1485
Hung-Te Linf641d302018-04-18 15:09:35 +08001486 class Find(SubCommand):
1487 """Locate a partition by its GUID.
1488
1489 Find a partition by its UUID or label. With no specified DRIVE it scans all
1490 physical drives.
1491
1492 The partition type may also be given as one of these aliases:
1493
1494 firmware ChromeOS firmware
1495 kernel ChromeOS kernel
1496 rootfs ChromeOS rootfs
1497 data Linux data
1498 reserved ChromeOS reserved
1499 efi EFI System Partition
1500 unused Unused (nonexistent) partition
1501 """
1502 def DefineArgs(self, parser):
1503 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001504 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001505 help='Search for Partition Type GUID')
1506 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001507 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001508 help='Search for Partition Unique GUID')
1509 parser.add_argument(
1510 '-l', '--label',
1511 help='Search for Label')
1512 parser.add_argument(
1513 '-n', '--numeric', action='store_true',
1514 help='Numeric output only.')
1515 parser.add_argument(
1516 '-1', '--single-match', action='store_true',
1517 help='Fail if more than one match is found.')
1518 parser.add_argument(
1519 '-M', '--match-file', type=str,
1520 help='Matching partition data must also contain MATCH_FILE content.')
1521 parser.add_argument(
1522 '-O', '--offset', type=int, default=0,
1523 help='Byte offset into partition to match content (default 0).')
1524 parser.add_argument(
1525 'drive', type=argparse.FileType('rb+'), nargs='?',
1526 help='Drive or disk image file to find.')
1527
1528 def Execute(self, args):
1529 if not any((args.type_guid, args.unique_guid, args.label)):
1530 raise GPTError('You must specify at least one of -t, -u, or -l')
1531
1532 drives = [args.drive.name] if args.drive else (
1533 '/dev/%s' % name for name in subprocess.check_output(
1534 'lsblk -d -n -r -o name', shell=True).split())
1535
1536 match_pattern = None
1537 if args.match_file:
1538 with open(args.match_file) as f:
1539 match_pattern = f.read()
1540
1541 found = 0
1542 for drive in drives:
1543 try:
1544 gpt = GPT.LoadFromFile(drive)
1545 except GPTError:
1546 if args.drive:
1547 raise
1548 # When scanning all block devices on system, ignore failure.
1549
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001550 def Unmatch(a, b):
1551 return a is not None and a != b
1552
Hung-Te Linf641d302018-04-18 15:09:35 +08001553 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001554 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001555 Unmatch(args.label, p.Names) or
1556 Unmatch(args.unique_guid, p.UniqueGUID) or
1557 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001558 continue
1559 if match_pattern:
1560 with open(drive, 'rb') as f:
1561 f.seek(p.offset + args.offset)
1562 if f.read(len(match_pattern)) != match_pattern:
1563 continue
1564 # Found the partition, now print.
1565 found += 1
1566 if args.numeric:
1567 print(p.number)
1568 else:
1569 # This is actually more for block devices.
1570 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1571 p.number))
1572
1573 if found < 1 or (args.single_match and found > 1):
1574 return 1
1575 return 0
1576
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001577
1578def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001579 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001580 parser = argparse.ArgumentParser(description='GPT Utility.')
1581 parser.add_argument('--verbose', '-v', action='count', default=0,
1582 help='increase verbosity.')
1583 parser.add_argument('--debug', '-d', action='store_true',
1584 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001585 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001586
1587 args = parser.parse_args()
1588 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1589 if args.debug:
1590 log_level = logging.DEBUG
1591 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1592 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001593 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001594 code = commands.Execute(args)
1595 if type(code) is int:
1596 sys.exit(code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001597 except Exception as e:
1598 if args.verbose or args.debug:
1599 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001600 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001601
1602
1603if __name__ == '__main__':
1604 main()